const std = @import("std"); const logging = @import("logging.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 pscopes = core.algorithm.pscopes; const tracy = @import("tracy").t; 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 SetupFuncFn = *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, createObjectLock: bool = false, // better name for these engineObject objects is actually 'engine object' engineObjectLUF: u64 = 0, 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) = .{}, // if an engine object is created with a "singletonName" it will be added here. engineObjectsByName: std.StringHashMapUnmanaged(EngineObjectRef) = .{}, prepares: 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, rendererCtx: *anyopaque = undefined, rendererSetupFunc: ?PollFuncFn = null, engineStartTime: f64 = 0, sessionStamp: i64 = 0, delegates: EngineDelegates, rootTimer: std.time.Timer, scopesContext: *pscopes.ScopesContext, timeTilCalibration: f64 = 1.0, // in seconds calibrationPeriod: f64 = 1.0, // in seconds 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 = .{}, .rootTimer = try std.time.Timer.start(), .frameNumber = 1, .exitListeners = .{}, .scopesContext = try pscopes.ScopesContext.create(allocator), // .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(); self.engineObjectsByName.deinit(self.allocator); self.scopesContext.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.prepares.deinit(self.allocator); 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); } pub fn createObject(self: *@This(), comptime T: type, params: NeonObjectParams) !*T { const rv = try self.createObjectVTable(&T.NeonObjectTable, params); return @ptrCast(@alignCast(rv)); } // creates an engine object using the engine's allocator. pub fn createObjectVTable(self: *@This(), vtable: *core.EngineObjectVTable, params: NeonObjectParams) !*anyopaque { if (self.createObjectLock) { core.engine_err("RECURSIVE OBJECT CREATION NOT ALLOWED", .{}); return error.BadInit; } self.engineObjectLUF = self.frameNumber; // bump this every time an engine object mutation has happened self.createObjectLock = true; defer self.createObjectLock = false; const newIndex = self.engineObjects.items.len; const newObjectPtr = try vtable.init_func(self.allocator); // call this thing with the special allocator that adds vtable. slackSize to it. const newObjectRef = EngineObjectRef{ .ptr = @as(*anyopaque, @ptrCast(newObjectPtr)), .vtable = vtable, }; try self.engineObjects.append(self.allocator, newObjectRef); if (vtable.singletonName) |singletonName| { if (self.engineObjectsByName.contains(vtable.singletonName.?)) { return error.DuplicateEngineObject; } try self.engineObjectsByName.put(self.allocator, singletonName, newObjectRef); } if (params.isCore) { try self.destroyListCore.append(self.allocator, newObjectRef); } else { try self.destroyListSimple.append(self.allocator, newObjectRef); } if (params.can_tick) |t| { if (t) { if (vtable.tick_func != null) { try self.tickables.append(self.allocator, newIndex); } } } else { if (vtable.tick_func != null) { try self.tickables.append(self.allocator, newIndex); } } //if (@hasDecl(T, "engineDraw")) { if (vtable.engineDraw_func != null) { try self.renderers.append(self.allocator, newObjectRef); } if (vtable.prepare_func != null) { try self.prepares.append(self.allocator, newObjectRef); } //if (@hasDecl(T, "preTick")) { if (vtable.preTick_func != null) { try self.preTickables.append(self.allocator, newObjectRef); } //if (@hasDecl(T, "processEvents")) { if (vtable.processEvents != null) { try self.eventors.append(self.allocator, newObjectRef); // } //if (@hasDecl(T, "onExitSignal")) { if (vtable.exitSignal_func != null) { try core.assert(vtable.readyToExit_func != null); //@hasDecl(T, "readyToExit")); try self.exitListeners.append(self.allocator, newObjectRef); // } //if (@hasDecl(T, "postInit")) { if (vtable.postInit_func != null) { try vtable.postInit_func.?(newObjectPtr); } return newObjectPtr; //@as(*T, @ptrCast(@alignCast(newObjectPtr))); } pub fn tick(self: *@This()) !void { // ------------ frame updates --------- tracy.FrameMark(); tracy.FrameMarkStart("frame"); defer tracy.FrameMarkEnd("frame"); const newTime = time.getEngineTime(); var z1 = tracy.ZoneN(@src(), "time updates"); var shouldCalibrate: bool = false; if (self.first) { self.first = false; self.engineStartTime = newTime; self.lastEngineTime = newTime; self.sessionStamp = std.time.microTimestamp(); shouldCalibrate = true; } if (newTime < self.lastEngineTime) { try core.assertf(false, "negative deltaTime this should not be possible", .{}); 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); self.timeTilCalibration -= self.deltaTime; if (self.timeTilCalibration < 0 or shouldCalibrate) { const zcalibrate = core.tracy.ZoneN(@src(), "calibrate timing scopeContext"); self.scopesContext.calibrate(); zcalibrate.End(); self.timeTilCalibration = self.calibrationPeriod; } 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; // ------------------------------------------- // sync 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"); } // pretick 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(); // tick 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; // renderer 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"); _ = ctx; } }; 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); } pub fn runPrepares(self: *@This()) !void { var z = core.tracy.Zone(@src()); for (self.prepares.items) |prep| { var z1 = core.tracy.ZoneN(@src(), "Preparing game"); core.tracy.Message(prep.vtable.typeName); prep.vtable.prepare_func.?(prep.ptr) catch unreachable; z1.End(); } z.End(); } fn mainLoop(self: *@This()) !void { var exitSignaled: bool = false; try self.runPrepares(); while (true) { self.tick() catch unreachable; self.jobManager.bump(); // try self.nfdRuntime.processMessages(); if (self.platformPollFunc) |func| { try func(self.platformCtx); } if (!exitSignaled and self.exitSignal.load(.seq_cst)) { exitSignaled = true; core.engine_logs("Processing exit signals"); for (self.exitListeners.items) |ref| { ref.vtable.exitSignal_func.?(ref.ptr) catch unreachable; } } if (exitSignaled) { var readyToExit: bool = true; core.engine_logs("checking everything is ready to exit"); for (self.exitListeners.items) |pending| { core.engine_log("checking everything is ready to exit {any}", .{pending}); if (!pending.vtable.readyToExit_func.?(pending.ptr)) { readyToExit = false; } } if (readyToExit) { core.engine_logs("exiting"); self.exitConfirmed.store(true, .seq_cst); break; } } } } 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 = null, responds_to_events: bool = false, isCore: bool = false, // if set true, object destruction order matters. }; const Src = std.builtin.SourceLocation; pub fn loopDelay(comptime src: Src, interval: f64, dt: f64, comptime S: type, capture: S) void { const C = struct { pub const s = src; pub var __timeleft: f64 = 0.0; pub var __interval: f64 = 0.0; }; C.__timeleft -= dt; if (C.__timeleft <= 0.0) { S.func(capture); while (C.__timeleft < 0) C.__timeleft += interval; } } test "comptime registration implementation" {}