renderer-mvp/asset-pipeline #53
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,4 +1,4 @@ | ||||
| # Generated assets | ||||
| /zig-cache | ||||
| /.zig-cache | ||||
| /zig-out | ||||
| *.glsl.zig | ||||
|  | ||||
| @ -10,8 +10,8 @@ | ||||
|     }, | ||||
|     .dependencies = .{ | ||||
|         .sokol = .{ | ||||
|             .url = "git+https://github.com/floooh/sokol-zig.git#796a3d3d54c22d20da9e91a9a9120d5423d1e700", | ||||
|             .hash = "12209a187e071d76af00c02865677d170f844376866d062b1b5f82e4ecbd750c4e18", | ||||
|             .url = "git+https://github.com/floooh/sokol-zig.git#7c25767e51aa06dd5fb0684e4a8f2200d182ef27", | ||||
| 			.hash = "1220fa7f47fbaf2f1ed8c17fab2d23b6a85bcbbc4aa0b3802c90a3e8bf6fca1f8569", | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| @ -32,7 +32,7 @@ pub fn create_event(self: *Self, label: []const u8) std.mem.Allocator.Error!Even | ||||
| 
 | ||||
| 	const index = self.event_systems.len(); | ||||
| 
 | ||||
| 	try self.event_systems.push(systems); | ||||
| 	try self.event_systems.push_grow(systems); | ||||
| 
 | ||||
| 	return @enumFromInt(index); | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,7 @@ pub const ascii = @import("./ascii.zig"); | ||||
| 
 | ||||
| pub const dag = @import("./dag.zig"); | ||||
| 
 | ||||
| pub const debug = @import("./debug.zig"); | ||||
| pub const files = @import("./files.zig"); | ||||
| 
 | ||||
| pub const hashes = @import("./hashes.zig"); | ||||
| 
 | ||||
|  | ||||
| @ -21,7 +21,7 @@ pub fn Graph(comptime Payload: type) type { | ||||
| 		pub fn append(self: *Self, payload: Payload) std.mem.Allocator.Error!Node { | ||||
| 			const node = @as(Node, @enumFromInt(self.table.len())); | ||||
| 
 | ||||
| 			try self.table.push(.{ | ||||
| 			try self.table.push_grow(.{ | ||||
| 				.payload = payload, | ||||
| 				.edges = .{.allocator = self.table.allocator}, | ||||
| 			}); | ||||
| @ -83,7 +83,7 @@ pub fn Graph(comptime Payload: type) type { | ||||
| 			}; | ||||
| 
 | ||||
| 			if (slices.index_of(edges.values, 0, edge_node) == null) { | ||||
| 				try edges.push(edge_node); | ||||
| 				try edges.push_grow(edge_node); | ||||
| 			} | ||||
| 
 | ||||
| 			return true; | ||||
|  | ||||
| @ -1,5 +0,0 @@ | ||||
| const std = @import("std"); | ||||
| 
 | ||||
| pub fn assert_ok(error_union: anytype) @typeInfo(@TypeOf(error_union)).ErrorUnion.payload { | ||||
| 	return error_union catch unreachable; | ||||
| } | ||||
							
								
								
									
										124
									
								
								src/coral/files.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/coral/files.zig
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | ||||
| const builtin = @import("builtin"); | ||||
| 
 | ||||
| const io = @import("./io.zig"); | ||||
| 
 | ||||
| const std = @import("std"); | ||||
| 
 | ||||
| pub const Error = error { | ||||
| 	FileNotFound, | ||||
| 	FileInaccessible, | ||||
| }; | ||||
| 
 | ||||
| pub const Stat = struct { | ||||
| 	size: u64, | ||||
| }; | ||||
| 
 | ||||
| pub const Storage = struct { | ||||
| 	userdata: *anyopaque, | ||||
| 	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, | ||||
| 	}; | ||||
| 
 | ||||
| 	pub fn read_bytes(self: Storage, path: []const u8, offset: usize, output: []io.Byte) Error!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 { | ||||
| 		const decoded = (try self.read_native(path, offset, Type)) orelse { | ||||
| 			return null; | ||||
| 		}; | ||||
| 
 | ||||
| 		return switch (@typeInfo(Type)) { | ||||
| 			.Struct => std.mem.byteSwapAllFields(Type, &decoded), | ||||
| 			else => @byteSwap(decoded), | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn read_native(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type { | ||||
| 		var buffer = @as([@sizeOf(Type)]io.Byte, undefined); | ||||
| 
 | ||||
| 		if (try self.vtable.read(self.userdata, path, offset, &buffer) != buffer.len) { | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		return @as(*align(1) const Type, @ptrCast(&buffer)).*; | ||||
| 	} | ||||
| 
 | ||||
| 	pub const read_little = switch (native_endian) { | ||||
| 		.little => read_native, | ||||
| 		.big => read_foreign, | ||||
| 	}; | ||||
| 
 | ||||
| 	pub const read_big = switch (native_endian) { | ||||
| 		.little => read_foreign, | ||||
| 		.big => read_native, | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| pub const bundle = init: { | ||||
| 	const Bundle = struct { | ||||
| 		fn full_path(path: []const u8) Error![4095:0]u8 { | ||||
| 			var buffer = [_:0]u8{0} ** 4095; | ||||
| 
 | ||||
| 			_ = std.fs.cwd().realpath(path, &buffer) catch { | ||||
| 				return error.FileInaccessible; | ||||
| 			}; | ||||
| 
 | ||||
| 			return buffer; | ||||
| 		} | ||||
| 
 | ||||
| 		fn read(_: *anyopaque, path: []const u8, offset: usize, output: []io.Byte) Error!usize { | ||||
| 			var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| { | ||||
| 				return switch (open_error) { | ||||
| 					error.FileNotFound => error.FileNotFound, | ||||
| 					else => error.FileInaccessible, | ||||
| 				}; | ||||
| 			}; | ||||
| 
 | ||||
| 			defer file.close(); | ||||
| 
 | ||||
| 			if (offset != 0) { | ||||
| 				file.seekTo(offset) catch { | ||||
| 					return error.FileInaccessible; | ||||
| 				}; | ||||
| 			} | ||||
| 
 | ||||
| 			return file.read(output) catch error.FileInaccessible; | ||||
| 		} | ||||
| 
 | ||||
| 		fn stat(_: *anyopaque, path: []const u8) Error!Stat { | ||||
| 			const file_stat = get: { | ||||
| 				var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| { | ||||
| 					return switch (open_error) { | ||||
| 						error.FileNotFound => error.FileNotFound, | ||||
| 						else => error.FileInaccessible, | ||||
| 					}; | ||||
| 				}; | ||||
| 
 | ||||
| 				defer file.close(); | ||||
| 
 | ||||
| 				break: get file.stat() catch { | ||||
| 					return error.FileInaccessible; | ||||
| 				}; | ||||
| 			}; | ||||
| 
 | ||||
| 			return .{ | ||||
| 				.size = file_stat.size, | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	break: init Storage{ | ||||
| 		.userdata = undefined, | ||||
| 
 | ||||
| 		.vtable = &.{ | ||||
| 			.stat = Bundle.stat, | ||||
| 			.read = Bundle.read, | ||||
| 		}, | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| const native_endian = builtin.cpu.arch.endian(); | ||||
| @ -8,21 +8,10 @@ const std = @import("std"); | ||||
| 
 | ||||
| pub const Byte = u8; | ||||
| 
					
					kayomn marked this conversation as resolved
					
				 | ||||
| 
 | ||||
| pub const Decoder = coral.io.Functor(coral.io.Error!void, &.{[]coral.Byte}); | ||||
| 
 | ||||
| pub const Error = error { | ||||
| 	UnavailableResource, | ||||
| }; | ||||
| 
 | ||||
| pub fn FixedBuffer(comptime len: usize, comptime default_value: anytype) type { | ||||
| 	const Value = @TypeOf(default_value); | ||||
| 
 | ||||
| 	return struct { | ||||
| 		filled: usize = 0, | ||||
| 		values: [len]Value = [_]Value{default_value} ** len, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| pub fn Functor(comptime Output: type, comptime input_types: []const type) type { | ||||
| 	const InputTuple = std.meta.Tuple(input_types); | ||||
| 
 | ||||
| @ -119,20 +108,6 @@ pub fn Generator(comptime Output: type, comptime input_types: []const type) type | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| pub const NullWritable = struct { | ||||
| 	written: usize = 0, | ||||
| 
 | ||||
| 	pub fn writer(self: *NullWritable) Writer { | ||||
| 		return Writer.bind(NullWritable, self, write); | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn write(self: *NullWritable, buffer: []const u8) !usize { | ||||
| 		self.written += buffer.len; | ||||
| 
 | ||||
| 		return buffer.len; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| pub const PrintError = Error || error { | ||||
| 	IncompleteWrite, | ||||
| }; | ||||
| @ -141,8 +116,6 @@ pub const Reader = Generator(Error!usize, &.{[]coral.Byte}); | ||||
| 
 | ||||
| pub const Writer = Generator(Error!usize, &.{[]const coral.Byte}); | ||||
| 
 | ||||
| const native_endian = builtin.cpu.arch.endian(); | ||||
| 
 | ||||
| pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.Byte { | ||||
| 	const buffer = coral.Stack(coral.Byte){.allocator = allocator}; | ||||
| 
 | ||||
| @ -153,20 +126,6 @@ pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral. | ||||
| 	return buffer.to_allocation(streamed); | ||||
| } | ||||
| 
 | ||||
| pub fn are_equal(a: []const Byte, b: []const Byte) bool { | ||||
| 	if (a.len != b.len) { | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
| 	for (0 .. a.len) |i| { | ||||
| 		if (a[i] != b[i]) { | ||||
| 			return false; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return true; | ||||
| } | ||||
| 
 | ||||
| pub const bits_per_byte = 8; | ||||
| 
 | ||||
| pub fn bytes_of(value: anytype) []const Byte { | ||||
| @ -179,14 +138,6 @@ pub fn bytes_of(value: anytype) []const Byte { | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| pub fn ends_with(haystack: []const Byte, needle: []const Byte) bool { | ||||
| 	if (needle.len > haystack.len) { | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
| 	return are_equal(haystack[haystack.len - needle.len ..], needle); | ||||
| } | ||||
| 
 | ||||
| pub fn print(writer: Writer, utf8: []const u8) PrintError!void { | ||||
| 	if (try writer.yield(.{utf8}) != utf8.len) { | ||||
| 		return error.IncompleteWrite; | ||||
| @ -208,35 +159,6 @@ pub fn skip_n(input: Reader, distance: u64) Error!void { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| pub fn read_foreign(input: Reader, comptime Type: type) Error!Type { | ||||
| 	const decoded = try read_native(input, Type); | ||||
| 
 | ||||
| 	return switch (@typeInfo(input)) { | ||||
| 		.Struct => std.mem.byteSwapAllFields(Type, &decoded), | ||||
| 		else => @byteSwap(decoded), | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| pub fn read_native(input: Reader, comptime Type: type) Error!Type { | ||||
| 	var buffer = @as([@sizeOf(Type)]coral.Byte, undefined); | ||||
| 
 | ||||
| 	if (try input.yield(.{&buffer}) != buffer.len) { | ||||
| 		return error.UnavailableResource; | ||||
| 	} | ||||
| 
 | ||||
| 	return @as(*align(1) const Type, @ptrCast(&buffer)).*; | ||||
| } | ||||
| 
 | ||||
| pub const read_little = switch (native_endian) { | ||||
| 	.little => read_native, | ||||
| 	.big => read_foreign, | ||||
| }; | ||||
| 
 | ||||
| pub const read_big = switch (native_endian) { | ||||
| 	.little => read_foreign, | ||||
| 	.big => read_native, | ||||
| }; | ||||
| 
 | ||||
| pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) { | ||||
| 	var len = @as(usize, 0); | ||||
| 
 | ||||
|  | ||||
| @ -29,6 +29,10 @@ pub fn Sequential(comptime Value: type) type { | ||||
| 		} | ||||
| 
 | ||||
| 		pub fn grow(self: *Self, additional: usize) std.mem.Allocator.Error!void { | ||||
| 			if (additional == 0) { | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			const grown_capacity = self.cap + additional; | ||||
| 			const buffer = try self.allocator.alloc(Value, grown_capacity); | ||||
| 
 | ||||
| @ -87,7 +91,39 @@ pub fn Sequential(comptime Value: type) type { | ||||
| 			return true; | ||||
| 		} | ||||
| 
 | ||||
| 		pub fn push(self: *Self, value: Value) std.mem.Allocator.Error!void { | ||||
| 		pub fn push(self: *Self, value: Value) bool { | ||||
| 			if (self.values.len == self.cap) { | ||||
| 				return false; | ||||
| 			} | ||||
| 
 | ||||
| 			const offset_index = self.values.len; | ||||
| 
 | ||||
| 			self.values = self.values.ptr[0 .. self.values.len + 1]; | ||||
| 
 | ||||
| 			self.values[offset_index] = value; | ||||
| 
 | ||||
| 			return true; | ||||
| 		} | ||||
| 
 | ||||
| 		pub fn push_all(self: *Self, values: []const Value) bool { | ||||
| 			const new_length = self.values.len + values.len; | ||||
| 
 | ||||
| 			if (new_length > self.cap) { | ||||
| 				return false; | ||||
| 			} | ||||
| 
 | ||||
| 			const offset_index = self.values.len; | ||||
| 
 | ||||
| 			self.values = self.values.ptr[0 .. new_length]; | ||||
| 
 | ||||
| 			for (0 .. values.len) |index| { | ||||
| 				self.values[offset_index + index] = values[index]; | ||||
| 			} | ||||
| 
 | ||||
| 			return true; | ||||
| 		} | ||||
| 
 | ||||
| 		pub fn push_grow(self: *Self, value: Value) std.mem.Allocator.Error!void { | ||||
| 			if (self.values.len == self.cap) { | ||||
| 				try self.grow(@max(1, self.cap)); | ||||
| 			} | ||||
| @ -99,27 +135,11 @@ pub fn Sequential(comptime Value: type) type { | ||||
| 			self.values[offset_index] = value; | ||||
| 		} | ||||
| 
 | ||||
| 		pub fn push_all(self: *Self, values: []const Value) std.mem.Allocator.Error!void { | ||||
| 			const new_length = self.values.len + values.len; | ||||
| 
 | ||||
| 			if (new_length > self.cap) { | ||||
| 				try self.grow(new_length); | ||||
| 			} | ||||
| 
 | ||||
| 			const offset_index = self.values.len; | ||||
| 
 | ||||
| 			self.values = self.values.ptr[0 .. new_length]; | ||||
| 
 | ||||
| 			for (0 .. values.len) |index| { | ||||
| 				self.values[offset_index + index] = values[index]; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		pub fn push_many(self: *Self, n: usize, value: Value) std.mem.Allocator.Error!void { | ||||
| 			const new_length = self.values.len + n; | ||||
| 
 | ||||
| 			if (new_length > self.cap) { | ||||
| 				try self.grow(new_length); | ||||
| 				return false; | ||||
| 			} | ||||
| 
 | ||||
| 			const offset_index = self.values.len; | ||||
| @ -129,6 +149,8 @@ pub fn Sequential(comptime Value: type) type { | ||||
| 			for (0 .. n) |index| { | ||||
| 				self.values[offset_index + index] = value; | ||||
| 			} | ||||
| 
 | ||||
| 			return true; | ||||
| 		} | ||||
| 
 | ||||
| 		pub fn resize(self: *Self, size: usize, default_value: Value) std.mem.Allocator.Error!void { | ||||
| @ -237,7 +259,7 @@ pub fn Parallel(comptime Value: type) type { | ||||
| 			return self.values.len; | ||||
| 		} | ||||
| 
 | ||||
| 		pub fn push(self: *Self, value: Value) std.mem.Allocator.Error!void { | ||||
| 		pub fn push_grow(self: *Self, value: Value) std.mem.Allocator.Error!void { | ||||
| 			if (self.len() == self.cap) { | ||||
| 				try self.grow(@max(1, self.cap)); | ||||
| 			} | ||||
|  | ||||
| @ -51,7 +51,7 @@ pub const BindContext = struct { | ||||
| 		const id = resource.type_id(Resource); | ||||
| 
 | ||||
| 		if (!self.accesses_resource(.read_write, id)) { | ||||
| 			try self.systems.graph.get_ptr(self.node).?.resource_accesses.push(.{.read_write = id}); | ||||
| 			try self.systems.graph.get_ptr(self.node).?.resource_accesses.push_grow(.{.read_write = id}); | ||||
| 		} | ||||
| 
 | ||||
| 		const read_write_resource_nodes = lazily_create: { | ||||
| @ -65,7 +65,7 @@ pub const BindContext = struct { | ||||
| 		}; | ||||
| 
 | ||||
| 		if (slices.index_of(read_write_resource_nodes.values, 0, self.node) == null) { | ||||
| 			try read_write_resource_nodes.push(self.node); | ||||
| 			try read_write_resource_nodes.push_grow(self.node); | ||||
| 		} | ||||
| 
 | ||||
| 		return value; | ||||
| @ -79,7 +79,7 @@ pub const BindContext = struct { | ||||
| 		const id = resource.type_id(Resource); | ||||
| 
 | ||||
| 		if (!self.accesses_resource(.read_only, id)) { | ||||
| 			try self.systems.graph.get_ptr(self.node).?.resource_accesses.push(.{.read_only = id}); | ||||
| 			try self.systems.graph.get_ptr(self.node).?.resource_accesses.push_grow(.{.read_only = id}); | ||||
| 		} | ||||
| 
 | ||||
| 		const read_only_resource_nodes = lazily_create: { | ||||
| @ -93,7 +93,7 @@ pub const BindContext = struct { | ||||
| 		}; | ||||
| 
 | ||||
| 		if (slices.index_of(read_only_resource_nodes.values, 0, self.node) == null) { | ||||
| 			try read_only_resource_nodes.push(self.node); | ||||
| 			try read_only_resource_nodes.push_grow(self.node); | ||||
| 		} | ||||
| 
 | ||||
| 		return value; | ||||
| @ -291,7 +291,7 @@ pub const Schedule = struct { | ||||
| 						try populate_bundle(bundle, graph, edge); | ||||
| 					} | ||||
| 
 | ||||
| 					try bundle.push(node); | ||||
| 					try bundle.push_grow(node); | ||||
| 				} | ||||
| 
 | ||||
| 				fn sort(schedule: *Schedule) !void { | ||||
| @ -304,7 +304,7 @@ pub const Schedule = struct { | ||||
| 							continue; | ||||
| 						} | ||||
| 
 | ||||
| 						try schedule.parallel_work_bundles.push(.{.allocator = heap.allocator}); | ||||
| 						try schedule.parallel_work_bundles.push_grow(.{.allocator = heap.allocator}); | ||||
| 
 | ||||
| 						const bundle = schedule.parallel_work_bundles.get_ptr().?; | ||||
| 
 | ||||
| @ -328,8 +328,9 @@ pub const Schedule = struct { | ||||
| 								.main => { | ||||
| 									const extracted_work = work.values[index ..]; | ||||
| 
 | ||||
| 									try schedule.blocking_work.push_all(extracted_work); | ||||
| 									try schedule.blocking_work.grow(extracted_work.len); | ||||
| 
 | ||||
| 									std.debug.assert(schedule.blocking_work.push_all(extracted_work)); | ||||
| 									std.debug.assert(work.pop_many(extracted_work.len)); | ||||
| 								}, | ||||
| 							} | ||||
| @ -488,7 +489,7 @@ pub const Schedule = struct { | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		try nodes.push(node); | ||||
| 		try nodes.push_grow(node); | ||||
| 
 | ||||
| 		self.invalidate_work(); | ||||
| 	} | ||||
|  | ||||
| @ -2,8 +2,6 @@ const ascii = @import("./ascii.zig"); | ||||
| 
 | ||||
| const coral = @import("./coral.zig"); | ||||
| 
 | ||||
| const debug = @import("./debug.zig"); | ||||
| 
 | ||||
| const io = @import("./io.zig"); | ||||
| 
 | ||||
| const std = @import("std"); | ||||
| @ -16,7 +14,7 @@ pub fn alloc_formatted(allocator: std.mem.Allocator, comptime format: []const u8 | ||||
| 
 | ||||
| 	errdefer buffer.deinit(); | ||||
| 
 | ||||
| 	debug.assert_try(print_formatted(buffer.writer(), format, args)); | ||||
| 	print_formatted(buffer.writer(), format, args) catch unreachable; | ||||
| 
 | ||||
| 	return buffer.to_allocation(formatted_len, 0); | ||||
| } | ||||
| @ -24,7 +22,7 @@ pub fn alloc_formatted(allocator: std.mem.Allocator, comptime format: []const u8 | ||||
| fn count_formatted(comptime format: []const u8, args: anytype) usize { | ||||
| 	var count = io.defaultWritable{}; | ||||
| 
 | ||||
| 	debug.assert_try(print_formatted(count.writer(), format, args)); | ||||
| 	print_formatted(count.writer(), format, args) catch unreachable; | ||||
| 
 | ||||
| 	return count.written; | ||||
| } | ||||
|  | ||||
							
								
								
									
										37
									
								
								src/main.zig
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								src/main.zig
									
									
									
									
									
								
							| @ -6,6 +6,7 @@ const ona = @import("ona"); | ||||
| 
 | ||||
| const Actors = struct { | ||||
| 	instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator}, | ||||
| 	quad_mesh_2d: ona.gfx.Handle = .none, | ||||
| 	body_texture: ona.gfx.Handle = .none, | ||||
| }; | ||||
| 
 | ||||
| @ -21,45 +22,29 @@ pub fn main() !void { | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| fn load(display: coral.ReadBlocking(ona.gfx.Display), actors: coral.Write(Actors), gfx: ona.gfx.Work) !void { | ||||
| 	display.res.resize(1280, 720); | ||||
| fn load(display: coral.Write(ona.gfx.Display), actors: coral.Write(Actors), assets: coral.Write(ona.gfx.Assets)) !void { | ||||
| 	display.res.width, display.res.height = .{1280, 720}; | ||||
| 	actors.res.body_texture = try assets.res.open_file(coral.files.bundle, "actor.bmp"); | ||||
| 	actors.res.quad_mesh_2d = try assets.res.open_quad_mesh_2d(@splat(1)); | ||||
| 
 | ||||
| 	const crap = [_]u32{ | ||||
| 		0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 0xFF000000, | ||||
| 		0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, | ||||
| 		0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, 0xFF000000, | ||||
| 		0xFF000000, 0xFFFFFFFF, 0xFF000000, 0xFFFFFFFF, | ||||
| 	}; | ||||
| 
 | ||||
| 	actors.res.body_texture = try gfx.queue.open(.{ | ||||
| 		.resource = .{ | ||||
| 			.texture = .{ | ||||
| 				.data = coral.io.bytes_of(&crap), | ||||
| 				.width = 4, | ||||
| 				.access = .static, | ||||
| 				.format = .bgra8888, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}); | ||||
| 
 | ||||
| 	try actors.res.instances.push(.{0, 0}); | ||||
| 	try actors.res.instances.push_grow(.{0, 0}); | ||||
| } | ||||
| 
 | ||||
| fn exit(actors: coral.Write(Actors)) void { | ||||
| 	actors.res.instances.deinit(); | ||||
| } | ||||
| 
 | ||||
| fn render(gfx: ona.gfx.Work, actors: coral.Write(Actors)) !void { | ||||
| fn render(queue: ona.gfx.Queue, actors: coral.Write(Actors)) !void { | ||||
| 	for (actors.res.instances.values) |instance| { | ||||
| 		try gfx.queue.draw(.{ | ||||
| 		try queue.commands.append(.{ | ||||
| 			.instance_2d = .{ | ||||
| 				.mesh_2d = gfx.primitives.quad_mesh, | ||||
| 				.mesh_2d = actors.res.quad_mesh_2d, | ||||
| 				.texture = actors.res.body_texture, | ||||
| 
 | ||||
| 				.transform = .{ | ||||
| 					.origin = instance, | ||||
| 					.xbasis = .{100, 0}, | ||||
| 					.ybasis = .{0, 100}, | ||||
| 					.xbasis = .{64, 0}, | ||||
| 					.ybasis = .{0, 64}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}); | ||||
|  | ||||
							
								
								
									
										274
									
								
								src/ona/gfx.zig
									
									
									
									
									
								
							
							
						
						
									
										274
									
								
								src/ona/gfx.zig
									
									
									
									
									
								
							| @ -1,37 +1,112 @@ | ||||
| const App = @import("./App.zig"); | ||||
| 
 | ||||
| pub const Queue = @import("./gfx/Queue.zig"); | ||||
| 
 | ||||
| const coral = @import("coral"); | ||||
| 
 | ||||
| const Device = @import("./gfx/Device.zig"); | ||||
| const device = @import("./gfx/device.zig"); | ||||
| 
 | ||||
| const ext = @import("./ext.zig"); | ||||
| 
 | ||||
| const formats = @import("./gfx/formats.zig"); | ||||
| 
 | ||||
| const msg = @import("./msg.zig"); | ||||
| 
 | ||||
| const std = @import("std"); | ||||
| 
 | ||||
| pub const Assets = struct { | ||||
| 	context: device.Context, | ||||
| 	formats: coral.stack.Sequential(Format), | ||||
| 	staging_arena: std.heap.ArenaAllocator, | ||||
| 
 | ||||
| 	pub const Format = struct { | ||||
| 		extension: []const u8, | ||||
| 		file_desc: *const fn (*std.heap.ArenaAllocator, coral.files.Storage, []const u8) Error!Desc, | ||||
| 
 | ||||
| 		pub const Error = std.mem.Allocator.Error || coral.files.Error || error { | ||||
| 			FormatUnsupported, | ||||
| 		}; | ||||
| 	}; | ||||
| 
 | ||||
| 	pub fn open_file(self: *Assets, storage: coral.files.Storage, path: []const u8) (OpenError || Format.Error)!Handle { | ||||
| 
					
					kayomn marked this conversation as resolved
					
						
						
							Outdated
						
					
				 
				
					
						kayomn
						commented  Wants to accept the desired storage source as an argument for loading resources from non-bundled locations like user storage or caller-defined storage implementations. Wants to accept the desired storage source as an argument for loading resources from non-bundled locations like user storage or caller-defined storage implementations. | ||||
| 		defer { | ||||
| 			const max_cache_size = 536870912; | ||||
| 
 | ||||
| 			if (!self.staging_arena.reset(.{.retain_with_limit = max_cache_size})) { | ||||
| 				std.log.warn("failed to retain staging arena size of {} bytes", .{max_cache_size}); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		for (self.formats.values) |format| { | ||||
| 			if (!std.mem.endsWith(u8, path, format.extension)) { | ||||
| 				continue; | ||||
| 			} | ||||
| 
 | ||||
| 			return self.context.open(try format.file_desc(&self.staging_arena, storage, path)); | ||||
| 		} | ||||
| 
 | ||||
| 		return .none; | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn open_quad_mesh_2d(self: *Assets, extents: Point2D) OpenError!Handle { | ||||
| 		const width, const height = extents / @as(Point2D, @splat(2)); | ||||
| 
 | ||||
| 		return self.context.open(.{ | ||||
| 			.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 Color = @Vector(4, f32); | ||||
| 
 | ||||
| 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 { | ||||
| 		data: []const coral.io.Byte, | ||||
| 		width: u16, | ||||
| 		format: Format, | ||||
| 		access: Access, | ||||
| 
 | ||||
| 		pub const Access = enum { | ||||
| 			static, | ||||
| 		}; | ||||
| 
 | ||||
| 		pub const Format = enum { | ||||
| 			rgba8, | ||||
| 			bgra8, | ||||
| 
 | ||||
| 			pub fn byte_size(self: Format) usize { | ||||
| 				return switch (self) { | ||||
| 					.rgba8, .bgra8 => 4, | ||||
| 				}; | ||||
| 			} | ||||
| 		}; | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| pub const Display = struct { | ||||
| 	sdl_window: *ext.SDL_Window, | ||||
| 	device: Device, | ||||
| 
 | ||||
| 	pub fn resize(self: Display, width: u16, height: u16) void { | ||||
| 		ext.SDL_SetWindowSize(self.sdl_window, width, height); | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn retitle(self: Display, title: []const u8) void { | ||||
| 		var sentineled_title = [_:0]u8{0} ** 255; | ||||
| 
 | ||||
| 		@memcpy(sentineled_title[0 .. @min(title.len, sentineled_title.len)], title); | ||||
| 		ext.SDL_SetWindowTitle(self.sdl_window, &sentineled_title); | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn set_resizable(self: Display, resizable: bool) void { | ||||
| 		ext.SDL_SetWindowResizable(self.sdl_window, @intFromBool(resizable)); | ||||
| 	} | ||||
| 	width: u16 = 1280, | ||||
| 	height: u16 = 720, | ||||
| 	clear_color: Color = colors.black, | ||||
| }; | ||||
| 
 | ||||
| pub const Handle = enum (usize) { | ||||
| @ -62,71 +137,68 @@ 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(_: coral.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 Transform2D = extern struct { | ||||
| 	xbasis: Point2D = .{1, 0}, | ||||
| 	ybasis: Point2D = .{0, 1}, | ||||
| 	origin: Point2D = @splat(0), | ||||
| }; | ||||
| 
 | ||||
| pub const Work = struct { | ||||
| 	queue: *Queue.Buffer, | ||||
| 	primitives: *const Primitives, | ||||
| 
 | ||||
| 	const Primitives = struct { | ||||
| 		quad_mesh: Handle, | ||||
| 	}; | ||||
| 
 | ||||
| 	pub const State = struct { | ||||
| 		queue: *Queue, | ||||
| 		primitives: *const Primitives, | ||||
| 	}; | ||||
| 
 | ||||
| 	pub fn bind(context: coral.system.BindContext) std.mem.Allocator.Error!State { | ||||
| 		const queue = try Queue.create(); | ||||
| 
 | ||||
| 		return .{ | ||||
| 			.primitives = (try context.register_read_only_resource_access(.none, Primitives)) orelse create: { | ||||
| 				const buffer = queue.pending(); | ||||
| 				const half_extent = 0.5; | ||||
| 
 | ||||
| 				try context.world.set_resource(.none, Primitives{ | ||||
| 					.quad_mesh = try buffer.open(.{ | ||||
| 						.label = "quad mesh primitive", | ||||
| 
 | ||||
| 						.resource = .{ | ||||
| 							.mesh_2d = .{ | ||||
| 								.indices = &.{0, 1, 2, 0, 2, 3}, | ||||
| 
 | ||||
| 								.vertices = &.{ | ||||
| 									.{.xy = .{-half_extent, half_extent}, .uv = .{0, 1}}, | ||||
| 									.{.xy = .{half_extent, half_extent}, .uv = .{1, 1}}, | ||||
| 									.{.xy = .{half_extent, -half_extent}, .uv = .{1, 0}}, | ||||
| 									.{.xy = .{-half_extent, -half_extent},  .uv = .{0, 0}}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}), | ||||
| 				}); | ||||
| 
 | ||||
| 				break: create (try context.register_read_only_resource_access(.none, Primitives)).?; | ||||
| 			}, | ||||
| 
 | ||||
| 			.queue = queue, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn init(state: *State) Work { | ||||
| 		return .{ | ||||
| 			.queue = state.queue.pending(), | ||||
| 			.primitives = state.primitives, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn unbind(state: *State) void { | ||||
| 		state.queue.release(); | ||||
| 	} | ||||
| const builtin_formats = [_]Assets.Format{ | ||||
| 	.{ | ||||
| 		.extension = "bmp", | ||||
| 		.file_desc = formats.bmp_file_desc, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| pub const colors = struct { | ||||
| @ -156,43 +228,45 @@ pub fn poll(app: coral.Write(App), inputs: msg.Send(Input)) !void { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| pub fn setup(world: *coral.World, events: App.Events) (error {SDLError} || std.Thread.SpawnError || std.mem.Allocator.Error)!void { | ||||
| pub fn setup(world: *coral.World, events: App.Events) (error {Unsupported} || std.Thread.SpawnError || std.mem.Allocator.Error)!void { | ||||
| 	if (ext.SDL_Init(ext.SDL_INIT_VIDEO) != 0) { | ||||
| 		return error.SDLError; | ||||
| 		return error.Unsupported; | ||||
| 	} | ||||
| 
 | ||||
| 	const sdl_window = create: { | ||||
| 		const position = ext.SDL_WINDOWPOS_CENTERED; | ||||
| 		const flags = ext.SDL_WINDOW_OPENGL; | ||||
| 		const width = 640; | ||||
| 		const height = 480; | ||||
| 	var context = try device.Context.init(); | ||||
| 
 | ||||
| 		break: create ext.SDL_CreateWindow("Ona", position, position, width, height, flags) orelse { | ||||
| 			return error.SDLError; | ||||
| 		}; | ||||
| 	}; | ||||
| 	errdefer context.deinit(); | ||||
| 
 | ||||
| 	errdefer ext.SDL_DestroyWindow(sdl_window); | ||||
| 	var registered_formats = coral.stack.Sequential(Assets.Format){.allocator = coral.heap.allocator}; | ||||
| 
 | ||||
| 	var device = try Device.init(sdl_window); | ||||
| 	errdefer registered_formats.deinit(); | ||||
| 
 | ||||
| 	errdefer device.deinit(); | ||||
| 	try registered_formats.grow(builtin_formats.len); | ||||
| 	std.debug.assert(registered_formats.push_all(&builtin_formats)); | ||||
| 
 | ||||
| 	try world.set_resource(.main, Display{ | ||||
| 		.device = device, | ||||
| 		.sdl_window = sdl_window, | ||||
| 	try world.set_resource(.none, Assets{ | ||||
| 		.staging_arena = std.heap.ArenaAllocator.init(coral.heap.allocator), | ||||
| 		.formats = registered_formats, | ||||
| 		.context = context, | ||||
| 	}); | ||||
| 
 | ||||
| 	try world.set_resource(.none, Display{}); | ||||
| 	try world.on_event(events.pre_update, coral.system_fn(poll), .{.label = "poll gfx"}); | ||||
| 	try world.on_event(events.exit, coral.system_fn(stop), .{.label = "stop gfx"}); | ||||
| 	try world.on_event(events.finish, coral.system_fn(submit), .{.label = "submit gfx"}); | ||||
| 	try world.on_event(events.finish, coral.system_fn(synchronize), .{.label = "synchronize gfx"}); | ||||
| } | ||||
| 
 | ||||
| pub fn stop(display: coral.WriteBlocking(Display)) void { | ||||
| 	display.res.device.deinit(); | ||||
| 	ext.SDL_DestroyWindow(display.res.sdl_window); | ||||
| pub fn stop(assets: coral.Write(Assets)) void { | ||||
| 	assets.res.staging_arena.deinit(); | ||||
| 	assets.res.formats.deinit(); | ||||
| 	assets.res.context.deinit(); | ||||
| } | ||||
| 
 | ||||
| pub fn submit(display: coral.WriteBlocking(Display)) void { | ||||
| 	display.res.device.submit(); | ||||
| pub fn synchronize(assets: coral.Write(Assets), display: coral.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, | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| @ -1,534 +0,0 @@ | ||||
| const Queue = @import("./Queue.zig"); | ||||
| 
 | ||||
| const coral = @import("coral"); | ||||
| 
 | ||||
| const ext = @import("../ext.zig"); | ||||
| 
 | ||||
| const gfx = @import("../gfx.zig"); | ||||
| 
 | ||||
| const sokol = @import("sokol"); | ||||
| 
 | ||||
| const std = @import("std"); | ||||
| 
 | ||||
| thread: std.Thread, | ||||
| clear_color: gfx.Color = gfx.colors.black, | ||||
| state: *State, | ||||
| 
 | ||||
| const AtomicBool = std.atomic.Value(bool); | ||||
| 
 | ||||
| const Frame = struct { | ||||
| 	width: u16, | ||||
| 	height: u16, | ||||
| 	flushed_instance_2d_count: usize = 0, | ||||
| 	pushed_instance_2d_count: usize = 0, | ||||
| 	mesh_2d: gfx.Handle = .none, | ||||
| 	texture: gfx.Handle = .none, | ||||
| 
 | ||||
| 	fn unflushed_instance_2d_count(self: Frame) usize { | ||||
| 		return self.pushed_instance_2d_count - self.flushed_instance_2d_count; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const Render = struct { | ||||
| 	resources: coral.stack.Sequential(Resource), | ||||
| 	instance_2d_pipeline: sokol.gfx.Pipeline, | ||||
| 	instance_2d_buffers: coral.stack.Sequential(sokol.gfx.Buffer), | ||||
| 
 | ||||
| 	const Instance2D = 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, | ||||
| 		}; | ||||
| 
 | ||||
| 		const instances_per_buffer = 512; | ||||
| 
 | ||||
| 		const shader = @import("./shaders/instance_2d.glsl.zig"); | ||||
| 	}; | ||||
| 
 | ||||
| 	const Resource = union (enum) { | ||||
| 		empty, | ||||
| 		mesh_2d: Mesh2D, | ||||
| 		texture: Texture, | ||||
| 
 | ||||
| 		const Mesh2D = struct { | ||||
| 			index_count: u32, | ||||
| 			vertex_buffer: sokol.gfx.Buffer, | ||||
| 			index_buffer: sokol.gfx.Buffer, | ||||
| 		}; | ||||
| 
 | ||||
| 		const Texture = struct { | ||||
| 			image: sokol.gfx.Image, | ||||
| 			sampler: sokol.gfx.Sampler, | ||||
| 		}; | ||||
| 	}; | ||||
| 
 | ||||
| 	fn deinit(self: *Render) void { | ||||
| 		for (self.instance_2d_buffers.values) |buffer| { | ||||
| 			sokol.gfx.destroyBuffer(buffer); | ||||
| 		} | ||||
| 
 | ||||
| 		self.instance_2d_buffers.deinit(); | ||||
| 		sokol.gfx.destroyPipeline(self.instance_2d_pipeline); | ||||
| 		self.resources.deinit(); | ||||
| 	} | ||||
| 
 | ||||
| 	fn init() Render { | ||||
| 		sokol.gfx.setup(.{ | ||||
| 			.environment = .{ | ||||
| 				.defaults = .{ | ||||
| 					.color_format = .RGBA8, | ||||
| 					.depth_format = .DEPTH_STENCIL, | ||||
| 					.sample_count = 1, | ||||
| 				}, | ||||
| 			}, | ||||
| 
 | ||||
| 			.logger = .{ | ||||
| 				.func = sokol.log.func, | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
| 		return .{ | ||||
| 			.instance_2d_pipeline = sokol.gfx.makePipeline(.{ | ||||
| 				.label = "2D drawing pipeline", | ||||
| 
 | ||||
| 				.layout = .{ | ||||
| 					.attrs = get: { | ||||
| 						var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_mesh_xy] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.mesh, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_mesh_uv] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.mesh, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_xbasis] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_ybasis] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_origin] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_tint] = .{ | ||||
| 							.format = .UBYTE4N, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_depth] = .{ | ||||
| 							.format = .FLOAT, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_rect] = .{ | ||||
| 							.format = .FLOAT4, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						break: get attrs; | ||||
| 					}, | ||||
| 
 | ||||
| 					.buffers = get: { | ||||
| 						var buffers = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8; | ||||
| 
 | ||||
| 						buffers[Instance2D.buffer_indices.instance].step_func = .PER_INSTANCE; | ||||
| 
 | ||||
| 						break: get buffers; | ||||
| 					}, | ||||
| 				}, | ||||
| 
 | ||||
| 				.shader = sokol.gfx.makeShader(Instance2D.shader.draw2dShaderDesc(sokol.gfx.queryBackend())), | ||||
| 				.index_type = .UINT16, | ||||
| 			}), | ||||
| 
 | ||||
| 			.instance_2d_buffers = .{.allocator = coral.heap.allocator}, | ||||
| 			.resources = .{.allocator = coral.heap.allocator}, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	fn insert_resource(self: *Render, handle: gfx.Handle, resource: Resource) !void { | ||||
| 		const handle_index = handle.index() orelse { | ||||
| 			return error.InvalidHandle; | ||||
| 		}; | ||||
| 
 | ||||
| 		const resource_count = self.resources.len(); | ||||
| 
 | ||||
| 		if (handle_index < resource_count) { | ||||
| 			const empty_resource = &self.resources.values[handle_index]; | ||||
| 
 | ||||
| 			if (empty_resource.* != .empty) { | ||||
| 				return error.InvalidHandle; | ||||
| 			} | ||||
| 
 | ||||
| 			empty_resource.* = resource; | ||||
| 		} else { | ||||
| 			if (handle_index != resource_count) { | ||||
| 				return error.InvalidIndex; | ||||
| 			} | ||||
| 
 | ||||
| 			try self.resources.push(resource); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	fn flush_instance_2ds(self: *Render, frame: *Frame) void { | ||||
| 		const unflushed_count = frame.unflushed_instance_2d_count(); | ||||
| 
 | ||||
| 		if (unflushed_count == 0) { | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		sokol.gfx.applyPipeline(self.instance_2d_pipeline); | ||||
| 
 | ||||
| 		sokol.gfx.applyUniforms(.VS, Instance2D.shader.SLOT_Screen, sokol.gfx.asRange(&Instance2D.shader.Screen{ | ||||
| 			.screen_size = .{@floatFromInt(frame.width), @floatFromInt(frame.height)}, | ||||
| 		})); | ||||
| 
 | ||||
| 		const mesh_2d = self.resources.values[frame.mesh_2d.index().?].mesh_2d; | ||||
| 		const texture = self.resources.values[frame.texture.index().?].texture; | ||||
| 
 | ||||
| 		var bindings = sokol.gfx.Bindings{ | ||||
| 			.vertex_buffers = get: { | ||||
| 				var buffers = [_]sokol.gfx.Buffer{.{}} ** 8; | ||||
| 
 | ||||
| 				buffers[Instance2D.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; | ||||
| 				}, | ||||
| 			}, | ||||
| 		}; | ||||
| 
 | ||||
| 		while (frame.flushed_instance_2d_count < frame.pushed_instance_2d_count) { | ||||
| 			const buffer_index = frame.flushed_instance_2d_count / Instance2D.instances_per_buffer; | ||||
| 			const buffer_offset = frame.flushed_instance_2d_count % Instance2D.instances_per_buffer; | ||||
| 			const instances_to_flush = @min(Instance2D.instances_per_buffer - buffer_offset, unflushed_count); | ||||
| 
 | ||||
| 			bindings.vertex_buffers[Instance2D.buffer_indices.instance] = self.instance_2d_buffers.values[buffer_index]; | ||||
| 			bindings.vertex_buffer_offsets[Instance2D.buffer_indices.instance] = @intCast(buffer_offset); | ||||
| 
 | ||||
| 			sokol.gfx.applyBindings(bindings); | ||||
| 			sokol.gfx.draw(0, mesh_2d.index_count, @intCast(instances_to_flush)); | ||||
| 
 | ||||
| 			frame.flushed_instance_2d_count += instances_to_flush; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	fn push_instance_2d(self: *Render, frame: *Frame, command: Queue.DrawCommand.Instance) std.mem.Allocator.Error!void { | ||||
| 		if (command.mesh_2d != frame.mesh_2d or command.texture != frame.texture) { | ||||
| 			self.flush_instance_2ds(frame); | ||||
| 		} | ||||
| 
 | ||||
| 		frame.mesh_2d = command.mesh_2d; | ||||
| 		frame.texture = command.texture; | ||||
| 
 | ||||
| 		const has_filled_buffer = (frame.pushed_instance_2d_count % Instance2D.instances_per_buffer) == 0; | ||||
| 		const pushed_buffer_count = frame.pushed_instance_2d_count / Instance2D.instances_per_buffer; | ||||
| 
 | ||||
| 		if (has_filled_buffer and pushed_buffer_count == self.instance_2d_buffers.len()) { | ||||
| 			const instance_buffer = sokol.gfx.makeBuffer(.{ | ||||
| 				.size = @sizeOf(Instance2D) * Instance2D.instances_per_buffer, | ||||
| 				.usage = .STREAM, | ||||
| 				.label = "2D drawing instance buffer", | ||||
| 			}); | ||||
| 
 | ||||
| 			errdefer sokol.gfx.destroyBuffer(instance_buffer); | ||||
| 
 | ||||
| 			try self.instance_2d_buffers.push(instance_buffer); | ||||
| 		} | ||||
| 
 | ||||
| 		_ = sokol.gfx.appendBuffer(self.instance_2d_buffers.get().?, sokol.gfx.asRange(&Instance2D{ | ||||
| 			.transform = command.transform, | ||||
| 		})); | ||||
| 
 | ||||
| 		frame.pushed_instance_2d_count += 1; | ||||
| 	} | ||||
| 
 | ||||
| 	fn remove_resource(self: *Render, handle: gfx.Handle) ?Resource { | ||||
| 		if (handle.index()) |handle_index| { | ||||
| 			const resource = self.resources.values[handle_index]; | ||||
| 
 | ||||
| 			if (resource != .empty) { | ||||
| 				self.resources.values[handle_index] = .empty; | ||||
| 
 | ||||
| 				return resource; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const Self = @This(); | ||||
| 
 | ||||
| const State = struct { | ||||
| 	finished: std.Thread.Semaphore = .{}, | ||||
| 	is_running: AtomicBool = AtomicBool.init(true), | ||||
| 	ready: std.Thread.Semaphore = .{}, | ||||
| 	clear_color: gfx.Color = gfx.colors.black, | ||||
| }; | ||||
| 
 | ||||
| pub fn deinit(self: *Self) void { | ||||
| 	self.state.is_running.store(false, .monotonic); | ||||
| 	self.state.ready.post(); | ||||
| 	self.thread.join(); | ||||
| 	coral.heap.allocator.destroy(self.state); | ||||
| 
 | ||||
| 	self.* = undefined; | ||||
| } | ||||
| 
 | ||||
| pub fn init(window: *ext.SDL_Window) (std.mem.Allocator.Error || std.Thread.SpawnError)!Self { | ||||
| 	const state = try coral.heap.allocator.create(State); | ||||
| 
 | ||||
| 	errdefer coral.heap.allocator.destroy(state); | ||||
| 
 | ||||
| 	state.* = .{}; | ||||
| 
 | ||||
| 	const thread = try std.Thread.spawn(.{}, run, .{window, state}); | ||||
| 
 | ||||
| 	thread.setName("Ona Graphics") catch { | ||||
| 		std.log.warn("failed to name the graphics thread", .{}); | ||||
| 	}; | ||||
| 
 | ||||
| 	return .{ | ||||
| 		.thread = thread, | ||||
| 		.state = state, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| fn process_close_command(command: Queue.CloseCommand, rendering: *Render) !void { | ||||
| 	const resource = &rendering.resources.values[command.handle.index().?]; | ||||
| 
 | ||||
| 	switch (resource.*) { | ||||
| 		.empty => {}, // TODO: Handle this. | ||||
| 
 | ||||
| 		.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); | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	resource.* = .empty; | ||||
| } | ||||
| 
 | ||||
| fn process_draw_command(command: Queue.DrawCommand, render: *Render, frame: *Frame) !void { | ||||
| 	switch (command) { | ||||
| 		.instance_2d => |instance_2d| { | ||||
| 			try render.push_instance_2d(frame, instance_2d); | ||||
| 		}, | ||||
| 
 | ||||
| 		.post_process => |post_process| { | ||||
| 			render.flush_instance_2ds(frame); | ||||
| 			// sokol.gfx.applyPipeline(self.post_process_pipeline); | ||||
| 
 | ||||
| 			_ = post_process; | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	render.flush_instance_2ds(frame); | ||||
| } | ||||
| 
 | ||||
| fn process_open_command(command: Queue.OpenCommand, render: *Render) !void { | ||||
| 	switch (command.resource) { | ||||
| 		.texture => |texture| { | ||||
| 			const stride = texture.width * texture.format.byte_size(); | ||||
| 
 | ||||
| 			const image = sokol.gfx.makeImage(.{ | ||||
| 				.width = texture.width, | ||||
| 				.height = @intCast(texture.data.len / stride), | ||||
| 
 | ||||
| 				.data = .{ | ||||
| 					.subimage = get: { | ||||
| 						var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6; | ||||
| 
 | ||||
| 						subimage[0][0] = sokol.gfx.asRange(texture.data); | ||||
| 
 | ||||
| 						break: get subimage; | ||||
| 					}, | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
| 			errdefer sokol.gfx.destroyImage(image); | ||||
| 
 | ||||
| 			const sampler = sokol.gfx.makeSampler(.{}); | ||||
| 
 | ||||
| 			errdefer sokol.gfx.destroySampler(sampler); | ||||
| 
 | ||||
| 			try render.insert_resource(command.handle, .{ | ||||
| 				.texture = .{ | ||||
| 					.sampler = sampler, | ||||
| 					.image = image, | ||||
| 				}, | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		.mesh_2d => |mesh_2d| { | ||||
| 			const index_buffer = sokol.gfx.makeBuffer(.{ | ||||
| 				.data = sokol.gfx.asRange(mesh_2d.indices), | ||||
| 				.type = .INDEXBUFFER, | ||||
| 			}); | ||||
| 
 | ||||
| 			const vertex_buffer = sokol.gfx.makeBuffer(.{ | ||||
| 				.data = sokol.gfx.asRange(mesh_2d.vertices), | ||||
| 				.type = .VERTEXBUFFER, | ||||
| 			}); | ||||
| 
 | ||||
| 			errdefer { | ||||
| 				sokol.gfx.destroyBuffer(index_buffer); | ||||
| 				sokol.gfx.destroyBuffer(vertex_buffer); | ||||
| 			} | ||||
| 
 | ||||
| 			if (mesh_2d.indices.len > std.math.maxInt(u32)) { | ||||
| 				return error.OutOfMemory; | ||||
| 			} | ||||
| 
 | ||||
| 			try render.insert_resource(command.handle, .{ | ||||
| 				.mesh_2d = .{ | ||||
| 					.index_buffer = index_buffer, | ||||
| 					.vertex_buffer = vertex_buffer, | ||||
| 					.index_count = @intCast(mesh_2d.indices.len), | ||||
| 				}, | ||||
| 			}); | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| fn run(window: *ext.SDL_Window, state: *State) !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 render = Render.init(); | ||||
| 
 | ||||
| 	defer render.deinit(); | ||||
| 
 | ||||
| 	state.finished.post(); | ||||
| 
 | ||||
| 	while (state.is_running.load(.monotonic)) { | ||||
| 		state.ready.wait(); | ||||
| 
 | ||||
| 		defer state.finished.post(); | ||||
| 
 | ||||
| 		var frame = init_frame: { | ||||
| 			var width, var height = [_]c_int{0, 0}; | ||||
| 
 | ||||
| 			ext.SDL_GL_GetDrawableSize(window, &width, &height); | ||||
| 			std.debug.assert(width > 0 and height > 0); | ||||
| 
 | ||||
| 			break: init_frame Frame{ | ||||
| 				.width = @intCast(width), | ||||
| 				.height = @intCast(height), | ||||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		sokol.gfx.beginPass(.{ | ||||
| 			.swapchain = .{ | ||||
| 				.width = frame.width, | ||||
| 				.height = frame.height, | ||||
| 				.sample_count = 1, | ||||
| 				.color_format = .RGBA8, | ||||
| 				.depth_format = .DEPTH_STENCIL, | ||||
| 				.gl = .{.framebuffer = 0}, | ||||
| 			}, | ||||
| 
 | ||||
| 			.action = .{ | ||||
| 				.colors = get: { | ||||
| 					var actions = [_]sokol.gfx.ColorAttachmentAction{.{}} ** 4; | ||||
| 
 | ||||
| 					actions[0] = .{ | ||||
| 						.load_action = .CLEAR, | ||||
| 						.clear_value = @as(sokol.gfx.Color, @bitCast(state.clear_color)), | ||||
| 					}; | ||||
| 
 | ||||
| 					break: get actions; | ||||
| 				}, | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
| 		try Queue.visit_open_commands(process_open_command, .{&render}); | ||||
| 		try Queue.visit_draw_commands(process_draw_command, .{&render, &frame}); | ||||
| 		try Queue.visit_close_commands(process_close_command, .{&render}); | ||||
| 
 | ||||
| 		sokol.gfx.endPass(); | ||||
| 		sokol.gfx.commit(); | ||||
| 		ext.SDL_GL_SwapWindow(window); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| pub fn submit(self: *Self) void { | ||||
| 	self.state.finished.wait(); | ||||
| 
 | ||||
| 	self.state.clear_color = self.clear_color; | ||||
| 
 | ||||
| 	Queue.swap(); | ||||
| 	self.state.ready.post(); | ||||
| } | ||||
| @ -1,309 +0,0 @@ | ||||
| const coral = @import("coral"); | ||||
| 
 | ||||
| const gfx = @import("../gfx.zig"); | ||||
| 
 | ||||
| const std = @import("std"); | ||||
| 
 | ||||
| buffers: [2]Buffer, | ||||
| is_swapped: bool = false, | ||||
| ref_count: AtomicCount = AtomicCount.init(1), | ||||
| has_next: ?*Self = null, | ||||
| has_prev: ?*Self = null, | ||||
| 
 | ||||
| const AtomicCount = std.atomic.Value(usize); | ||||
| 
 | ||||
| pub const Buffer = struct { | ||||
| 	arena: std.heap.ArenaAllocator, | ||||
| 	closed_handles: coral.stack.Sequential(usize), | ||||
| 	open_commands: coral.stack.Sequential(OpenCommand), | ||||
| 	draw_commands: coral.stack.Sequential(DrawCommand), | ||||
| 	close_commands: coral.stack.Sequential(CloseCommand), | ||||
| 
 | ||||
| 	pub fn clear(self: *Buffer) void { | ||||
| 		self.close_commands.clear(); | ||||
| 		self.draw_commands.clear(); | ||||
| 		self.open_commands.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: *Buffer) void { | ||||
| 		self.arena.deinit(); | ||||
| 		self.closed_handles.deinit(); | ||||
| 		self.open_commands.deinit(); | ||||
| 		self.draw_commands.deinit(); | ||||
| 		self.close_commands.deinit(); | ||||
| 	} | ||||
| 
 | ||||
| 	fn init(allocator: std.mem.Allocator) Buffer { | ||||
| 		return .{ | ||||
| 			.arena = std.heap.ArenaAllocator.init(allocator), | ||||
| 			.closed_handles = .{.allocator = allocator}, | ||||
| 			.open_commands = .{.allocator = allocator}, | ||||
| 			.draw_commands = .{.allocator = allocator}, | ||||
| 			.close_commands = .{.allocator = allocator}, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn draw(self: *Buffer, command: DrawCommand) std.mem.Allocator.Error!void { | ||||
| 		try self.draw_commands.push(switch (command) { | ||||
| 			.instance_2d => |instance_2d| .{.instance_2d = instance_2d}, | ||||
| 			.post_process => |post_process| .{.post_process = post_process}, | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	pub fn open(self: *Buffer, command: OpenCommand) std.mem.Allocator.Error!gfx.Handle { | ||||
| 		const reserved_handle = @as(gfx.Handle, switch (command.handle) { | ||||
| 			.none => @enumFromInt(reserve_handle: { | ||||
| 				if (self.closed_handles.get()) |handle| { | ||||
| 					std.debug.assert(self.closed_handles.pop()); | ||||
| 
 | ||||
| 					break: reserve_handle handle; | ||||
| 				} | ||||
| 
 | ||||
| 				break: reserve_handle next_handle.fetchAdd(1, .monotonic); | ||||
| 			}), | ||||
| 
 | ||||
| 			_ => |handle| handle, | ||||
| 		}); | ||||
| 
 | ||||
| 		std.debug.assert(reserved_handle != .none); | ||||
| 
 | ||||
| 		const arena_allocator = self.arena.allocator(); | ||||
| 
 | ||||
| 		try self.open_commands.push(.{ | ||||
| 			.resource = switch (command.resource) { | ||||
| 				.texture => |texture| .{ | ||||
| 					.texture = .{ | ||||
| 						.data = try arena_allocator.dupe(coral.io.Byte, texture.data), | ||||
| 						.width = texture.width, | ||||
| 						.format = texture.format, | ||||
| 						.access = texture.access, | ||||
| 					}, | ||||
| 				}, | ||||
| 
 | ||||
| 				.mesh_2d => |mesh_2d| .{ | ||||
| 					.mesh_2d = .{ | ||||
| 						.indices = try arena_allocator.dupe(u16, mesh_2d.indices), | ||||
| 						.vertices = try arena_allocator.dupe(OpenCommand.Mesh2D.Vertex, mesh_2d.vertices), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 
 | ||||
| 			.label = if (command.label) |label| try arena_allocator.dupe(coral.io.Byte, label) else null, | ||||
| 			.handle = reserved_handle, | ||||
| 		}); | ||||
| 
 | ||||
| 		return reserved_handle; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| pub const CloseCommand = struct { | ||||
| 	handle: gfx.Handle, | ||||
| }; | ||||
| 
 | ||||
| pub const DrawCommand = union (enum) { | ||||
| 	instance_2d: Instance, | ||||
| 	post_process: PostProcess, | ||||
| 
 | ||||
| 	pub const Instance = struct { | ||||
| 		texture: gfx.Handle, | ||||
| 		mesh_2d: gfx.Handle, | ||||
| 		transform: gfx.Transform2D, | ||||
| 	}; | ||||
| 
 | ||||
| 	pub const PostProcess = struct { | ||||
| 
 | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| pub const OpenCommand = struct { | ||||
| 	handle: gfx.Handle = .none, | ||||
| 	label: ?[]const u8 = null, | ||||
| 
 | ||||
| 	resource: union (enum) { | ||||
| 		texture: Texture, | ||||
| 		mesh_2d: Mesh2D, | ||||
| 	}, | ||||
| 
 | ||||
| 	pub const Mesh2D = struct { | ||||
| 		vertices: []const Vertex, | ||||
| 		indices: []const u16, | ||||
| 
 | ||||
| 		pub const Vertex = struct { | ||||
| 			xy: gfx.Point2D, | ||||
| 			uv: gfx.Point2D, | ||||
| 		}; | ||||
| 	}; | ||||
| 
 | ||||
| 	pub const Texture = struct { | ||||
| 		data: []const coral.io.Byte, | ||||
| 		width: u16, | ||||
| 		format: Format, | ||||
| 		access: Access, | ||||
| 
 | ||||
| 		pub const Access = enum { | ||||
| 			static, | ||||
| 		}; | ||||
| 
 | ||||
| 		pub const Format = enum { | ||||
| 			rgba8888, | ||||
| 			bgra8888, | ||||
| 			argb8888, | ||||
| 			rgb888, | ||||
| 			bgr888, | ||||
| 
 | ||||
| 			pub fn byte_size(self: Format) usize { | ||||
| 				return switch (self) { | ||||
| 					.rgba8888, .bgra8888, .argb8888 => 4, | ||||
| 					.rgb888, .bgr888 => 3, | ||||
| 				}; | ||||
| 			} | ||||
| 		}; | ||||
| 	}; | ||||
| }; | ||||
| 
 | ||||
| pub fn acquire(self: *Self) void { | ||||
| 	self.ref_count.fetchAdd(1, .monotonic); | ||||
| } | ||||
| 
 | ||||
| pub fn create() std.mem.Allocator.Error!*Self { | ||||
| 	const queue = try coral.heap.allocator.create(Self); | ||||
| 
 | ||||
| 	errdefer coral.heap.allocator.destroy(queue); | ||||
| 
 | ||||
| 	queue.* = .{ | ||||
| 		.buffers = .{Buffer.init(coral.heap.allocator), Buffer.init(coral.heap.allocator)}, | ||||
| 	}; | ||||
| 
 | ||||
| 	mutex.lock(); | ||||
| 
 | ||||
| 	defer mutex.unlock(); | ||||
| 
 | ||||
| 	if (has_tail) |tail| { | ||||
| 		tail.has_next = queue; | ||||
| 		queue.has_prev = tail; | ||||
| 	} else { | ||||
| 		std.debug.assert(has_head == null); | ||||
| 
 | ||||
| 		has_head = queue; | ||||
| 	} | ||||
| 
 | ||||
| 	has_tail = queue; | ||||
| 
 | ||||
| 	return queue; | ||||
| } | ||||
| 
 | ||||
| pub fn pending(self: *Self) *Buffer { | ||||
| 	return &self.buffers[@intFromBool(self.is_swapped)]; | ||||
| } | ||||
| 
 | ||||
| pub fn release(self: *Self) void { | ||||
| 	if (self.ref_count.fetchSub(1, .monotonic) == 1) { | ||||
| 		mutex.lock(); | ||||
| 
 | ||||
| 		defer mutex.unlock(); | ||||
| 
 | ||||
| 		if (self.has_prev) |prev| { | ||||
| 			prev.has_next = self.has_next; | ||||
| 		} else { | ||||
| 			has_head = self.has_next; | ||||
| 		} | ||||
| 
 | ||||
| 		if (self.has_next) |next| { | ||||
| 			next.has_prev = self.has_prev; | ||||
| 		} else { | ||||
| 			has_tail = self.has_prev; | ||||
| 		} | ||||
| 
 | ||||
| 		for (&self.buffers) |*buffer| { | ||||
| 			buffer.deinit(); | ||||
| 		} | ||||
| 
 | ||||
| 		coral.heap.allocator.destroy(self); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| pub fn submitted(self: *Self) *Buffer { | ||||
| 	return &self.buffers[@intFromBool(!self.is_swapped)]; | ||||
| } | ||||
| 
 | ||||
| const Self = @This(); | ||||
| 
 | ||||
| var has_head = @as(?*Self, null); | ||||
| 
 | ||||
| var has_tail = @as(?*Self, null); | ||||
| 
 | ||||
| var mutex = std.Thread.Mutex{}; | ||||
| 
 | ||||
| var next_handle = AtomicCount.init(1); | ||||
| 
 | ||||
| pub fn swap() void { | ||||
| 	mutex.lock(); | ||||
| 
 | ||||
| 	defer mutex.unlock(); | ||||
| 
 | ||||
| 	var has_node = has_head; | ||||
| 
 | ||||
| 	while (has_node) |node| : (has_node = node.has_next) { | ||||
| 		node.is_swapped = !node.is_swapped; | ||||
| 
 | ||||
| 		node.pending().clear(); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| pub fn visit_close_commands(visit: anytype, args: anytype) !void { | ||||
| 	mutex.lock(); | ||||
| 
 | ||||
| 	defer mutex.unlock(); | ||||
| 
 | ||||
| 	var has_node = has_head; | ||||
| 	var iterations = @as(usize, 0); | ||||
| 
 | ||||
| 	while (has_node) |node| : ({ | ||||
| 		has_node = node.has_next; | ||||
| 		iterations += 1; | ||||
| 	}) { | ||||
| 		for (node.submitted().close_commands.values) |command| { | ||||
| 			try @call(.auto, visit, .{command} ++ args); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| pub fn visit_draw_commands(visit: anytype, args: anytype) !void { | ||||
| 	mutex.lock(); | ||||
| 
 | ||||
| 	defer mutex.unlock(); | ||||
| 
 | ||||
| 	var has_node = has_head; | ||||
| 	var iterations = @as(usize, 0); | ||||
| 
 | ||||
| 	while (has_node) |node| : ({ | ||||
| 		has_node = node.has_next; | ||||
| 		iterations += 1; | ||||
| 	}) { | ||||
| 		for (node.submitted().draw_commands.values) |command| { | ||||
| 			try @call(.auto, visit, .{command} ++ args); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| pub fn visit_open_commands(visit: anytype, args: anytype) !void { | ||||
| 	mutex.lock(); | ||||
| 
 | ||||
| 	defer mutex.unlock(); | ||||
| 
 | ||||
| 	var has_node = has_head; | ||||
| 	var iterations = @as(usize, 0); | ||||
| 
 | ||||
| 	while (has_node) |node| : ({ | ||||
| 		has_node = node.has_next; | ||||
| 		iterations += 1; | ||||
| 	}) { | ||||
| 		for (node.submitted().open_commands.values) |command| { | ||||
| 			try @call(.auto, visit, .{command} ++ args); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										80
									
								
								src/ona/gfx/commands.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/ona/gfx/commands.zig
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | ||||
| 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}, | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										691
									
								
								src/ona/gfx/device.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										691
									
								
								src/ona/gfx/device.zig
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,691 @@ | ||||
| const commands = @import("./commands.zig"); | ||||
| 
 | ||||
| const coral = @import("coral"); | ||||
| 
 | ||||
| const ext = @import("../ext.zig"); | ||||
| 
 | ||||
| const gfx = @import("../gfx.zig"); | ||||
| 
 | ||||
| const sokol = @import("sokol"); | ||||
| 
 | ||||
| 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 handle_index = handle.index() orelse { | ||||
| 			return; | ||||
| 		}; | ||||
| 
 | ||||
| 		const close_commands = self.loop.closes.pending(); | ||||
| 
 | ||||
| 		std.debug.assert(close_commands.stack.cap > close_commands.stack.len()); | ||||
| 		close_commands.append(.{.index = handle_index}) catch unreachable; | ||||
| 	} | ||||
| 
 | ||||
| 	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 open(self: *Context, desc: gfx.Desc) gfx.OpenError!gfx.Handle { | ||||
| 		const open_commands = self.loop.opens.pending(); | ||||
| 		const index = self.loop.closed_indices.get() orelse open_commands.stack.len(); | ||||
| 
 | ||||
| 		try open_commands.append(.{ | ||||
| 			.index = index, | ||||
| 			.desc = desc, | ||||
| 		}); | ||||
| 
 | ||||
| 		const pending_closes = self.loop.closes.pending(); | ||||
| 
 | ||||
| 		if (pending_closes.stack.len() == pending_closes.stack.cap) { | ||||
| 			try pending_closes.stack.grow(1); | ||||
| 		} | ||||
| 
 | ||||
| 		_ = self.loop.closed_indices.pop(); | ||||
| 
 | ||||
| 		return @enumFromInt(index + 1); | ||||
| 	} | ||||
| 
 | ||||
| 	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.opens.swap(); | ||||
| 		self.loop.closes.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 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 = &.{}, | ||||
| 	closes: CloseChain = CloseChain.init(coral.heap.allocator), | ||||
| 	opens: OpenChain = OpenChain.init(coral.heap.allocator), | ||||
| 	closed_indices: coral.stack.Sequential(usize) = .{.allocator = coral.heap.allocator}, | ||||
| 
 | ||||
| 	const AtomicBool = std.atomic.Value(bool); | ||||
| 
 | ||||
| 	const CloseCommand = struct { | ||||
| 		index: usize, | ||||
| 	}; | ||||
| 
 | ||||
| 	const OpenCommand = struct { | ||||
| 		index: usize, | ||||
| 		desc: gfx.Desc, | ||||
| 
 | ||||
| 		fn clone(command: OpenCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!OpenCommand { | ||||
| 			const allocator = arena.allocator(); | ||||
| 
 | ||||
| 			return .{ | ||||
| 				.desc = switch (command.desc) { | ||||
| 					.texture => |texture| .{ | ||||
| 						.texture = .{ | ||||
| 							.data = try allocator.dupe(coral.io.Byte, texture.data), | ||||
| 							.width = texture.width, | ||||
| 							.format = texture.format, | ||||
| 							.access = texture.access, | ||||
| 						}, | ||||
| 					}, | ||||
| 
 | ||||
| 					.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), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 
 | ||||
| 				.index = command.index, | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const CloseChain = commands.Chain(CloseCommand, null); | ||||
| 
 | ||||
| 	const OpenChain = commands.Chain(OpenCommand, OpenCommand.clone); | ||||
| 
 | ||||
| 	fn deinit(self: *Loop) void { | ||||
| 		self.closes.deinit(); | ||||
| 		self.opens.deinit(); | ||||
| 		self.closed_indices.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 = Rendering.init(); | ||||
| 
 | ||||
| 		defer rendering.deinit(); | ||||
| 
 | ||||
| 		self.finished.post(); | ||||
| 
 | ||||
| 		while (self.is_running.load(.monotonic)) { | ||||
| 			self.ready.wait(); | ||||
| 
 | ||||
| 			defer self.finished.post(); | ||||
| 
 | ||||
| 			const open_commands = self.opens.submitted(); | ||||
| 
 | ||||
| 			defer open_commands.clear(); | ||||
| 
 | ||||
| 			for (open_commands.stack.values) |command| { | ||||
| 				switch (command.desc) { | ||||
| 					.texture => |texture| { | ||||
| 						const stride = texture.width * texture.format.byte_size(); | ||||
| 
 | ||||
| 						const image = sokol.gfx.makeImage(.{ | ||||
| 							.width = texture.width, | ||||
| 							.height = @intCast(texture.data.len / stride), | ||||
| 
 | ||||
| 							.data = .{ | ||||
| 								.subimage = get: { | ||||
| 									var subimage = [_][16]sokol.gfx.Range{.{.{}} ** 16} ** 6; | ||||
| 
 | ||||
| 									subimage[0][0] = sokol.gfx.asRange(texture.data); | ||||
| 
 | ||||
| 									break: get subimage; | ||||
| 								}, | ||||
| 							}, | ||||
| 
 | ||||
| 							.pixel_format = switch (texture.format) { | ||||
| 								.rgba8 => .RGBA8, | ||||
| 								.bgra8 => .BGRA8, | ||||
| 							}, | ||||
| 						}); | ||||
| 
 | ||||
| 						errdefer sokol.gfx.destroyImage(image); | ||||
| 
 | ||||
| 						const sampler = sokol.gfx.makeSampler(.{}); | ||||
| 
 | ||||
| 						errdefer sokol.gfx.destroySampler(sampler); | ||||
| 
 | ||||
| 						try rendering.insert_object(command.index, .{ | ||||
| 							.texture = .{ | ||||
| 								.sampler = sampler, | ||||
| 								.image = image, | ||||
| 							}, | ||||
| 						}); | ||||
| 					}, | ||||
| 
 | ||||
| 					.mesh_2d => |mesh_2d| { | ||||
| 						const index_buffer = sokol.gfx.makeBuffer(.{ | ||||
| 							.data = sokol.gfx.asRange(mesh_2d.indices), | ||||
| 							.type = .INDEXBUFFER, | ||||
| 						}); | ||||
| 
 | ||||
| 						const vertex_buffer = sokol.gfx.makeBuffer(.{ | ||||
| 							.data = sokol.gfx.asRange(mesh_2d.vertices), | ||||
| 							.type = .VERTEXBUFFER, | ||||
| 						}); | ||||
| 
 | ||||
| 						errdefer { | ||||
| 							sokol.gfx.destroyBuffer(index_buffer); | ||||
| 							sokol.gfx.destroyBuffer(vertex_buffer); | ||||
| 						} | ||||
| 
 | ||||
| 						if (mesh_2d.indices.len > std.math.maxInt(u32)) { | ||||
| 							return error.OutOfMemory; | ||||
| 						} | ||||
| 
 | ||||
| 						try rendering.insert_object(command.index, .{ | ||||
| 							.mesh_2d = .{ | ||||
| 								.index_buffer = index_buffer, | ||||
| 								.vertex_buffer = vertex_buffer, | ||||
| 								.index_count = @intCast(mesh_2d.indices.len), | ||||
| 							}, | ||||
| 						}); | ||||
| 					}, | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			var frame = init_frame: { | ||||
| 				var width, var height = [_]c_int{0, 0}; | ||||
| 
 | ||||
| 				ext.SDL_GL_GetDrawableSize(window, &width, &height); | ||||
| 				std.debug.assert(width > 0 and height > 0); | ||||
| 
 | ||||
| 				break: init_frame Rendering.Frame{ | ||||
| 					.width = @intCast(width), | ||||
| 					.height = @intCast(height), | ||||
| 				}; | ||||
| 			}; | ||||
| 
 | ||||
| 			sokol.gfx.beginPass(.{ | ||||
| 				.swapchain = .{ | ||||
| 					.width = frame.width, | ||||
| 					.height = frame.height, | ||||
| 					.sample_count = 1, | ||||
| 					.color_format = .RGBA8, | ||||
| 					.depth_format = .DEPTH_STENCIL, | ||||
| 					.gl = .{.framebuffer = 0}, | ||||
| 				}, | ||||
| 
 | ||||
| 				.action = .{ | ||||
| 					.colors = get: { | ||||
| 						var actions = [_]sokol.gfx.ColorAttachmentAction{.{}} ** 4; | ||||
| 
 | ||||
| 						actions[0] = .{ | ||||
| 							.load_action = .CLEAR, | ||||
| 							.clear_value = @as(sokol.gfx.Color, @bitCast(self.clear_color)), | ||||
| 						}; | ||||
| 
 | ||||
| 						break: get actions; | ||||
| 					}, | ||||
| 				}, | ||||
| 			}); | ||||
| 
 | ||||
| 			for (self.renders) |*render| { | ||||
| 				const render_commands = render.submitted(); | ||||
| 
 | ||||
| 				defer render_commands.clear(); | ||||
| 
 | ||||
| 				for (render_commands.stack.values) |command| { | ||||
| 					switch (command) { | ||||
| 						.instance_2d => |instance_2d| { | ||||
| 							try rendering.push_instance_2d(&frame, instance_2d); | ||||
| 						}, | ||||
| 
 | ||||
| 						.post_process => |post_process| { | ||||
| 							rendering.flush_instance_2ds(&frame); | ||||
| 							// sokol.gfx.applyPipeline(self.post_process_pipeline); | ||||
| 
 | ||||
| 							_ = post_process; | ||||
| 						}, | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			rendering.flush_instance_2ds(&frame); | ||||
| 			sokol.gfx.endPass(); | ||||
| 			sokol.gfx.commit(); | ||||
| 			ext.SDL_GL_SwapWindow(window); | ||||
| 
 | ||||
| 			const close_commands = self.closes.submitted(); | ||||
| 
 | ||||
| 			defer close_commands.clear(); | ||||
| 
 | ||||
| 			for (close_commands.stack.values) |command| { | ||||
| 				const object = &rendering.objects.values[command.index]; | ||||
| 
 | ||||
| 				switch (object.*) { | ||||
| 					.empty => {}, // TODO: Handle double-closes. | ||||
| 
 | ||||
| 					.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); | ||||
| 					}, | ||||
| 				} | ||||
| 
 | ||||
| 				object.* = .empty; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| const Rendering = struct { | ||||
| 	objects: coral.stack.Sequential(Object), | ||||
| 	instance_2d_pipeline: sokol.gfx.Pipeline, | ||||
| 	instance_2d_buffers: coral.stack.Sequential(sokol.gfx.Buffer), | ||||
| 
 | ||||
| 	const Instance2D = 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, | ||||
| 		}; | ||||
| 
 | ||||
| 		const instances_per_buffer = 512; | ||||
| 
 | ||||
| 		const shader = @import("./shaders/instance_2d.glsl.zig"); | ||||
| 	}; | ||||
| 
 | ||||
| 	const Frame = struct { | ||||
| 		width: u16, | ||||
| 		height: u16, | ||||
| 		flushed_instance_2d_count: usize = 0, | ||||
| 		pushed_instance_2d_count: usize = 0, | ||||
| 		mesh_2d: gfx.Handle = .none, | ||||
| 		texture: gfx.Handle = .none, | ||||
| 
 | ||||
| 		fn unflushed_instance_2d_count(self: Frame) usize { | ||||
| 			return self.pushed_instance_2d_count - self.flushed_instance_2d_count; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const Object = union (enum) { | ||||
| 		empty, | ||||
| 
 | ||||
| 		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, | ||||
| 		}, | ||||
| 	}; | ||||
| 
 | ||||
| 	fn deinit(self: *Rendering) void { | ||||
| 		for (self.instance_2d_buffers.values) |buffer| { | ||||
| 			sokol.gfx.destroyBuffer(buffer); | ||||
| 		} | ||||
| 
 | ||||
| 		self.instance_2d_buffers.deinit(); | ||||
| 		sokol.gfx.destroyPipeline(self.instance_2d_pipeline); | ||||
| 		self.objects.deinit(); | ||||
| 	} | ||||
| 
 | ||||
| 	fn init() Rendering { | ||||
| 		sokol.gfx.setup(.{ | ||||
| 			.environment = .{ | ||||
| 				.defaults = .{ | ||||
| 					.color_format = .RGBA8, | ||||
| 					.depth_format = .DEPTH_STENCIL, | ||||
| 					.sample_count = 1, | ||||
| 				}, | ||||
| 			}, | ||||
| 
 | ||||
| 			.logger = .{ | ||||
| 				.func = sokol.log.func, | ||||
| 			}, | ||||
| 		}); | ||||
| 
 | ||||
| 		return .{ | ||||
| 			.instance_2d_pipeline = sokol.gfx.makePipeline(.{ | ||||
| 				.label = "2D drawing pipeline", | ||||
| 
 | ||||
| 				.layout = .{ | ||||
| 					.attrs = get: { | ||||
| 						var attrs = [_]sokol.gfx.VertexAttrState{.{}} ** 16; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_mesh_xy] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.mesh, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_mesh_uv] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.mesh, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_xbasis] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_ybasis] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_origin] = .{ | ||||
| 							.format = .FLOAT2, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_tint] = .{ | ||||
| 							.format = .UBYTE4N, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_depth] = .{ | ||||
| 							.format = .FLOAT, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						attrs[Instance2D.shader.ATTR_vs_instance_rect] = .{ | ||||
| 							.format = .FLOAT4, | ||||
| 							.buffer_index = Instance2D.buffer_indices.instance, | ||||
| 						}; | ||||
| 
 | ||||
| 						break: get attrs; | ||||
| 					}, | ||||
| 
 | ||||
| 					.buffers = get: { | ||||
| 						var buffers = [_]sokol.gfx.VertexBufferLayoutState{.{}} ** 8; | ||||
| 
 | ||||
| 						buffers[Instance2D.buffer_indices.instance].step_func = .PER_INSTANCE; | ||||
| 
 | ||||
| 						break: get buffers; | ||||
| 					}, | ||||
| 				}, | ||||
| 
 | ||||
| 				.shader = sokol.gfx.makeShader(Instance2D.shader.draw2dShaderDesc(sokol.gfx.queryBackend())), | ||||
| 				.index_type = .UINT16, | ||||
| 			}), | ||||
| 
 | ||||
| 			.instance_2d_buffers = .{.allocator = coral.heap.allocator}, | ||||
| 			.objects = .{.allocator = coral.heap.allocator}, | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	fn flush_instance_2ds(self: *Rendering, frame: *Frame) void { | ||||
| 		const unflushed_count = frame.unflushed_instance_2d_count(); | ||||
| 
 | ||||
| 		if (unflushed_count == 0) { | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		sokol.gfx.applyPipeline(self.instance_2d_pipeline); | ||||
| 
 | ||||
| 		sokol.gfx.applyUniforms(.VS, Instance2D.shader.SLOT_Screen, sokol.gfx.asRange(&Instance2D.shader.Screen{ | ||||
| 			.screen_size = .{@floatFromInt(frame.width), @floatFromInt(frame.height)}, | ||||
| 		})); | ||||
| 
 | ||||
| 		const mesh_2d = self.objects.values[frame.mesh_2d.index().?].mesh_2d; | ||||
| 		const texture = self.objects.values[frame.texture.index().?].texture; | ||||
| 
 | ||||
| 		var bindings = sokol.gfx.Bindings{ | ||||
| 			.vertex_buffers = get: { | ||||
| 				var buffers = [_]sokol.gfx.Buffer{.{}} ** 8; | ||||
| 
 | ||||
| 				buffers[Instance2D.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; | ||||
| 				}, | ||||
| 			}, | ||||
| 		}; | ||||
| 
 | ||||
| 		while (frame.flushed_instance_2d_count < frame.pushed_instance_2d_count) { | ||||
| 			const buffer_index = frame.flushed_instance_2d_count / Instance2D.instances_per_buffer; | ||||
| 			const buffer_offset = frame.flushed_instance_2d_count % Instance2D.instances_per_buffer; | ||||
| 			const instances_to_flush = @min(Instance2D.instances_per_buffer - buffer_offset, unflushed_count); | ||||
| 
 | ||||
| 			bindings.vertex_buffers[Instance2D.buffer_indices.instance] = self.instance_2d_buffers.values[buffer_index]; | ||||
| 			bindings.vertex_buffer_offsets[Instance2D.buffer_indices.instance] = @intCast(buffer_offset); | ||||
| 
 | ||||
| 			sokol.gfx.applyBindings(bindings); | ||||
| 			sokol.gfx.draw(0, mesh_2d.index_count, @intCast(instances_to_flush)); | ||||
| 
 | ||||
| 			frame.flushed_instance_2d_count += instances_to_flush; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	fn insert_object(self: *Rendering, index: usize, object: Object) !void { | ||||
| 		const resource_count = self.objects.len(); | ||||
| 
 | ||||
| 		if (index < resource_count) { | ||||
| 			const empty_object = &self.objects.values[index]; | ||||
| 
 | ||||
| 			if (empty_object.* != .empty) { | ||||
| 				return error.InvalidHandle; | ||||
| 			} | ||||
| 
 | ||||
| 			empty_object.* = object; | ||||
| 		} else { | ||||
| 			if (index != resource_count) { | ||||
| 				return error.InvalidIndex; | ||||
| 			} | ||||
| 
 | ||||
| 			try self.objects.push_grow(object); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	fn push_instance_2d(self: *Rendering, frame: *Frame, command: RenderCommand.Instance) std.mem.Allocator.Error!void { | ||||
| 		if (command.mesh_2d != frame.mesh_2d or command.texture != frame.texture) { | ||||
| 			self.flush_instance_2ds(frame); | ||||
| 		} | ||||
| 
 | ||||
| 		frame.mesh_2d = command.mesh_2d; | ||||
| 		frame.texture = command.texture; | ||||
| 
 | ||||
| 		const has_filled_buffer = (frame.pushed_instance_2d_count % Instance2D.instances_per_buffer) == 0; | ||||
| 		const pushed_buffer_count = frame.pushed_instance_2d_count / Instance2D.instances_per_buffer; | ||||
| 
 | ||||
| 		if (has_filled_buffer and pushed_buffer_count == self.instance_2d_buffers.len()) { | ||||
| 			const instance_buffer = sokol.gfx.makeBuffer(.{ | ||||
| 				.size = @sizeOf(Instance2D) * Instance2D.instances_per_buffer, | ||||
| 				.usage = .STREAM, | ||||
| 				.label = "2D drawing instance buffer", | ||||
| 			}); | ||||
| 
 | ||||
| 			errdefer sokol.gfx.destroyBuffer(instance_buffer); | ||||
| 
 | ||||
| 			try self.instance_2d_buffers.push_grow(instance_buffer); | ||||
| 		} | ||||
| 
 | ||||
| 		_ = sokol.gfx.appendBuffer(self.instance_2d_buffers.get().?, sokol.gfx.asRange(&Instance2D{ | ||||
| 			.transform = command.transform, | ||||
| 		})); | ||||
| 
 | ||||
| 		frame.pushed_instance_2d_count += 1; | ||||
| 	} | ||||
| 
 | ||||
| 	fn remove_object(self: *Rendering, index: usize) ?Object { | ||||
| 		const object = self.objects.values[index]; | ||||
| 
 | ||||
| 		if (object != .empty) { | ||||
| 			self.objects.values[index] = .empty; | ||||
| 
 | ||||
| 			return object; | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| pub const RenderCommand = union (enum) { | ||||
| 	instance_2d: Instance, | ||||
| 	post_process: PostProcess, | ||||
| 
 | ||||
| 	pub const Instance = struct { | ||||
| 		texture: gfx.Handle, | ||||
| 		mesh_2d: gfx.Handle, | ||||
| 		transform: gfx.Transform2D, | ||||
| 	}; | ||||
| 
 | ||||
| 	pub const PostProcess = struct { | ||||
| 
 | ||||
| 	}; | ||||
| 
 | ||||
| 	fn clone(self: RenderCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!RenderCommand { | ||||
| 		_ = arena; | ||||
| 
 | ||||
| 		return switch (self) { | ||||
| 			.instance_2d => |instance_2d| .{.instance_2d = instance_2d}, | ||||
| 			.post_process => |post_process| .{.post_process = post_process}, | ||||
| 		}; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| pub const RenderChain = commands.Chain(RenderCommand, RenderCommand.clone); | ||||
| 
 | ||||
| pub const RenderList = commands.List(RenderCommand, RenderCommand.clone); | ||||
							
								
								
									
										81
									
								
								src/ona/gfx/formats.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/ona/gfx/formats.zig
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | ||||
| const coral = @import("coral"); | ||||
| 
 | ||||
| const gfx = @import("../gfx.zig"); | ||||
| 
 | ||||
| const std = @import("std"); | ||||
| 
 | ||||
| pub fn bmp_file_desc( | ||||
| 	arena: *std.heap.ArenaAllocator, | ||||
| 	storage: coral.files.Storage, | ||||
| 	path: []const u8, | ||||
| ) gfx.Assets.Format.Error!gfx.Desc { | ||||
| 	const header = try storage.read_little(path, 0, extern struct { | ||||
| 		type: [2]u8 align (1), | ||||
| 		file_size: u32 align (1), | ||||
| 		reserved: [2]u16 align (1), | ||||
| 		image_offset: u32 align (1), | ||||
| 		header_size: u32 align (1), | ||||
| 		pixel_width: i32 align (1), | ||||
| 		pixel_height: i32 align (1), | ||||
| 		color_planes: u16 align (1), | ||||
| 		bits_per_pixel: u16 align (1), | ||||
| 		compression_method: u32 align (1), | ||||
| 		image_size: u32 align(1), | ||||
| 		pixels_per_meter_x: i32 align (1), | ||||
| 		pixels_per_meter_y: i32 align (1), | ||||
| 		palette_colors_used: u32 align (1), | ||||
| 		important_colors_used: u32 align (1), | ||||
| 	}) orelse { | ||||
| 		return error.FormatUnsupported; | ||||
| 	}; | ||||
| 
 | ||||
| 	if (!std.mem.eql(u8, &header.type, "BM")) { | ||||
| 		return error.FormatUnsupported; | ||||
| 	} | ||||
| 
 | ||||
| 	const pixel_width = std.math.cast(u16, header.pixel_width) orelse { | ||||
| 		return error.FormatUnsupported; | ||||
| 	}; | ||||
| 
 | ||||
| 	const pixels = try arena.allocator().alloc(coral.io.Byte, header.image_size); | ||||
| 	const bytes_per_pixel = header.bits_per_pixel / coral.io.bits_per_byte; | ||||
| 	const alignment = 4; | ||||
| 	const byte_stride = pixel_width * bytes_per_pixel; | ||||
| 	const padded_byte_stride = alignment * @divTrunc((byte_stride + alignment - 1), alignment); | ||||
| 	const byte_padding = coral.scalars.sub(padded_byte_stride, byte_stride) orelse 0; | ||||
| 	var buffer_offset: usize = 0; | ||||
| 	var file_offset = @as(usize, header.image_offset); | ||||
| 
 | ||||
| 	switch (header.bits_per_pixel) { | ||||
| 		32 => { | ||||
| 			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) { | ||||
| 					return error.FormatUnsupported; | ||||
| 				} | ||||
| 
 | ||||
| 				for (0 .. pixel_width) |i| { | ||||
| 					const line_offset = i * 4; | ||||
| 					const pixel = line[line_offset .. line_offset + 4]; | ||||
| 
 | ||||
| 					std.mem.swap(u8, &pixel[0], &pixel[2]); | ||||
| 				} | ||||
| 
 | ||||
| 				file_offset += line.len + byte_padding; | ||||
| 				buffer_offset += padded_byte_stride; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		else => return error.FormatUnsupported, | ||||
| 	} | ||||
| 
 | ||||
| 	return .{ | ||||
| 		.texture = .{ | ||||
| 			.width = pixel_width, | ||||
| 			.data = pixels, | ||||
| 			.format = .rgba8, | ||||
| 			.access = .static, | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
| @ -51,6 +51,10 @@ out vec4 texel; | ||||
| 
 | ||||
| void main() { | ||||
| 	texel = texture(sampler2D(tex, smp), uv) * color; | ||||
| 
 | ||||
| 	if (texel.a == 0) { | ||||
| 		discard; | ||||
| 	} | ||||
| } | ||||
| @end | ||||
| 
 | ||||
|  | ||||
| @ -7,7 +7,7 @@ const std = @import("std"); | ||||
| fn Channel(comptime Message: type) type { | ||||
| 	return struct { | ||||
| 		buffers: [2]coral.stack.Sequential(Message), | ||||
| 		swapped: bool = false, | ||||
| 		swap_index: u1 = 0, | ||||
| 		ticks: u1 = 0, | ||||
| 
 | ||||
| 		const Self = @This(); | ||||
| @ -28,9 +28,9 @@ fn Channel(comptime Message: type) type { | ||||
| 			channel.res.ticks = coral.scalars.add(channel.res.ticks, 1) orelse 0; | ||||
| 
 | ||||
| 			if (channel.res.ticks == 0) { | ||||
| 				channel.res.swapped = !channel.res.swapped; | ||||
| 				channel.res.swap_index ^= 1; | ||||
| 
 | ||||
| 				channel.res.buffers[@intFromBool(channel.res.swapped)].clear(); | ||||
| 				channel.res.buffers[channel.res.swap_index].clear(); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| @ -44,11 +44,11 @@ fn Channel(comptime Message: type) type { | ||||
| 		} | ||||
| 
 | ||||
| 		fn messages(self: Self) []const Message { | ||||
| 			return self.buffers[@intFromBool(!self.swapped)].values; | ||||
| 			return self.buffers[self.swap_index ^ 1].values; | ||||
| 		} | ||||
| 
 | ||||
| 		fn push(self: *Self, message: Message) std.mem.Allocator.Error!void { | ||||
| 			try self.buffers[@intFromBool(self.swapped)].push(message); | ||||
| 			try self.buffers[self.swap_index].push_grow(message); | ||||
| 		} | ||||
| 	}; | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	
Unused addition.