diff --git a/readme.md b/readme.md index ef57137..d31c918 100644 --- a/readme.md +++ b/readme.md @@ -4,12 +4,14 @@ ## Table of Contents -1. [Overview](#overview) -1. [Goals](#goals) -1. [Technical Details](#technical-details) - 1. [Requirements](#requirements) - 1. [Building](#building) - 1. [Packaging](#packaging) +- [Ona](#ona) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Goals](#goals) + - [Technical Details](#technical-details) + - [Requirements](#requirements) + - [Building](#building) + - [Packaging](#packaging) ## Overview @@ -84,5 +86,5 @@ app.root_module.addImport("coral", ona_dependency.module("coral")); b.installArtifact(app); ``` - 5. Create a `main.zig` containing a valid Ona app declaration. - 6. Run `zig build` to build your new game application. + 1. Create a `main.zig` containing a valid Ona app declaration. + 2. Run `zig build` to build your new game application. diff --git a/src/coral/bytes.zig b/src/coral/bytes.zig index 187dad8..4e49575 100644 --- a/src/coral/bytes.zig +++ b/src/coral/bytes.zig @@ -101,6 +101,18 @@ pub fn allocFormatted(allocator: std.mem.Allocator, comptime format: []const u8, return @constCast(printFormatted(buffer, format, args) catch unreachable); } +pub fn altFormat(value: anytype, comptime format: fn (@TypeOf(value), Writable) ReadWriteError!void) struct { + formattable: @TypeOf(value), + + pub fn writeFormat(self: @This(), output: Writable) ReadWriteError!void { + return format(self.formattable, output); + } +} { + return .{ + .formattable = value, + }; +} + pub fn countFormatted(comptime format: []const u8, args: anytype) usize { var count = WriteCount{}; diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index 4d7cf35..c19a23f 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -10,14 +10,9 @@ const ona = @import("./ona.zig"); const std = @import("std"); -pub const Display = struct { - width: u16, - height: u16, - is_hidden: bool, -}; - const Context = struct { window: *ext.SDL_Window, + frame_arena: std.heap.ArenaAllocator, gpu_device: *ext.SDL_GPUDevice, shader_compiler: ext.shaderc_compiler_t, spirv_options: ext.shaderc_compile_options_t, @@ -29,8 +24,11 @@ const Context = struct { fragment, }; - pub fn assemble(self: *Context, allocator: std.mem.Allocator, kind: AssemblyKind, name: [*:0]const u8, source: []const u8) AssembleError![]u8 { - const result = ext.shaderc_compile_into_spv(self.shader_compiler, source.ptr, source.len, switch (kind) { + pub fn assembleShader(self: *Context, kind: AssemblyKind, name: [*:0]const u8, source: []const u8, injections: anytype) AssembleError![]const u8 { + const frame_allocator = self.frame_arena.allocator(); + const injected_source = try glsl.inject(frame_allocator, source, 430, injections); + + const result = ext.shaderc_compile_into_spv(self.shader_compiler, injected_source.ptr, injected_source.len, switch (kind) { .fragment => ext.shaderc_glsl_vertex_shader, .vertex => ext.shaderc_glsl_vertex_shader, }, name, "main", self.spirv_options) orelse { @@ -46,12 +44,12 @@ const Context = struct { const compiled_len = ext.shaderc_result_get_length(result); const compiled_ptr = ext.shaderc_result_get_bytes(result); - return allocator.dupe(u8, compiled_ptr[0..compiled_len]); + return frame_allocator.dupe(u8, compiled_ptr[0..compiled_len]); }, ext.shaderc_compilation_status_compilation_error, ext.shaderc_compilation_status_invalid_stage => { std.log.err("{s}", .{ext.shaderc_result_get_error_message(result)}); - std.log.debug("problematic shader:\n{s}", .{source}); + std.log.debug("problematic shader:\n{s}", .{injected_source}); return error.BadSyntax; }, @@ -61,11 +59,16 @@ const Context = struct { }; } + pub fn assembleShaderEmbedded(self: *Context, kind: AssemblyKind, comptime path: [:0]const u8, injections: anytype) AssembleError![]const u8 { + return self.assembleShader(kind, path, @embedFile(path), injections); + } + pub fn deinit(self: *Context) void { ext.shaderc_compile_options_release(self.spirv_options); ext.shaderc_compiler_release(self.shader_compiler); ext.SDL_DestroyGPUDevice(self.gpu_device); ext.SDL_DestroyWindow(self.window); + self.frame_arena.deinit(); self.* = undefined; } @@ -75,67 +78,44 @@ const Context = struct { }; }; +pub const Display = struct { + width: u16, + height: u16, + is_hidden: bool, +}; + +pub const Effect = struct { + pipeline: *ext.SDL_GPUGraphicsPipeline, +}; + fn compile_shaders(context: ona.Write(Context)) !void { const Camera = extern struct { projection: [4]@Vector(4, f32), }; - var arena = std.heap.ArenaAllocator.init(coral.heap.allocator); + const vertex_spirv = try context.ptr.assembleShaderEmbedded(.vertex, "./gfx/canvas.vert", .{ + .model_xy = glsl.nativeInput(0, @Vector(2, f32)), + .model_uv = glsl.nativeInput(1, @Vector(2, f32)), + .model_rgba = glsl.nativeInput(2, @Vector(4, f32)), + .instance_uv_offset = glsl.nativeInput(3, @Vector(2, f32)), + .instance_uv_scale = glsl.nativeInput(4, @Vector(2, f32)), + .instance_xbasis = glsl.nativeInput(5, @Vector(2, f32)), + .instance_ybasis = glsl.nativeInput(6, @Vector(2, f32)), + .instance_origin = glsl.nativeInput(7, @Vector(2, f32)), + .instance_pivot = glsl.nativeInput(8, @Vector(2, f32)), + .instance_rgb = glsl.nativeInput(9, @Vector(3, f32)), + .instance_bits = glsl.nativeInput(10, u32), + .uv = glsl.nativeOutput(0, @Vector(2, f32)), + .rgba = glsl.nativeOutput(1, @Vector(4, f32)), + .camera = glsl.nativeUniformBlock(0, Camera), + }); - defer { - arena.deinit(); - } - - const arena_allocator = arena.allocator(); - - const vertex_spirv = try context.ptr.assemble(arena_allocator, .vertex, "./gfx/effect.vert", try glsl.inject(arena_allocator, @embedFile("./gfx/effect.vert"), .{ - .inputs = &.{ - .{ "model_xy", .vec2 }, - .{ "model_uv", .vec2 }, - .{ "model_rgba", .vec4 }, - - .{ "instance_rect", .vec4 }, - .{ "instance_xbasis", .vec2 }, - .{ "instance_ybasis", .vec2 }, - .{ "instance_origin", .vec2 }, - .{ "instance_color", .vec4 }, - .{ "instance_depth", .float }, - }, - - .outputs = &.{ - .{ "color", .vec4 }, - .{ "uv", .vec2 }, - }, - - .uniforms = &.{ - .{ "camera", .init(0, Camera) }, - }, - })); - - const fragment_spirv = try context.ptr.assemble(arena_allocator, .fragment, "./gfx/effect.frag", try glsl.inject(arena_allocator, @embedFile("./gfx/effect.frag"), .{ - .inputs = &.{ - .{ "vertex_color", .vec4 }, - .{ "vertex_uv", .vec2 }, - }, - - .samplers = &.{ - .{ "source_texture", .{ .sampler_2d = 0 } }, - }, - - .outputs = &.{ - .{ "color", .vec4 }, - }, - - .uniforms = &.{ - .{ "camera", .init(0, Camera) }, - }, - })); - - const effect_fragment = ext.SDL_CreateGPUShader(context.ptr.gpu_device, &.{ - .code = fragment_spirv.ptr, - .code_size = fragment_spirv.len, - .format = ext.SDL_GPU_SHADERFORMAT_SPIRV, - .entrypoint = "main", + const fragment_spirv = try context.ptr.assembleShaderEmbedded(.fragment, "./gfx/canvas.frag", .{ + .vertex_uv = glsl.nativeInput(0, @Vector(2, f32)), + .vertex_rgba = glsl.nativeInput(1, @Vector(4, f32)), + .source_texture = glsl.Sampler{ .sampler_2d = 0 }, + .color = glsl.nativeOutput(0, @Vector(4, f32)), + .camera = glsl.nativeUniformBlock(0, Camera), }); const effect_vertex = ext.SDL_CreateGPUShader(context.ptr.gpu_device, &.{ @@ -145,8 +125,20 @@ fn compile_shaders(context: ona.Write(Context)) !void { .entrypoint = "main", }); - _ = effect_fragment; - _ = effect_vertex; + defer { + ext.SDL_ReleaseGPUShader(context.ptr.gpu_device, effect_vertex); + } + + const effect_fragment = ext.SDL_CreateGPUShader(context.ptr.gpu_device, &.{ + .code = fragment_spirv.ptr, + .code_size = fragment_spirv.len, + .format = ext.SDL_GPU_SHADERFORMAT_SPIRV, + .entrypoint = "main", + }); + + defer { + ext.SDL_ReleaseGPUShader(context.ptr.gpu_device, effect_fragment); + } } pub fn poll(exit: ona.Send(ona.App.Exit), hid_events: ona.Send(ona.hid.Event)) !void { @@ -297,6 +289,7 @@ pub fn setup(app: *ona.App) !void { }); try app.setState(Context{ + .frame_arena = .init(coral.heap.allocator), .shader_compiler = shader_compiler, .spirv_options = spirv_options, .window = window, @@ -324,4 +317,8 @@ pub fn synchronize(context: ona.Write(Context), display: ona.Read(Display)) !voi std.log.warn("failed to change window visibility", .{}); } } + + if (!context.ptr.frame_arena.reset(.{ .retain_with_limit = 1024 * 1024 })) { + std.log.warn("failed to shrink frame arena during reset, freeing instead...", .{}); + } } diff --git a/src/ona/gfx/canvas.frag b/src/ona/gfx/canvas.frag new file mode 100644 index 0000000..cc833a3 --- /dev/null +++ b/src/ona/gfx/canvas.frag @@ -0,0 +1,4 @@ + +void main() { + color = vertex_rgba * texture(source_texture, vertex_uv); +} diff --git a/src/ona/gfx/canvas.vert b/src/ona/gfx/canvas.vert new file mode 100644 index 0000000..a59d21b --- /dev/null +++ b/src/ona/gfx/canvas.vert @@ -0,0 +1,12 @@ + +void main() { + const vec2 local_xy = model_xy + instance_pivot; + const vec2 world_position = instance_origin + (local_xy.x * instance_xbasis) + (local_xy.y * instance_ybasis); + const vec2 projected = (camera.projection * vec4(world_position, 0, 1)).xy; + const vec2 depth_alpha = unpackHalf2x16(instance_bits); + + gl_Position = vec4(projected, depth_alpha.x, 1.0); + + rgba = model_rgba * vec4(instance_rgb, depth_alpha.y); + uv = instance_uv_offset + (model_uv * instance_uv_scale); +} diff --git a/src/ona/gfx/effect.frag b/src/ona/gfx/effect.frag deleted file mode 100644 index 5448564..0000000 --- a/src/ona/gfx/effect.frag +++ /dev/null @@ -1,4 +0,0 @@ - -void main() { - color = vertex_color * texture(source_texture, vertex_uv); -} diff --git a/src/ona/gfx/effect.vert b/src/ona/gfx/effect.vert deleted file mode 100644 index 23f04f3..0000000 --- a/src/ona/gfx/effect.vert +++ /dev/null @@ -1,11 +0,0 @@ - -void main() { - const vec2 world_position = instance_origin + model_xy.x * instance_xbasis + model_xy.y * instance_ybasis; - const vec2 projected_position = (camera.projection * vec4(world_position, 0, 1)).xy; - const vec2 rect_size = instance_rect.zw - instance_rect.xy; - const vec4 screen_position = vec4(projected_position, instance_depth, 1.0); - - gl_Position = screen_position; - color = model_rgba * instance_color; - uv = instance_rect.xy + (model_uv * rect_size); -} diff --git a/src/ona/gfx/glsl.zig b/src/ona/gfx/glsl.zig index 8be9ddb..0422b20 100644 --- a/src/ona/gfx/glsl.zig +++ b/src/ona/gfx/glsl.zig @@ -4,85 +4,64 @@ const ona = @import("../ona.zig"); const std = @import("std"); -pub const InjectOptions = struct { - version: u64 = 430, - inputs: []const Entry(Primitive) = &.{}, - outputs: []const Entry(Primitive) = &.{}, - uniforms: []const Entry(Uniform) = &.{}, - samplers: []const Entry(Sampler) = &.{}, +pub const Field = struct { + name: [:0]const u8, + primitive: Primitive, + has_next: ?*const Field, - pub fn Entry(comptime Item: type) type { - return struct { [:0]const u8, Item }; - } - - pub fn writeFormat(self: InjectOptions, source: coral.bytes.Writable) coral.bytes.ReadWriteError!void { - try coral.bytes.writeFormatted(source, "#version {version}\n\n", .{ .version = coral.utf8.cDec(self.version) }); - - for (0..self.inputs.len, self.inputs) |location, input| { - const name, const primitive = input; - - try coral.bytes.writeFormatted(source, "layout (location = {location}) in {primitive} {name};\n", .{ - .location = coral.utf8.cDec(location), - .primitive = @tagName(primitive), - .name = name, - }); + fn of(comptime uniform_fields: []const std.builtin.Type.StructField) *const Field { + if (uniform_fields.len == 0) { + @compileError("Struct contains no fields"); } - try coral.bytes.writeAll(source, "\n"); + const first_uniform_field = uniform_fields[0]; - for (0..self.outputs.len, self.outputs) |location, output| { - const name, const primitive = output; + const field = struct { + const instance = Field{ + .primitive = nativePrimitive(first_uniform_field.type), + .name = first_uniform_field.name, - try coral.bytes.writeFormatted(source, "layout (location = {location}) out {primitive} {name};\n", .{ - .location = coral.utf8.cDec(location), - .primitive = @tagName(primitive), - .name = name, - }); - } - - try coral.bytes.writeAll(source, "\n"); - - for (self.samplers) |named_sampler| { - const name, const sampler = named_sampler; - - switch (sampler) { - .sampler_2d => |binding| { - try coral.bytes.writeFormatted(source, "layout (binding = {binding}) uniform sampler2D {name};\n\n", .{ - .binding = coral.utf8.cDec(binding), - .name = name, - }); + .has_next = switch (uniform_fields.len) { + 1 => null, + else => .of(uniform_fields[1..]), }, - } - } + }; + }; - for (self.uniforms) |named_uniform| { - const name, const uniform = named_uniform; + return &field.instance; + } +}; - try coral.bytes.writeFormatted(source, "layout (binding = {binding}) uniform {type_name} {{\n", .{ - .binding = coral.utf8.cDec(uniform.binding), - .type_name = uniform.name, - }); +pub const Input = struct { + location: usize, + primitive: Primitive, - var field = uniform.field; + pub fn serialize(self: Input, name: []const u8, output: coral.bytes.Writable) coral.bytes.ReadWriteError!void { + return coral.bytes.writeFormatted(output, "layout (location = {location}) in {primitive} {name};\n", .{ + .primitive = @tagName(self.primitive), + .location = coral.utf8.cDec(self.location), + .name = name, + }); + } +}; - while (true) : (field = field.has_next orelse { - break; - }) { - try coral.bytes.writeFormatted(source, "\t{primitive} {name};\n", .{ - .primitive = @tagName(field.primitive), - .name = field.name, - }); - } +pub const Output = struct { + location: usize, + primitive: Primitive, - try coral.bytes.writeFormatted(source, "}} {name};\n\n", .{ .name = name }); - } - - try coral.bytes.writeAll(source, "#line 1\n\n"); + pub fn serialize(self: Output, name: []const u8, output: coral.bytes.Writable) coral.bytes.ReadWriteError!void { + return coral.bytes.writeFormatted(output, "layout (location = {location}) out {primitive} {name};\n", .{ + .primitive = @tagName(self.primitive), + .location = coral.utf8.cDec(self.location), + .name = name, + }); } }; pub const Primitive = enum { float, + uint, + int, vec2, vec3, vec4, @@ -91,72 +70,128 @@ pub const Primitive = enum { pub const Sampler = union(enum) { sampler_2d: usize, -}; -pub const Uniform = struct { - name: [:0]const u8, - binding: usize, - field: *const Field, - - pub const Field = struct { - name: [:0]const u8, - primitive: Primitive, - has_next: ?*const Field, - - fn of(comptime uniform_fields: []const std.builtin.Type.StructField) *const Field { - if (uniform_fields.len == 0) { - @compileError("Struct contains no fields"); - } - - const field = struct { - const instance = Field{ - .name = uniform_fields[0].name, - - .primitive = switch (uniform_fields[0].type) { - @Vector(4, f32) => .vec4, - [4]@Vector(4, f32) => .mat4, - else => @compileError("Unsupported uniform type"), - }, - - .has_next = switch (uniform_fields.len) { - 1 => null, - else => .of(uniform_fields[1..]), - }, - }; - }; - - return &field.instance; - } - }; - - pub fn init(binding: usize, comptime Struct: type) Uniform { - // TODO: Review how GLSL identifier-safe names are generated. - const struct_name = @typeName(Struct); - - const struct_type = switch (@typeInfo(Struct)) { - .@"struct" => |@"struct"| @"struct", - else => @compileError("`Struct` must be a struct type"), - }; - - if (struct_type.is_tuple) { - @compileError("`Struct` must be a non-tuple struct"); - } - - if (struct_type.layout != .@"extern") { - @compileError("`Struct` must use an extern layout"); - } - - return .{ - .name = if (std.mem.lastIndexOfScalar(u8, struct_name, '.')) |index| struct_name[index + 1 ..] else struct_name, - .field = .of(struct_type.fields), - .binding = binding, + pub fn serialize(self: Sampler, name: []const u8, output: coral.bytes.Writable) coral.bytes.ReadWriteError!void { + return switch (self) { + .sampler_2d => |binding| coral.bytes.writeFormatted(output, "layout (binding = {binding}) uniform sampler2D {name};\n", .{ + .binding = coral.utf8.cDec(binding), + .name = name, + }), }; } }; -pub fn inject(allocator: std.mem.Allocator, source_base: []const u8, options: InjectOptions) std.mem.Allocator.Error![:0]u8 { - return coral.bytes.allocFormatted(allocator, "{injected_source}\n{original_source}\n", .{ - .injected_source = options, +pub const UniformBlock = struct { + name: [:0]const u8, + binding: usize, + field: *const Field, + + pub fn serialize(self: UniformBlock, name: []const u8, output: coral.bytes.Writable) coral.bytes.ReadWriteError!void { + var field = self.field; + + try coral.bytes.writeFormatted(output, "layout (binding = {binding}) uniform {name} {{\n", .{ + .binding = coral.utf8.cDec(self.binding), + .name = self.name, + }); + + while (true) : (field = field.has_next orelse { + break; + }) { + try coral.bytes.writeFormatted(output, "\t{primitive} {name};\n", .{ + .primitive = @tagName(field.primitive), + .name = field.name, + }); + } + + try coral.bytes.writeFormatted(output, "}} {namespace};\n", .{ + .namespace = name, + }); + } +}; + +pub fn nativeInput(location: usize, comptime Value: type) Input { + return .{ + .primitive = nativePrimitive(Value), + .location = location, + }; +} + +pub fn nativePrimitive(comptime Value: type) Primitive { + return switch (Value) { + f32 => .float, + i32 => .int, + u32 => .uint, + @Vector(2, f32) => .vec2, + @Vector(3, f32) => .vec3, + @Vector(4, f32) => .vec4, + [4]@Vector(4, f32) => .mat4, + else => @compileError(std.fmt.comptimePrint("{s} is not a GLSL-compatible type", .{@typeName(Value)})), + }; +} + +pub fn nativeOutput(location: usize, comptime Value: type) Output { + return .{ + .primitive = nativePrimitive(Value), + .location = location, + }; +} + +pub fn nativeUniformBlock(binding: usize, comptime Struct: type) UniformBlock { + // TODO: Review how GLSL identifier-safe names are generated. + const struct_name = @typeName(Struct); + + const struct_type = switch (@typeInfo(Struct)) { + .@"struct" => |@"struct"| @"struct", + else => @compileError("`Struct` must be a struct type"), + }; + + if (struct_type.is_tuple) { + @compileError("`Struct` must be a non-tuple struct"); + } + + if (struct_type.layout != .@"extern") { + @compileError("`Struct` must use an extern layout"); + } + + return .{ + .name = if (std.mem.lastIndexOfScalar(u8, struct_name, '.')) |index| struct_name[index + 1 ..] else struct_name, + .field = .of(struct_type.fields), + .binding = binding, + }; +} + +pub fn inject(allocator: std.mem.Allocator, source_base: []const u8, version: usize, interface: anytype) std.mem.Allocator.Error![:0]u8 { + const Interface = @TypeOf(interface); + + const interface_info = switch (@typeInfo(Interface)) { + .@"struct" => |@"struct"| @"struct", + else => @compileError("`interface` must be a struct"), + }; + + if (interface_info.is_tuple) { + @compileError("`interface` cannot be a tuple"); + } + + const serialization = struct { + fn write(value: Interface, output: coral.bytes.Writable) coral.bytes.ReadWriteError!void { + inline for (interface_info.fields) |field| { + const field_value = @field(value, field.name); + const FieldValue = @TypeOf(field_value); + + if (!@hasDecl(FieldValue, "serialize")) { + @compileError(std.fmt.comptimePrint("`interface.{s}` is not a GLSL-serializable field", .{field.name})); + } + + try field_value.serialize(field.name, output); + } + + try coral.bytes.writeAll(output, "#line 1\n"); + } + }; + + return coral.bytes.allocFormatted(allocator, "#version {version}\n{injected_source}\n{original_source}\n", .{ + .version = coral.utf8.cDec(version), + .injected_source = coral.bytes.altFormat(interface, serialization.write), .original_source = source_base, }); } diff --git a/testing.spv b/testing.spv deleted file mode 100644 index 62956f1..0000000 --- a/testing.spv +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:755f1dfdd33dd69b0d33e1f6ddac926a5df0b52327ecad47539695bae6ade536 -size 2576