const std = @import("std"); const logging = @import("logging.zig"); const input = @import("input.zig"); const engineObject = @import("engineObject.zig"); const time = @import("engineTime.zig"); const core = @import("core.zig"); const jobs = @import("jobs.zig"); const math = @import("math.zig"); const tracy = @import("tracy"); const p2 = @import("p2"); const nfd = @import("nfd"); const Atomic = std.atomic.Value; const EngineDataEventError = engineObject.EngineDataEventError; const Name = p2.Name; const MakeName = p2.MakeName; const EngineObjectRef = engineObject.EngineObjectRef; const ArrayList = std.ArrayList; const ArrayListUnmanaged = std.ArrayListUnmanaged; const AutoHashMap = std.AutoHashMap; const JobManager = jobs.JobManager; const engine_log = logging.engine_log; pub const PollFuncFn = *const fn (*anyopaque) EngineDataEventError!void; pub const ProcEventsFn = *const fn (*anyopaque, u64) EngineDataEventError!void; const EngineDelegates = @import("EngineDelegates.zig"); // perhaps a better name for this guy isn't actually engine, rather 'orchestrator' is more apt. // but that's so avant-garde pub const Engine = struct { exitSignal: Atomic(bool) = Atomic(bool).init(false), exitConfirmed: Atomic(bool) = Atomic(bool).init(false), dependentsDestroyed: Atomic(bool) = Atomic(bool).init(false), allocator: std.mem.Allocator, // better name for these engineObject objects is actually 'engine object' engineObjects: ArrayListUnmanaged(EngineObjectRef), eventors: ArrayListUnmanaged(EngineObjectRef), exitListeners: ArrayListUnmanaged(EngineObjectRef), preTickables: ArrayListUnmanaged(EngineObjectRef), renderers: ArrayListUnmanaged(EngineObjectRef) = .{}, tickables: ArrayListUnmanaged(usize), // todo: this maybe should just be a list of objects jobManager: *JobManager, // the destroy list is a list of things to destroy when the engine shuts down. // the main difference between core and simple objects, is that when the // simple object shuts down it's an destroyListSimple: ArrayListUnmanaged(EngineObjectRef) = .{}, destroyListCore: ArrayListUnmanaged(EngineObjectRef) = .{}, lastEngineTime: f64, deltaTime: f64, // delta time for this frame from the previous frame frameNumber: u64, averageFrameTime: f64 = 0, averageFrameSampleWindow: u32 = 60, // rolling weighted average systemsThreadTime: f64 = 0, renderThreadTime: f64 = 0, platformCtx: *anyopaque = undefined, platformPollFunc: ?PollFuncFn = null, platformProcEventsFunc: ?ProcEventsFn = null, engineStartTime: f64 = 0, nfdRuntime: *nfd.NFDRuntime, delegates: EngineDelegates, first: bool = true, pub fn init(allocator: std.mem.Allocator) !@This() { const rv = Engine{ .allocator = allocator, .engineObjects = .{}, .tickables = .{}, .preTickables = .{}, .deltaTime = 0.0, .lastEngineTime = 0.0, .jobManager = try JobManager.create(allocator), .eventors = .{}, .frameNumber = 0, .exitListeners = .{}, .nfdRuntime = try nfd.NFDRuntime.create(allocator, .{}), .delegates = EngineDelegates.init(allocator), }; return rv; } pub fn deinit(self: *@This()) void { if (!self.dependentsDestroyed.load(.seq_cst)) { self.destroyDependents(); } core.engine_logs("shutting down job Manager"); self.jobManager.destroy(); if (self.destroyListCore.items.len > 0) { var i: i32 = @intCast(self.destroyListCore.items.len - 1); while (i >= 0) : (i -= 1) { const item = self.destroyListCore.items[@as(usize, @intCast(i))]; if (item.vtable.deinit_func) |deinitFn| { deinitFn(item.ptr); } } } self.destroyListCore.deinit(self.allocator); self.destroyListSimple.deinit(self.allocator); self.renderers.deinit(self.allocator); core.engine_logs("destroying engine objects"); self.engineObjects.deinit(self.allocator); core.engine_logs("destroying eventors"); self.eventors.deinit(self.allocator); core.engine_logs("destroying tickables"); self.tickables.deinit(self.allocator); self.preTickables.deinit(self.allocator); self.nfdRuntime.destroy(); self.delegates.deinit(); core.engine_logs("calling onexit listeners"); self.exitListeners.deinit(self.allocator); self.allocator.destroy(self); } // creates an engine object using the engine's allocator. pub fn createObject(self: *@This(), comptime T: type, params: NeonObjectParams) !*T { const newIndex = self.engineObjects.items.len; const vtable = &@field(T, "NeonObjectTable"); const newObjectPtr = try vtable.init_func(self.allocator); const newObjectRef = EngineObjectRef{ .ptr = @as(*anyopaque, @ptrCast(newObjectPtr)), .vtable = vtable, }; try self.engineObjects.append(self.allocator, newObjectRef); if (params.isCore) { try self.destroyListCore.append(self.allocator, newObjectRef); } else { try self.destroyListSimple.append(self.allocator, newObjectRef); } if (params.can_tick) { if (!@hasDecl(T, "tick")) { return error.RequestedTickNotAvailable; // tried to register a tickable for an object which does not implement tick } // register to tick table try self.tickables.append(self.allocator, newIndex); } if (@hasDecl(T, "engineDraw")) { try self.renderers.append(self.allocator, newObjectRef); } if (@hasDecl(T, "preTick")) { try self.preTickables.append(self.allocator, newObjectRef); } if (@hasDecl(T, "processEvents")) { try self.eventors.append(self.allocator, newObjectRef); // } if (@hasDecl(T, "onExitSignal")) { try core.assert(@hasDecl(T, "readyToExit")); try self.exitListeners.append(self.allocator, newObjectRef); // } if (@hasDecl(T, "postInit")) { try vtable.postInit_func.?(newObjectPtr); } return @as(*T, @ptrCast(@alignCast(newObjectPtr))); } pub fn tick(self: *@This()) !void { tracy.FrameMark(); tracy.FrameMarkStart("frame"); defer tracy.FrameMarkEnd("frame"); const newTime = time.getEngineTime(); var z1 = tracy.ZoneN(@src(), "time updates"); if (self.first) { self.first = false; self.engineStartTime = newTime; self.lastEngineTime = newTime; } if (newTime < self.lastEngineTime) { std.debug.print("Warning! negative deltaTime? clamping to 0.0 newTime: {d} lastEngineTime:{d}", .{ newTime, self.lastEngineTime, }); } self.deltaTime = @max(newTime - self.lastEngineTime, 0.0); math.rollingAverage(&self.averageFrameTime, self.deltaTime, @floatFromInt(self.averageFrameSampleWindow)); z1.End(); var z2 = tracy.ZoneN(@src(), "debug updates"); for (self.delegates.onFrameDebugInfoEmitted.items) |l| { try l.func(l.ctx, self.averageFrameTime); } z2.End(); self.frameNumber += 1; var z3 = tracy.ZoneN(@src(), "platform event updates"); if (self.platformProcEventsFunc) |procEventsFn| { try procEventsFn(self.platformCtx, self.frameNumber); } z3.End(); for (self.eventors.items) |*objectRef| { objectRef.vtable.processEvents.?(objectRef.ptr, self.frameNumber) catch @panic("process event error"); } self.nfdRuntime.processCallbacks(); var z4 = tracy.ZoneN(@src(), "pretick event updates"); for (self.preTickables.items) |*objectRef| { objectRef.vtable.preTick_func.?(objectRef.ptr, self.deltaTime) catch @panic("pretick event error"); } z4.End(); var z5 = tracy.ZoneN(@src(), "system object ticks"); var index: isize = @as(isize, @intCast(self.tickables.items.len)) - 1; while (index >= 0) : (index -= 1) { var z = tracy.Zone(@src()); const objectRef = self.engineObjects.items[self.tickables.items[@as(usize, @intCast(index))]]; objectRef.vtable.tick_func.?(objectRef.ptr, self.deltaTime); z.Name(objectRef.vtable.typeName); z.End(); } z5.End(); const systemsThreadTime: f64 = time.getEngineTime() - newTime; for (self.renderers.items) |*renderer| { var z = tracy.Zone(@src()); z.Name(renderer.vtable.typeName); renderer.vtable.engineDraw_func.?(renderer.ptr, self.deltaTime); z.End(); } math.rollingAverage(&self.systemsThreadTime, systemsThreadTime, @floatFromInt(self.averageFrameSampleWindow)); self.lastEngineTime = newTime; } pub fn run(self: *@This()) !void { core.engine_logs("engine loop started"); const SystemsThread = struct { engine: *Engine, pub fn func(ctx: @This(), job: *core.JobContext) void { _ = job; tracy.SetThreadName("Systems Thread"); var exitSignaled: bool = false; while (true) { ctx.engine.tick() catch unreachable; if (!exitSignaled and ctx.engine.exitSignal.load(.seq_cst)) { exitSignaled = true; core.engine_logs("Processing exit signals"); for (ctx.engine.exitListeners.items) |ref| { ref.vtable.exitSignal_func.?(ref.ptr) catch unreachable; } } if (exitSignaled) { var readyToExit: bool = true; for (ctx.engine.exitListeners.items) |pending| { if (!pending.vtable.readyToExit_func.?(pending.ptr)) { readyToExit = false; } } if (readyToExit) { break; } } } ctx.engine.exitConfirmed.store(true, .seq_cst); } }; try core.dispatchJob(SystemsThread{ .engine = self }); try self.mainLoop(); self.destroyDependents(); } fn destroyDependents(self: *@This()) void { if (self.destroyListSimple.items.len > 0) { var i: i32 = @intCast(self.destroyListSimple.items.len - 1); while (i >= 0) : (i -= 1) { const item = self.destroyListSimple.items[@as(usize, @intCast(i))]; if (item.vtable.deinit_func) |deinitFn| { deinitFn(item.ptr); } } } self.dependentsDestroyed.store(true, .seq_cst); } fn mainLoop(self: *@This()) !void { while (!self.exitConfirmed.load(.acquire)) { if (self.platformPollFunc) |pollFunc| { const z = core.tracy.ZoneN(@src(), "glfw input polling"); try pollFunc(self.platformCtx); defer z.End(); } self.jobManager.bump(); std.time.sleep(1000 * 1000); // 1ms delay between polling functions, effectively limits input to 1khz. // (there is a noticable power consumption draw on laptops and battery based systems if this is unlimited) try self.nfdRuntime.processMessages(); } } pub fn exit(self: *@This()) void { self.exitSignal.store(true, .release); } pub fn isShuttingDown(self: *@This()) bool { return self.exitSignal.load(.monotonic); } pub fn exitFinished(self: *@This()) bool { return self.exitConfirmed.load(.monotonic); } }; pub const NeonObjectParams = struct { can_tick: bool = false, responds_to_events: bool = false, isCore: bool = false, }; test "comptime registration implementation" {}