Tidy up GLSL code injection interface

This commit is contained in:
kayomn 2025-07-31 20:37:27 +01:00
parent 8dacc9b080
commit e1d41ded4a
9 changed files with 261 additions and 217 deletions

View File

@ -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.

View File

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

View File

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

4
src/ona/gfx/canvas.frag Normal file
View File

@ -0,0 +1,4 @@
void main() {
color = vertex_rgba * texture(source_texture, vertex_uv);
}

12
src/ona/gfx/canvas.vert Normal file
View File

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

View File

@ -1,4 +0,0 @@
void main() {
color = vertex_color * texture(source_texture, vertex_uv);
}

View File

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

View File

@ -4,100 +4,6 @@ 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 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,
});
}
try coral.bytes.writeAll(source, "\n");
for (0..self.outputs.len, self.outputs) |location, output| {
const name, const primitive = output;
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,
});
},
}
}
for (self.uniforms) |named_uniform| {
const name, const uniform = named_uniform;
try coral.bytes.writeFormatted(source, "layout (binding = {binding}) uniform {type_name} {{\n", .{
.binding = coral.utf8.cDec(uniform.binding),
.type_name = uniform.name,
});
var field = uniform.field;
while (true) : (field = field.has_next orelse {
break;
}) {
try coral.bytes.writeFormatted(source, "\t{primitive} {name};\n", .{
.primitive = @tagName(field.primitive),
.name = field.name,
});
}
try coral.bytes.writeFormatted(source, "}} {name};\n\n", .{ .name = name });
}
try coral.bytes.writeAll(source, "#line 1\n\n");
}
};
pub const Primitive = enum {
float,
vec2,
vec3,
vec4,
mat4,
};
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,
@ -108,15 +14,12 @@ pub const Uniform = struct {
@compileError("Struct contains no fields");
}
const first_uniform_field = uniform_fields[0];
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"),
},
.primitive = nativePrimitive(first_uniform_field.type),
.name = first_uniform_field.name,
.has_next = switch (uniform_fields.len) {
1 => null,
@ -129,7 +32,111 @@ pub const Uniform = struct {
}
};
pub fn init(binding: usize, comptime Struct: type) Uniform {
pub const Input = struct {
location: usize,
primitive: Primitive,
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,
});
}
};
pub const Output = struct {
location: usize,
primitive: Primitive,
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,
mat4,
};
pub const Sampler = union(enum) {
sampler_2d: usize,
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 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);
@ -152,11 +159,39 @@ pub const Uniform = struct {
.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"),
};
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,
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,
});
}

BIN
testing.spv (Stored with Git LFS)

Binary file not shown.