Compare commits

...

2 Commits

Author SHA1 Message Date
56342d9d8e Use uniform blocks instead of storage buffers for common data
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-21 00:45:24 +01:00
f3efb60c05 Update dependencies in readme 2024-07-20 15:23:47 +01:00
10 changed files with 560 additions and 466 deletions

View File

@ -7,7 +7,7 @@ layout (location = 1) in vec2 uv;
layout (location = 0) out vec4 texel; layout (location = 0) out vec4 texel;
layout (binding = 0) readonly buffer Effect { layout (binding = 0) uniform Effect {
float effect_magnitude; float effect_magnitude;
}; };

View File

@ -33,7 +33,7 @@ Ona is also the Catalan word for "wave".
Ona currently depends the following third-party tools to build it: Ona currently depends the following third-party tools to build it:
* Platform support for SDL2 at version 2.0.20 or above. * Platform support for SDL2 at version 2.0.20 or above.
* SPIR-V shader compilation system utilities, namely `glslangValidator` and `spirv-link`. * SPIR-V shader compilation system utilities, namely `glslangValidator`.
* Zig compiler toolchain. * Zig compiler toolchain.
As the project evolves, dependencies on libraries external to the project codebase will be minimized or removed outright to meet the goals of the project as closely as possible. As the project evolves, dependencies on libraries external to the project codebase will be minimized or removed outright to meet the goals of the project as closely as possible.

View File

