Simplify 2D rendering interfaces
continuous-integration/drone/push Build is passing Details

This commit is contained in:
kayomn 2024-07-17 23:32:10 +01:00
parent 9c54ed4683
commit 99c3818477
26 changed files with 1452 additions and 1367 deletions

28
debug/ca.frag Normal file
View File

@ -0,0 +1,28 @@
#version 430
layout (binding = 0) uniform sampler2D sprite;
layout (location = 0) in vec4 color;
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 {
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);
texel = vec4(texture(sprite, red_channel_uv).r, original_texel.g, texture(sprite, blue_channel_uv).b, original_texel.a) * color;
if (texel.a == 0) {
discard;
}
}

BIN
debug/test.bmp (Stored with Git LFS)

Binary file not shown.

View File

@ -7,7 +7,7 @@ const scalars = @import("./scalars.zig");
const std = @import("std");
pub const DecimalFormat = struct {
delimiter: []const coral.Byte,
delimiter: []const coral.io.Byte,
positive_prefix: enum {none, plus, space},
pub const default = DecimalFormat{
@ -115,9 +115,9 @@ pub const DecimalFormat = struct {
}
}
pub fn format(self: DecimalFormat, writer: io.Writer, value: anytype) io.PrintError!void {
pub fn format(self: DecimalFormat, writer: io.Writer, value: anytype) io.Error!void {
if (value == 0) {
return io.print(writer, switch (self.positive_prefix) {
return io.write_all(writer, switch (self.positive_prefix) {
.none => "0",
.plus => "+0",
.space => " 0",
@ -151,21 +151,21 @@ pub const DecimalFormat = struct {
}
}
return io.print(writer, buffer[buffer_start ..]);
return io.write_all(writer, buffer[buffer_start ..]);
},
.Float => |float| {
if (value < 0) {
try io.print(writer, "-");
try io.write_all(writer, "-");
}
const Float = @TypeOf(value);
const Int = std.meta.Int(.unsigned, float.bits);
const integer = @as(Int, @intFromFloat(value));
try self.print(writer, integer);
try io.print(writer, ".");
try self.print(writer, @as(Int, @intFromFloat((value - @as(Float, @floatFromInt(integer))) * 100)));
try self.format(writer, integer);
try io.write_all(writer, ".");
try self.format(writer, @as(Int, @intFromFloat((value - @as(Float, @floatFromInt(integer))) * 100)));
},
else => @compileError("`" ++ @typeName(Value) ++ "` cannot be formatted to a decimal string"),

76
src/coral/asyncio.zig Normal file
View File

@ -0,0 +1,76 @@
const std = @import("std");
pub fn BlockingQueue(comptime max: usize, comptime T: type) type {
return struct {
buffer: [max]T = undefined,
head: usize = 0,
tail: usize = 0,
mutex: std.Thread.Mutex = .{},
not_empty: std.Thread.Condition = .{},
not_full: std.Thread.Condition = .{},
const Self = @This();
pub fn enqueue(self: *Self, item: T) void {
self.mutex.lock();
defer self.mutex.unlock();
while (((self.tail + 1) % max) == self.head) {
self.not_full.wait(&self.mutex);
}
self.buffer[self.tail] = item;
self.tail = (self.tail + 1) % max;
self.not_empty.signal();
}
pub fn dequeue(self: *Self) T {
self.mutex.lock();
defer self.mutex.unlock();
while (self.head == self.tail) {
self.not_empty.wait(&self.mutex);
}
const item = self.buffer[self.head];
self.buffer[self.head] = undefined;
self.head = (self.head + 1) % max;
self.not_full.signal();
return item;
}
};
}
pub fn Future(comptime T: type) type {
return struct {
value: ?T = null,
resolved: std.Thread.ResetEvent = .{},
const Self = @This();
pub fn get(self: *Self) *T {
// TODO: Make async.
self.resolved.wait();
return &self.value.?;
}
pub fn resolve(self: *Self, value: T) bool {
if (self.resolved.isSet()) {
return false;
}
self.value = value;
self.resolved.set();
return true;
}
};
}

View File

@ -1,5 +1,7 @@
pub const ascii = @import("./ascii.zig");
pub const asyncio = @import("./asyncio.zig");
pub const files = @import("./files.zig");
pub const hashes = @import("./hashes.zig");
@ -16,6 +18,108 @@ pub const slices = @import("./slices.zig");
pub const stack = @import("./stack.zig");
const std = @import("std");
pub const utf8 = @import("./utf8.zig");
pub const vectors = @import("./vectors.zig");
pub fn Pool(comptime Value: type) type {
return struct {
entries: stack.Sequential(Entry),
first_free_index: usize = 0,
const Entry = union (enum) {
free_index: usize,
occupied: Value,
};
pub const Values = struct {
cursor: usize = 0,
pool: *const Self,
pub fn next(self: *Values) ?*Value {
while (self.cursor < self.pool.entries.len()) {
defer self.cursor += 1;
switch (self.pool.entries.values[self.cursor]) {
.free_index => {
continue;
},
.occupied => |*occupied| {
return occupied;
},
}
}
return null;
}
};
const Self = @This();
pub fn deinit(self: *Self) void {
self.entries.deinit();
self.* = undefined;
}
pub fn get(self: *Self, key: usize) ?*Value {
return switch (self.entries.values[key]) {
.free_index => null,
.occupied => |*occupied| occupied,
};
}
pub fn init(allocator: std.mem.Allocator) Self {
return .{
.entries = .{.allocator = allocator},
};
}
pub fn insert(self: *Self, value: Value) std.mem.Allocator.Error!usize {
const entries_count = self.entries.len();
if (self.first_free_index == entries_count) {
try self.entries.push_grow(.{.occupied = value});
self.first_free_index += 1;
return entries_count;
}
const insersion_index = self.first_free_index;
self.first_free_index = self.entries.values[self.first_free_index].free_index;
self.entries.values[insersion_index] = .{.occupied = value};
return insersion_index;
}
pub fn remove(self: *Self, key: usize) ?Value {
if (key >= self.entries.len()) {
return null;
}
switch (self.entries.values[key]) {
.free_index => {
return null;
},
.occupied => |occupied| {
self.entries.values[key] = .{.free_index = self.first_free_index};
self.first_free_index = key;
return occupied;
},
}
}
pub fn values(self: *const Self) Values {
return .{
.pool = self,
};
}
};
}

View File

@ -4,11 +4,20 @@ const io = @import("./io.zig");
const std = @import("std");
pub const Error = error {
pub const AccessError = error {
FileNotFound,
FileInaccessible,
};
pub const ReadAllError = AccessError || error {
ReadIncomplete,
};
pub const ReadAllOptions = struct {
offset: u64 = 0,
limit: u64 = std.math.maxInt(u64),
};
pub const Stat = struct {
size: u64,
};
@ -18,15 +27,37 @@ pub const Storage = struct {
vtable: *const VTable,
pub const VTable = struct {
stat: *const fn (*anyopaque, []const u8) Error!Stat,
read: *const fn (*anyopaque, []const u8, usize, []io.Byte) Error!usize,
stat: *const fn (*anyopaque, []const u8) AccessError!Stat,
read: *const fn (*anyopaque, []const u8, usize, []io.Byte) AccessError!usize,
};
pub fn read_bytes(self: Storage, path: []const u8, offset: usize, output: []io.Byte) Error!usize {
pub fn read(self: Storage, path: []const u8, output: []io.Byte, offset: u64) AccessError!usize {
return self.vtable.read(self.userdata, path, offset, output);
}
pub fn read_foreign(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type {
pub fn read_all(self: Storage, path: []const u8, output: []io.Byte, options: ReadAllOptions) ReadAllError![]const io.Byte {
const bytes_read = try self.vtable.read(self.userdata, path, options.offset, output);
if (try self.vtable.read(self.userdata, path, options.offset, output) != output.len) {
return error.ReadIncomplete;
}
return output[0 .. bytes_read];
}
pub fn read_alloc(self: Storage, path: []const u8, allocator: std.mem.Allocator, options: ReadAllOptions) (std.mem.Allocator.Error || ReadAllError)![]io.Byte {
const buffer = try allocator.alloc(io.Byte, @min((try self.stat(path)).size, options.limit));
errdefer allocator.free(buffer);
if (try self.vtable.read(self.userdata, path, options.offset, buffer) != buffer.len) {
return error.ReadIncomplete;
}
return buffer;
}
pub fn read_foreign(self: Storage, path: []const u8, offset: u64, comptime Type: type) AccessError!?Type {
const decoded = (try self.read_native(path, offset, Type)) orelse {
return null;
};
@ -37,7 +68,7 @@ pub const Storage = struct {
};
}
pub fn read_native(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type {
pub fn read_native(self: Storage, path: []const u8, offset: u64, comptime Type: type) AccessError!?Type {
var buffer = @as([@sizeOf(Type)]io.Byte, undefined);
if (try self.vtable.read(self.userdata, path, offset, &buffer) != buffer.len) {
@ -47,6 +78,10 @@ pub const Storage = struct {
return @as(*align(1) const Type, @ptrCast(&buffer)).*;
}
pub fn stat(self: Storage, path: []const u8) AccessError!Stat {
return self.vtable.stat(self.userdata, path);
}
pub const read_little = switch (native_endian) {
.little => read_native,
.big => read_foreign,
@ -60,7 +95,7 @@ pub const Storage = struct {
pub const bundle = init: {
const Bundle = struct {
fn full_path(path: []const u8) Error![4095:0]u8 {
fn full_path(path: []const u8) AccessError![4095:0]u8 {
var buffer = [_:0]u8{0} ** 4095;
_ = std.fs.cwd().realpath(path, &buffer) catch {
@ -70,7 +105,7 @@ pub const bundle = init: {
return buffer;
}
fn read(_: *anyopaque, path: []const u8, offset: usize, output: []io.Byte) Error!usize {
fn read(_: *anyopaque, path: []const u8, offset: usize, output: []io.Byte) AccessError!usize {
var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| {
return switch (open_error) {
error.FileNotFound => error.FileNotFound,
@ -89,7 +124,7 @@ pub const bundle = init: {
return file.read(output) catch error.FileInaccessible;
}
fn stat(_: *anyopaque, path: []const u8) Error!Stat {
fn stat(_: *anyopaque, path: []const u8) AccessError!Stat {
const file_stat = get: {
var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| {
return switch (open_error) {

View File

@ -138,16 +138,6 @@ pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.
pub const bits_per_byte = 8;
pub fn bytes_of(value: anytype) []const Byte {
const pointer_info = @typeInfo(@TypeOf(value)).Pointer;
return switch (pointer_info.size) {
.One => @as([*]const Byte, @ptrCast(value))[0 .. @sizeOf(pointer_info.child)],
.Slice => @as([*]const Byte, @ptrCast(value.ptr))[0 .. @sizeOf(pointer_info.child) * value.len],
else => @compileError("`value` must be single-element pointer or slice type"),
};
}
pub fn skip_n(input: Reader, distance: u64) Error!void {
var buffer = @as([512]coral.Byte, undefined);
var remaining = distance;
@ -163,16 +153,6 @@ pub fn skip_n(input: Reader, distance: u64) Error!void {
}
}
pub fn slice_sentineled(comptime Sentinel: type, comptime sen: Sentinel, ptr: [*:sen]const Sentinel) [:sen]const Sentinel {
var len = @as(usize, 0);
while (ptr[len] != sen) {
len += 1;
}
return ptr[0 .. len:sen];
}
pub fn stream_all(input: Reader, output: Writer) Error!usize {
var buffer = @as([512]coral.Byte, undefined);
var copied = @as(usize, 0);

View File

@ -147,7 +147,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime traits: Traits(
self.* = undefined;
}
pub fn get_ptr(self: Self, key: Key) ?*Value {
pub fn get(self: Self, key: Key) ?*Value {
if (self.len == 0) {
return null;
}
@ -169,14 +169,6 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime traits: Traits(
return null;
}
pub fn get(self: Self, key: Key) ?Value {
if (self.get_ptr(key)) |value| {
return value.*;
}
return null;
}
pub fn emplace(self: *Self, key: Key, value: Value) std.mem.Allocator.Error!bool {
try self.rehash(load_max);

View File

@ -115,24 +115,6 @@ pub fn get_ptr(slice: anytype, index: usize) ?ElementPtr(@TypeOf(slice)) {
return &slice[index];
}
pub fn index_of(haystack: anytype, offset: usize, needle: std.meta.Child(@TypeOf(haystack))) ?usize {
for (offset .. haystack.len) |i| {
if (haystack[i] == needle) {
return i;
}
}
return null;
}
pub fn index_of_any(haystack: anytype, offset: usize, needle: []const std.meta.Child(@TypeOf(haystack))) ?usize {
return std.mem.indexOfAnyPos(std.meta.Child(@TypeOf(haystack)), haystack, offset, needle);
}
pub fn index_of_seq(haystack: anytype, offset: usize, needle: []const std.meta.Child(@TypeOf(haystack))) ?usize {
return std.mem.indexOfPos(std.meta.Child(@TypeOf(haystack)), haystack, offset, needle);
}
pub fn parallel_alloc(comptime Element: type, allocator: std.mem.Allocator, n: usize) std.mem.Allocator.Error!Parallel(Element) {
const alignment = @alignOf(Element);
const Slices = Parallel(Element);

View File

@ -6,17 +6,14 @@ const io = @import("./io.zig");
const std = @import("std");
pub fn alloc_formatted(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) std.mem.Allocator.Error![]coral.Byte {
var buffer = coral.Stack(coral.Byte){.allocator = allocator};
pub fn alloc_formatted(allocator: std.mem.Allocator, comptime format: []const u8, args: anytype) std.mem.Allocator.Error![:0]u8 {
const formatted_len = count_formatted(format, args);
const buffer = try allocator.allocSentinel(u8, formatted_len, 0);
try buffer.grow(formatted_len);
errdefer allocator.free(buffer);
errdefer buffer.deinit();
print_formatted(buffer.writer(), format, args) catch unreachable;
return buffer.to_allocation(formatted_len, 0);
// TODO: This is dumb.
return @constCast(print_formatted(buffer, format, args) catch unreachable);
}
pub fn count_formatted(comptime format: []const u8, args: anytype) usize {
@ -27,7 +24,7 @@ pub fn count_formatted(comptime format: []const u8, args: anytype) usize {
return count.written;
}
pub fn print_formatted(buffer: [:0]coral.io.Byte, comptime format: []const u8, args: anytype) io.Error![:0]u8 {
pub fn print_formatted(buffer: [:0]coral.io.Byte, comptime format: []const u8, args: anytype) io.Error![:0]const u8 {
const Seekable = struct {
buffer: []coral.io.Byte,
cursor: usize,
@ -143,8 +140,8 @@ noinline fn print_formatted_value(writer: io.Writer, value: anytype) io.Error!vo
const Value = @TypeOf(value);
return switch (@typeInfo(Value)) {
.Int => ascii.DecimalFormat.default.print(writer, value),
.Float => ascii.DecimalFormat.default.print(writer, value),
.Int => ascii.DecimalFormat.default.format(writer, value),
.Float => ascii.DecimalFormat.default.format(writer, value),
.Enum => io.print(writer, @tagName(value)),
.Pointer => |pointer| switch (pointer.size) {

View File

@ -80,7 +80,7 @@ pub fn Graph(comptime Payload: type) type {
return false;
};
if (coral.slices.index_of(edges.values, 0, edge_node) == null) {
if (std.mem.indexOfScalar(Node, edges.values, edge_node) == null) {
try edges.push_grow(edge_node);
}

View File

@ -18,7 +18,7 @@ pub const Table = struct {
}
pub fn get(self: Table, comptime Resource: type) ?*Resource {
if (self.table.get_ptr(type_id(Resource))) |entry| {
if (self.table.get(type_id(Resource))) |entry| {
return @ptrCast(@alignCast(entry.ptr));
}
@ -42,7 +42,7 @@ pub const Table = struct {
const Value = @TypeOf(value);
const value_id = type_id(Value);
if (self.table.get_ptr(value_id)) |entry| {
if (self.table.get(value_id)) |entry| {
@as(*Value, @ptrCast(@alignCast(entry.ptr))).* = value;
} else {
const resource_allocator = self.arena.allocator();

View File

@ -49,16 +49,16 @@ pub const BindContext = struct {
}
const read_write_resource_nodes = lazily_create: {
break: lazily_create self.systems.read_write_resource_id_nodes.get_ptr(id) orelse insert: {
break: lazily_create self.systems.read_write_resource_id_nodes.get(id) orelse insert: {
std.debug.assert(try self.systems.read_write_resource_id_nodes.emplace(id, .{
.allocator = coral.heap.allocator,
}));
break: insert self.systems.read_write_resource_id_nodes.get_ptr(id).?;
break: insert self.systems.read_write_resource_id_nodes.get(id).?;
};
};
if (coral.slices.index_of(read_write_resource_nodes.values, 0, self.node) == null) {
if (std.mem.indexOfScalar(dag.Node, read_write_resource_nodes.values, self.node) == null) {
try read_write_resource_nodes.push_grow(self.node);
}
@ -77,16 +77,16 @@ pub const BindContext = struct {
}
const read_only_resource_nodes = lazily_create: {
break: lazily_create self.systems.read_only_resource_id_nodes.get_ptr(id) orelse insert: {
break: lazily_create self.systems.read_only_resource_id_nodes.get(id) orelse insert: {
std.debug.assert(try self.systems.read_only_resource_id_nodes.emplace(id, .{
.allocator = coral.heap.allocator,
}));
break: insert self.systems.read_only_resource_id_nodes.get_ptr(id).?;
break: insert self.systems.read_only_resource_id_nodes.get(id).?;
};
};
if (coral.slices.index_of(read_only_resource_nodes.values, 0, self.node) == null) {
if (std.mem.indexOfScalar(dag.Node, read_only_resource_nodes.values, self.node) == null) {
try read_only_resource_nodes.push_grow(self.node);
}
@ -415,12 +415,12 @@ pub const Schedule = struct {
const nodes = lazily_create: {
const system_id = @intFromPtr(info);
break: lazily_create self.system_id_nodes.get_ptr(system_id) orelse insert: {
break: lazily_create self.system_id_nodes.get(system_id) orelse insert: {
std.debug.assert(try self.system_id_nodes.emplace(system_id, .{
.allocator = self.system_id_nodes.allocator,
}));
break: insert self.system_id_nodes.get_ptr(system_id).?;
break: insert self.system_id_nodes.get(system_id).?;
};
};

View File

@ -5,10 +5,9 @@ const std = @import("std");
const ona = @import("ona");
const Actors = struct {
instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator},
quad_mesh_2d: ?*ona.gfx.Handle = null,
body_texture: ?*ona.gfx.Handle = null,
render_texture: ?*ona.gfx.Handle = null,
instances: coral.stack.Sequential(@Vector(2, f32)) = .{.allocator = coral.heap.allocator},
body_texture: ona.gfx.Texture = .default,
render_texture: ona.gfx.Texture = .default,
};
const Player = struct {
@ -23,20 +22,17 @@ pub fn main() !void {
});
}
fn load(display: ona.Write(ona.gfx.Display), actors: ona.Write(Actors), assets: ona.Write(ona.gfx.Assets)) !void {
display.res.width, display.res.height = .{1280, 720};
actors.res.body_texture = try assets.res.create_from_file(coral.files.bundle, "actor.bmp");
actors.res.quad_mesh_2d = try assets.res.create_quad_mesh_2d(@splat(1));
fn load(config: ona.Write(ona.gfx.Config), actors: ona.Write(Actors), assets: ona.Write(ona.gfx.Assets)) !void {
config.res.width, config.res.height = .{1280, 720};
actors.res.body_texture = try assets.res.load_texture_file(coral.files.bundle, "actor.bmp");
actors.res.render_texture = try assets.res.context.create(.{
.texture = .{
.format = .rgba8,
actors.res.render_texture = try assets.res.load_texture(.{
.format = .rgba8,
.access = .{
.render = .{
.width = display.res.width,
.height = display.res.height,
},
.access = .{
.render = .{
.width = config.res.width,
.height = config.res.height,
},
},
});
@ -48,41 +44,41 @@ fn exit(actors: ona.Write(Actors)) void {
actors.res.instances.deinit();
}
fn render(queue: ona.gfx.Queue, actors: ona.Write(Actors), display: ona.Read(ona.gfx.Display)) !void {
try queue.commands.append(.{.target = .{
.texture = actors.res.render_texture.?,
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,
});
for (actors.res.instances.values) |instance| {
try queue.commands.append(.{.instance_2d = .{
.mesh_2d = actors.res.quad_mesh_2d.?,
.texture = actors.res.body_texture.?,
try commands.draw_texture(.{
.texture = actors.res.body_texture,
.transform = .{
.origin = instance,
.xbasis = .{64, 0},
.ybasis = .{0, 64},
},
}});
});
}
try queue.commands.append(.{.target = .{
try commands.set_target(.{
.clear_color = null,
.clear_depth = null,
}});
.clear_stencil = null,
});
try queue.commands.append(.{.instance_2d = .{
.mesh_2d = actors.res.quad_mesh_2d.?,
.texture = actors.res.render_texture.?,
try commands.draw_texture(.{
.texture = actors.res.render_texture,
.transform = .{
.origin = .{@floatFromInt(display.res.width / 2), @floatFromInt(display.res.height / 2)},
.xbasis = .{@floatFromInt(display.res.width), 0},
.ybasis = .{0, @floatFromInt(display.res.height)},
.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 {

View File

@ -1,40 +1,89 @@
const App = @import("./App.zig");
pub const colors = @import("./gfx/colors.zig");
const coral = @import("coral");
const device = @import("./gfx/device.zig");
const bmp = @import("./gfx/bmp.zig");
const ext = @import("./ext.zig");
const flow = @import("flow");
const formats = @import("./gfx/formats.zig");
const handles = @import("./gfx/handles.zig");
pub const lina = @import("./gfx/lina.zig");
const msg = @import("./msg.zig");
const spirv = @import("./gfx/spirv.zig");
const rendering = @import("./gfx/rendering.zig");
const std = @import("std");
pub const AssetFormat = struct {
extension: []const u8,
file_desc: *const FileDesc,
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 Error = std.mem.Allocator.Error || coral.files.Error || error {
pub const LoadError = std.mem.Allocator.Error;
pub const LoadFileError = LoadError || coral.files.AccessError || error {
FormatUnsupported,
};
pub const FileDesc = fn (*std.heap.ArenaAllocator, coral.files.Storage, []const u8) Error!Desc;
};
pub const TextureFormat = struct {
extension: []const u8,
load_file: *const fn (*std.heap.ArenaAllocator, coral.files.Storage, []const u8) LoadFileError!Texture.Desc,
};
pub const Assets = struct {
context: device.Context,
formats: coral.stack.Sequential(AssetFormat),
staging_arena: std.heap.ArenaAllocator,
fn deinit(self: *Assets) void {
rendering.enqueue_work(.shutdown);
self.staging_arena.deinit();
self.texture_formats.deinit();
}
pub fn create_from_file(
self: *Assets,
storage: coral.files.Storage,
path: []const u8,
) (OpenError || AssetFormat.Error)!*Handle {
fn init() !Assets {
const window = create: {
const position = ext.SDL_WINDOWPOS_CENTERED;
const flags = ext.SDL_WINDOW_OPENGL;
const width = 640;
const height = 480;
break: create ext.SDL_CreateWindow("Ona", position, position, width, height, flags) orelse {
return error.Unsupported;
};
};
errdefer {
ext.SDL_DestroyWindow(window);
}
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_texture(_: *Assets, desc: Texture.Desc) std.mem.Allocator.Error!Texture {
var loaded = coral.asyncio.Future(std.mem.Allocator.Error!Texture){};
rendering.enqueue_work(.{
.load_texture = .{
.desc = desc,
.loaded = &loaded,
},
});
return loaded.get().*;
}
pub fn load_texture_file(self: *Assets, storage: coral.files.Storage, path: []const u8) LoadFileError!Texture {
defer {
const max_cache_size = 536870912;
@ -43,107 +92,62 @@ pub const Assets = struct {
}
}
for (self.formats.values) |format| {
for (self.texture_formats.values) |format| {
if (!std.mem.endsWith(u8, path, format.extension)) {
continue;
}
return self.context.create(try format.file_desc(&self.staging_arena, storage, path));
return self.load_texture(try format.load_file(&self.staging_arena, storage, path));
}
return error.FormatUnsupported;
}
pub fn create_quad_mesh_2d(self: *Assets, extents: Point2D) OpenError!*Handle {
const width, const height = extents / @as(Point2D, @splat(2));
return self.context.create(.{
.mesh_2d = .{
.indices = &.{0, 1, 2, 0, 2, 3},
.vertices = &.{
.{.xy = .{-width, -height}, .uv = .{0, 1}},
.{.xy = .{width, -height}, .uv = .{1, 1}},
.{.xy = .{width, height}, .uv = .{1, 0}},
.{.xy = .{-width, height}, .uv = .{0, 0}},
},
},
});
}
pub const thread_restriction = .main;
};
pub const Color = @Vector(4, f32);
pub const Commands = struct {
list: *rendering.Commands.List,
pub const Command = union (enum) {
instance_2d: Instance2D,
target: Target,
pub const Instance2D = struct {
texture: *Handle,
mesh_2d: *Handle,
transform: Transform2D,
pub const State = struct {
commands: *rendering.Commands,
};
pub const Target = struct {
texture: ?*Handle = null,
clear_color: ?Color,
clear_depth: ?f32,
};
pub fn bind(_: flow.system.BindContext) std.mem.Allocator.Error!State {
var created_commands = coral.asyncio.Future(std.mem.Allocator.Error!*rendering.Commands){};
rendering.enqueue_work(.{
.create_commands = .{
.created = &created_commands,
},
});
return .{
.commands = try created_commands.get().*,
};
}
pub fn init(state: *State) Commands {
return .{
.list = state.commands.pending_list(),
};
}
pub fn draw_texture(self: Commands, command: rendering.Command.DrawTexture) std.mem.Allocator.Error!void {
try self.list.append(.{.draw_texture = command});
}
pub fn set_target(self: Commands, command: rendering.Command.SetTarget) std.mem.Allocator.Error!void {
try self.list.append(.{.set_target = command});
}
};
pub const Desc = union (enum) {
texture: Texture,
mesh_2d: Mesh2D,
pub const Mesh2D = struct {
vertices: []const Vertex,
indices: []const u16,
pub const Vertex = struct {
xy: Point2D,
uv: Point2D,
};
};
pub const Texture = struct {
format: Format,
access: Access,
pub const Access = union (enum) {
static: struct {
width: u16,
data: []const coral.io.Byte,
},
render: struct {
width: u16,
height: u16,
},
};
pub const Format = enum {
rgba8,
bgra8,
pub fn byte_size(self: Format) usize {
return switch (self) {
.rgba8, .bgra8 => 4,
};
}
};
};
};
pub const Display = struct {
pub const Config = struct {
width: u16 = 1280,
height: u16 = 720,
clear_color: Color = colors.black,
clear_color: lina.Color = colors.black,
};
pub const Handle = opaque {};
pub const Input = union (enum) {
key_up: Key,
key_down: Key,
@ -160,57 +164,6 @@ pub const Input = union (enum) {
};
};
pub const OpenError = std.mem.Allocator.Error || error {
};
pub const Point2D = @Vector(2, f32);
pub const Queue = struct {
commands: *device.RenderList,
pub const State = struct {
command_index: usize,
};
pub fn bind(_: flow.system.BindContext) std.mem.Allocator.Error!State {
// TODO: Review how good of an idea this global state is, even if bind is guaranteed to always be ran on main.
if (renders.is_empty()) {
renders = .{.allocator = coral.heap.allocator};
}
const command_index = renders.len();
try renders.push_grow(device.RenderChain.init(coral.heap.allocator));
return .{
.command_index = command_index,
};
}
pub fn init(state: *State) Queue {
return .{
.commands = renders.values[state.command_index].pending(),
};
}
pub fn unbind(state: *State) void {
std.debug.assert(!renders.is_empty());
const render = &renders.values[state.command_index];
render.deinit();
std.mem.swap(device.RenderChain, render, renders.get_ptr().?);
std.debug.assert(renders.pop());
if (renders.is_empty()) {
Queue.renders.deinit();
}
}
var renders = coral.stack.Sequential(device.RenderChain){.allocator = coral.heap.allocator};
};
pub const Rect = struct {
left: f32,
top: f32,
@ -218,32 +171,7 @@ pub const Rect = struct {
bottom: f32,
};
pub const Transform2D = extern struct {
xbasis: Point2D = .{1, 0},
ybasis: Point2D = .{0, 1},
origin: Point2D = @splat(0),
};
const builtin_formats = [_]AssetFormat{
.{
.extension = "bmp",
.file_desc = formats.bmp_file_desc,
},
};
pub const colors = struct {
pub const black = greyscale(0);
pub const white = greyscale(1);
pub fn greyscale(v: f32) Color {
return .{v, v, v, 1};
}
pub fn rgb(r: f32, g: f32, b: f32) Color {
return .{r, g, b, 1};
}
};
pub const Texture = handles.Texture;
pub fn poll(app: flow.Write(App), inputs: msg.Send(Input)) !void {
var event = @as(ext.SDL_Event, undefined);
@ -263,40 +191,71 @@ pub fn setup(world: *flow.World, events: App.Events) (error {Unsupported} || std
return error.Unsupported;
}
var context = try device.Context.init();
const assets = create: {
var assets = try Assets.init();
errdefer context.deinit();
errdefer {
assets.deinit();
}
var registered_formats = coral.stack.Sequential(AssetFormat){.allocator = coral.heap.allocator};
break: create try world.set_get_resource(assets);
};
errdefer registered_formats.deinit();
assets.frame_rendered.set();
try registered_formats.grow(builtin_formats.len);
std.debug.assert(registered_formats.push_all(&builtin_formats));
errdefer {
assets.deinit();
}
try world.set_resource(Assets{
.staging_arena = std.heap.ArenaAllocator.init(coral.heap.allocator),
.formats = registered_formats,
.context = context,
});
const builtin_texture_formats = [_]Assets.TextureFormat{
.{
.extension = "bmp",
.load_file = bmp.load_file,
},
};
try world.set_resource(Display{});
for (builtin_texture_formats) |format| {
try assets.texture_formats.push_grow(format);
}
try world.set_resource(Config{});
try world.on_event(events.pre_update, flow.system_fn(poll), .{.label = "poll gfx"});
try world.on_event(events.exit, flow.system_fn(stop), .{.label = "stop gfx"});
try world.on_event(events.finish, flow.system_fn(synchronize), .{.label = "synchronize gfx"});
}
pub fn stop(assets: flow.Write(Assets)) void {
assets.res.staging_arena.deinit();
assets.res.formats.deinit();
assets.res.context.deinit();
assets.res.deinit();
}
pub fn synchronize(assets: flow.Write(Assets), display: flow.Read(Display)) !void {
assets.res.context.submit(.{
.width = display.res.width,
.height = display.res.height,
.clear_color = display.res.clear_color,
.renders = Queue.renders.values,
pub fn synchronize(assets: flow.Write(Assets), config: flow.Read(Config)) !void {
assets.res.frame_rendered.wait();
assets.res.frame_rendered.reset();
var commands_swapped = std.Thread.ResetEvent{};
rendering.enqueue_work(.{
.rotate_commands = .{
.finished = &commands_swapped,
},
});
var display_width, var display_height = [_]c_int{0, 0};
ext.SDL_GL_GetDrawableSize(assets.res.window, &display_width, &display_height);
if (config.res.width != display_width or config.res.height != display_height) {
ext.SDL_SetWindowSize(assets.res.window, config.res.width, config.res.height);
}
commands_swapped.wait();
rendering.enqueue_work(.{
.render_frame = .{
.width = config.res.width,
.height = config.res.height,
.clear_color = config.res.clear_color,
.finished = &assets.res.frame_rendered,
},
});
}

View File

@ -1,14 +1,10 @@
const coral = @import("coral");
const gfx = @import("../gfx.zig");
const handles = @import("./handles.zig");
const std = @import("std");
pub fn bmp_file_desc(
arena: *std.heap.ArenaAllocator,
storage: coral.files.Storage,
path: []const u8,
) gfx.AssetFormat.Error!gfx.Desc {
pub fn load_file(arena: *std.heap.ArenaAllocator, storage: coral.files.Storage, path: []const u8) !handles.Texture.Desc {
const header = try storage.read_little(path, 0, extern struct {
type: [2]u8 align (1),
file_size: u32 align (1),
@ -51,7 +47,7 @@ pub fn bmp_file_desc(
while (buffer_offset < pixels.len) {
const line = pixels[buffer_offset .. buffer_offset + byte_stride];
if (try storage.read_bytes(path, file_offset, line) != byte_stride) {
if (try storage.read(path, line, file_offset) != byte_stride) {
return error.FormatUnsupported;
}
@ -71,15 +67,13 @@ pub fn bmp_file_desc(
}
return .{
.texture = .{
.format = .rgba8,
.format = .rgba8,
.access = .{
.static = .{
.width = pixel_width,
.data = pixels,
},
.access = .{
.static = .{
.width = pixel_width,
.data = pixels,
},
}
},
};
}

13
src/ona/gfx/colors.zig Normal file
View File

@ -0,0 +1,13 @@
const lina = @import("./lina.zig");
pub const black = greyscale(0);
pub const white = greyscale(1);
pub fn greyscale(v: f32) lina.Color {
return .{v, v, v, 1};
}
pub fn rgb(r: f32, g: f32, b: f32) lina.Color {
return .{r, g, b, 1};
}

View File

@ -1,80 +0,0 @@
const coral = @import("coral");
const std = @import("std");
pub fn Chain(comptime Command: type, comptime clone_command: ?Clone(Command)) type {
return struct {
swap_lists: [2]CommandList,
swap_state: u1 = 0,
const CommandList = List(Command, clone_command);
const Self = @This();
pub fn deinit(self: *Self) void {
for (&self.swap_lists) |*list| {
list.deinit();
}
self.* = undefined;
}
pub fn init(allocator: std.mem.Allocator) Self {
return .{
.swap_lists = .{CommandList.init(allocator), CommandList.init(allocator)},
};
}
pub fn pending(self: *Self) *CommandList {
return &self.swap_lists[self.swap_state];
}
pub fn submitted(self: *Self) *CommandList {
return &self.swap_lists[self.swap_state ^ 1];
}
pub fn swap(self: *Self) void {
self.swap_state ^= 1;
}
};
}
pub fn Clone(comptime Command: type) type {
return fn (Command, *std.heap.ArenaAllocator) std.mem.Allocator.Error!Command;
}
pub fn List(comptime Command: type, comptime clone_command: ?Clone(Command)) type {
return struct {
arena: std.heap.ArenaAllocator,
stack: coral.stack.Sequential(Command),
const Self = @This();
pub fn append(self: *Self, command: Command) std.mem.Allocator.Error!void {
return self.stack.push_grow(if (clone_command) |clone| try clone(command, &self.arena) else command);
}
pub fn clear(self: *Self) void {
self.stack.clear();
if (!self.arena.reset(.retain_capacity)) {
std.log.warn("failed to reset the buffer of a gfx queue with retained capacity", .{});
}
}
pub fn deinit(self: *Self) void {
self.arena.deinit();
self.stack.deinit();
self.* = undefined;
}
pub fn init(allocator: std.mem.Allocator) Self {
return .{
.arena = std.heap.ArenaAllocator.init(allocator),
.stack = .{.allocator = allocator},
};
}
};
}

View File

@ -1,847 +0,0 @@
const commands = @import("./commands.zig");
const coral = @import("coral");
const ext = @import("../ext.zig");
const gfx = @import("../gfx.zig");
const sokol = @import("sokol");
const spirv = @import("./spirv.zig");
const std = @import("std");
pub const Context = struct {
window: *ext.SDL_Window,
thread: std.Thread,
loop: *Loop,
pub const Submission = struct {
width: u16,
height: u16,
clear_color: gfx.Color = gfx.colors.black,
renders: []RenderChain,
};
pub fn close(self: *Context, handle: *gfx.Handle) void {
const close_commands = self.loop.closes.pending();
std.debug.assert(close_commands.stack.cap > close_commands.stack.len());
close_commands.append(handle) catch unreachable;
}
pub fn create(self: *Context, desc: gfx.Desc) gfx.OpenError!*gfx.Handle {
const resource = try coral.heap.allocator.create(Resource);
errdefer coral.heap.allocator.destroy(resource);
try self.loop.creations.pending().append(.{
.resource = resource,
.desc = desc,
});
const pending_destroys = self.loop.destroys.pending();
if (pending_destroys.stack.len() == pending_destroys.stack.cap) {
try pending_destroys.stack.grow(1);
}
return @ptrCast(resource);
}
pub fn deinit(self: *Context) void {
self.loop.is_running.store(false, .monotonic);
self.loop.ready.post();
self.thread.join();
self.loop.deinit();
coral.heap.allocator.destroy(self.loop);
ext.SDL_DestroyWindow(self.window);
self.* = undefined;
}
pub fn init() !Context {
const window = create: {
const position = ext.SDL_WINDOWPOS_CENTERED;
const flags = ext.SDL_WINDOW_OPENGL;
const width = 640;
const height = 480;
break: create ext.SDL_CreateWindow("Ona", position, position, width, height, flags) orelse {
return error.Unsupported;
};
};
errdefer ext.SDL_DestroyWindow(window);
const loop = try coral.heap.allocator.create(Loop);
errdefer coral.heap.allocator.destroy(loop);
loop.* = .{};
return .{
.loop = loop,
.window = window,
.thread = spawn: {
const thread = try std.Thread.spawn(.{}, Loop.run, .{loop, window});
thread.setName("Ona Graphics") catch {
std.log.warn("failed to name the graphics thread", .{});
};
break: spawn thread;
},
};
}
pub fn submit(self: *Context, submission: Submission) void {
self.loop.finished.wait();
defer self.loop.ready.post();
for (submission.renders) |*render| {
render.swap();
}
self.loop.creations.swap();
self.loop.destroys.swap();
var last_width, var last_height = [_]c_int{0, 0};
ext.SDL_GetWindowSize(self.window, &last_width, &last_height);
if (submission.width != last_width or submission.height != last_height) {
ext.SDL_SetWindowSize(self.window, submission.width, submission.height);
}
self.loop.clear_color = submission.clear_color;
self.loop.renders = submission.renders;
self.loop.ready.post();
}
};
const Frame = struct {
projection: Projection = .{@splat(0), @splat(0), @splat(0), @splat(0)},
flushed_batch_count: usize = 0,
pushed_batch_count: usize = 0,
render_passes: usize = 0,
mesh: ?*gfx.Handle = null,
texture: ?*gfx.Handle = null,
material: ?*gfx.Handle = null,
};
const Loop = struct {
ready: std.Thread.Semaphore = .{},
finished: std.Thread.Semaphore = .{},
clear_color: gfx.Color = gfx.colors.black,
is_running: AtomicBool = AtomicBool.init(true),
renders: []RenderChain = &.{},
destroys: DestroyChain = DestroyChain.init(coral.heap.allocator),
creations: CreateChain = CreateChain.init(coral.heap.allocator),
has_resource_head: ?*Resource = null,
has_resource_tail: ?*Resource = null,
const AtomicBool = std.atomic.Value(bool);
const CreateCommand = struct {
resource: *Resource,
desc: gfx.Desc,
fn clone(command: CreateCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!CreateCommand {
const allocator = arena.allocator();
return .{
.desc = switch (command.desc) {
.texture => |texture| .{
.texture = .{
.access = switch (texture.access) {
.static => |static| .{
.static = .{
.data = try allocator.dupe(coral.io.Byte, static.data),
.width = static.width,
},
},
.render => |render| .{
.render = .{
.width = render.width,
.height = render.height,
},
},
},
.format = texture.format,
},
},
.mesh_2d => |mesh_2d| .{
.mesh_2d = .{
.vertices = try allocator.dupe(gfx.Desc.Mesh2D.Vertex, mesh_2d.vertices),
.indices = try allocator.dupe(u16, mesh_2d.indices),
},
},
},
.resource = command.resource,
};
}
};
const CreateChain = commands.Chain(CreateCommand, CreateCommand.clone);
const DestroyChain = commands.Chain(*Resource, null);
fn consume_creations(self: *Loop) std.mem.Allocator.Error!void {
const create_commands = self.creations.submitted();
defer create_commands.clear();
for (create_commands.stack.values) |command| {
errdefer coral.heap.allocator.destroy(command.resource);
command.resource.* = .{
.payload = switch (command.desc) {
.texture => |texture| init: {
const pixel_format = switch (texture.format) {
.rgba8 => sokol.gfx.PixelFormat.RGBA8,
.bgra8 => sokol.gfx.PixelFormat.BGRA8,
};
switch (texture.access) {
.static => |static| {
break: init .{
.texture = .{
.image = sokol.gfx.makeImage(.{
.width = static.width,
.height = @intCast(static.data.len / (static.width * texture.format.byte_size())),
.pixel_format = pixel_format,
.data = .{
.subimage = get: {
var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6;
subimage[0][0] = sokol.gfx.asRange(static.data);
break: get subimage;
},
},
}),
.sampler = sokol.gfx.makeSampler(.{}),
},
};
},
.render => |render| {
const color_image = sokol.gfx.makeImage(.{
.width = render.width,
.height = render.height,
.render_target = true,
.pixel_format = pixel_format,
});
const depth_image = sokol.gfx.makeImage(.{
.width = render.width,
.height = render.height,
.render_target = true,
.pixel_format = .DEPTH_STENCIL,
});
break: init .{
.render_target = .{
.attachments = sokol.gfx.makeAttachments(.{
.colors = get: {
var attachments = [_]sokol.gfx.AttachmentDesc{.{}} ** 4;
attachments[0] = .{
.image = color_image,
};
break: get attachments;
},
.depth_stencil = .{
.image = depth_image,
},
}),
.sampler = sokol.gfx.makeSampler(.{}),
.color_image = color_image,
.depth_image = depth_image,
.width = render.width,
.height = render.height,
},
};
},
}
},
.mesh_2d => |mesh_2d| init: {
if (mesh_2d.indices.len > std.math.maxInt(u32)) {
return error.OutOfMemory;
}
break: init .{
.mesh_2d = .{
.index_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(mesh_2d.indices),
.type = .INDEXBUFFER,
}),
.vertex_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(mesh_2d.vertices),
.type = .VERTEXBUFFER,
}),
.index_count = @intCast(mesh_2d.indices.len),
},
};
},
},
.has_prev = self.has_resource_tail,
.has_next = null,
};
if (self.has_resource_tail) |resource_tail| {
resource_tail.has_next = command.resource;
} else {
std.debug.assert(self.has_resource_head == null);
self.has_resource_head = command.resource;
}
self.has_resource_tail = command.resource;
}
}
fn consume_destroys(self: *Loop) std.mem.Allocator.Error!void {
const destroy_commands = self.destroys.submitted();
defer destroy_commands.clear();
for (destroy_commands.stack.values) |resource| {
defer coral.heap.allocator.destroy(resource);
resource.destroy();
if (resource.has_prev) |resource_prev| {
resource_prev.has_next = resource.has_next;
}
if (resource.has_next) |resource_next| {
resource_next.has_prev = resource.has_prev;
}
}
}
fn deinit(self: *Loop) void {
self.destroys.deinit();
self.creations.deinit();
}
fn run(self: *Loop, window: *ext.SDL_Window) !void {
const context = configure_and_create: {
var result = @as(c_int, 0);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_FLAGS, ext.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_PROFILE_MASK, ext.SDL_GL_CONTEXT_PROFILE_CORE);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MAJOR_VERSION, 3);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MINOR_VERSION, 3);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_DOUBLEBUFFER, 1);
if (result != 0) {
return error.Unsupported;
}
break: configure_and_create ext.SDL_GL_CreateContext(window);
};
sokol.gfx.setup(.{
.environment = .{
.defaults = .{
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.sample_count = 1,
},
},
.logger = .{
.func = sokol.log.func,
},
});
defer {
sokol.gfx.shutdown();
ext.SDL_GL_DeleteContext(context);
}
var rendering_2d = try Rendering2D.init();
defer rendering_2d.deinit();
self.finished.post();
while (self.is_running.load(.monotonic)) {
self.ready.wait();
defer self.finished.post();
try self.consume_creations();
var frame = Frame{};
rendering_2d.set_target(&frame, window, .{
.clear_color = self.clear_color,
.clear_depth = 0,
});
if (self.renders.len != 0) {
const renders_tail_index = self.renders.len - 1;
for (self.renders[0 .. renders_tail_index]) |*render| {
const render_commands = render.submitted();
defer render_commands.clear();
for (render_commands.stack.values) |command| {
try switch (command) {
.instance_2d => |instance_2d| rendering_2d.batch(&frame, instance_2d),
.target => |target| rendering_2d.set_target(&frame, window, target),
};
}
rendering_2d.set_target(&frame, window, .{
.clear_color = null,
.clear_depth = null,
});
}
const render_commands = self.renders[renders_tail_index].submitted();
defer render_commands.clear();
for (render_commands.stack.values) |command| {
try switch (command) {
.instance_2d => |instance_2d| rendering_2d.batch(&frame, instance_2d),
.target => |target| rendering_2d.set_target(&frame, window, target),
};
}
rendering_2d.flush(&frame);
}
sokol.gfx.endPass();
sokol.gfx.commit();
ext.SDL_GL_SwapWindow(window);
try self.consume_destroys();
}
var has_next_resource = self.has_resource_head;
while (has_next_resource) |resource| {
std.log.info("destroying remaining gfx device resource {x}", .{@intFromPtr(resource)});
resource.destroy();
has_next_resource = resource.has_next;
coral.heap.allocator.destroy(resource);
}
}
};
const Projection = [4]@Vector(4, f32);
pub const RenderChain = commands.Chain(gfx.Command, clone_command);
pub const RenderList = commands.List(gfx.Command, clone_command);
const Rendering2D = struct {
batching_pipeline: sokol.gfx.Pipeline,
batching_buffers: coral.stack.Sequential(sokol.gfx.Buffer),
const Instance = extern struct {
transform: gfx.Transform2D,
tint: @Vector(4, u8) = @splat(std.math.maxInt(u8)),
depth: f32 = 0,
texture_offset: gfx.Point2D = @splat(0),
texture_size: gfx.Point2D = @splat(1),
};
const buffer_indices = .{
.mesh = 0,
.instance = 1,
};
fn batch(self: *Rendering2D, frame: *Frame, instance: gfx.Command.Instance2D) std.mem.Allocator.Error!void {
if (instance.mesh_2d != frame.mesh or instance.texture != frame.texture) {
self.flush(frame);
}
frame.mesh = instance.mesh_2d;
frame.texture = instance.texture;
const has_filled_buffer = (frame.pushed_batch_count % instances_per_buffer) == 0;
const pushed_buffer_count = frame.pushed_batch_count / instances_per_buffer;
if (has_filled_buffer and pushed_buffer_count == self.batching_buffers.len()) {
var name_buffer = [_:0]u8{0} ** 64;
const name_view = std.fmt.bufPrint(&name_buffer, "instance 2d buffer #{}", .{self.batching_buffers.len()}) catch {
unreachable;
};
const instance_buffer = sokol.gfx.makeBuffer(.{
.label = name_view.ptr,
.size = @sizeOf(Instance) * instances_per_buffer,
.usage = .STREAM,
});
errdefer sokol.gfx.destroyBuffer(instance_buffer);
try self.batching_buffers.push_grow(instance_buffer);
}
_ = sokol.gfx.appendBuffer(self.batching_buffers.get().?, sokol.gfx.asRange(&Instance{
.transform = instance.transform,
}));
frame.pushed_batch_count += 1;
}
fn deinit(self: *Rendering2D) void {
for (self.batching_buffers.values) |buffer| {
sokol.gfx.destroyBuffer(buffer);
}
self.batching_buffers.deinit();
sokol.gfx.destroyPipeline(self.batching_pipeline);
}
fn flush(self: *Rendering2D, frame: *Frame) void {
const unflushed_count = frame.pushed_batch_count - frame.flushed_batch_count;
if (unflushed_count == 0) {
return;
}
sokol.gfx.applyPipeline(self.batching_pipeline);
sokol.gfx.applyUniforms(.VS, 0, sokol.gfx.asRange(&frame.projection));
const mesh_2d = resource_cast(frame.mesh.?).payload.mesh_2d;
const texture_image, const texture_sampler = switch (resource_cast(frame.texture.?).payload) {
.texture => |texture| .{texture.image, texture.sampler},
.render_target => |render_target| .{render_target.color_image, render_target.sampler},
else => unreachable,
};
var bindings = sokol.gfx.Bindings{
.vertex_buffers = get: {
var buffers = [_]sokol.gfx.Buffer{.{}} ** 8;
buffers[buffer_indices.mesh] = mesh_2d.vertex_buffer;
break: get buffers;
},
.index_buffer = mesh_2d.index_buffer,
.fs = .{
.images = get: {
var images = [_]sokol.gfx.Image{.{}} ** 12;
images[0] = texture_image;
break: get images;
},
.samplers = get: {
var samplers = [_]sokol.gfx.Sampler{.{}} ** 8;
samplers[0] = texture_sampler;
break: get samplers;
},
},
.vs = .{
.images = get: {
var images = [_]sokol.gfx.Image{.{}} ** 12;
images[0] = texture_image;
break: get images;
},
.samplers = get: {
var samplers = [_]sokol.gfx.Sampler{.{}} ** 8;
samplers[0] = texture_sampler;
break: get samplers;
},
},
};
while (frame.flushed_batch_count < frame.pushed_batch_count) {
const buffer_index = frame.flushed_batch_count / instances_per_buffer;
const buffer_offset = frame.flushed_batch_count % instances_per_buffer;
const instances_to_flush = @min(instances_per_buffer - buffer_offset, unflushed_count);
bindings.vertex_buffers[buffer_indices.instance] = self.batching_buffers.values[buffer_index];
bindings.vertex_buffer_offsets[buffer_indices.instance] = @intCast(@sizeOf(Instance) * buffer_offset);
sokol.gfx.applyBindings(bindings);
sokol.gfx.draw(0, mesh_2d.index_count, @intCast(instances_to_flush));
frame.flushed_batch_count += instances_to_flush;
}
}
fn init() spirv.Error!Rendering2D {
sokol.gfx.setup(.{
.environment = .{
.defaults = .{
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.sample_count = 1,
},
},
.logger = .{
.func = sokol.log.func,
},
});
var spirv_unit = try spirv.Unit.init();
defer spirv_unit.deinit();
const shader_spirv = spirv.embed_shader("./shaders/render_2d.spv");
try spirv_unit.compile(shader_spirv, .vertex);
std.log.info("{s}\n", .{spirv_unit.shader_desc.vs.source});
try spirv_unit.compile(shader_spirv, .fragment);
std.log.info("{s}\n", .{spirv_unit.shader_desc.fs.source});
return .{
.batching_pipeline = sokol.gfx.makePipeline(.{
.label = "2D drawing pipeline",
.layout = .{
.attrs = get: {
var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16;
attrs[0] = .{
.format = .FLOAT2,
.buffer_index = buffer_indices.mesh,
};
attrs[1] = .{
.format = .FLOAT2,
.buffer_index = buffer_indices.mesh,
};
attrs[2] = .{
.format = .FLOAT2,
.buffer_index = buffer_indices.instance,
};
attrs[3] = .{
.format = .FLOAT2,
.buffer_index = buffer_indices.instance,
};
attrs[4] = .{
.format = .FLOAT2,
.buffer_index = buffer_indices.instance,
};
attrs[5] = .{
.format = .UBYTE4N,
.buffer_index = buffer_indices.instance,
};
attrs[6] = .{
.format = .FLOAT,
.buffer_index = buffer_indices.instance,
};
attrs[7] = .{
.format = .FLOAT4,
.buffer_index = buffer_indices.instance,
};
break: get attrs;
},
.buffers = get: {
var buffers = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8;
buffers[buffer_indices.instance].step_func = .PER_INSTANCE;
break: get buffers;
},
},
.shader = sokol.gfx.makeShader(spirv_unit.shader_desc),
.index_type = .UINT16,
}),
.batching_buffers = .{.allocator = coral.heap.allocator},
};
}
const instances_per_buffer = 512;
fn set_target(self: *Rendering2D, frame: *Frame, window: *ext.SDL_Window, target: gfx.Command.Target) void {
defer frame.render_passes += 1;
if (frame.render_passes != 0) {
self.flush(frame);
sokol.gfx.endPass();
}
var pass = sokol.gfx.Pass{
.action = .{
.stencil = .{.load_action = .CLEAR},
},
};
if (target.clear_color) |color| {
pass.action.colors[0] = .{
.load_action = .CLEAR,
.clear_value = @bitCast(color),
};
} else {
pass.action.colors[0] = .{.load_action = .LOAD};
}
if (target.clear_depth) |depth| {
pass.action.depth = .{
.load_action = .CLEAR,
.clear_value = depth,
};
} else {
pass.action.depth = .{.load_action = .LOAD};
}
if (target.texture) |render_texture| {
const render_target = resource_cast(render_texture).payload.render_target;
pass.attachments = render_target.attachments;
frame.projection = orthographic_projection(-1.0, 1.0, .{
.left = 0,
.top = 0,
.right = @floatFromInt(render_target.width),
.bottom = @floatFromInt(render_target.height),
});
} else {
var target_width, var target_height = [_]c_int{0, 0};
ext.SDL_GL_GetDrawableSize(window, &target_width, &target_height);
std.debug.assert(target_width > 0 and target_height > 0);
pass.swapchain = .{
.width = target_width,
.height = target_height,
.sample_count = 1,
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.gl = .{.framebuffer = 0},
};
frame.projection = orthographic_projection(-1.0, 1.0, .{
.left = 0,
.top = 0,
.right = @floatFromInt(target_width),
.bottom = @floatFromInt(target_height),
});
}
sokol.gfx.beginPass(pass);
}
};
const Resource = struct {
has_prev: ?*Resource,
has_next: ?*Resource,
payload: union (enum) {
mesh_2d: struct {
index_count: u32,
vertex_buffer: sokol.gfx.Buffer,
index_buffer: sokol.gfx.Buffer,
},
texture: struct {
image: sokol.gfx.Image,
sampler: sokol.gfx.Sampler,
},
render_target: struct {
sampler: sokol.gfx.Sampler,
color_image: sokol.gfx.Image,
depth_image: sokol.gfx.Image,
attachments: sokol.gfx.Attachments,
width: u16,
height: u16,
},
},
fn destroy(self: Resource) void {
switch (self.payload) {
.mesh_2d => |mesh_2d| {
sokol.gfx.destroyBuffer(mesh_2d.vertex_buffer);
sokol.gfx.destroyBuffer(mesh_2d.index_buffer);
},
.texture => |texture| {
sokol.gfx.destroyImage(texture.image);
sokol.gfx.destroySampler(texture.sampler);
},
.render_target => |render_target| {
sokol.gfx.destroyImage(render_target.color_image);
sokol.gfx.destroyImage(render_target.depth_image);
sokol.gfx.destroySampler(render_target.sampler);
sokol.gfx.destroyAttachments(render_target.attachments);
},
}
}
};
fn clone_command(self: gfx.Command, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!gfx.Command {
_ = arena;
return switch (self) {
.instance_2d => |instance_2d| .{.instance_2d = instance_2d},
.target => |target| .{.target = target},
};
}
fn orthographic_projection(near: f32, far: f32, viewport: gfx.Rect) Projection {
const width = viewport.right - viewport.left;
const height = viewport.bottom - viewport.top;
return .{
.{2 / width, 0, 0, 0},
.{0, 2 / height, 0, 0},
.{0, 0, 1 / (far - near), 0},
.{-((viewport.left + viewport.right) / width), -((viewport.top + viewport.bottom) / height), near / (near - far), 1},
};
}
fn resource_cast(handle: *gfx.Handle) *Resource {
return @ptrCast(@alignCast(handle));
}

45
src/ona/gfx/handles.zig Normal file
View File

@ -0,0 +1,45 @@
const coral = @import("coral");
const std = @import("std");
fn Handle(comptime HandleDesc: type) type {
return enum (u32) {
default,
_,
pub const Desc = HandleDesc;
const Self = @This();
};
}
pub const Texture = Handle(struct {
format: Format,
access: Access,
pub const Access = union (enum) {
static: Static,
render: Render,
pub const Static = struct {
width: u16,
data: []const coral.io.Byte,
};
pub const Render = struct {
width: u16,
height: u16,
};
};
pub const Format = enum {
rgba8,
bgra8,
pub fn byte_size(self: Format) usize {
return switch (self) {
.rgba8, .bgra8 => 4,
};
}
};
});

29
src/ona/gfx/lina.zig Normal file
View File

@ -0,0 +1,29 @@
const gfx = @import("../gfx.zig");
pub const Color = @Vector(4, f32);
pub fn EvenOrderMatrix(comptime n: usize, comptime Element: type) type {
return [n]@Vector(n, Element);
}
pub const Transform2D = extern struct {
xbasis: Vector = .{1, 0},
ybasis: Vector = .{0, 1},
origin: Vector = @splat(0),
const Vector = @Vector(2, f32);
};
pub const ProjectionMatrix = EvenOrderMatrix(4, f32);
pub fn orthographic_projection(near: f32, far: f32, viewport: gfx.Rect) EvenOrderMatrix(4, f32) {
const width = viewport.right - viewport.left;
const height = viewport.bottom - viewport.top;
return .{
.{2 / width, 0, 0, 0},
.{0, 2 / height, 0, 0},
.{0, 0, 1 / (far - near), 0},
.{-((viewport.left + viewport.right) / width), -((viewport.top + viewport.bottom) / height), near / (near - far), 1},
};
}

662
src/ona/gfx/rendering.zig Normal file
View File

@ -0,0 +1,662 @@
const coral = @import("coral");
const ext = @import("../ext.zig");
const handles = @import("./handles.zig");
const lina = @import("./lina.zig");
const resources = @import("./resources.zig");
const spirv = @import("./spirv.zig");
const sokol = @import("sokol");
const std = @import("std");
pub const Command = union (enum) {
draw_texture: DrawTexture,
set_target: SetTarget,
pub const DrawTexture = struct {
texture: handles.Texture,
transform: lina.Transform2D,
};
pub const SetTarget = struct {
texture: ?handles.Texture = null,
clear_color: ?lina.Color,
clear_depth: ?f32,
clear_stencil: ?u8,
};
fn clone(self: Command, arena: *std.heap.ArenaAllocator) !Command {
_ = arena;
return switch (self) {
.draw_texture => |draw_texture| .{.draw_texture = draw_texture},
.set_target => |set_target| .{.set_target = set_target},
};
}
fn process(self: Command, pools: *Pools, frame: *Frame) !void {
try switch (self) {
.draw_texture => |draw_texture| frame.draw_texture(pools, draw_texture),
.set_target => |set_target| frame.set_target(pools, set_target),
};
}
};
pub const Commands = struct {
swap_lists: [2]List,
swap_state: u1 = 0,
has_next: ?*Commands = null,
has_prev: ?*Commands = null,
pub const List = struct {
arena: std.heap.ArenaAllocator,
stack: coral.stack.Sequential(Command),
pub fn append(self: *List, command: Command) std.mem.Allocator.Error!void {
return self.stack.push_grow(try command.clone(&self.arena));
}
pub fn clear(self: *List) void {
self.stack.clear();
if (!self.arena.reset(.retain_capacity)) {
std.log.warn("failed to reset the buffer of a gfx queue with retained capacity", .{});
}
}
fn deinit(self: *List) void {
self.arena.deinit();
self.stack.deinit();
self.* = undefined;
}
fn init(allocator: std.mem.Allocator) List {
return .{
.arena = std.heap.ArenaAllocator.init(allocator),
.stack = .{.allocator = allocator},
};
}
};
const Self = @This();
pub fn deinit(self: *Self) void {
for (&self.swap_lists) |*list| {
list.deinit();
}
self.* = undefined;
}
pub fn init(allocator: std.mem.Allocator) Self {
return .{
.swap_lists = .{List.init(allocator), List.init(allocator)},
};
}
pub fn pending_list(self: *Self) *List {
return &self.swap_lists[self.swap_state];
}
fn rotate(self: *Self) void {
const swapped_state = self.swap_state ^ 1;
self.swap_lists[swapped_state].clear();
self.swap_state = swapped_state;
}
pub fn submitted_commands(self: *const Self) []const Command {
return self.swap_lists[self.swap_state ^ 1].stack.values;
}
};
const Frame = struct {
swapchain: sokol.gfx.Swapchain,
drawn_count: usize = 0,
flushed_count: usize = 0,
current_source_texture: handles.Texture = .default,
current_target_texture: ?handles.Texture = null,
const DrawTexture = extern struct {
transform: lina.Transform2D,
tint: @Vector(4, u8) = @splat(std.math.maxInt(u8)),
depth: f32 = 0,
texture_offset: @Vector(2, f32) = @splat(0),
texture_size: @Vector(2, f32) = @splat(1),
};
pub fn draw_texture(self: *Frame, pools: *Pools, command: Command.DrawTexture) !void {
if (command.texture != self.current_source_texture) {
self.flush(pools);
}
self.current_source_texture = command.texture;
const has_filled_current_buffer = (self.drawn_count % batches_per_buffer) == 0;
const buffer_count = self.drawn_count / batches_per_buffer;
if (has_filled_current_buffer and buffer_count == texture_batch_buffers.len()) {
const instance_buffer = sokol.gfx.makeBuffer(.{
.size = @sizeOf(DrawTexture) * batches_per_buffer,
.usage = .STREAM,
});
errdefer sokol.gfx.destroyBuffer(instance_buffer);
try texture_batch_buffers.push_grow(instance_buffer);
}
_ = sokol.gfx.appendBuffer(texture_batch_buffers.get().?, sokol.gfx.asRange(&DrawTexture{
.transform = command.transform,
}));
self.drawn_count += 1;
}
pub fn flush(self: *Frame, pools: *Pools) void {
if (self.flushed_count == self.drawn_count) {
return;
}
var bindings = sokol.gfx.Bindings{
.index_buffer = quad_index_buffer,
};
bindings.vertex_buffers[vertex_indices.mesh] = quad_vertex_buffer;
switch (pools.textures.get(@intFromEnum(self.current_source_texture)).?.*) {
.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);
while (true) {
const buffer_index = self.flushed_count / batches_per_buffer;
const buffer_offset = self.flushed_count % batches_per_buffer;
const instances_to_flush = @min(batches_per_buffer - buffer_offset, self.drawn_count - self.flushed_count);
self.flushed_count += instances_to_flush;
bindings.vertex_buffers[vertex_indices.instance] = texture_batch_buffers.values[buffer_index];
bindings.vertex_buffer_offsets[vertex_indices.instance] = @intCast(@sizeOf(DrawTexture) * buffer_offset);
sokol.gfx.applyBindings(bindings);
sokol.gfx.draw(0, 6, @intCast(instances_to_flush));
if (self.flushed_count == self.drawn_count) {
break;
}
}
}
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}},
};
if (command.clear_color) |color| {
pass.action.colors[0] = .{
.load_action = .CLEAR,
.clear_value = @bitCast(color),
};
} else {
pass.action.colors[0] = .{.load_action = .LOAD};
}
if (command.clear_depth) |depth| {
pass.action.depth = .{
.load_action = .CLEAR,
.clear_value = depth,
};
} else {
pass.action.depth = .{.load_action = .LOAD};
}
if (command.texture) |texture| {
pass.attachments = switch (pools.textures.get(@intFromEnum(texture)).?.*) {
.static => @panic("Cannot render to static textures"),
.render => |render| render.attachments,
};
self.current_target_texture = command.texture;
} else {
pass.swapchain = self.swapchain;
self.current_target_texture = null;
}
sokol.gfx.beginPass(pass);
}
const storage_bindings = .{
.engine = 0,
.material = 1,
};
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 {
textures: TexturePool,
const TexturePool = coral.Pool(resources.Texture);
fn create_texture(self: *Pools, desc: handles.Texture.Desc) !handles.Texture {
var texture = try resources.Texture.init(desc);
errdefer texture.deinit();
return @enumFromInt(try self.textures.insert(texture));
}
fn deinit(self: *Pools) void {
var textures = self.textures.values();
while (textures.next()) |texture| {
texture.deinit();
}
self.textures.deinit();
}
fn destroy_texture(self: *Pools, texture_key: handles.Texture) bool {
switch (texture_key) {
.default => {},
else => {
var texture = self.textures.remove(@intFromEnum(texture_key)) orelse {
return false;
};
texture.deinit();
},
}
return true;
}
fn init(allocator: std.mem.Allocator) !Pools {
var pools = Pools{
.textures = TexturePool.init(allocator),
};
errdefer pools.deinit();
const default_texture_data = [_]u32{
0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF,
0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF,
0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF,
0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF,
0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF,
0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF,
0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF,
0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF, 0x800080FF, 0x000000FF,
};
_ = try pools.create_texture(.{
.format = .rgba8,
.access = .{
.static = .{
.data = std.mem.asBytes(&default_texture_data),
.width = 8,
},
},
});
return pools;
}
};
pub const Work = union (enum) {
create_commands: CreateCommandsWork,
load_texture: LoadTextureWork,
render_frame: RenderFrameWork,
rotate_commands: RotateCommandsWork,
shutdown,
unload_texture: UnloadTextureWork,
pub const CreateCommandsWork = struct {
created: *coral.asyncio.Future(std.mem.Allocator.Error!*Commands),
};
pub const LoadTextureWork = struct {
desc: handles.Texture.Desc,
loaded: *coral.asyncio.Future(std.mem.Allocator.Error!handles.Texture),
};
pub const RotateCommandsWork = struct {
finished: *std.Thread.ResetEvent,
};
pub const RenderFrameWork = struct {
clear_color: lina.Color,
width: u16,
height: u16,
finished: *std.Thread.ResetEvent,
};
pub const UnloadTextureWork = struct {
handle: handles.Texture,
};
var pending: coral.asyncio.BlockingQueue(1024, Work) = .{};
};
pub fn enqueue_work(work: Work) void {
Work.pending.enqueue(work);
}
pub fn startup(window: *ext.SDL_Window) !void {
work_thread = try std.Thread.spawn(.{}, run, .{window});
}
var quad_index_buffer: sokol.gfx.Buffer = undefined;
var quad_vertex_buffer: sokol.gfx.Buffer = undefined;
var view_buffer: sokol.gfx.Buffer = undefined;
const vertex_indices = .{
.mesh = 0,
.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);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_FLAGS, ext.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_PROFILE_MASK, ext.SDL_GL_CONTEXT_PROFILE_CORE);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MAJOR_VERSION, 3);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MINOR_VERSION, 3);
result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_DOUBLEBUFFER, 1);
if (result != 0) {
return error.Unsupported;
}
break: configure_and_create ext.SDL_GL_CreateContext(window);
};
defer ext.SDL_GL_DeleteContext(context);
sokol.gfx.setup(.{
.environment = .{
.defaults = .{
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.sample_count = 1,
},
},
.logger = .{
.func = sokol.log.func,
},
});
defer {
sokol.gfx.shutdown();
}
var pools = try Pools.init(coral.heap.allocator);
defer {
pools.deinit();
}
const Vertex = struct {
xy: @Vector(2, f32),
uv: @Vector(2, f32),
};
const quad_indices = [_]u16{0, 1, 2, 0, 2, 3};
const quad_vertices = [_]Vertex{
.{.xy = .{-0.5, -0.5}, .uv = .{0, 1}},
.{.xy = .{0.5, -0.5}, .uv = .{1, 1}},
.{.xy = .{0.5, 0.5}, .uv = .{1, 0}},
.{.xy = .{-0.5, 0.5}, .uv = .{0, 0}},
};
quad_index_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(&quad_indices),
.type = .INDEXBUFFER,
});
quad_vertex_buffer = sokol.gfx.makeBuffer(.{
.data = sokol.gfx.asRange(&quad_vertices),
.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,
});
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;
while (true) {
switch (Work.pending.dequeue()) {
.create_commands => |create_commands| {
const commands = coral.heap.allocator.create(Commands) catch {
_ = create_commands.created.resolve(error.OutOfMemory);
continue;
};
commands.* = Commands.init(coral.heap.allocator);
if (create_commands.created.resolve(commands)) {
if (has_commands_tail) |tail_commands| {
tail_commands.has_next = commands;
commands.has_prev = tail_commands;
} else {
std.debug.assert(has_commands_head == null);
has_commands_head = commands;
}
has_commands_tail = commands;
} else {
coral.heap.allocator.destroy(commands);
}
},
.load_texture => |load| {
const texture = try pools.create_texture(load.desc);
if (!load.loaded.resolve(texture)) {
std.debug.assert(pools.destroy_texture(texture));
}
},
.render_frame => |render_frame| {
var frame = Frame{
.swapchain = .{
.width = render_frame.width,
.height = render_frame.height,
.sample_count = 1,
.color_format = .RGBA8,
.depth_format = .DEPTH_STENCIL,
.gl = .{.framebuffer = 0},
}
};
sokol.gfx.beginPass(swapchain_pass: {
var pass = sokol.gfx.Pass{
.swapchain = frame.swapchain,
.action = .{
.stencil = .{.load_action = .CLEAR},
.depth = .{.load_action = .CLEAR},
},
};
pass.action.colors[0] = .{
.clear_value = @bitCast(render_frame.clear_color),
.load_action = .CLEAR,
};
break: swapchain_pass pass;
});
var has_commands = has_commands_head;
while (has_commands) |commands| : (has_commands = commands.has_next) {
for (commands.submitted_commands()) |command| {
try command.process(&pools, &frame);
}
if (frame.current_target_texture != .default) {
frame.set_target(&pools, .{
.clear_color = null,
.clear_depth = null,
.clear_stencil = null,
});
}
}
frame.flush(&pools);
sokol.gfx.endPass();
sokol.gfx.commit();
ext.SDL_GL_SwapWindow(window);
render_frame.finished.set();
},
.rotate_commands => |rotate_commands| {
var has_commands = has_commands_head;
while (has_commands) |commands| : (has_commands = commands.has_next) {
commands.rotate();
}
rotate_commands.finished.set();
},
.shutdown => {
break;
},
.unload_texture => |unload| {
if (!pools.destroy_texture(unload.handle)) {
@panic("Attempt to unload a non-existent texture");
}
},
}
}
}
var work_thread: std.Thread = undefined;

149
src/ona/gfx/resources.zig Normal file
View File

@ -0,0 +1,149 @@
const coral = @import("coral");
const handles = @import("./handles.zig");
const lina = @import("./lina.zig");
const sokol = @import("sokol");
const spirv = @import("./spirv.zig");
const std = @import("std");
pub const Texture = union (enum) {
render: Render,
static: Static,
const Render = struct {
sampler: sokol.gfx.Sampler,
color_image: sokol.gfx.Image,
depth_image: sokol.gfx.Image,
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 {
image: sokol.gfx.Image,
sampler: sokol.gfx.Sampler,
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 {
switch (self.*) {
.static => |*static| static.deinit(),
.render => |*render| render.deinit(),
}
}
pub fn init(desc: handles.Texture.Desc) std.mem.Allocator.Error!Texture {
return switch (desc.access) {
.static => |static| .{.static = try Static.init(desc.format, static)},
.render => |render| .{.render = try Render.init(desc.format, render)},
};
}
};

View File

@ -7,6 +7,10 @@ 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;

View File

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

View File

@ -12,14 +12,22 @@ pub const Error = std.mem.Allocator.Error || error {
UnsupportedTarget,
InvalidSPIRV,
UnsupportedSPIRV,
UnsupportedBackend,
};
pub const Unit = struct {
arena: std.heap.ArenaAllocator,
context: ext.spvc_context,
shader_desc: sokol.gfx.ShaderDesc,
attrs_used: u32 = 0,
pub fn compile(self: *Unit, spirv: []const u32, stage: Stage) Error!void {
pub fn compile(self: *Unit, spirv_data: []const u8, stage: Stage) Error!void {
if ((spirv_data.len % @alignOf(u32)) != 0) {
return error.InvalidSPIRV;
}
const spirv_ops: []const u32 = @alignCast(std.mem.bytesAsSlice(u32, spirv_data));
const execution_model, const stage_desc = switch (stage) {
.vertex => .{ext.SpvExecutionModelVertex, &self.shader_desc.vs},
.fragment => .{ext.SpvExecutionModelFragment, &self.shader_desc.fs},
@ -30,7 +38,7 @@ pub const Unit = struct {
option_values: []const struct {ext.spvc_compiler_option, c_uint},
};
const backend: Backend = switch (sokol.gfx.queryBackend()) {
const backend: Backend = try switch (sokol.gfx.queryBackend()) {
.GLCORE => .{
.target = ext.SPVC_BACKEND_GLSL,
@ -43,13 +51,13 @@ pub const Unit = struct {
},
},
else => @panic("Unimplemented"),
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.ptr, spirv.len, &parsed_ir));
try to_error(ext.spvc_context_parse_spirv(self.context, spirv_ops.ptr, spirv_ops.len, &parsed_ir));
var compiler: ext.spvc_compiler = null;
@ -60,17 +68,14 @@ pub const Unit = struct {
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| {
var name_buffer = [_:0]u8{0} ** 255;
const name = coral.utf8.print_formatted(&name_buffer, "{image_name}_{sampler_name}", .{
.image_name = coral.io.slice_sentineled(u8, 0, ext.spvc_compiler_get_name(compiler, combined_image_sampler.image_id)),
.sampler_name = coral.io.slice_sentineled(u8, 0, ext.spvc_compiler_get_name(compiler, combined_image_sampler.sampler_id)),
}) catch {
return error.InvalidSPIRV;
};
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))),
});
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);
@ -91,7 +96,8 @@ pub const Unit = struct {
break: create resources;
};
try reflect_uniform_blocks(compiler, resources, stage_desc);
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 to_error(ext.spvc_compiler_install_compiler_options(compiler, create: {
@ -109,15 +115,18 @@ pub const Unit = struct {
}));
try to_error(ext.spvc_compiler_compile(compiler, @ptrCast(&stage_desc.source)));
std.log.info("{s}", .{stage_desc.source});
}
pub fn deinit(self: *Unit) void {
ext.spvc_context_destroy(self.context);
self.arena.deinit();
self.* = undefined;
}
pub fn init() std.mem.Allocator.Error!Unit {
pub fn init(allocator: std.mem.Allocator) std.mem.Allocator.Error!Unit {
var context: ext.spvc_context = null;
if (ext.spvc_context_create(&context) != ext.SPVC_SUCCESS) {
@ -129,6 +138,7 @@ pub const Unit = struct {
ext.spvc_context_set_error_callback(context, log_context_errors, null);
return .{
.arena = std.heap.ArenaAllocator.init(allocator),
.context = context,
.shader_desc = .{
@ -144,44 +154,33 @@ pub const Stage = enum {
vertex,
};
pub fn embed_shader(comptime path: []const u8) []const u32 {
const shader = @embedFile(path);
const alignment = @alignOf(u32);
if ((shader.len % alignment) != 0) {
@compileError("size of file contents at " ++ path ++ " must be aligned to a multiple of 4");
}
return @as(*const [shader.len / alignment]u32, @ptrCast(@alignCast(shader)));
}
fn log_context_errors(userdata: ?*anyopaque, error_message: [*c]const u8) callconv(.C) void {
std.debug.assert(userdata == null);
std.log.err("{s}", .{error_message});
}
fn reflect_image_samplers(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage: *sokol.gfx.ShaderStageDesc) Error!void {
var reflected_sampled_images: []const ext.spvc_reflected_resource = &.{};
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(&reflected_sampled_images.ptr),
&reflected_sampled_images.len,
@ptrCast(&sampled_images.ptr),
&sampled_images.len,
));
if (reflected_sampled_images.len > stage.image_sampler_pairs.len) {
if (sampled_images.len > stage_desc.image_sampler_pairs.len) {
return error.UnsupportedSPIRV;
}
for (0 .. reflected_sampled_images.len, reflected_sampled_images) |i, reflected_sampled_image| {
const sampled_image_type = ext.spvc_compiler_get_type_handle(compiler, reflected_sampled_image.type_id);
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.images[i] = .{
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)) {
@ -197,13 +196,13 @@ fn reflect_image_samplers(compiler: ext.spvc_compiler, resources: ext.spvc_resou
.used = true,
};
stage.samplers[i] = .{
stage_desc.samplers[i] = .{
.sampler_type = .DEFAULT,
.used = true,
};
stage.image_sampler_pairs[i] = .{
.glsl_name = ext.spvc_compiler_get_name(compiler, reflected_sampled_image.id),
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,
@ -211,85 +210,56 @@ fn reflect_image_samplers(compiler: ext.spvc_compiler, resources: ext.spvc_resou
}
}
fn reflect_uniform_blocks(compiler: ext.spvc_compiler, resources: ext.spvc_resources, stage: *sokol.gfx.ShaderStageDesc) Error!void {
var reflected_uniform_buffers: []const ext.spvc_reflected_resource = &.{};
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(&reflected_uniform_buffers.ptr),
&reflected_uniform_buffers.len,
@ptrCast(&uniform_buffers.ptr),
&uniform_buffers.len,
));
if (reflected_uniform_buffers.len > stage.uniform_blocks.len) {
return error.UnsupportedSPIRV;
if (uniform_buffers.len != 0) {
return error.InvalidSPIRV;
}
for (stage.uniform_blocks[0 .. reflected_uniform_buffers.len], reflected_uniform_buffers) |*uniform_block, reflected_uniform_buffer| {
const uniform_buffer_type = ext.spvc_compiler_get_type_handle(compiler, reflected_uniform_buffer.type_id);
if (ext.spvc_type_get_basetype(uniform_buffer_type) != ext.SPVC_BASETYPE_STRUCT) {
return error.InvalidSPIRV;
}
const member_count = ext.spvc_type_get_num_member_types(uniform_buffer_type);
if (member_count > uniform_block.uniforms.len) {
return error.UnsupportedSPIRV;
}
try to_error(ext.spvc_compiler_get_declared_struct_size(compiler, uniform_buffer_type, &uniform_block.size));
var uniform_blocks_used: u32 = 0;
while (uniform_blocks_used < member_count) : (uniform_blocks_used += 1) {
const member_type_id = ext.spvc_type_get_member_type(uniform_buffer_type, uniform_blocks_used);
const member_type = ext.spvc_compiler_get_type_handle(compiler, member_type_id);
uniform_block.uniforms[uniform_blocks_used] = .{
// .name = ext.spvc_compiler_get_member_name(compiler, ext.spvc_type_get_base_type_id(uniform_buffer_type), uniform_blocks_used),
.array_count = @intCast(try switch (ext.spvc_type_get_num_array_dimensions(member_type)) {
0 => 0,
1 => switch (ext.spvc_type_array_dimension_is_literal(member_type, 1) != 0) {
true => ext.spvc_type_get_array_dimension(member_type, 1),
false => error.InvalidSPIRV,
},
else => error.InvalidSPIRV,
}),
.type = try switch (ext.spvc_type_get_basetype(member_type)) {
ext.SPVC_BASETYPE_INT32 => switch (ext.spvc_type_get_vector_size(member_type)) {
1 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.INT else error.InvalidSPIRV,
2 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.INT2 else error.InvalidSPIRV,
3 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.INT3 else error.InvalidSPIRV,
4 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.INT4 else error.InvalidSPIRV,
else => error.InvalidSPIRV,
},
ext.SPVC_BASETYPE_FP32 => switch (ext.spvc_type_get_vector_size(member_type)) {
1 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.FLOAT else error.InvalidSPIRV,
2 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.FLOAT2 else error.InvalidSPIRV,
3 => if (ext.spvc_type_get_columns(member_type) == 1) sokol.gfx.UniformType.FLOAT3 else error.InvalidSPIRV,
4 => switch (ext.spvc_type_get_columns(member_type)) {
1 => sokol.gfx.UniformType.FLOAT4,
4 => sokol.gfx.UniformType.MAT4,
else => error.InvalidSPIRV,
},
else => error.InvalidSPIRV,
},
else => error.InvalidSPIRV,
},
};
// uniform_block.uniforms[uniform_blocks_used].name = ext.spvc_compiler_get_member_name(compiler, ext.spvc_type_get_base_type_id(uniform_buffer_type), uniform_blocks_used);
}
}
// TODO: Support for older APIs?
_ = stage_desc;
_ = compiler;
}
fn to_error(result: ext.spvc_result) Error!void {