408 lines
13 KiB
Zig
408 lines
13 KiB
Zig
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 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 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'
|
|
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) = .{},
|
|
|
|
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,
|
|
|
|
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.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);
|
|
}
|
|
|
|
// creates an engine object using the engine's allocator.
|
|
pub fn createObject(self: *@This(), comptime T: type, params: NeonObjectParams) !*T {
|
|
if (self.createObjectLock) {
|
|
core.engine_err("RECURSIVE OBJECT CREATION NOT ALLOWED", .{});
|
|
return error.BadInit;
|
|
}
|
|
self.createObjectLock = true;
|
|
defer self.createObjectLock = false;
|
|
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) |t| {
|
|
if (t and !@hasDecl(T, "tick")) {
|
|
return error.RequestedTickNotAvailable;
|
|
}
|
|
|
|
if (t) {
|
|
try self.tickables.append(self.allocator, newIndex);
|
|
}
|
|
} else {
|
|
if (@hasDecl(T, "tick")) {
|
|
try self.tickables.append(self.allocator, newIndex);
|
|
}
|
|
}
|
|
|
|
if (@hasDecl(T, "engineDraw")) {
|
|
try self.renderers.append(self.allocator, newObjectRef);
|
|
}
|
|
|
|
if (@hasDecl(T, "prepare")) {
|
|
try self.prepares.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");
|
|
|
|
_ = 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 (!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,
|
|
};
|
|
|
|
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.__interval -= dt;
|
|
|
|
if (C.__timeleft <= 0.0) {
|
|
S.func(capture);
|
|
while (C.__timeleft < 0)
|
|
C.__timeleft += interval;
|
|
}
|
|
}
|
|
|
|
test "comptime registration implementation" {}
|