Add gfx.Commands.draw_texture (closes #67)
continuous-integration/drone/push Build is passing Details

This commit is contained in:
kayomn 2024-07-27 01:44:06 +01:00
parent 13c58bf106
commit bb1d383ccb
6 changed files with 287 additions and 177 deletions

View File

@ -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},
});

View File

@ -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,
});
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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"});

View File

@ -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),