@ -5,7 +5,8 @@ const std = @import("std");
const ona = @import("ona"); const ona = @import("ona");
const ChromaticAberration = extern struct { const ChromaticAberration = extern struct {
magnitude: f32, effect_magnitude: f32,
padding: [12]u8 = undefined,
}; };
const Actors = struct { const Actors = struct {
@ -53,7 +54,7 @@ fn exit(actors: ona.Write(Actors)) void {
fn render(commands: ona.gfx.Commands, actors: ona.Write(Actors)) !void { fn render(commands: ona.gfx.Commands, actors: ona.Write(Actors)) !void {
try commands.set_effect(actors.res.ca_effect, ChromaticAberration{ try commands.set_effect(actors.res.ca_effect, ChromaticAberration{
.magnitude = 15.0, .effect_magnitude = 15.0,
}); });
for (actors.res.instances.values) |instance| { for (actors.res.instances.values) |instance| {

View File

@ -73,20 +73,22 @@ pub const Assets = struct {
} }
const fragment_file_stat = try storage.stat(path); const fragment_file_stat = try storage.stat(path);
const fragment_spirv = try coral.heap.allocator.alloc(u32, fragment_file_stat.size / @alignOf(u32)); const fragment_spirv_ops = try coral.heap.allocator.alloc(u32, fragment_file_stat.size / @alignOf(u32));
defer { defer {
coral.heap.allocator.free(fragment_spirv); coral.heap.allocator.free(fragment_spirv_ops);
} }
_ = try storage.read_all(path, std.mem.sliceAsBytes(fragment_spirv), .{}); const bytes_read = try storage.read_all(path, std.mem.sliceAsBytes(fragment_spirv_ops), .{});
std.debug.assert(bytes_read.len == fragment_file_stat.size);
var loaded = coral.asyncio.Future(std.mem.Allocator.Error!Effect){}; var loaded = coral.asyncio.Future(std.mem.Allocator.Error!Effect){};
rendering.enqueue_work(.{ rendering.enqueue_work(.{
.load_effect = .{ .load_effect = .{
.desc = .{ .desc = .{
.fragment_spirv = fragment_spirv, .fragment_spirv_ops = fragment_spirv_ops,
}, },
.loaded = &loaded, .loaded = &loaded,
@ -127,20 +129,6 @@ pub const Assets = struct {
return error.FormatUnsupported; 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; pub const thread_restriction = .main;
}; };
@ -213,13 +201,6 @@ pub const Input = union (enum) {
pub const Effect = handles.Effect; pub const Effect = handles.Effect;
pub const Rect = struct {
left: f32,
top: f32,
right: f32,
bottom: f32,
};
pub const Texture = handles.Texture; pub const Texture = handles.Texture;
pub fn poll(app: flow.Write(App), inputs: msg.Send(Input)) !void { pub fn poll(app: flow.Write(App), inputs: msg.Send(Input)) !void {

View File

@ -3,7 +3,7 @@ const coral = @import("coral");
const std = @import("std"); const std = @import("std");
pub const Effect = Handle(struct { pub const Effect = Handle(struct {
fragment_spirv: []const u32, fragment_spirv_ops: []const u32,
}); });
fn Handle(comptime HandleDesc: type) type { fn Handle(comptime HandleDesc: type) type {

View File

@ -1,5 +1,3 @@
const gfx = @import("../gfx.zig");
pub const Color = @Vector(4, f32); pub const Color = @Vector(4, f32);
pub fn EvenOrderMatrix(comptime n: usize, comptime Element: type) type { pub fn EvenOrderMatrix(comptime n: usize, comptime Element: type) type {
@ -16,7 +14,14 @@ pub const Transform2D = extern struct {
pub const ProjectionMatrix = EvenOrderMatrix(4, f32); pub const ProjectionMatrix = EvenOrderMatrix(4, f32);
pub fn orthographic_projection(near: f32, far: f32, viewport: gfx.Rect) EvenOrderMatrix(4, f32) { pub const Rect = struct {
left: f32,
top: f32,
right: f32,
bottom: f32,
};
pub fn orthographic_projection(near: f32, far: f32, viewport: Rect) EvenOrderMatrix(4, f32) {
const width = viewport.right - viewport.left; const width = viewport.right - viewport.left;
const height = viewport.bottom - viewport.top; const height = viewport.bottom - viewport.top;

View File

@ -185,7 +185,7 @@ const Frame = struct {
bindings.vertex_buffers[vertex_indices.mesh] = quad_vertex_buffer; bindings.vertex_buffers[vertex_indices.mesh] = quad_vertex_buffer;
switch (pools.textures.get(@intFromEnum(self.current_source_texture)).?.*) { switch (pools.textures.get(@intFromEnum(self.current_source_texture)).?.access) {
.render => |render| { .render => |render| {
bindings.fs.images[0] = render.color_image; bindings.fs.images[0] = render.color_image;
bindings.fs.samplers[0] = render.sampler; bindings.fs.samplers[0] = render.sampler;
@ -197,22 +197,30 @@ const Frame = struct {
}, },
} }
if (self.current_target_texture) |target_texture| {
const target_view_buffer = pools.textures.get(@intFromEnum(target_texture)).?.render.view_buffer;
bindings.vs.storage_buffers[0] = target_view_buffer;
} else {
bindings.vs.storage_buffers[0] = view_buffer;
}
const effect = pools.effects.get(@intFromEnum(self.current_effect)).?; const effect = pools.effects.get(@intFromEnum(self.current_effect)).?;
sokol.gfx.applyPipeline(effect.pipeline); sokol.gfx.applyPipeline(effect.pipeline);
if (effect.has_properties_buffer) |properties_buffer| { if (self.current_target_texture) |target_texture| {
bindings.fs.storage_buffers[0] = properties_buffer; const texture = pools.textures.get(@intFromEnum(target_texture)).?;
sokol.gfx.applyUniforms(.VS, 0, sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{
.left = 0,
.top = 0,
.right = @floatFromInt(texture.width),
.bottom = @floatFromInt(texture.height),
})));
} else {
sokol.gfx.applyUniforms(.VS, 0, sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{
.left = 0,
.top = 0,
.right = @floatFromInt(self.swapchain.width),
.bottom = @floatFromInt(self.swapchain.height),
})));
} }
sokol.gfx.applyUniforms(.FS, 0, sokol.gfx.asRange(effect.properties));
while (true) { while (true) {
const buffer_index = self.flushed_count / batches_per_buffer; const buffer_index = self.flushed_count / batches_per_buffer;
const buffer_offset = self.flushed_count % batches_per_buffer; const buffer_offset = self.flushed_count % batches_per_buffer;
@ -238,10 +246,8 @@ const Frame = struct {
self.current_effect = command.effect; self.current_effect = command.effect;
if (command.properties.len != 0) { if (pools.effects.get(@intFromEnum(self.current_effect))) |effect| {
if (pools.effects.get(@intFromEnum(self.current_effect)).?.has_properties_buffer) |properties_buffer| { @memcpy(effect.properties, command.properties);
sokol.gfx.updateBuffer(properties_buffer, sokol.gfx.asRange(command.properties));
}
} }
} }
@ -275,7 +281,7 @@ const Frame = struct {
} }
if (command.texture) |texture| { if (command.texture) |texture| {
pass.attachments = switch (pools.textures.get(@intFromEnum(texture)).?.*) { pass.attachments = switch (pools.textures.get(@intFromEnum(texture)).?.access) {
.static => @panic("Cannot render to static textures"), .static => @panic("Cannot render to static textures"),
.render => |render| render.attachments, .render => |render| render.attachments,
}; };
@ -376,7 +382,7 @@ const Pools = struct {
} }
_ = try pools.create_effect(.{ _ = try pools.create_effect(.{
.fragment_spirv = &spirv.to_ops(@embedFile("./shaders/2d_default.frag.spv")), .fragment_spirv_ops = &spirv.to_ops(@embedFile("./shaders/2d_default.frag.spv")),
}); });
const default_texture_data = [_]u32{ const default_texture_data = [_]u32{
@ -463,8 +469,6 @@ var quad_index_buffer: sokol.gfx.Buffer = undefined;
var quad_vertex_buffer: sokol.gfx.Buffer = undefined; var quad_vertex_buffer: sokol.gfx.Buffer = undefined;
var view_buffer: sokol.gfx.Buffer = undefined;
const vertex_indices = .{ const vertex_indices = .{
.mesh = 0, .mesh = 0,
.instance = 1, .instance = 1,
@ -537,18 +541,6 @@ fn run(window: *ext.SDL_Window) !void {
.type = .VERTEXBUFFER, .type = .VERTEXBUFFER,
}); });
view_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{
.left = 0,
.top = 0,
.right = @floatFromInt(1280),
.bottom = @floatFromInt(720),
})),
.type = .STORAGEBUFFER,
.usage = .IMMUTABLE,
});
var has_commands_head: ?*Commands = null; var has_commands_head: ?*Commands = null;
var has_commands_tail: ?*Commands = null; var has_commands_tail: ?*Commands = null;

View File

@ -13,29 +13,50 @@ const std = @import("std");
pub const Effect = struct { pub const Effect = struct {
shader: sokol.gfx.Shader, shader: sokol.gfx.Shader,
pipeline: sokol.gfx.Pipeline, pipeline: sokol.gfx.Pipeline,
has_properties_buffer: ?sokol.gfx.Buffer = null, properties: []coral.io.Byte,
pub fn deinit(self: *Effect) void { pub fn deinit(self: *Effect) void {
coral.heap.allocator.free(self.properties);
sokol.gfx.destroyPipeline(self.pipeline); sokol.gfx.destroyPipeline(self.pipeline);
sokol.gfx.destroyShader(self.shader); sokol.gfx.destroyShader(self.shader);
if (self.has_properties_buffer) |parameters_buffer| {
sokol.gfx.destroyBuffer(parameters_buffer);
}
self.* = undefined; self.* = undefined;
} }
pub fn init(desc: handles.Effect.Desc) !Effect { pub fn init(desc: handles.Effect.Desc) spirv.Error!Effect {
var spirv_unit = try spirv.Unit.init(coral.heap.allocator); var spirv_arena = std.heap.ArenaAllocator.init(coral.heap.allocator);
defer spirv_unit.deinit(); defer {
spirv_arena.deinit();
}
try spirv_unit.compile(&spirv.to_ops(@embedFile("./shaders/2d_default.vert.spv")), .vertex); const spirv_program = try spirv.analyze(&spirv_arena, .{
try spirv_unit.compile(desc.fragment_spirv, .fragment); .target = try switch (sokol.gfx.queryBackend()) {
.GLCORE => spirv.Target.glsl,
else => error.InvalidSPIRV,
},
const shader = sokol.gfx.makeShader(spirv_unit.shader_desc()); .vertex_source = .{
.ops = &spirv.to_ops(@embedFile("./shaders/2d_default.vert.spv")),
},
.fragment_source = .{
.ops = desc.fragment_spirv_ops,
},
});
const shader = sokol.gfx.makeShader(shader_desc: {
const shader_desc = sokol.gfx.ShaderDesc{
.vs = stage_desc(spirv_program.vertex_stage),
.fs = stage_desc(spirv_program.fragment_stage),
};
// TODO: Vertex attributes, for some reason they aren't needed?
break: shader_desc shader_desc;
});
// TODO: Review blending rules.
const pipeline = sokol.gfx.makePipeline(pipeline_desc: { const pipeline = sokol.gfx.makePipeline(pipeline_desc: {
var pipeline_desc = sokol.gfx.PipelineDesc{ var pipeline_desc = sokol.gfx.PipelineDesc{
.label = "Effect pipeline", .label = "Effect pipeline",
@ -58,33 +79,106 @@ pub const Effect = struct {
break: pipeline_desc pipeline_desc; break: pipeline_desc pipeline_desc;
}); });
const properties = try coral.heap.allocator.alloc(
coral.io.Byte,
if (spirv_program.fragment_stage.has_uniform_blocks[0]) |uniform_block| uniform_block.size() else 0,
);
errdefer { errdefer {
coral.heap.allocator.free(properties);
sokol.gfx.destroyPipeline(pipeline); sokol.gfx.destroyPipeline(pipeline);
sokol.gfx.destroyShader(shader); sokol.gfx.destroyShader(shader);
} }
if (spirv_unit.shader_stage(.fragment).has_storage_buffer[0]) |storage_buffer| { return .{
const properties_buffer = sokol.gfx.makeBuffer(.{ .shader = shader,
.size = storage_buffer.minimum_size, .pipeline = pipeline,
.type = .STORAGEBUFFER, .properties = properties,
.usage = .STREAM, };
}
fn stage_desc(spirv_stage: spirv.Stage) sokol.gfx.ShaderStageDesc {
var stage = sokol.gfx.ShaderStageDesc{
.entry = spirv_stage.entry_point,
.source = spirv_stage.source,
};
for (0 .. spirv.Stage.max_uniform_blocks) |slot| {
const uniform_block = &(spirv_stage.has_uniform_blocks[slot] orelse {
continue;
}); });
errdefer { const stage_uniform_block = &stage.uniform_blocks[slot];
sokol.gfx.destroyBuffer(properties_buffer);
}
return .{ stage_uniform_block.layout = switch (uniform_block.layout) {
.shader = shader, .std140 => .STD140,
.pipeline = pipeline,
.has_properties_buffer = properties_buffer,
}; };
} else {
return .{ stage_uniform_block.size = uniform_block.size();
.shader = shader,
.pipeline = pipeline, for (stage_uniform_block.uniforms[0 .. uniform_block.uniforms.len], uniform_block.uniforms) |*stage_uniform, uniform| {
stage_uniform.* = .{
.type = switch (uniform.type) {
.float => .FLOAT,
.float2 => .FLOAT2,
.float3 => .FLOAT3,
.float4 => .FLOAT4,
.int => .INT,
.int2 => .INT2,
.int3 => .INT3,
.int4 => .INT4,
.mat4 => .MAT4,
},
.name = uniform.name,
.array_count = uniform.len,
};
}
}
for (0 .. spirv.Stage.max_images) |slot| {
const image = &(spirv_stage.has_images[slot] orelse {
continue;
});
stage.images[slot] = .{
.multisampled = image.is_multisampled,
.image_type = ._2D,
.sample_type = .FLOAT,
.used = true,
}; };
} }
for (0 .. spirv.Stage.max_samplers) |slot| {
const sampler = &(spirv_stage.has_samplers[slot] orelse {
continue;
});
stage.samplers[slot] = .{
.sampler_type = switch (sampler.*) {
.filtering => .FILTERING,
.non_filtering => .NONFILTERING,
.comparison => .COMPARISON,
},
.used = true,
};
}
for (0 .. spirv.Stage.max_image_sampler_pairs) |slot| {
const image_sampler_pair = &(spirv_stage.has_image_sampler_pairs[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;
} }
const vertex_layout_state = sokol.gfx.VertexLayoutState{ const vertex_layout_state = sokol.gfx.VertexLayoutState{
@ -144,140 +238,135 @@ pub const Effect = struct {
}; };
}; };
pub const Texture = union (enum) { pub const Texture = struct {
render: Render, width: u16,
static: Static, height: u16,
access: Access,
const Render = struct { pub const Access = union (enum) {
render: RenderAccess,
static: StaticAccess,
};
pub const RenderAccess = struct {
sampler: sokol.gfx.Sampler, sampler: sokol.gfx.Sampler,
color_image: sokol.gfx.Image, color_image: sokol.gfx.Image,
depth_image: sokol.gfx.Image, depth_image: sokol.gfx.Image,
attachments: sokol.gfx.Attachments, attachments: sokol.gfx.Attachments,
view_buffer: sokol.gfx.Buffer,
fn deinit(self: *Render) void {
sokol.gfx.destroyImage(self.color_image);
sokol.gfx.destroyImage(self.depth_image);
sokol.gfx.destroySampler(self.sampler);
sokol.gfx.destroyAttachments(self.attachments);
sokol.gfx.destroyBuffer(self.view_buffer);
self.* = undefined;
}
fn init(format: handles.Texture.Desc.Format, access: handles.Texture.Desc.Access.Render) std.mem.Allocator.Error!Render {
const color_image = sokol.gfx.makeImage(.{
.pixel_format = switch (format) {
.rgba8 => sokol.gfx.PixelFormat.RGBA8,
.bgra8 => sokol.gfx.PixelFormat.BGRA8,
},
.width = access.width,
.height = access.height,
.render_target = true,
});
const depth_image = sokol.gfx.makeImage(.{
.width = access.width,
.height = access.height,
.render_target = true,
.pixel_format = .DEPTH_STENCIL,
});
const attachments = sokol.gfx.makeAttachments(attachments_desc: {
var desc = sokol.gfx.AttachmentsDesc{
.depth_stencil = .{
.image = depth_image,
},
};
desc.colors[0] = .{
.image = color_image,
};
break: attachments_desc desc;
});
const sampler = sokol.gfx.makeSampler(.{});
const view_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(&lina.orthographic_projection(-1.0, 1.0, .{
.left = 0,
.top = 0,
.right = @floatFromInt(access.width),
.bottom = @floatFromInt(access.height),
})),
.type = .STORAGEBUFFER,
.usage = .IMMUTABLE,
});
return .{
.attachments = attachments,
.sampler = sampler,
.color_image = color_image,
.depth_image = depth_image,
.view_buffer = view_buffer,
};
}
}; };
const Static = struct { pub const StaticAccess = struct {
image: sokol.gfx.Image,
sampler: sokol.gfx.Sampler, sampler: sokol.gfx.Sampler,
image: sokol.gfx.Image,
fn deinit(self: *Static) void {
sokol.gfx.destroyImage(self.image);
sokol.gfx.destroySampler(self.sampler);
self.* = undefined;
}
fn init(format: handles.Texture.Desc.Format, access: handles.Texture.Desc.Access.Static) std.mem.Allocator.Error!Static {
const image = sokol.gfx.makeImage(image_desc: {
var desc = sokol.gfx.ImageDesc{
.height = std.math.cast(i32, access.data.len / (access.width * format.byte_size())) orelse {
return error.OutOfMemory;
},
.pixel_format = switch (format) {
.rgba8 => sokol.gfx.PixelFormat.RGBA8,
.bgra8 => sokol.gfx.PixelFormat.BGRA8,
},
.width = access.width,
};
desc.data.subimage[0][0] = sokol.gfx.asRange(access.data);
break: image_desc desc;
});
const sampler = sokol.gfx.makeSampler(.{});
errdefer {
sokol.gfx.destroySampler(sampler);
sokol.gfx.destroyImage(image);
}
return .{
.image = image,
.sampler = sampler,
};
}
}; };
pub fn deinit(self: *Texture) void { pub fn deinit(self: *Texture) void {
switch (self.*) { switch (self.access) {
.static => |*static| static.deinit(), .render => |render| {
.render => |*render| render.deinit(), sokol.gfx.destroyImage(render.color_image);
sokol.gfx.destroyImage(render.depth_image);
sokol.gfx.destroySampler(render.sampler);
sokol.gfx.destroyAttachments(render.attachments);
},
.static => |static| {
sokol.gfx.destroyImage(static.image);
sokol.gfx.destroySampler(static.sampler);
},
} }
self.* = undefined;
} }
pub fn init(desc: handles.Texture.Desc) std.mem.Allocator.Error!Texture { pub fn init(desc: handles.Texture.Desc) std.mem.Allocator.Error!Texture {
return switch (desc.access) { const pixel_format = switch (desc.format) {
.static => |static| .{.static = try Static.init(desc.format, static)}, .rgba8 => sokol.gfx.PixelFormat.RGBA8,
.render => |render| .{.render = try Render.init(desc.format, render)}, .bgra8 => sokol.gfx.PixelFormat.BGRA8,
}; };
switch (desc.access) {
.render => |render| {
const color_image = sokol.gfx.makeImage(.{
.pixel_format = pixel_format,
.width = render.width,
.height = render.height,
.render_target = true,
});
const depth_image = sokol.gfx.makeImage(.{
.width = render.width,
.height = render.height,
.render_target = true,
.pixel_format = .DEPTH_STENCIL,
});
const attachments = sokol.gfx.makeAttachments(attachments_desc: {
var attachments_desc = sokol.gfx.AttachmentsDesc{
.depth_stencil = .{
.image = depth_image,
},
};
attachments_desc.colors[0] = .{
.image = color_image,
};
break: attachments_desc attachments_desc;
});
const sampler = sokol.gfx.makeSampler(.{});
return .{
.width = render.width,
.height = render.height,
.access = .{
.render = .{
.attachments = attachments,
.sampler = sampler,
.color_image = color_image,
.depth_image = depth_image,
},
}
};
},
.static => |static| {
const height = std.math.cast(u16, static.data.len / (static.width * desc.format.byte_size())) orelse {
return error.OutOfMemory;
};
const image = sokol.gfx.makeImage(image_desc: {
var image_desc = sokol.gfx.ImageDesc{
.height = height,
.pixel_format = pixel_format,
.width = static.width,
};
image_desc.data.subimage[0][0] = sokol.gfx.asRange(static.data);
break: image_desc image_desc;
});
const sampler = sokol.gfx.makeSampler(.{});
errdefer {
sokol.gfx.destroySampler(sampler);
sokol.gfx.destroyImage(image);
}
return .{
.width = static.width,
.height = height,
.access = .{
.static = .{
.image = image,
.sampler = sampler,
},
},
};
},
}
} }
}; };

View File

@ -13,7 +13,7 @@ layout (location = 7) in vec4 instance_rect;
layout (location = 0) out vec4 color; layout (location = 0) out vec4 color;
layout (location = 1) out vec2 uv; layout (location = 1) out vec2 uv;
layout (binding = 0) readonly buffer View { layout (binding = 0) uniform View {
mat4 projection_matrix; mat4 projection_matrix;
}; };

View File

@ -9,18 +9,33 @@ const sokol = @import("sokol");
const std = @import("std"); const std = @import("std");
pub const Error = std.mem.Allocator.Error || error { pub const Error = std.mem.Allocator.Error || error {
UnsupportedTarget,
InvalidSPIRV, InvalidSPIRV,
UnsupportedSPIRV, UnsupportedSPIRV,
UnsupportedBackend,
}; };
pub const Shader = struct { pub const Options = struct {
decompiled_source: [:0]const u8 = "", target: Target,
has_storage_buffer: [max_storage_buffers]?StorageBuffer = [_]?StorageBuffer{null} ** max_storage_buffers, vertex_source: Source,
has_image: [max_images]?Image = [_]?Image{null} ** max_images, fragment_source: Source,
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 const Program = struct {
vertex_stage: Stage,
fragment_stage: Stage,
};
pub const Source = struct {
entry_point: []const u8 = "main",
ops: []const u32,
};
pub const Stage = struct {
entry_point: [:0]const u8,
source: [:0]const u8,
has_image_sampler_pairs: [max_image_sampler_pairs]?ImageSamplerPair = [_]?ImageSamplerPair{null} ** max_image_sampler_pairs,
has_images: [max_images]?Image = [_]?Image{null} ** max_images,
has_samplers: [max_samplers]?Sampler = [_]?Sampler{null} ** max_samplers,
has_uniform_blocks: [max_uniform_blocks]?UniformBlock = [_]?UniformBlock{null} ** max_uniform_blocks,
pub const Image = struct { pub const Image = struct {
is_multisampled: bool, is_multisampled: bool,
@ -32,15 +47,65 @@ pub const Shader = struct {
name: [:0]const u8, name: [:0]const u8,
}; };
pub const Layout = enum {
std140,
};
pub const Sampler = enum { pub const Sampler = enum {
filtering, filtering,
non_filtering, non_filtering,
comparison, comparison,
}; };
pub const StorageBuffer = struct { pub const Uniform = struct {
minimum_size: usize, name: [:0]const u8,
is_readonly: bool, type: Type,
len: u16,
pub const Type = enum {
float,
float2,
float3,
float4,
int,
int2,
int3,
int4,
mat4,
};
};
pub const UniformBlock = struct {
layout: Layout,
uniforms: []const Uniform,
pub const max_uniforms = 16;
pub fn size(self: UniformBlock) usize {
const alignment: usize = switch (self.layout) {
.std140 => 16,
};
var accumulated_size: usize = 0;
for (self.uniforms) |uniform| {
const type_size = @max(1, uniform.len) * @as(usize, switch (uniform.type) {
.float => 4,
.float2 => 8,
.float3 => 12,
.float4 => 16,
.int => 4,
.int2 => 8,
.int3 => 12,
.int4 => 16,
.mat4 => 64,
});
accumulated_size += (type_size + (alignment - 1)) & ~(alignment - 1);
}
return accumulated_size;
}
}; };
pub const max_images = 12; pub const max_images = 12;
@ -51,7 +116,10 @@ pub const Shader = struct {
pub const max_storage_buffers = 8; pub const max_storage_buffers = 8;
fn reflect_image_samplers(self: *Shader, _: Stage, compiler: ext.spvc_compiler, resources: ext.spvc_resources) Error!void { pub const max_uniform_blocks = 8;
fn reflect_images_samplers(self: *Stage, arena: *std.heap.ArenaAllocator, compiler: ext.spvc_compiler, resources: ext.spvc_resources) Error!void {
const arena_allocator = arena.allocator();
var sampled_images: []const ext.spvc_reflected_resource = &.{}; var sampled_images: []const ext.spvc_reflected_resource = &.{};
try to_error(ext.spvc_resources_get_resource_list_for_type( try to_error(ext.spvc_resources_get_resource_list_for_type(
@ -82,289 +150,247 @@ pub const Shader = struct {
else => error.InvalidSPIRV, else => error.InvalidSPIRV,
}; };
self.has_image[i] = .{ self.has_images[i] = .{
.is_multisampled = ext.spvc_type_get_image_multisampled(sampled_image_type) != 0, .is_multisampled = ext.spvc_type_get_image_multisampled(sampled_image_type) != 0,
}; };
self.has_sampler[i] = .filtering; self.has_samplers[i] = .filtering;
self.has_image_sampler_pair[i] = .{ self.has_image_sampler_pairs[i] = .{
.name = std.mem.span(ext.spvc_compiler_get_name(compiler, sampled_image.id)), .name = try arena_allocator.dupeZ(u8, std.mem.span(ext.spvc_compiler_get_name(compiler, sampled_image.id))),
.image_slot = @intCast(i), .image_slot = @intCast(i),
.sampler_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 { fn reflect_uniform_blocks(self: *Stage, arena: *std.heap.ArenaAllocator, compiler: ext.spvc_compiler, resources: ext.spvc_resources) Error!void {
var storage_buffers: []const ext.spvc_reflected_resource = &.{}; const arena_allocator = arena.allocator();
var reflected_resources: []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( try to_error(ext.spvc_resources_get_resource_list_for_type(
resources, resources,
ext.SPVC_RESOURCE_TYPE_UNIFORM_BUFFER, ext.SPVC_RESOURCE_TYPE_UNIFORM_BUFFER,
@ptrCast(&uniform_buffers.ptr), @ptrCast(&reflected_resources.ptr),
&uniform_buffers.len, &reflected_resources.len,
)); ));
if (uniform_buffers.len != 0) { if (reflected_resources.len > max_uniform_blocks) {
return error.InvalidSPIRV; return error.InvalidSPIRV;
} }
// TODO: Support for older APIs? for (0 .. reflected_resources.len, reflected_resources) |i, reflected_resource| {
_ = self; const type_handle = ext.spvc_compiler_get_type_handle(compiler, reflected_resource.base_type_id);
_ = compiler; const is_std430 = ext.spvc_compiler_has_decoration(compiler, reflected_resource.id, ext.SpvDecorationBufferBlock) == ext.SPVC_TRUE;
}
pub fn stage_desc(self: Shader) sokol.gfx.ShaderStageDesc { if (is_std430) {
var stage = sokol.gfx.ShaderStageDesc{ return error.UnsupportedSPIRV;
.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 {
target: ext.spvc_backend,
option_values: []const struct {ext.spvc_compiler_option, c_uint},
};
const backend: Backend = try switch (sokol.gfx.queryBackend()) {
.GLCORE => .{
.target = ext.SPVC_BACKEND_GLSL,
.option_values = &.{
.{ext.SPVC_COMPILER_OPTION_GLSL_VERSION, 430},
.{ext.SPVC_COMPILER_OPTION_GLSL_ES, @intFromBool(false)},
.{ext.SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, @intFromBool(false)},
.{ext.SPVC_COMPILER_OPTION_GLSL_EMIT_UNIFORM_BUFFER_AS_PLAIN_UNIFORMS, @intFromBool(true)},
.{ext.SPVC_COMPILER_OPTION_FLIP_VERTEX_Y, @intFromBool(true)},
},
},
else => error.UnsupportedBackend,
};
const compiler = parse_and_configure: {
var parsed_ir: ext.spvc_parsed_ir = null;
try to_error(ext.spvc_context_parse_spirv(self.context, spirv_ops.ptr, spirv_ops.len, &parsed_ir));
var compiler: ext.spvc_compiler = null;
try to_error(ext.spvc_context_create_compiler(self.context, backend.target, parsed_ir, ext.SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &compiler));
try to_error(ext.spvc_compiler_build_combined_image_samplers(compiler));
var combined_image_samplers: []const ext.spvc_combined_image_sampler = &.{};
try to_error(ext.spvc_compiler_get_combined_image_samplers(compiler, @ptrCast(&combined_image_samplers.ptr), &combined_image_samplers.len));
const arena_allocator = self.arena.allocator();
var binding: u32 = 0;
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(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);
ext.spvc_compiler_set_decoration(compiler, combined_image_sampler.combined_id, ext.SpvDecorationBinding, binding);
binding += 1;
} }
break: parse_and_configure compiler; const uniform_count = ext.spvc_type_get_num_member_types(type_handle);
};
try to_error(ext.spvc_compiler_set_entry_point(compiler, "main", @intCast(execution_model))); if (uniform_count > UniformBlock.max_uniforms) {
return error.UnsupportedSPIRV;
const resources = create: {
var resources: ext.spvc_resources = null;
try to_error(ext.spvc_compiler_create_shader_resources(compiler, &resources));
break: create resources;
};
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 compiler_options: ext.spvc_compiler_options = null;
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(compiler_options, entry, value));
} }
break: create compiler_options; const uniforms = try arena_allocator.alloc(Uniform, uniform_count);
}));
try to_error(ext.spvc_compiler_compile(compiler, @ptrCast(&shader.decompiled_source.ptr))); for (uniforms, 0 .. uniform_count) |*uniform, j| {
const member_index: c_uint = @intCast(j);
const member_type_handle = ext.spvc_compiler_get_type_handle(compiler, ext.spvc_type_get_member_type(type_handle, member_index));
shader.decompiled_source.len = std.mem.span(shader.decompiled_source.ptr).len; if (ext.spvc_type_get_num_array_dimensions(member_type_handle) > 1) {
return error.UnsupportedSPIRV;
}
std.log.info("{s}", .{shader.decompiled_source}); uniform.* = .{
} .name = try coral.utf8.alloc_formatted(arena_allocator, "_{id}.{member_name}", .{
.id = reflected_resource.id,
.member_name = std.mem.span(ext.spvc_compiler_get_member_name(compiler, reflected_resource.base_type_id, member_index)),
}),
pub fn deinit(self: *Unit) void { .type = try switch (ext.spvc_type_get_basetype(member_type_handle)) {
ext.spvc_context_destroy(self.context); ext.SPVC_BASETYPE_FP32 => switch (ext.spvc_type_get_vector_size(member_type_handle)) {
self.arena.deinit(); 4 => switch (ext.spvc_type_get_columns(member_type_handle)) {
4 => Uniform.Type.mat4,
1 => Uniform.Type.float4,
else => error.UnsupportedSPIRV,
},
self.* = undefined; 1 => Uniform.Type.float,
} 2 => Uniform.Type.float2,
3 => Uniform.Type.float3,
else => error.UnsupportedSPIRV,
},
pub fn init(allocator: std.mem.Allocator) std.mem.Allocator.Error!Unit { ext.SPVC_BASETYPE_INT32 => try switch (ext.spvc_type_get_vector_size(member_type_handle)) {
var context: ext.spvc_context = null; 1 => Uniform.Type.int,
2 => Uniform.Type.int2,
3 => Uniform.Type.int3,
4 => Uniform.Type.int4,
else => error.UnsupportedSPIRV,
},
if (ext.spvc_context_create(&context) != ext.SPVC_SUCCESS) { else => error.UnsupportedSPIRV,
return error.OutOfMemory; },
.len = std.math.cast(u16, ext.spvc_type_get_array_dimension(member_type_handle, 0)) orelse {
return error.UnsupportedSPIRV;
},
};
}
self.has_uniform_blocks[i] = .{
.uniforms = uniforms,
.layout = .std140,
};
} }
errdefer ext.spvc_context_destroy(context);
ext.spvc_context_set_error_callback(context, log_context_errors, null);
return .{
.arena = std.heap.ArenaAllocator.init(allocator),
.context = context,
};
}
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 { pub const Target = enum {
glsl,
};
pub fn analyze(arena: *std.heap.ArenaAllocator, options: Options) Error!Program {
var context: ext.spvc_context = null;
if (ext.spvc_context_create(&context) != ext.SPVC_SUCCESS) {
return error.OutOfMemory;
}
defer {
ext.spvc_context_destroy(context);
}
ext.spvc_context_set_error_callback(context, log_errors, null);
const arena_allocator = arena.allocator();
return .{
.vertex_stage = vertex_stage: {
const compiler = try parse(arena, context, options.target, options.vertex_source);
var stage = Stage{
.entry_point = try arena_allocator.dupeZ(u8, options.vertex_source.entry_point),
.source = try compile(arena, compiler),
};
const resources = create: {
var resources: ext.spvc_resources = null;
try to_error(ext.spvc_compiler_create_shader_resources(compiler, &resources));
break: create resources;
};
try stage.reflect_images_samplers(arena, compiler, resources);
try stage.reflect_uniform_blocks(arena, compiler, resources);
std.log.info("{s}", .{stage.source});
break: vertex_stage stage;
},
.fragment_stage = fragment_stage: {
const compiler = try parse(arena, context, options.target, options.fragment_source);
var stage = Stage{
.entry_point = try arena_allocator.dupeZ(u8, options.fragment_source.entry_point),
.source = try compile(arena, compiler),
};
const resources = create: {
var resources: ext.spvc_resources = null;
try to_error(ext.spvc_compiler_create_shader_resources(compiler, &resources));
break: create resources;
};
try stage.reflect_images_samplers(arena, compiler, resources);
try stage.reflect_uniform_blocks(arena, compiler, resources);
std.log.info("{s}", .{stage.source});
break: fragment_stage stage;
},
};
}
fn compile(arena: *std.heap.ArenaAllocator, compiler: ext.spvc_compiler) Error![:0]const u8 {
const arena_allocator = arena.allocator();
var source: [*:0]const u8 = "";
try to_error(ext.spvc_compiler_compile(compiler, @ptrCast(&source)));
return arena_allocator.dupeZ(u8, std.mem.span(source));
}
fn log_errors(userdata: ?*anyopaque, error_message: [*c]const u8) callconv(.C) void {
std.debug.assert(userdata == null); std.debug.assert(userdata == null);
std.log.err("{s}", .{error_message}); std.log.err("{s}", .{error_message});
} }
fn parse(arena: *std.heap.ArenaAllocator, context: ext.spvc_context, target: Target, source: Source) Error!ext.spvc_compiler {
var parsed_ir: ext.spvc_parsed_ir = null;
try to_error(ext.spvc_context_parse_spirv(context, source.ops.ptr, source.ops.len, &parsed_ir));
var compiler: ext.spvc_compiler = null;
const spvc_target = switch (target) {
.glsl => ext.SPVC_BACKEND_GLSL,
};
try to_error(ext.spvc_context_create_compiler(context, spvc_target, parsed_ir, ext.SPVC_CAPTURE_MODE_TAKE_OWNERSHIP, &compiler));
try to_error(ext.spvc_compiler_build_combined_image_samplers(compiler));
var combined_image_samplers: []const ext.spvc_combined_image_sampler = &.{};
try to_error(ext.spvc_compiler_get_combined_image_samplers(compiler, @ptrCast(&combined_image_samplers.ptr), &combined_image_samplers.len));
const arena_allocator = arena.allocator();
var binding: u32 = 0;
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(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);
ext.spvc_compiler_set_decoration(compiler, combined_image_sampler.combined_id, ext.SpvDecorationBinding, binding);
binding += 1;
}
const option_values: []const struct {ext.spvc_compiler_option, c_uint} = switch (target) {
.glsl => &.{
.{ext.SPVC_COMPILER_OPTION_GLSL_VERSION, 430},
.{ext.SPVC_COMPILER_OPTION_GLSL_ES, @intFromBool(false)},
.{ext.SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, @intFromBool(false)},
.{ext.SPVC_COMPILER_OPTION_GLSL_EMIT_UNIFORM_BUFFER_AS_PLAIN_UNIFORMS, @intFromBool(true)},
.{ext.SPVC_COMPILER_OPTION_FLIP_VERTEX_Y, @intFromBool(true)},
},
};
var compiler_options: ext.spvc_compiler_options = null;
try to_error(ext.spvc_compiler_create_compiler_options(compiler, &compiler_options));
for (option_values) |option_value| {
const entry, const value = option_value;
try to_error(ext.spvc_compiler_options_set_uint(compiler_options, entry, value));
}
try to_error(ext.spvc_compiler_install_compiler_options(compiler, compiler_options));
return compiler;
}
fn to_error(result: ext.spvc_result) Error!void { fn to_error(result: ext.spvc_result) Error!void {
return switch (result) { return switch (result) {
ext.SPVC_SUCCESS => {}, ext.SPVC_SUCCESS => {},