Game now uses execution graph to schedule systems

This commit is contained in:
duck
2025-05-08 21:05:08 +05:00
parent d239e775f3
commit a87d86b335
6 changed files with 134 additions and 41 deletions

View File

@@ -1,48 +1,90 @@
const std = @import("std");
const builtin = @import("builtin");
const sdl = @import("sdl");
const Graph = @import("graph.zig");
const Graphics = @import("graphics.zig");
graphics: Graphics,
running: bool,
// TODO:
// - Do something about deallocating `Resource`s when `Graph` fails
const RunInfo = struct { running: bool };
alloc: std.mem.Allocator,
graph: Graph,
const Self = @This();
pub fn init() GameError!Self {
pub fn init(alloc: std.mem.Allocator) GameError!Self {
var graph = try Graph.init(alloc);
errdefer graph.deinit();
const graphics = try Graphics.create();
var controller = try graph.getController();
controller.addResource(graphics);
try graph.freeController(controller);
return Self{
.graphics = try Graphics.create(),
.running = false,
.alloc = alloc,
.graph = graph,
};
}
pub fn run(self: *Self) GameError!void {
self.running = true;
{
var controller = try self.graph.getController();
controller.addResource(RunInfo{ .running = true });
try self.graph.freeController(controller);
}
while (true) {
try self.processEvents();
if (!self.running) {
if (!self.graph.getResource(RunInfo).?.running) {
break;
}
try self.update();
try self.draw();
var controller = try self.graph.getController();
controller.queue(.{
.events = processEvents,
.draw = draw,
.ordered = true,
});
try self.graph.freeController(controller);
defer self.graph.reset();
try self.graph.runAllSystems();
}
}
fn update(self: *Self) GameError!void {
_ = self;
fn draw(graphics: *Graphics) GameError!void {
try graphics.beginDraw();
{
errdefer graphics.endDraw() catch {};
try graphics.drawDebug();
}
try graphics.endDraw();
}
fn draw(self: *Self) GameError!void {
try self.graphics.beginDraw();
try self.graphics.drawDebug();
try self.graphics.endDraw();
fn clean(graphics: *Graphics) !void {
graphics.destroy();
// TODO: Also remove the resource
}
fn processEvents(self: *Self) GameError!void {
fn processEvents(graphics: *Graphics, run_info: *RunInfo) GameError!void {
sdl.PumpEvents();
while (true) {
var buffer: [16]sdl.Event = undefined;
const count: usize = @intCast(sdl.PeepEvents(&buffer, buffer.len, sdl.GETEVENT, sdl.EVENT_FIRST, sdl.EVENT_LAST));
if (count == -1) return GameError.SdlError;
for (buffer[0..count]) |event| {
self.processEvent(event);
switch (event.type) {
sdl.EVENT_QUIT => {
run_info.running = false;
},
sdl.EVENT_WINDOW_RESIZED => {
if (event.window.windowID != sdl.GetWindowID(graphics.window)) return;
graphics.resize(@intCast(event.window.data1), @intCast(event.window.data2));
},
else => {},
}
}
if (count < buffer.len) {
break;
@@ -51,25 +93,21 @@ fn processEvents(self: *Self) GameError!void {
sdl.FlushEvents(sdl.EVENT_FIRST, sdl.EVENT_LAST);
}
fn processEvent(self: *Self, event: sdl.Event) void {
switch (event.type) {
sdl.EVENT_QUIT => {
self.running = false;
},
sdl.EVENT_WINDOW_RESIZED => {
if (event.window.windowID != sdl.GetWindowID(self.graphics.window)) return;
self.graphics.resize(@intCast(event.window.data1), @intCast(event.window.data2));
},
else => {},
}
}
pub fn deinit(self: *Self) void {
self.graphics.destroy();
var controller = self.graph.getController() catch unreachable;
controller.queue(clean);
self.graph.freeController(controller) catch unreachable;
self.graph.runAllSystems() catch unreachable;
self.graph.deinit();
sdl.Quit();
}
pub const GameError = error{
SdlError,
OSError,
OutOfMemory,
MissingResource,
SystemDeadlock,
};

View File

@@ -8,6 +8,9 @@ const System = @import("graph/system.zig");
// - Resolve missing resource problem
// - Parse system sets into a properly defined data structure instead of relying on `@typeInfo`
// - Find a better way to represent system sets
// - Organize a better way to execute single commands on graph
// - Handle system errors
// - Removing of resources
pub const Controller = @import("graph/controller.zig");
@@ -86,12 +89,40 @@ pub fn deinit(self: *Self) void {
self.duds.deinit(self.alloc);
}
/// Clear all internal data in preparation for the next run cycle
/// Does not clear any `Resource`s
pub fn reset(self: *Self) void {
// Controller cleanup
for (self.controllers.items, 0..) |*controller, i| {
for (controller.commands()) |*command| {
switch (command.*) {
.add_resource => |*resource| resource.deinit(controller.alloc),
.queue_system => |*system| system.deinit(controller.alloc),
}
}
controller.clear();
controller.setDuds(
i * self.duds.items.len / self.controllers.items.len,
(i + 1) * self.duds.items.len / self.controllers.items.len,
);
}
// System cleanup
for (self.system_queue.items) |*system| {
system.deinit(self.alloc);
}
self.system_queue.clearRetainingCapacity();
// Duds cleanup
for (self.duds.items) |*dud| {
dud.required_count = 0;
}
}
fn enqueueSystem(self: *Self, system: System) !void {
errdefer system.deinit(self.alloc);
try self.system_queue.append(self.alloc, system);
}
fn runAllSystems(self: *Self) GraphError!void {
pub fn runAllSystems(self: *Self) GraphError!void {
while (self.system_queue.items.len > 0) {
var swap_with = self.system_queue.items.len - 1;
@@ -169,7 +200,7 @@ fn applyCommands(self: *Self, commands: []const Controller.Command) !void {
}
}
fn getController(self: *Self) !Controller {
pub fn getController(self: *Self) !Controller {
if (self.controllers.pop()) |c| {
return c;
}
@@ -185,7 +216,7 @@ fn getController(self: *Self) !Controller {
}
/// Evaluates and clears the controller (even if errors out)
fn freeController(self: *Self, controller: Controller) !void {
pub fn freeController(self: *Self, controller: Controller) !void {
var c = controller;
try self.applyCommands(c.commands());
c.clear();

View File

@@ -34,7 +34,7 @@ pub fn create(alloc: std.mem.Allocator) !Controller {
/// Returns command queue, caller is responsible for freeing it's data
/// Call `clean()` afterwards, to clear the command queue
pub fn commands(self: *Controller) []const Command {
pub fn commands(self: *Controller) []Command {
return self.command_buffer.items;
}

View File

@@ -8,9 +8,9 @@ requires_dud: ?Dud.Id,
submit_dud: ?Dud.Id,
pub const Dud = struct {
pub const Id = u16;
pub const Id = usize;
required_count: usize = 0,
required_count: u16 = 0,
};
pub const Request = union(enum) {

View File

@@ -32,7 +32,8 @@ pub fn validateResource(comptime resource_type: type) void {
pub fn validateSystem(comptime system: anytype) void {
const info = @typeInfo(@TypeOf(system));
if (info != .@"fn") @compileError("System can only be a function, got " ++ @typeName(system));
if (info.@"fn".return_type != void) @compileError("Systems are not allowed to return any value (" ++ @typeName(info.Fn.return_type.?) ++ " returned)");
if (@typeInfo(info.@"fn".return_type.?) != .void and
@typeInfo(info.@"fn".return_type.?) != .error_union) @compileError("Systems are not allowed to return any value (" ++ @typeName(info.@"fn".return_type.?) ++ " returned)");
if (info.@"fn".is_var_args) @compileError("System cannot be variadic");
if (info.@"fn".is_generic) @compileError("System cannot be generic");
@@ -85,7 +86,13 @@ pub fn generateRunner(comptime system: anytype) fn ([]const *anyopaque) void {
inline for (0..@typeInfo(@TypeOf(system)).@"fn".params.len) |index| {
args[index] = @alignCast(@ptrCast(resources[index]));
}
@call(.always_inline, system, args);
switch (@typeInfo(@typeInfo(@TypeOf(system)).@"fn".return_type.?)) {
.void => @call(.always_inline, system, args),
.error_union => @call(.always_inline, system, args) catch |err| {
std.debug.print("System error: {s}\n", .{@errorName(err)});
},
else => unreachable,
}
}
};
return RunnerImpl.runner;

View File

@@ -1,9 +1,26 @@
const std = @import("std");
const builtin = @import("builtin");
const sdl = @import("sdl");
const Game = @import("game.zig");
pub fn runGame() !void {
var game = try Game.init();
var allocator = switch (builtin.mode) {
.ReleaseSafe, .Debug => std.heap.DebugAllocator(.{}).init,
.ReleaseFast => std.heap.smp_allocator,
.ReleaseSmall => std.heap.c_allocator,
};
defer switch (builtin.mode) {
.ReleaseSafe, .Debug => std.debug.assert(allocator.deinit() == .ok),
else => {},
};
var game = try Game.init(
switch (builtin.mode) {
.ReleaseSafe, .Debug => allocator.allocator(),
.ReleaseFast => allocator,
.ReleaseSmall => allocator,
},
);
defer game.deinit();
try game.run();
}