diff --git a/build.zig b/build.zig index 8393b7e..c702a40 100644 --- a/build.zig +++ b/build.zig @@ -194,17 +194,7 @@ pub fn build(b: *std.Build) !void { const vertex_binary_sub_path_utf8 = vertex_binary_sub_path.utf8(); const fragment_binary_sub_path_utf8 = fragment_binary_sub_path.utf8(); - const link_command = b.addSystemCommand(&.{ - "spirv-link", - vertex_binary_sub_path_utf8, - fragment_binary_sub_path_utf8, - "-o", - pending_shader.binary_sub_path.utf8(), - }); - - exe.step.dependOn(&link_command.step); - - link_command.step.dependOn(compile_vertex: { + exe.step.dependOn(compile_vertex: { const compile_command = b.addSystemCommand(&.{ "glslangValidator", "-V", @@ -216,7 +206,7 @@ pub fn build(b: *std.Build) !void { break: compile_vertex &compile_command.step; }); - link_command.step.dependOn(compile_fragment: { + exe.step.dependOn(compile_fragment: { const compile_command = b.addSystemCommand(&.{ "glslangValidator", "-V", diff --git a/debug/ca.frag b/debug/ca.frag index b4218e0..1f579bf 100644 --- a/debug/ca.frag +++ b/debug/ca.frag @@ -7,22 +7,19 @@ layout (location = 1) in vec2 uv; layout (location = 0) out vec4 texel; -layout (binding = 0) readonly buffer Camera { - mat4 projection_matrix; -}; - -layout (binding = 1) readonly buffer Material { +layout (binding = 0) readonly buffer Effect { float effect_magnitude; }; void main() { - const vec2 red_channel_uv = uv + vec2(effect_magnitude, 0.0); - const vec2 blue_channel_uv = uv + vec2(-effect_magnitude, 0.0); - const vec4 original_texel = texture(sprite, uv); + vec4 color1 = texture(sprite, uv) / 3.0; + vec4 color2 = texture(sprite, uv + 0.002 * effect_magnitude) / 3.0; + vec4 color3 = texture(sprite, uv - 0.002 * effect_magnitude) / 3.0; - texel = vec4(texture(sprite, red_channel_uv).r, original_texel.g, texture(sprite, blue_channel_uv).b, original_texel.a) * color; + color1 *= 2.0; + color2.g = 0.0; + color2.b = 0.0; + color3.r = 0.0; - if (texel.a == 0) { - discard; - } + texel = color * (color1 + color2 + color3); } diff --git a/src/main.zig b/src/main.zig index f345239..0a5b230 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,10 +4,15 @@ const std = @import("std"); const ona = @import("ona"); +const ChromaticAberration = extern struct { + magnitude: f32, +}; + const Actors = struct { instances: coral.stack.Sequential(@Vector(2, f32)) = .{.allocator = coral.heap.allocator}, body_texture: ona.gfx.Texture = .default, render_texture: ona.gfx.Texture = .default, + ca_effect: ona.gfx.Effect = .default, }; const Player = struct { @@ -37,6 +42,8 @@ fn load(config: ona.Write(ona.gfx.Config), actors: ona.Write(Actors), assets: on }, }); + actors.res.ca_effect = try assets.res.load_effect_file(coral.files.bundle, "./ca.frag.spv"); + try actors.res.instances.push_grow(.{0, 0}); } @@ -44,12 +51,9 @@ fn exit(actors: ona.Write(Actors)) void { actors.res.instances.deinit(); } -fn render(commands: ona.gfx.Commands, actors: ona.Write(Actors), config: ona.Read(ona.gfx.Config)) !void { - try commands.set_target(.{ - .texture = actors.res.render_texture, - .clear_color = .{0, 0, 0, 0}, - .clear_depth = 0, - .clear_stencil = 0, +fn render(commands: ona.gfx.Commands, actors: ona.Write(Actors)) !void { + try commands.set_effect(actors.res.ca_effect, ChromaticAberration{ + .magnitude = 15.0, }); for (actors.res.instances.values) |instance| { @@ -63,22 +67,6 @@ fn render(commands: ona.gfx.Commands, actors: ona.Write(Actors), config: ona.Rea }, }); } - - try commands.set_target(.{ - .clear_color = null, - .clear_depth = null, - .clear_stencil = null, - }); - - try commands.draw_texture(.{ - .texture = actors.res.render_texture, - - .transform = .{ - .origin = .{@floatFromInt(config.res.width / 2), @floatFromInt(config.res.height / 2)}, - .xbasis = .{@floatFromInt(config.res.width), 0}, - .ybasis = .{0, @floatFromInt(config.res.height)}, - }, - }); } fn update(player: ona.Read(Player), actors: ona.Write(Actors), mapping: ona.Read(ona.act.Mapping)) !void { diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index 8a6780f..4f0d7d5 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -25,12 +25,11 @@ const std = @import("std"); pub const Assets = struct { window: *ext.SDL_Window, texture_formats: coral.stack.Sequential(TextureFormat), - staging_arena: std.heap.ArenaAllocator, frame_rendered: std.Thread.ResetEvent = .{}, pub const LoadError = std.mem.Allocator.Error; - pub const LoadFileError = LoadError || coral.files.AccessError || error { + pub const LoadFileError = LoadError || coral.files.ReadAllError || error { FormatUnsupported, }; @@ -41,7 +40,6 @@ pub const Assets = struct { fn deinit(self: *Assets) void { rendering.enqueue_work(.shutdown); - self.staging_arena.deinit(); self.texture_formats.deinit(); } @@ -64,12 +62,40 @@ pub const Assets = struct { try rendering.startup(window); return .{ - .staging_arena = std.heap.ArenaAllocator.init(coral.heap.allocator), .texture_formats = .{.allocator = coral.heap.allocator}, .window = window, }; } + pub fn load_effect_file(_: *Assets, storage: coral.files.Storage, path: []const u8) LoadFileError!Effect { + if (!std.mem.endsWith(u8, path, ".spv")) { + return error.FormatUnsupported; + } + + const fragment_file_stat = try storage.stat(path); + const fragment_spirv = try coral.heap.allocator.alloc(u32, fragment_file_stat.size / @alignOf(u32)); + + defer { + coral.heap.allocator.free(fragment_spirv); + } + + _ = try storage.read_all(path, std.mem.sliceAsBytes(fragment_spirv), .{}); + + var loaded = coral.asyncio.Future(std.mem.Allocator.Error!Effect){}; + + rendering.enqueue_work(.{ + .load_effect = .{ + .desc = .{ + .fragment_spirv = fragment_spirv, + }, + + .loaded = &loaded, + }, + }); + + return loaded.get().*; + } + pub fn load_texture(_: *Assets, desc: Texture.Desc) std.mem.Allocator.Error!Texture { var loaded = coral.asyncio.Future(std.mem.Allocator.Error!Texture){}; @@ -84,12 +110,10 @@ pub const Assets = struct { } pub fn load_texture_file(self: *Assets, storage: coral.files.Storage, path: []const u8) LoadFileError!Texture { - defer { - const max_cache_size = 536870912; + var arena = std.heap.ArenaAllocator.init(coral.heap.allocator); - if (!self.staging_arena.reset(.{.retain_with_limit = max_cache_size})) { - std.log.warn("failed to retain staging arena size of {} bytes", .{max_cache_size}); - } + defer { + arena.deinit(); } for (self.texture_formats.values) |format| { @@ -97,12 +121,26 @@ pub const Assets = struct { continue; } - return self.load_texture(try format.load_file(&self.staging_arena, storage, path)); + return self.load_texture(try format.load_file(&arena, storage, path)); } return error.FormatUnsupported; } + pub fn update_effect(_: *Assets, effect: Effect, properties: anytype) bool { + var setted = coral.asyncio.Future(bool){}; + + rendering.enqueue_work(.{ + .update_effect = .{ + .properties = std.mem.asBytes(properties), + .effect = effect, + .setted = &setted, + }, + }); + + return setted.get().*; + } + pub const thread_restriction = .main; }; @@ -137,6 +175,15 @@ pub const Commands = struct { try self.list.append(.{.draw_texture = command}); } + pub fn set_effect(self: Commands, effect: handles.Effect, properties: anytype) std.mem.Allocator.Error!void { + try self.list.append(.{ + .set_effect = .{ + .properties = std.mem.asBytes(&properties), + .effect = effect, + }, + }); + } + pub fn set_target(self: Commands, command: rendering.Command.SetTarget) std.mem.Allocator.Error!void { try self.list.append(.{.set_target = command}); } @@ -164,6 +211,8 @@ pub const Input = union (enum) { }; }; +pub const Effect = handles.Effect; + pub const Rect = struct { left: f32, top: f32, diff --git a/src/ona/gfx/handles.zig b/src/ona/gfx/handles.zig index 8c8a014..da4010a 100644 --- a/src/ona/gfx/handles.zig +++ b/src/ona/gfx/handles.zig @@ -2,6 +2,10 @@ const coral = @import("coral"); const std = @import("std"); +pub const Effect = Handle(struct { + fragment_spirv: []const u32, +}); + fn Handle(comptime HandleDesc: type) type { return enum (u32) { default, diff --git a/src/ona/gfx/rendering.zig b/src/ona/gfx/rendering.zig index 6727443..6735fd5 100644 --- a/src/ona/gfx/rendering.zig +++ b/src/ona/gfx/rendering.zig @@ -16,6 +16,7 @@ const std = @import("std"); pub const Command = union (enum) { draw_texture: DrawTexture, + set_effect: SetEffect, set_target: SetTarget, pub const DrawTexture = struct { @@ -23,6 +24,11 @@ pub const Command = union (enum) { transform: lina.Transform2D, }; + pub const SetEffect = struct { + effect: handles.Effect, + properties: []const coral.io.Byte, + }; + pub const SetTarget = struct { texture: ?handles.Texture = null, clear_color: ?lina.Color, @@ -31,10 +37,16 @@ pub const Command = union (enum) { }; fn clone(self: Command, arena: *std.heap.ArenaAllocator) !Command { - _ = arena; - return switch (self) { .draw_texture => |draw_texture| .{.draw_texture = draw_texture}, + + .set_effect => |set_effect| .{ + .set_effect = .{ + .properties = try arena.allocator().dupe(coral.io.Byte, set_effect.properties), + .effect = set_effect.effect, + }, + }, + .set_target => |set_target| .{.set_target = set_target}, }; } @@ -42,6 +54,7 @@ pub const Command = union (enum) { fn process(self: Command, pools: *Pools, frame: *Frame) !void { try switch (self) { .draw_texture => |draw_texture| frame.draw_texture(pools, draw_texture), + .set_effect => |set_effect| frame.set_effect(pools, set_effect), .set_target => |set_target| frame.set_target(pools, set_target), }; } @@ -123,6 +136,7 @@ const Frame = struct { flushed_count: usize = 0, current_source_texture: handles.Texture = .default, current_target_texture: ?handles.Texture = null, + current_effect: handles.Effect = .default, const DrawTexture = extern struct { transform: lina.Transform2D, @@ -175,29 +189,29 @@ const Frame = struct { .render => |render| { bindings.fs.images[0] = render.color_image; bindings.fs.samplers[0] = render.sampler; - bindings.vs.images[0] = render.color_image; - bindings.vs.samplers[0] = render.sampler; }, .static => |static| { bindings.fs.images[0] = static.image; bindings.fs.samplers[0] = static.sampler; - bindings.vs.images[0] = static.image; - bindings.vs.samplers[0] = static.sampler; }, } if (self.current_target_texture) |target_texture| { const target_view_buffer = pools.textures.get(@intFromEnum(target_texture)).?.render.view_buffer; - bindings.fs.storage_buffers[0] = target_view_buffer; bindings.vs.storage_buffers[0] = target_view_buffer; } else { - bindings.fs.storage_buffers[0] = view_buffer; bindings.vs.storage_buffers[0] = view_buffer; } - sokol.gfx.applyPipeline(texture_batching_pipeline); + const effect = pools.effects.get(@intFromEnum(self.current_effect)).?; + + sokol.gfx.applyPipeline(effect.pipeline); + + if (effect.has_properties_buffer) |properties_buffer| { + bindings.fs.storage_buffers[0] = properties_buffer; + } while (true) { const buffer_index = self.flushed_count / batches_per_buffer; @@ -217,11 +231,29 @@ const Frame = struct { } } + pub fn set_effect(self: *Frame, pools: *Pools, command: Command.SetEffect) void { + if (command.effect != self.current_effect) { + self.flush(pools); + } + + self.current_effect = command.effect; + + if (command.properties.len != 0) { + if (pools.effects.get(@intFromEnum(self.current_effect)).?.has_properties_buffer) |properties_buffer| { + sokol.gfx.updateBuffer(properties_buffer, sokol.gfx.asRange(command.properties)); + } + } + } + pub fn set_target(self: *Frame, pools: *Pools, command: Command.SetTarget) void { sokol.gfx.endPass(); var pass = sokol.gfx.Pass{ - .action = .{.stencil = .{.load_action = .CLEAR}}, + .action = .{ + .stencil = .{ + .load_action = .CLEAR, + }, + }, }; if (command.clear_color) |color| { @@ -265,15 +297,24 @@ const Frame = struct { var texture_batch_buffers = coral.stack.Sequential(sokol.gfx.Buffer){.allocator = coral.heap.allocator}; const batches_per_buffer = 512; - - var texture_batching_pipeline: sokol.gfx.Pipeline = undefined; }; const Pools = struct { + effects: EffectPool, textures: TexturePool, + const EffectPool = coral.Pool(resources.Effect); + const TexturePool = coral.Pool(resources.Texture); + fn create_effect(self: *Pools, desc: handles.Effect.Desc) !handles.Effect { + var effect = try resources.Effect.init(desc); + + errdefer effect.deinit(); + + return @enumFromInt(try self.effects.insert(effect)); + } + fn create_texture(self: *Pools, desc: handles.Texture.Desc) !handles.Texture { var texture = try resources.Texture.init(desc); @@ -292,12 +333,28 @@ const Pools = struct { self.textures.deinit(); } - fn destroy_texture(self: *Pools, texture_key: handles.Texture) bool { - switch (texture_key) { + fn destroy_effect(self: *Pools, handle: handles.Effect) bool { + switch (handle) { .default => {}, else => { - var texture = self.textures.remove(@intFromEnum(texture_key)) orelse { + var effect = self.effects.remove(@intFromEnum(handle)) orelse { + return false; + }; + + effect.deinit(); + }, + } + + return true; + } + + fn destroy_texture(self: *Pools, handle: handles.Texture) bool { + switch (handle) { + .default => {}, + + else => { + var texture = self.textures.remove(@intFromEnum(handle)) orelse { return false; }; @@ -310,10 +367,17 @@ const Pools = struct { fn init(allocator: std.mem.Allocator) !Pools { var pools = Pools{ + .effects = EffectPool.init(allocator), .textures = TexturePool.init(allocator), }; - errdefer pools.deinit(); + errdefer { + pools.deinit(); + } + + _ = try pools.create_effect(.{ + .fragment_spirv = &spirv.to_ops(@embedFile("./shaders/2d_default.frag.spv")), + }); const default_texture_data = [_]u32{ 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, @@ -343,16 +407,23 @@ const Pools = struct { pub const Work = union (enum) { create_commands: CreateCommandsWork, + load_effect: LoadEffectWork, load_texture: LoadTextureWork, render_frame: RenderFrameWork, rotate_commands: RotateCommandsWork, shutdown, + unload_effect: UnloadEffectWork, unload_texture: UnloadTextureWork, pub const CreateCommandsWork = struct { created: *coral.asyncio.Future(std.mem.Allocator.Error!*Commands), }; + pub const LoadEffectWork = struct { + desc: handles.Effect.Desc, + loaded: *coral.asyncio.Future(std.mem.Allocator.Error!handles.Effect), + }; + pub const LoadTextureWork = struct { desc: handles.Texture.Desc, loaded: *coral.asyncio.Future(std.mem.Allocator.Error!handles.Texture), @@ -369,6 +440,10 @@ pub const Work = union (enum) { finished: *std.Thread.ResetEvent, }; + pub const UnloadEffectWork = struct { + handle: handles.Effect, + }; + pub const UnloadTextureWork = struct { handle: handles.Texture, }; @@ -395,62 +470,6 @@ const vertex_indices = .{ .instance = 1, }; -const vertex_layout_state = sokol.gfx.VertexLayoutState{ - .attrs = get: { - var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16; - - attrs[0] = .{ - .format = .FLOAT2, - .buffer_index = vertex_indices.mesh, - }; - - attrs[1] = .{ - .format = .FLOAT2, - .buffer_index = vertex_indices.mesh, - }; - - attrs[2] = .{ - .format = .FLOAT2, - .buffer_index = vertex_indices.instance, - }; - - attrs[3] = .{ - .format = .FLOAT2, - .buffer_index = vertex_indices.instance, - }; - - attrs[4] = .{ - .format = .FLOAT2, - .buffer_index = vertex_indices.instance, - }; - - attrs[5] = .{ - .format = .UBYTE4N, - .buffer_index = vertex_indices.instance, - }; - - attrs[6] = .{ - .format = .FLOAT, - .buffer_index = vertex_indices.instance, - }; - - attrs[7] = .{ - .format = .FLOAT4, - .buffer_index = vertex_indices.instance, - }; - - break: get attrs; - }, - - .buffers = get: { - var vertex_buffer_layouts = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8; - - vertex_buffer_layouts[vertex_indices.instance].step_func = .PER_INSTANCE; - - break: get vertex_buffer_layouts; - }, -}; - fn run(window: *ext.SDL_Window) !void { const context = configure_and_create: { var result = @as(c_int, 0); @@ -530,21 +549,6 @@ fn run(window: *ext.SDL_Window) !void { .usage = .IMMUTABLE, }); - const shader_spirv = @embedFile("./shaders/2d_default.spv"); - var spirv_unit = try spirv.Unit.init(coral.heap.allocator); - - defer spirv_unit.deinit(); - - try spirv_unit.compile(shader_spirv, .vertex); - try spirv_unit.compile(shader_spirv, .fragment); - - Frame.texture_batching_pipeline = sokol.gfx.makePipeline(.{ - .label = "2D drawing pipeline", - .layout = vertex_layout_state, - .shader = sokol.gfx.makeShader(spirv_unit.shader_desc), - .index_type = .UINT16, - }); - var has_commands_head: ?*Commands = null; var has_commands_tail: ?*Commands = null; @@ -575,6 +579,14 @@ fn run(window: *ext.SDL_Window) !void { } }, + .load_effect => |load| { + const effect = try pools.create_effect(load.desc); + + if (!load.loaded.resolve(effect)) { + std.debug.assert(pools.destroy_effect(effect)); + } + }, + .load_texture => |load| { const texture = try pools.create_texture(load.desc); @@ -650,6 +662,12 @@ fn run(window: *ext.SDL_Window) !void { break; }, + .unload_effect => |unload| { + if (!pools.destroy_effect(unload.handle)) { + @panic("Attempt to unload a non-existent effect"); + } + }, + .unload_texture => |unload| { if (!pools.destroy_texture(unload.handle)) { @panic("Attempt to unload a non-existent texture"); diff --git a/src/ona/gfx/resources.zig b/src/ona/gfx/resources.zig index 6fc9942..1bf9c0f 100644 --- a/src/ona/gfx/resources.zig +++ b/src/ona/gfx/resources.zig @@ -10,6 +10,140 @@ const spirv = @import("./spirv.zig"); const std = @import("std"); +pub const Effect = struct { + shader: sokol.gfx.Shader, + pipeline: sokol.gfx.Pipeline, + has_properties_buffer: ?sokol.gfx.Buffer = null, + + pub fn deinit(self: *Effect) void { + sokol.gfx.destroyPipeline(self.pipeline); + sokol.gfx.destroyShader(self.shader); + + if (self.has_properties_buffer) |parameters_buffer| { + sokol.gfx.destroyBuffer(parameters_buffer); + } + + self.* = undefined; + } + + pub fn init(desc: handles.Effect.Desc) !Effect { + var spirv_unit = try spirv.Unit.init(coral.heap.allocator); + + defer spirv_unit.deinit(); + + try spirv_unit.compile(&spirv.to_ops(@embedFile("./shaders/2d_default.vert.spv")), .vertex); + try spirv_unit.compile(desc.fragment_spirv, .fragment); + + const shader = sokol.gfx.makeShader(spirv_unit.shader_desc()); + + const pipeline = sokol.gfx.makePipeline(pipeline_desc: { + var pipeline_desc = sokol.gfx.PipelineDesc{ + .label = "Effect pipeline", + .layout = vertex_layout_state, + .shader = shader, + .index_type = .UINT16, + .blend_color = .{.r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0}, + }; + + pipeline_desc.colors[0] = .{ + .write_mask = .RGBA, + + .blend = .{ + .enabled = true, + .src_factor_rgb = .SRC_ALPHA, + .dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, + }, + }; + + break: pipeline_desc pipeline_desc; + }); + + errdefer { + sokol.gfx.destroyPipeline(pipeline); + sokol.gfx.destroyShader(shader); + } + + if (spirv_unit.shader_stage(.fragment).has_storage_buffer[0]) |storage_buffer| { + const properties_buffer = sokol.gfx.makeBuffer(.{ + .size = storage_buffer.minimum_size, + .type = .STORAGEBUFFER, + .usage = .STREAM, + }); + + errdefer { + sokol.gfx.destroyBuffer(properties_buffer); + } + + return .{ + .shader = shader, + .pipeline = pipeline, + .has_properties_buffer = properties_buffer, + }; + } else { + return .{ + .shader = shader, + .pipeline = pipeline, + }; + } + } + + const vertex_layout_state = sokol.gfx.VertexLayoutState{ + .attrs = get: { + var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16; + + attrs[0] = .{ + .format = .FLOAT2, + .buffer_index = 0, + }; + + attrs[1] = .{ + .format = .FLOAT2, + .buffer_index = 0, + }; + + attrs[2] = .{ + .format = .FLOAT2, + .buffer_index = 1, + }; + + attrs[3] = .{ + .format = .FLOAT2, + .buffer_index = 1, + }; + + attrs[4] = .{ + .format = .FLOAT2, + .buffer_index = 1, + }; + + attrs[5] = .{ + .format = .UBYTE4N, + .buffer_index = 1, + }; + + attrs[6] = .{ + .format = .FLOAT, + .buffer_index = 1, + }; + + attrs[7] = .{ + .format = .FLOAT4, + .buffer_index = 1, + }; + + break: get attrs; + }, + + .buffers = get: { + var vertex_buffer_layouts = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8; + + vertex_buffer_layouts[1].step_func = .PER_INSTANCE; + + break: get vertex_buffer_layouts; + }, + }; +}; + pub const Texture = union (enum) { render: Render, static: Static, diff --git a/src/ona/gfx/shaders/2d_default.frag b/src/ona/gfx/shaders/2d_default.frag index 0ed5a9d..40e5c20 100644 --- a/src/ona/gfx/shaders/2d_default.frag +++ b/src/ona/gfx/shaders/2d_default.frag @@ -7,10 +7,6 @@ layout (location = 1) in vec2 uv; layout (location = 0) out vec4 texel; -layout (binding = 0) readonly buffer View { - mat4 projection_matrix; -}; - void main() { texel = texture(sprite, uv) * color; diff --git a/src/ona/gfx/spirv.zig b/src/ona/gfx/spirv.zig index bda5831..222ed02 100644 --- a/src/ona/gfx/spirv.zig +++ b/src/ona/gfx/spirv.zig @@ -15,22 +15,225 @@ pub const Error = std.mem.Allocator.Error || error { UnsupportedBackend, }; -pub const Unit = struct { - arena: std.heap.ArenaAllocator, - context: ext.spvc_context, - shader_desc: sokol.gfx.ShaderDesc, - attrs_used: u32 = 0, +pub const Shader = struct { + decompiled_source: [:0]const u8 = "", + has_storage_buffer: [max_storage_buffers]?StorageBuffer = [_]?StorageBuffer{null} ** max_storage_buffers, + has_image: [max_images]?Image = [_]?Image{null} ** max_images, + has_sampler: [max_samplers]?Sampler = [_]?Sampler{null} ** max_samplers, + has_image_sampler_pair: [max_image_sampler_pairs]?ImageSamplerPair = [_]?ImageSamplerPair{null} ** max_image_sampler_pairs, - pub fn compile(self: *Unit, spirv_data: []const u8, stage: Stage) Error!void { - if ((spirv_data.len % @alignOf(u32)) != 0) { + pub const Image = struct { + is_multisampled: bool, + }; + + pub const ImageSamplerPair = struct { + image_slot: usize, + sampler_slot: usize, + name: [:0]const u8, + }; + + pub const Sampler = enum { + filtering, + non_filtering, + comparison, + }; + + pub const StorageBuffer = struct { + minimum_size: usize, + is_readonly: bool, + }; + + pub const max_images = 12; + + pub const max_image_sampler_pairs = 12; + + pub const max_samplers = 8; + + pub const max_storage_buffers = 8; + + fn reflect_image_samplers(self: *Shader, _: Stage, compiler: ext.spvc_compiler, resources: ext.spvc_resources) Error!void { + var sampled_images: []const ext.spvc_reflected_resource = &.{}; + + try to_error(ext.spvc_resources_get_resource_list_for_type( + resources, + ext.SPVC_RESOURCE_TYPE_SAMPLED_IMAGE, + @ptrCast(&sampled_images.ptr), + &sampled_images.len, + )); + + if (sampled_images.len > max_image_sampler_pairs) { + return error.UnsupportedSPIRV; + } + + for (0 .. sampled_images.len, sampled_images) |i, sampled_image| { + const sampled_image_type = ext.spvc_compiler_get_type_handle(compiler, sampled_image.type_id); + + if (ext.spvc_type_get_basetype(sampled_image_type) != ext.SPVC_BASETYPE_SAMPLED_IMAGE) { + return error.InvalidSPIRV; + } + + try switch (ext.spvc_type_get_image_dimension(sampled_image_type)) { + ext.SpvDim2D => {}, + else => error.InvalidSPIRV, + }; + + try switch (ext.spvc_type_get_basetype(ext.spvc_compiler_get_type_handle(compiler, ext.spvc_type_get_image_sampled_type(sampled_image_type)))) { + ext.SPVC_BASETYPE_FP32 => {}, + else => error.InvalidSPIRV, + }; + + self.has_image[i] = .{ + .is_multisampled = ext.spvc_type_get_image_multisampled(sampled_image_type) != 0, + }; + + self.has_sampler[i] = .filtering; + + self.has_image_sampler_pair[i] = .{ + .name = std.mem.span(ext.spvc_compiler_get_name(compiler, sampled_image.id)), + .image_slot = @intCast(i), + .sampler_slot = @intCast(i), + }; + } + } + + fn reflect_storage_buffers(self: *Shader, stage: Stage, compiler: ext.spvc_compiler, resources: ext.spvc_resources) Error!void { + var storage_buffers: []const ext.spvc_reflected_resource = &.{}; + + try to_error(ext.spvc_resources_get_resource_list_for_type( + resources, + ext.SPVC_RESOURCE_TYPE_STORAGE_BUFFER, + @ptrCast(&storage_buffers.ptr), + &storage_buffers.len, + )); + + const binding_offset: c_uint = if (stage == .fragment) max_storage_buffers else 0; + + for (storage_buffers) |storage_buffer| { + const binding = ext.spvc_compiler_get_decoration(compiler, storage_buffer.id, ext.SpvDecorationBinding); + + if (binding >= max_storage_buffers) { + return error.InvalidSPIRV; + } + + ext.spvc_compiler_set_decoration(compiler, storage_buffer.id, ext.SpvDecorationBinding, binding_offset + binding); + + var block_decorations: []const ext.SpvDecoration = &.{}; + + try to_error(ext.spvc_compiler_get_buffer_block_decorations( + compiler, + storage_buffer.id, + @ptrCast(&block_decorations.ptr), + &block_decorations.len, + )); + + var minimum_size: usize = 0; + + try to_error(ext.spvc_compiler_get_declared_struct_size(compiler, ext.spvc_compiler_get_type_handle(compiler, storage_buffer.base_type_id), &minimum_size)); + + self.has_storage_buffer[binding] = .{ + .is_readonly = std.mem.indexOfScalar(ext.SpvDecoration, block_decorations, ext.SpvDecorationNonWritable) != null, + .minimum_size = minimum_size, + }; + } + } + + fn reflect_uniform_buffers(self: *Shader, _: Stage, compiler: ext.spvc_compiler, resources: ext.spvc_resources) Error!void { + var uniform_buffers: []const ext.spvc_reflected_resource = &.{}; + + try to_error(ext.spvc_resources_get_resource_list_for_type( + resources, + ext.SPVC_RESOURCE_TYPE_UNIFORM_BUFFER, + @ptrCast(&uniform_buffers.ptr), + &uniform_buffers.len, + )); + + if (uniform_buffers.len != 0) { return error.InvalidSPIRV; } - const spirv_ops: []const u32 = @alignCast(std.mem.bytesAsSlice(u32, spirv_data)); + // TODO: Support for older APIs? + _ = self; + _ = compiler; + } - const execution_model, const stage_desc = switch (stage) { - .vertex => .{ext.SpvExecutionModelVertex, &self.shader_desc.vs}, - .fragment => .{ext.SpvExecutionModelFragment, &self.shader_desc.fs}, + pub fn stage_desc(self: Shader) sokol.gfx.ShaderStageDesc { + var stage = sokol.gfx.ShaderStageDesc{ + .entry = "main", + .source = self.decompiled_source, + }; + + for (0 .. max_storage_buffers) |slot| { + const storage_buffer = &(self.has_storage_buffer[slot] orelse { + continue; + }); + + stage.storage_buffers[slot] = .{ + .readonly = storage_buffer.is_readonly, + .used = true, + }; + } + + for (0 .. max_images) |slot| { + const image = &(self.has_image[slot] orelse { + continue; + }); + + stage.images[slot] = .{ + .multisampled = image.is_multisampled, + .image_type = ._2D, + .sample_type = .FLOAT, + .used = true, + }; + } + + for (0 .. max_samplers) |slot| { + const sampler = &(self.has_sampler[slot] orelse { + continue; + }); + + stage.samplers[slot] = .{ + .sampler_type = switch (sampler.*) { + .filtering => .FILTERING, + .non_filtering => .NONFILTERING, + .comparison => .COMPARISON, + }, + + .used = true, + }; + } + + for (0 .. max_image_sampler_pairs) |slot| { + const image_sampler_pair = &(self.has_image_sampler_pair[slot] orelse { + continue; + }); + + stage.image_sampler_pairs[slot] = .{ + .glsl_name = image_sampler_pair.name, + .image_slot = @intCast(image_sampler_pair.image_slot), + .sampler_slot = @intCast(image_sampler_pair.sampler_slot), + .used = true, + }; + } + + return stage; + } +}; + +pub const Stage = enum { + vertex, + fragment, +}; + +pub const Unit = struct { + arena: std.heap.ArenaAllocator, + context: ext.spvc_context, + attrs_used: u32 = 0, + stages: [std.enums.values(Stage).len]Shader = .{.{}, .{}}, + + pub fn compile(self: *Unit, spirv_ops: []const u32, stage: Stage) Error!void { + const execution_model, const shader = switch (stage) { + .vertex => .{ext.SpvExecutionModelVertex, &self.stages[0]}, + .fragment => .{ext.SpvExecutionModelFragment, &self.stages[1]}, }; const Backend = struct { @@ -73,8 +276,8 @@ pub const Unit = struct { for (combined_image_samplers) |combined_image_sampler| { const name = try coral.utf8.alloc_formatted(arena_allocator, "{image_name}_{sampler_name}", .{ - .image_name = std.mem.span(@as([*:0]const u8, ext.spvc_compiler_get_name(compiler, combined_image_sampler.image_id))), - .sampler_name = std.mem.span(@as([*:0]const u8, ext.spvc_compiler_get_name(compiler, combined_image_sampler.sampler_id))), + .image_name = std.mem.span(ext.spvc_compiler_get_name(compiler, combined_image_sampler.image_id)), + .sampler_name = std.mem.span(ext.spvc_compiler_get_name(compiler, combined_image_sampler.sampler_id)), }); ext.spvc_compiler_set_name(compiler, combined_image_sampler.combined_id, name); @@ -86,7 +289,7 @@ pub const Unit = struct { break: parse_and_configure compiler; }; - try to_error(ext.spvc_compiler_set_entry_point(compiler, stage_desc.entry, @intCast(execution_model))); + try to_error(ext.spvc_compiler_set_entry_point(compiler, "main", @intCast(execution_model))); const resources = create: { var resources: ext.spvc_resources = null; @@ -96,27 +299,29 @@ pub const Unit = struct { break: create resources; }; - try reflect_uniform_buffers(compiler, resources, stage_desc); - try reflect_storage_buffers(compiler, resources, stage_desc); - try reflect_image_samplers(compiler, resources, stage_desc); + try shader.reflect_uniform_buffers(stage, compiler, resources); + try shader.reflect_storage_buffers(stage, compiler, resources); + try shader.reflect_image_samplers(stage, compiler, resources); try to_error(ext.spvc_compiler_install_compiler_options(compiler, create: { - var options: ext.spvc_compiler_options = null; + var compiler_options: ext.spvc_compiler_options = null; - try to_error(ext.spvc_compiler_create_compiler_options(compiler, &options)); + try to_error(ext.spvc_compiler_create_compiler_options(compiler, &compiler_options)); for (backend.option_values) |option_value| { const entry, const value = option_value; - try to_error(ext.spvc_compiler_options_set_uint(options, entry, value)); + try to_error(ext.spvc_compiler_options_set_uint(compiler_options, entry, value)); } - break: create options; + break: create compiler_options; })); - try to_error(ext.spvc_compiler_compile(compiler, @ptrCast(&stage_desc.source))); + try to_error(ext.spvc_compiler_compile(compiler, @ptrCast(&shader.decompiled_source.ptr))); - std.log.info("{s}", .{stage_desc.source}); + shader.decompiled_source.len = std.mem.span(shader.decompiled_source.ptr).len; + + std.log.info("{s}", .{shader.decompiled_source}); } pub fn deinit(self: *Unit) void { @@ -140,18 +345,19 @@ pub const Unit = struct { return .{ .arena = std.heap.ArenaAllocator.init(allocator), .context = context, - - .shader_desc = .{ - .vs = .{.entry = "main"}, - .fs = .{.entry = "main"}, - }, }; } -}; -pub const Stage = enum { - fragment, - vertex, + pub fn shader_desc(self: Unit) sokol.gfx.ShaderDesc { + return .{ + .vs = self.shader_stage(.vertex).stage_desc(), + .fs = self.shader_stage(.fragment).stage_desc(), + }; + } + + pub fn shader_stage(self: *const Unit, stage: Stage) *const Shader { + return &self.stages[@intFromEnum(stage)]; + } }; fn log_context_errors(userdata: ?*anyopaque, error_message: [*c]const u8) callconv(.C) void { @@ -159,109 +365,6 @@ fn log_context_errors(userdata: ?*anyopaque, error_message: [*c]const u8) callco std.log.err("{s}", .{error_message}); } -fn reflect_image_samplers(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage_desc: *sokol.gfx.ShaderStageDesc) Error!void { - var sampled_images: []const ext.spvc_reflected_resource = &.{}; - - try to_error(ext.spvc_resources_get_resource_list_for_type( - resources, - ext.SPVC_RESOURCE_TYPE_SAMPLED_IMAGE, - @ptrCast(&sampled_images.ptr), - &sampled_images.len, - )); - - if (sampled_images.len > stage_desc.image_sampler_pairs.len) { - return error.UnsupportedSPIRV; - } - - for (0 .. sampled_images.len, sampled_images) |i, sampled_image| { - const sampled_image_type = ext.spvc_compiler_get_type_handle(compiler, sampled_image.type_id); - - if (ext.spvc_type_get_basetype(sampled_image_type) != ext.SPVC_BASETYPE_SAMPLED_IMAGE) { - return error.InvalidSPIRV; - } - - stage_desc.images[i] = .{ - .multisampled = ext.spvc_type_get_image_multisampled(sampled_image_type) != 0, - - .image_type = try switch (ext.spvc_type_get_image_dimension(sampled_image_type)) { - ext.SpvDim2D => sokol.gfx.ImageType._2D, - else => error.InvalidSPIRV, - }, - - .sample_type = try switch (ext.spvc_type_get_basetype(ext.spvc_compiler_get_type_handle(compiler, ext.spvc_type_get_image_sampled_type(sampled_image_type)))) { - ext.SPVC_BASETYPE_FP32 => sokol.gfx.ImageSampleType.FLOAT, - else => error.InvalidSPIRV, - }, - - .used = true, - }; - - stage_desc.samplers[i] = .{ - .sampler_type = .DEFAULT, - .used = true, - }; - - stage_desc.image_sampler_pairs[i] = .{ - .glsl_name = ext.spvc_compiler_get_name(compiler, sampled_image.id), - .image_slot = @intCast(i), - .sampler_slot = @intCast(i), - .used = true, - }; - } -} - -fn reflect_storage_buffers(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage_desc: *sokol.gfx.ShaderStageDesc) Error!void { - var storage_buffers: []const ext.spvc_reflected_resource = &.{}; - - try to_error(ext.spvc_resources_get_resource_list_for_type( - resources, - ext.SPVC_RESOURCE_TYPE_STORAGE_BUFFER, - @ptrCast(&storage_buffers.ptr), - &storage_buffers.len, - )); - - for (storage_buffers) |storage_buffer| { - const binding = ext.spvc_compiler_get_decoration(compiler, storage_buffer.id, ext.SpvDecorationBinding); - - if (binding >= stage_desc.storage_buffers.len) { - return error.InvalidSPIRV; - } - - var block_decorations: []const ext.SpvDecoration = &.{}; - - try to_error(ext.spvc_compiler_get_buffer_block_decorations( - compiler, - storage_buffer.id, - @ptrCast(&block_decorations.ptr), - &block_decorations.len, - )); - - stage_desc.storage_buffers[binding] = .{ - .readonly = std.mem.indexOfScalar(ext.SpvDecoration, block_decorations, ext.SpvDecorationNonWritable) != null, - .used = true, - }; - } -} - -fn reflect_uniform_buffers(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage_desc: *sokol.gfx.ShaderStageDesc) Error!void { - var uniform_buffers: []const ext.spvc_reflected_resource = &.{}; - - try to_error(ext.spvc_resources_get_resource_list_for_type( - resources, - ext.SPVC_RESOURCE_TYPE_UNIFORM_BUFFER, - @ptrCast(&uniform_buffers.ptr), - &uniform_buffers.len, - )); - - if (uniform_buffers.len != 0) { - return error.InvalidSPIRV; - } - - // TODO: Support for older APIs? - _ = stage_desc; - _ = compiler; -} - fn to_error(result: ext.spvc_result) Error!void { return switch (result) { ext.SPVC_SUCCESS => {}, @@ -272,3 +375,11 @@ fn to_error(result: ext.spvc_result) Error!void { else => unreachable, }; } + +pub fn to_ops(raw: anytype) [raw.len / @alignOf(u32)]u32 { + var ops: [raw.len / @alignOf(u32)]u32 = undefined; + + @memcpy(std.mem.sliceAsBytes(&ops), raw); + + return ops; +}