diff --git a/demos/effects.zig b/demos/effects.zig index 8b9114a..8b22d54 100644 --- a/demos/effects.zig +++ b/demos/effects.zig @@ -13,6 +13,9 @@ const CRT = extern struct { const Effects = struct { render_texture: gfx.Texture = .default, + image_textures: [2]gfx.Texture = [_]gfx.Texture{.default} ** 2, + last_time: f64 = 0, + image_index: usize = 0, crt_effect: gfx.Effect = .default, }; @@ -31,14 +34,44 @@ fn load(display: ona.Write(gfx.Display), effects: ona.Write(Effects), assets: on }); effects.state.crt_effect = try assets.state.load_effect_file(ona.files.bundle, "./crt.frag.spv"); + + var descs = gfx.Descs.init(ona.heap.allocator); + + defer { + descs.deinit(); + } + + effects.state.image_textures = .{ + try assets.state.load_texture(try descs.checker_texture(.{ + .colors = .{gfx.colors.black, gfx.colors.purple}, + .width = 8, + .height = 8, + })), + + try assets.state.load_texture(try descs.checker_texture(.{ + .colors = .{gfx.colors.black, gfx.colors.grey}, + .width = 8, + .height = 8, + })) + }; } pub const main = ona.App.setup. with_module(gfx). with_state(Effects{}). with_system(.load, ona.system_fn(load), .{.label = "load effects"}). + with_system(.update, ona.system_fn(update), .{.label = "update effects"}). with_system(.render, ona.system_fn(render), .{.label = "render effects"}).build(); +fn update(effects: ona.Write(Effects), app: ona.Read(ona.App)) void { + const update_seconds = 5; + + if ((app.state.elapsed_time - effects.state.last_time) > update_seconds) { + effects.state.image_index = (effects.state.image_index + 1) % effects.state.image_textures.len; + effects.state.last_time = app.state.elapsed_time; + } +} + fn render(commands: gfx.Commands, effects: ona.Write(Effects), app: ona.Read(ona.App), display: ona.Write(gfx.Display)) !void { try commands.set_target(.{ .texture = effects.state.render_texture, @@ -55,7 +88,7 @@ fn render(commands: gfx.Commands, effects: ona.Write(Effects), app: ona.Read(ona }); try commands.draw_texture(.{ - .texture = .default, + .texture = effects.state.image_textures[effects.state.image_index], .transform = display_transform, .resolution = .{display.state.width, display.state.height}, }); diff --git a/demos/inputs.zig b/demos/inputs.zig index de9b1c6..16a34e0 100644 --- a/demos/inputs.zig +++ b/demos/inputs.zig @@ -7,29 +7,29 @@ const ona = @import("ona"); const std = @import("std"); const Spawned = struct { - transform: gfx.Transform2D, + visual: struct { + color: gfx.Color, + transform: gfx.Transform2D, + }, + lifetime_seconds: f32, }; const Visuals = struct { - spawned_keyboards: ona.stack.Parallel(Spawned) = .{}, - spawned_mouses: ona.stack.Parallel(Spawned) = .{}, - keyboard_icon: gfx.Texture = .default, - mouse_icon: gfx.Texture = .default, + spawned: ona.stack.Parallel(Spawned) = .{}, random: std.Random.Xoroshiro128, mouse_position: @Vector(2, f32) = @splat(0), }; fn cleanup(visuals: ona.Write(Visuals)) !void { - visuals.state.spawned_keyboards.deinit(); - visuals.state.spawned_mouses.deinit(); + visuals.state.spawned.deinit(); + visuals.state.spawned.deinit(); } fn load(visuals: ona.Write(Visuals)) !void { const initial_spawn_capacity = 1024; - try visuals.state.spawned_keyboards.grow(initial_spawn_capacity); - try visuals.state.spawned_mouses.grow(initial_spawn_capacity); + try visuals.state.spawned.grow(initial_spawn_capacity); } pub const main = ona.App.setup. @@ -42,8 +42,7 @@ pub const main = ona.App.setup. with_system(.exit, ona.system_fn(cleanup), .{.label = "clean up visuals"}).build(); fn update(visuals: ona.Write(Visuals), events: ona.Receive(hid.Event), display: ona.Read(gfx.Display)) !void { - update_spawned(&visuals.state.spawned_keyboards); - update_spawned(&visuals.state.spawned_mouses); + update_spawned(&visuals.state.spawned); const random = visuals.state.random.random(); const width: f32 = @floatFromInt(display.state.width); @@ -53,26 +52,34 @@ fn update(visuals: ona.Write(Visuals), events: ona.Receive(hid.Event), display: for (events.messages()) |event| { switch (event) { .key_down => { - try visuals.state.spawned_keyboards.push_grow(.{ + try visuals.state.spawned.push_grow(.{ .lifetime_seconds = 2.5 + (5 * random.float(f32)), - .transform = gfx.transform_2d(.{ - .translation = .{width * random.float(f32), height}, - .rotation = std.math.pi * random.float(f32), - .scale = icon_scale, - }), + .visual = .{ + .color = .{random.float(f32), random.float(f32), random.float(f32), random.float(f32)}, + + .transform = gfx.transform_2d(.{ + .translation = .{width * random.float(f32), height}, + .rotation = std.math.pi * random.float(f32), + .scale = icon_scale, + }), + }, }); }, .mouse_down => { - try visuals.state.spawned_mouses.push_grow(.{ + try visuals.state.spawned.push_grow(.{ .lifetime_seconds = 2.5 + (5 * random.float(f32)), - .transform = gfx.transform_2d(.{ - .translation = visuals.state.mouse_position, - .rotation = std.math.pi * random.float(f32), - .scale = icon_scale, - }), + .visual = .{ + .color = .{random.float(f32), random.float(f32), random.float(f32), random.float(f32)}, + + .transform = gfx.transform_2d(.{ + .translation = visuals.state.mouse_position, + .rotation = std.math.pi * random.float(f32), + .scale = icon_scale, + }), + }, }); }, @@ -88,8 +95,8 @@ fn update(visuals: ona.Write(Visuals), events: ona.Receive(hid.Event), display: fn update_spawned(spawned: *ona.stack.Parallel(Spawned)) void { const float_speed = 6; - for (spawned.values.slice(.transform)) |*transform| { - transform.* = transform.translated(.{0, -float_speed}); + for (spawned.values.slice(.visual)) |*visual| { + visual.transform = visual.transform.translated(.{0, -float_speed}); } { @@ -113,17 +120,10 @@ fn update_spawned(spawned: *ona.stack.Parallel(Spawned)) void { } fn render(visuals: ona.Write(Visuals), commands: gfx.Commands) !void { - for (visuals.state.spawned_keyboards.values.slice(.transform)) |transform| { - try commands.draw_texture(.{ - .texture = visuals.state.keyboard_icon, - .transform = transform, - }); - } - - for (visuals.state.spawned_mouses.values.slice(.transform)) |transform| { - try commands.draw_texture(.{ - .texture = visuals.state.keyboard_icon, - .transform = transform, + for (visuals.state.spawned.values.slice(.visual)) |visual| { + try commands.draw_rect(.{ + .transform = visual.transform, + .color = visual.color, }); } } diff --git a/src/gfx/Resources.zig b/src/gfx/Resources.zig index 8039883..67caff3 100644 --- a/src/gfx/Resources.zig +++ b/src/gfx/Resources.zig @@ -454,35 +454,26 @@ pub fn init() !Self { } const assert = struct { - fn is_handle(expected: anytype, actual: @TypeOf(expected)) void { - std.debug.assert(actual == expected); + fn is_default_handle(actual: anytype) void { + std.debug.assert(actual == .default); } }; - assert.is_handle(gfx.Effect.default, try pools.create_effect(.{ + var descs = gfx.Descs.init(ona.heap.allocator); + + defer { + descs.deinit(); + } + + assert.is_default_handle(try pools.create_effect(.{ .fragment_spirv_ops = &spirv.to_ops(@embedFile("./shaders/2d_default.frag.spv")), })); - assert.is_handle(gfx.Texture.default, try pools.create_texture(.{ - .format = .rgba8, - - .access = .{ - .static = .{ - .data = std.mem.asBytes(&[_]u32{ - 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, - 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, - 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, - 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, - 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, - 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, - 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, - 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, 0xFF800080, 0xFF000000, - }), - - .width = 8, - }, - }, - })); + assert.is_default_handle(try pools.create_texture(try descs.solid_texture(.{ + .width = 8, + .height = 8, + .color = gfx.colors.white, + }))); return pools; } diff --git a/src/gfx/colors.zig b/src/gfx/colors.zig deleted file mode 100644 index 5a2a20d..0000000 --- a/src/gfx/colors.zig +++ /dev/null @@ -1,13 +0,0 @@ -const gfx = @import("./gfx.zig"); - -pub const black = greyscale(0); - -pub fn greyscale(v: f32) gfx.Color { - return .{v, v, v, 1}; -} - -pub fn rgb(r: f32, g: f32, b: f32) gfx.Color { - return .{r, g, b, 1}; -} - -pub const white = greyscale(1); diff --git a/src/gfx/gfx.zig b/src/gfx/gfx.zig index 3c8f17c..dd7bff9 100644 --- a/src/gfx/gfx.zig +++ b/src/gfx/gfx.zig @@ -1,5 +1,3 @@ -pub const colors = @import("./colors.zig"); - const hid = @import("hid"); const ona = @import("ona"); @@ -14,7 +12,6 @@ const std = @import("std"); pub const Assets = struct { window: *ext.SDL_Window, - texture_formats: ona.stack.Sequential(TextureFormat), frame_rendered: std.Thread.ResetEvent = .{}, pending_work: WorkQueue = .{}, has_worker_thread: ?std.Thread = null, @@ -72,8 +69,6 @@ pub const Assets = struct { worker_thread.join(); } - self.texture_formats.deinit(); - self.* = undefined; } @@ -94,7 +89,6 @@ pub const Assets = struct { } return .{ - .texture_formats = .{.allocator = ona.heap.allocator}, .window = window, }; } @@ -143,24 +137,6 @@ pub const Assets = struct { return loaded.get().*; } - pub fn load_texture_file(self: *Assets, storage: ona.files.Storage, path: []const u8) LoadFileError!Texture { - var arena = std.heap.ArenaAllocator.init(ona.heap.allocator); - - defer { - arena.deinit(); - } - - for (self.texture_formats.values) |format| { - if (!std.mem.endsWith(u8, path, format.extension)) { - continue; - } - - return self.load_texture(try format.load_file(&arena, storage, path)); - } - - return error.FormatUnsupported; - } - pub const thread_restriction = .main; }; @@ -169,12 +145,18 @@ pub const Color = @Vector(4, f32); pub const Commands = struct { pending: *List, - const Command = union (enum) { + pub const Command = union (enum) { + draw_rect: DrawRectCommand, draw_texture: DrawTextureCommand, set_effect: SetEffectCommand, set_target: SetTargetCommand, }; + pub const DrawRectCommand = struct { + color: Color, + transform: Transform2D, + }; + pub const DrawTextureCommand = struct { texture: Texture, transform: Transform2D, @@ -264,6 +246,10 @@ pub const Commands = struct { }; } + pub fn draw_rect(self: Commands, command: DrawRectCommand) std.mem.Allocator.Error!void { + try self.pending.stack.push_grow(.{.draw_rect = command}); + } + pub fn draw_texture(self: Commands, command: DrawTextureCommand) std.mem.Allocator.Error!void { try self.pending.stack.push_grow(.{.draw_texture = command}); } @@ -286,6 +272,150 @@ pub const Commands = struct { } }; +pub const Descs = struct { + arena: std.heap.ArenaAllocator, + + pub const CheckerTextureDesc = struct { + width: u16, + height: u16, + colors: [2]Color, + }; + + pub const BmpTextureDesc = struct { + storage: ona.files.Storage, + path: []const u8, + }; + + pub const SolidTextureDesc = struct { + width: u16, + height: u16, + color: Color, + }; + + pub fn bmp_texture(self: *Descs, desc: BmpTextureDesc) (error { OutOfMemory, FormatUnsupported })!Texture.Desc { + const header = try desc.storage.read_little(desc.path, 0, extern struct { + type: [2]u8 align (1), + file_size: u32 align (1), + reserved: [2]u16 align (1), + image_offset: u32 align (1), + header_size: u32 align (1), + pixel_width: i32 align (1), + pixel_height: i32 align (1), + color_planes: u16 align (1), + bits_per_pixel: u16 align (1), + compression_method: u32 align (1), + image_size: u32 align(1), + pixels_per_meter_x: i32 align (1), + pixels_per_meter_y: i32 align (1), + palette_colors_used: u32 align (1), + important_colors_used: u32 align (1), + }) orelse { + return error.FormatUnsupported; + }; + + if (!std.mem.eql(u8, &header.type, "BM")) { + return error.FormatUnsupported; + } + + const pixel_width = std.math.cast(u16, header.pixel_width) orelse { + return error.FormatUnsupported; + }; + + const pixels = try self.arena.allocator().alloc(u8, header.image_size); + const bytes_per_pixel = header.bits_per_pixel / @bitSizeOf(u8); + const alignment = 4; + const byte_stride = pixel_width * bytes_per_pixel; + const padded_byte_stride = alignment * @divTrunc((byte_stride + alignment - 1), alignment); + const byte_padding = ona.scalars.sub(padded_byte_stride, byte_stride) orelse 0; + var buffer_offset: usize = 0; + var file_offset = @as(usize, header.image_offset); + + switch (header.bits_per_pixel) { + 32 => { + while (buffer_offset < pixels.len) { + const line = pixels[buffer_offset .. buffer_offset + byte_stride]; + + if (try desc.storage.read(desc.path, line, file_offset) != byte_stride) { + return error.FormatUnsupported; + } + + for (0 .. pixel_width) |i| { + const line_offset = i * 4; + const pixel = line[line_offset .. line_offset + 4]; + + std.mem.swap(u8, &pixel[0], &pixel[2]); + } + + file_offset += line.len + byte_padding; + buffer_offset += padded_byte_stride; + } + }, + + else => return error.FormatUnsupported, + } + + return .{ + .format = .rgba8, + + .access = .{ + .static = .{ + .width = pixel_width, + .data = pixels, + }, + }, + }; + } + + pub fn checker_texture(self: *Descs, desc: CheckerTextureDesc) std.mem.Allocator.Error!Texture.Desc { + const color_data = try self.arena.allocator().alloc(u32, desc.width * desc.height); + + for (color_data, 0 .. color_data.len) |*color, i| { + const row = i / desc.width; + const col = i % desc.width; + + color.* = colors.compress(desc.colors[(col + (row % 2)) % desc.colors.len]); + } + + return .{ + .access = .{ + .static = .{ + .data = std.mem.sliceAsBytes(color_data), + .width = desc.width, + }, + }, + + .format = .rgba8, + }; + } + + pub fn deinit(self: *Descs) void { + self.arena.deinit(); + } + + pub fn init(allocator: std.mem.Allocator) Descs { + return .{ + .arena = std.heap.ArenaAllocator.init(allocator), + }; + } + + pub fn solid_texture(self: *Descs, desc: SolidTextureDesc) std.mem.Allocator.Error!Texture.Desc { + const color_data = try self.arena.allocator().alloc(u32, desc.width * desc.height); + + @memset(color_data, colors.compress(desc.color)); + + return .{ + .access = .{ + .static = .{ + .data = std.mem.sliceAsBytes(color_data), + .width = desc.width, + }, + }, + + .format = .rgba8, + }; + } +}; + pub const Display = struct { width: u16 = 1280, height: u16 = 720, @@ -398,79 +528,30 @@ pub const Transform2D = extern struct { } }; -fn load_bmp_texture(arena: *std.heap.ArenaAllocator, storage: ona.files.Storage, path: []const u8) !Texture.Desc { - const header = try storage.read_little(path, 0, extern struct { - type: [2]u8 align (1), - file_size: u32 align (1), - reserved: [2]u16 align (1), - image_offset: u32 align (1), - header_size: u32 align (1), - pixel_width: i32 align (1), - pixel_height: i32 align (1), - color_planes: u16 align (1), - bits_per_pixel: u16 align (1), - compression_method: u32 align (1), - image_size: u32 align(1), - pixels_per_meter_x: i32 align (1), - pixels_per_meter_y: i32 align (1), - palette_colors_used: u32 align (1), - important_colors_used: u32 align (1), - }) orelse { - return error.FormatUnsupported; - }; +pub const colors = struct { + pub const black = greyscale(0); - if (!std.mem.eql(u8, &header.type, "BM")) { - return error.FormatUnsupported; + pub fn compress(color: Color) u32 { + const range: Color = @splat(255); + const r, const g, const b, const a = color * range; + + return @bitCast([_]u8{@intFromFloat(r), @intFromFloat(g), @intFromFloat(b), @intFromFloat(a)}); } - const pixel_width = std.math.cast(u16, header.pixel_width) orelse { - return error.FormatUnsupported; - }; + pub const grey = greyscale(0.5); - const pixels = try arena.allocator().alloc(u8, header.image_size); - const bytes_per_pixel = header.bits_per_pixel / @bitSizeOf(u8); - const alignment = 4; - const byte_stride = pixel_width * bytes_per_pixel; - const padded_byte_stride = alignment * @divTrunc((byte_stride + alignment - 1), alignment); - const byte_padding = ona.scalars.sub(padded_byte_stride, byte_stride) orelse 0; - var buffer_offset: usize = 0; - var file_offset = @as(usize, header.image_offset); - - switch (header.bits_per_pixel) { - 32 => { - while (buffer_offset < pixels.len) { - const line = pixels[buffer_offset .. buffer_offset + byte_stride]; - - if (try storage.read(path, line, file_offset) != byte_stride) { - return error.FormatUnsupported; - } - - for (0 .. pixel_width) |i| { - const line_offset = i * 4; - const pixel = line[line_offset .. line_offset + 4]; - - std.mem.swap(u8, &pixel[0], &pixel[2]); - } - - file_offset += line.len + byte_padding; - buffer_offset += padded_byte_stride; - } - }, - - else => return error.FormatUnsupported, + pub fn greyscale(v: f32) Color { + return .{v, v, v, 1}; } - return .{ - .format = .rgba8, + pub const purple = rgb(0.5, 0, 0.5); - .access = .{ - .static = .{ - .width = pixel_width, - .data = pixels, - }, - }, - }; -} + pub fn rgb(r: f32, g: f32, b: f32) Color { + return .{r, g, b, 1}; + } + + pub const white = greyscale(1); +}; pub fn poll(app: ona.Write(ona.App), events: ona.Send(hid.Event)) !void { var event = @as(ext.SDL_Event, undefined); @@ -551,17 +632,6 @@ pub fn setup(world: *ona.World, events: ona.App.Events) (error {Unsupported} || assets.window, }); - const builtin_texture_formats = [_]Assets.TextureFormat{ - .{ - .extension = "bmp", - .load_file = load_bmp_texture, - }, - }; - - for (builtin_texture_formats) |format| { - try assets.texture_formats.push_grow(format); - } - try world.set_state(Display{}); try world.on_event(events.pre_update, ona.system_fn(poll), .{.label = "poll gfx"}); try world.on_event(events.exit, ona.system_fn(stop), .{.label = "stop gfx"}); diff --git a/src/gfx/rendering.zig b/src/gfx/rendering.zig index ee0890a..26af3bf 100644 --- a/src/gfx/rendering.zig +++ b/src/gfx/rendering.zig @@ -69,14 +69,42 @@ const Frame = struct { }); return .{ - .texture_batch_buffers = .{.allocator = ona.heap.allocator}, + .texture_batch_buffers = .{}, .quad_index_buffer = quad_index_buffer, .quad_vertex_buffer = quad_vertex_buffer, }; } + pub fn draw_rect(self: *Frame, resources: *Resources, command: gfx.Commands.DrawRectCommand) !void { + if (self.current_source_texture != .default) { + self.flush(resources); + } + + self.current_source_texture = .default; + + const has_filled_current_buffer = (self.drawn_count % batches_per_buffer) == 0; + const buffer_count = self.drawn_count / batches_per_buffer; + + if (has_filled_current_buffer and buffer_count == self.texture_batch_buffers.len()) { + const instance_buffer = sokol.gfx.makeBuffer(.{ + .size = @sizeOf(DrawTexture) * batches_per_buffer, + .usage = .STREAM, + }); + + errdefer sokol.gfx.destroyBuffer(instance_buffer); + + try self.texture_batch_buffers.push_grow(instance_buffer); + } + + _ = sokol.gfx.appendBuffer(self.texture_batch_buffers.get().?.*, sokol.gfx.asRange(&DrawTexture{ + .transform = command.transform, + })); + + self.drawn_count += 1; + } + pub fn draw_texture(self: *Frame, resources: *Resources, command: gfx.Commands.DrawTextureCommand) !void { - if (command.texture != self.current_source_texture) { + if (self.current_source_texture != command.texture) { self.flush(resources); } @@ -377,6 +405,7 @@ pub fn process_work(pending_work: *gfx.Assets.WorkQueue, window: *ext.SDL_Window while (has_command_params) |command_params| : (has_command_params = command_params.has_next) { for (command_params.param.submitted_commands()) |command| { try switch (command) { + .draw_rect => |draw_rect| frame.draw_rect(&resources, draw_rect), .draw_texture => |draw_texture| frame.draw_texture(&resources, draw_texture), .set_effect => |set_effect| frame.set_effect(&resources, set_effect), .set_target => |set_target| frame.set_target(&resources, set_target),