Backlog initial-comit SDL gpu examples cooking shaders properly

This commit is contained in:
Peter Li 2025-04-07 19:51:46 -07:00
commit 641f38f295
3298 changed files with 2935495 additions and 0 deletions

10
.gitattributes vendored Normal file
View File

@ -0,0 +1,10 @@
*.fbx filter=lfs diff=lfs merge=lfs -text
lib/** linguist-vendored
engine/vkImgui/cimplot/** linguist-vendored
engine/vkImgui/cimgui/** linguist-vendored
engine/papyrus/src/stb_ttf.h linguist-vendored
*.zip filter=lfs diff=lfs merge=lfs
sdks/vulkan_sdk/1.3.268.0/aarch64.zip filter=lfs diff=lfs merge=lfs -text

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
.zig-cache/
zig-out/
*Saved/*
*zig-cache*
*zig-out*
.vscode/
*.rdbg
__pycache__
*.idea
*.orig
*_BACKUP_*
*_BASE_*
*_LOCAL_*
*_REMOTE_*
*.peterino
.vs/
CppProperties.json
packages/
*.blend1
*.DS_Store
*imgui.ini
*.stackdump

33
.gitmodules vendored Normal file
View File

@ -0,0 +1,33 @@
[submodule "engine/modules/graphics/lib/vulkan-zig"]
path = engine/modules/graphics/lib/vulkan-zig
url = https://github.com/peterino2/vulkan-zig.git
[submodule "engine/modules\\graphics\\lib\\objLoader"]
path = engine/modules/graphics/lib/objLoader
url = https://github.com/peterino2/zig_obj_loader.git
[submodule "engine/modules\\graphics\\lib\\cimgui"]
path = engine/modules/graphics/lib/cimgui
url = https://github.com/peterino2/cimgui.git
[submodule "engine/modules/audio/lib/miniaudio"]
path = engine/modules/audio/lib/miniaudio
url = https://github.com/mackron/miniaudio.git
[submodule "engine/projects/cognesia/zig-halcyon"]
path = engine/projects/cognesia/zig-halcyon
url = https://github.com/peterino2/zig-halcyon.git
[submodule "engine/modules/core/lib/p2"]
path = engine/modules/core/lib/p2
url = https://github.com/peterino2/p2-algorithms.git
[submodule "engine/modules/core/lib/zig-spng"]
path = engine/modules/core/lib/zig-spng
url = https://github.com/peterino2/zig-spng
[submodule "engine/modules/graphics/lib/zig-assimp"]
path = engine/modules/graphics/lib/zig-assimp
url = https://github.com/peterino2/zig-assimp.git
[submodule "engine/modules/game/zig-halcyon"]
path = engine/modules/game/zig-halcyon
url = https://github.com/peterino2/zig-halcyon.git
[submodule "engine/modules/graphics/lib/spirv-reflect-zig"]
path = engine/modules/graphics/lib/spirv-reflect-zig
url = https://github.com/peterino2/spirv-reflect-zig.git
[submodule "engine/modules/core/lib/zig_tracy"]
path = engine/modules/core/lib/zig_tracy
url = https://github.com/peterino2/zig_tracy.git

18
LICENSE Normal file
View File

@ -0,0 +1,18 @@
Copyright 2022 Peterlimail47@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# Backlog
zig version: 0.14
Backlog Labs Game Engine.
run tools/scripts/first-time-setup.py

193
build.zig Normal file
View File

@ -0,0 +1,193 @@
//!
// MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=1 -- use this if you're getting that odd crash on mac
b: *std.Build,
nw_builder: *std.Build,
target: std.Build.ResolvedTarget,
optimize: std.builtin.Mode,
nw_mod: *std.Build.Module,
spirvReflect: SpirvReflect.SpirvGenerator2,
gltf2ozz: ozz.GltfToOzz,
options: *std.Build.Step.Options,
const engineDepList = [_][]const u8{
"assets",
"audio",
"core",
"graphics",
"papyrus",
"platform",
"physics",
"ui",
"vkImgui",
};
const BuildSystem = @This();
const std = @import("std");
const Build = std.Build;
const LazyPath = Build.LazyPath;
const SpirvReflect = @import("SpirvReflect");
const ozz = @import("ozz");
pub const InitOptions = struct {
import_name: []const u8 = "Backlog",
target: Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
};
pub fn init(b: *std.Build, opts: InitOptions) BuildSystem {
const nwdep = b.dependency(opts.import_name, .{
.target = opts.target,
.optimize = opts.optimize,
});
const self = BuildSystem{
.b = b,
.nw_builder = nwdep.builder,
.target = opts.target,
.optimize = opts.optimize,
.nw_mod = nwdep.module("Backlog"),
.spirvReflect = SpirvReflect.SpirvGenerator2.init(nwdep.builder, .{}),
.options = createGameOptions(b),
.gltf2ozz = ozz.GltfToOzz.init(nwdep.builder, .{}),
};
b.installArtifact(self.spirvReflect.reflect);
b.installArtifact(self.gltf2ozz.exe);
const toolsInstall = b.addInstallArtifact(self.gltf2ozz.exe, .{
.dest_dir = .{ .override = .{ .custom = "tools" } },
});
const runArtifact = b.addRunArtifact(self.gltf2ozz.exe);
if (b.args) |args| {
runArtifact.addArgs(args);
}
const run_exe = b.step("gltf2ozz", "runs the gltf animation converter.");
run_exe.dependOn(&runArtifact.step);
const install_tools = b.step("tools", "installs tools needed to run the engine");
install_tools.dependOn(&toolsInstall.step);
return self;
}
pub const AddProgramOptions = struct {
name: []const u8,
desc: []const u8,
root_source_file: LazyPath,
imports: []const Build.Module.Import = &.{},
};
pub fn addProgram(self: *BuildSystem, opts: AddProgramOptions) *std.Build.Step.Compile {
const b = self.b;
const exe = self.nw_builder.addExecutable(.{
.name = opts.name,
.target = self.target,
.optimize = self.optimize,
.root_source_file = self.nw_builder.path("engine/main.zig"),
});
b.installArtifact(exe);
const runArtifact = b.addRunArtifact(exe);
if (b.args) |args| {
runArtifact.addArgs(args);
}
const run_exe = b.step(self.b.fmt("run-{s}", .{opts.name}), opts.desc);
run_exe.dependOn(&runArtifact.step);
// main path = name/main.zig
const mod = b.addModule(opts.name, .{
.target = self.target,
.optimize = self.optimize,
.root_source_file = opts.root_source_file,
.imports = opts.imports,
});
exe.root_module.addImport("main", mod);
exe.root_module.addImport("Backlog", self.nw_mod);
mod.addImport("Backlog", self.nw_mod);
exe.root_module.addOptions("BacklogOptions", self.options);
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/graphics/shaders/triangle_mesh.vert"), "triangle_mesh_vert");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/graphics/shaders/default_lit.frag"), "default_lit");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/graphics/shaders/debug.vert"), "debug_vert");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/graphics/shaders/debug.frag"), "debug_frag");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/graphics/shaders/skybox/skybox.vert"), "skybox_vert");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/graphics/shaders/skybox/skybox.frag"), "skybox_frag");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/ui/shaders/PapyrusRect.vert"), "papyrus_vk_vert");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/ui/shaders/PapyrusRect.frag"), "papyrus_vk_frag");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/ui/shaders/FontSDF.vert"), "FontSDF_vert");
self.spirvReflect.addShaderInstallRef(exe, self.nw_builder.path("engine/ui/shaders/FontSDF.frag"), "FontSDF_frag");
b.getInstallStep().dependOn(self.nw_builder.getInstallStep());
return exe;
}
pub fn createGameOptions(b: *std.Build) *std.Build.Step.Options {
const opts = b.addOptions();
// build options for core.zig
opts.addOption(
bool,
"mutex_job_queue",
b.option(bool, "mutex_job_queue", "temporary test, reverts to old mutex based queue behaviour in jobs.zig:JobManager") orelse false,
);
opts.addOption(
bool,
"zero_logging",
b.option(bool, "zero_logging", "disables all logging, only intended for use on job dispatch testing") orelse false,
);
opts.addOption(
bool,
"slow_logging",
b.option(bool, "slow_logging", "Disables buffered logging, takes a hit to performance but gain timing information on logging") orelse false,
);
opts.addOption(
bool,
"force_mailbox",
b.option(bool, "force_mailbox", "forces mailbox mode for present mode. unlocks framerate to irresponsible levels") orelse false,
);
opts.addOption(
bool,
"use_renderthread",
true,
);
return opts;
}
// ========= standalone build instance =======
// maybe it should be an engine launcher or something..
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const spirvDep = b.dependency("SpirvReflect", .{
.target = target,
.optimize = optimize,
});
_ = spirvDep;
const mod = b.addModule("Backlog", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("engine/backlog.zig"),
});
for (engineDepList) |depName| {
const dep = b.dependency(
depName,
.{
.target = target,
.optimize = optimize,
},
);
mod.addImport(depName, dep.module(depName));
}
}

21
build.zig.zon Normal file
View File

@ -0,0 +1,21 @@
.{
.name = "Backlog",
.version = "0.0.0",
.dependencies = .{
.assets = .{ .path = "engine/assets" },
.audio = .{ .path = "engine/audio" },
.core = .{ .path = "engine/core" },
.graphics = .{ .path = "engine/graphics" },
.papyrus = .{ .path = "engine/papyrus" },
.physics = .{ .path = "engine/physics" },
.platform = .{ .path = "engine/platform" },
.ui = .{ .path = "engine/ui" },
.vkImgui = .{ .path = "engine/vkImgui" },
.SpirvReflect = .{ .path = "lib/spirv-reflect-zig" },
.ozz = .{ .path = "lib/ozz" },
},
.paths = .{
"",
},
}

4
engine/.gitattributes vendored Normal file
View File

@ -0,0 +1,4 @@
*.fbx filter=lfs diff=lfs merge=lfs -text
modules/*/lib/* linguist-vendored
*.zip
sdks/vulkan_sdk/1.3.268.0/aarch64.zip filter=lfs diff=lfs merge=lfs -text

10
engine/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
*.swp
zig-cache/
zig-out/
content/*~
imgui.ini
.vs/
CppProperties.json
sampleOutput.json
Saved/
*.DS_Store

37
engine/assets/build.zig Normal file
View File

@ -0,0 +1,37 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("assets", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/assets.zig"),
});
const core_dep = b.dependency(
"core",
.{ .target = target, .optimize = optimize },
);
mod.addImport("core", core_dep.module("core"));
const packer_dep = b.dependency(
"packer",
.{ .target = target, .optimize = optimize },
);
mod.addImport("packer", packer_dep.module("packer"));
const test_step = b.step("test", "run unit tests for assets");
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tests/test.zig"),
});
tests.root_module.addImport("assets", mod);
const runArtifact = b.addRunArtifact(tests);
test_step.dependOn(&runArtifact.step);
b.installArtifact(tests);
}

View File

@ -0,0 +1,11 @@
.{
.name = "assets",
.version = "0.0.0",
.dependencies = .{
.core = .{ .path = "../core" },
.packer = .{ .path = "../../lib/packer" },
},
.paths = .{
"",
},
}

View File

@ -0,0 +1,27 @@
const std = @import("std");
const core = @import("core");
const assets = @import("assets.zig");
const JobContext = core.JobContext;
const JobWorker = core.JobWorker;
const JobManager = core.JobManager;
const AssetReference = assets.AssetReference;
pub const AsyncAssetJobContext = struct {
mutex: std.mutex.Mutex,
assetLoadList: []const AssetReference,
allocator: std.mem.Allocator,
pub fn loadAssets(assetList: []const AssetReference, allocator: std.mem.Allocator) !*@This() {
const this: *@This() = try allocator.create(@This());
this.* = .{
.mutex = .{},
.assetLoadList = assetList,
.allocator = allocator,
};
return this;
}
};

View File

@ -0,0 +1,210 @@
const std = @import("std");
const core = @import("core");
const tracy = core.tracy;
pub const AssetImportReference = struct {
assetRef: AssetRef,
properties: AssetPropertiesBag,
};
// MakeImportRef with defaults.
pub fn MakeImportRef(assetType: []const u8, name: []const u8, path: []const u8) AssetImportReference {
return .{
.assetRef = .{
.assetType = core.Name.MakeComptime(assetType),
.name = core.Name.MakeComptime(name),
},
.properties = .{ .path = path },
};
}
// Advanced version of MakeImportRef, allows user to explicitly construct the properties bag.
pub fn MakeImportRefOptions(assetType: []const u8, name: []const u8, properties: AssetPropertiesBag) AssetImportReference {
return .{
.assetRef = .{
.assetType = core.Name.MakeComptime(assetType),
.name = core.Name.MakeComptime(name),
},
.properties = properties,
};
}
// TODO, this needs to be replaced with some kind of polymorphic data bag I think,
// some kind of transmute is probably in order, maybe allocate 256 bytes of data
// for it.
pub const AssetPropertiesBag = struct {
path: []const u8 = "None",
soundVolume: f32 = 1.0,
soundLooping: bool = false,
textureUseBlockySampler: bool = true,
meshType: ?[]const u8 = null,
skeletonName: ?[]const u8 = null,
textureCube: bool = false,
textureList: ?[]const []const u8 = null, // if set, path texture will be loaded first, and this list of textures will be loaded together.
};
pub const AssetRef = struct {
name: core.Name,
assetType: core.Name,
};
// very minimal asset loading library.
pub const AssetLoaderError = error{
UnableToLoad,
};
pub const AssetLoaderStatus = enum {
AssetReady, // The asset was loaded and ready to be installed
LoadFailed, // The asset failed for one reason or another
};
pub const AssetLoaderMessage = struct {
assetRef: AssetRef,
status: AssetLoaderStatus,
message: ?[]u8,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, assetRef: AssetRef, status: AssetLoaderStatus, maybeMessage: ?[]const u8) !@This() {
var m: ?[]u8 = null;
if (maybeMessage) |message| {
m = try core.dupeString(message);
}
return .{ .message = m, .allocator = allocator, .assetRef = assetRef, .status = status };
}
pub fn deinit(self: *@This()) void {
if (self.message) |message| {
self.allocator.free(message);
}
}
};
pub const AssetLoaderInterface = struct {
typeName: []const u8,
typeSize: usize,
typeAlign: usize,
assetType: []const u8,
loadAsset: *const fn (*anyopaque, AssetRef, ?AssetPropertiesBag) AssetLoaderError!void,
destroy: *const fn (*anyopaque, std.mem.Allocator) void,
discardAll: *const fn (*anyopaque) void,
pub fn from(comptime assetType: []const u8, comptime TargetType: type) @This() {
const wrappedFuncs = struct {
pub fn loadAsset(
pointer: *anyopaque,
assetRef: AssetRef,
properties: ?AssetPropertiesBag,
) AssetLoaderError!void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
try ptr.loadAsset(assetRef, properties);
}
pub fn discardAll(pointer: *anyopaque) void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
ptr.discardAll();
}
pub fn destroy(
pointer: *anyopaque,
allocator: std.mem.Allocator,
) void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
ptr.destroy(allocator);
}
};
if (!@hasDecl(TargetType, "loadAsset")) {
@compileLog("Tried to generate AssetLoaderInterface for type ", TargetType, "but it's missing func loadAsset");
unreachable;
}
if (!@hasDecl(TargetType, "destroy")) {
@compileLog("Tried to generate AssetLoaderInterface for type ", TargetType, "but it's missing func destroy.");
unreachable;
}
const self = @This(){
.typeName = @typeName(TargetType),
.typeSize = @sizeOf(TargetType),
.typeAlign = @alignOf(TargetType),
.loadAsset = wrappedFuncs.loadAsset,
.discardAll = wrappedFuncs.discardAll,
.destroy = wrappedFuncs.destroy,
.assetType = assetType,
};
return self;
}
};
pub const AssetLoaderRef = struct {
target: *anyopaque,
vtable: *const AssetLoaderInterface,
size: usize,
pub fn loadAsset(self: *@This(), asset: AssetRef, propertiesBag: ?AssetPropertiesBag) !void {
try self.vtable.loadAsset(self.target, asset, propertiesBag);
}
pub fn discardAll(self: @This()) void {
self.vtable.discardAll(self.target);
}
pub fn destroy(self: *@This(), allocator: std.mem.Allocator) void {
self.vtable.destroy(self.target, allocator);
}
};
pub const AssetReferenceSys = struct {
loaders: std.AutoHashMapUnmanaged(u32, AssetLoaderRef),
allocator: std.mem.Allocator,
outstandingAssetJobs: std.atomic.Value(i32),
pub fn init(allocator: std.mem.Allocator) @This() {
return @This(){
.loaders = .{},
.allocator = allocator,
.outstandingAssetJobs = std.atomic.Value(i32).init(0),
};
}
pub fn registerLoader(self: *@This(), loader: anytype) !void {
const vtable = &@field(@TypeOf(loader.*), "LoaderInterfaceVTable");
var assetType = core.MakeName(vtable.assetType);
try self.loaders.put(self.allocator, assetType.handle(), .{
.vtable = vtable,
.target = loader,
.size = @sizeOf(@TypeOf(loader)),
});
}
pub fn loadRef(self: *@This(), asset: AssetRef, propertiesBag: ?AssetPropertiesBag) !void {
if (core.getEngine().isShuttingDown())
return;
_ = self.outstandingAssetJobs.fetchAdd(1, .acquire);
var z = tracy.ZoneN(@src(), "AssetReferenceSys LoadRef");
var assetName = asset.name;
var assetType = asset.assetType;
if (propertiesBag) |props| {
core.engine_log("loading asset {s} ({s}) [{s}]", .{ assetName.utf8(), assetType.utf8(), props.path });
} else {
core.engine_log("loading asset {s} ({s})", .{ assetName.utf8(), assetType.utf8() });
}
var n = asset.assetType;
try self.loaders.getPtr(n.handle()).?.loadAsset(asset, propertiesBag);
z.End();
}
pub fn deinit(self: *@This()) void {
var iter = self.loaders.valueIterator();
while (iter.next()) |i| {
i.destroy(self.allocator);
}
self.loaders.deinit(self.allocator);
}
};

View File

@ -0,0 +1,68 @@
const std = @import("std");
const core = @import("core");
const memory = core.MemoryTracker;
pub const asset_references = @import("asset_references.zig");
pub const asset_jobs = @import("asset_jobs.zig");
pub const cook = @import("cook.zig");
pub const AssetRef = asset_references.AssetRef;
pub const AssetReference = asset_references.AssetReference;
pub const AssetLoaderError = asset_references.AssetLoaderError;
pub const AssetLoaderInterface = asset_references.AssetLoaderInterface;
pub const AssetImportReference = asset_references.AssetImportReference;
pub const AssetPropertiesBag = asset_references.AssetPropertiesBag;
pub const AssetReferenceSys = asset_references.AssetReferenceSys;
pub const MakeImportRef = asset_references.MakeImportRef;
pub const MakeImportRefOptions = asset_references.MakeImportRefOptions;
pub const AsyncAssetJobContext = asset_jobs.AsyncAssetJobContext;
pub var gAssetSys: *AssetReferenceSys = undefined;
pub const Module = core.ModuleDescription{
.name = "assets",
.enabledByDefault = true,
};
var cooking: bool = false;
pub fn start_module(comptime spec: anytype, args: anytype, allocator: std.mem.Allocator) !void {
_ = args;
if (@hasField(@TypeOf(spec), "cooking")) {
core.engine_logs("Cooking Enabled");
cooking = true;
try cook.startup(allocator);
}
gAssetSys = allocator.create(AssetReferenceSys) catch @panic("unable to initialize asset reference");
gAssetSys.* = AssetReferenceSys.init(allocator);
memory.MTPrintStatsDelta();
}
pub fn shutdown_module(allocator: std.mem.Allocator) void {
if (cooking) {
cook.shutdown();
}
gAssetSys.deinit();
allocator.destroy(gAssetSys);
}
pub fn loadList(assetList: anytype) !void {
for (assetList) |assetImport| {
try load(assetImport);
}
}
pub fn load(assetImport: AssetImportReference) !void {
var z1 = core.tracy.ZoneN(@src(), "Loading asset");
var assetRefName = assetImport.assetRef.name;
core.tracy.Message(assetRefName.utf8());
core.tracy.Message(assetImport.properties.path);
try gAssetSys.loadRef(assetImport.assetRef, assetImport.properties);
z1.End();
}

279
engine/assets/src/cook.zig Normal file
View File

@ -0,0 +1,279 @@
pub const CookResultEnum = enum {
Success,
Failure,
};
pub const FileBytes = struct {
path: []const u8,
bytes: []u8,
};
pub const ExtraFiles = std.ArrayList(FileBytes);
pub const CookResult = struct {
bytes: std.ArrayList(u8),
result: CookResultEnum = .Success,
extraFiles: ?ExtraFiles = null,
pub fn deinit(self: *@This()) void {
self.bytes.deinit();
}
};
pub const CookParams = struct {
noSave: bool = false,
fileName: []const u8,
cookFileName: []const u8,
};
pub const CookInfo = struct {
assetType: []const u8 = "none",
};
pub const CookRegistry = struct {
allocator: std.mem.Allocator,
cooks: std.StringHashMapUnmanaged(CookFunction) = .{},
generates: std.StringHashMapUnmanaged(CookGenerateFunction) = .{},
typesByExt: std.StringHashMapUnmanaged([]const u8) = .{},
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
};
gCookRegistry = self;
return self;
}
pub fn install(self: *@This(), typeName: []const u8, generate: CookGenerateFunction, cook: CookFunction, extensions: []const []const u8) !void {
try self.generates.put(self.allocator, typeName, generate);
try self.cooks.put(self.allocator, typeName, cook);
for (extensions) |ext| {
try self.typesByExt.put(self.allocator, ext, typeName);
}
}
pub fn destroy(self: *@This()) void {
self.cooks.deinit(self.allocator);
self.typesByExt.deinit(self.allocator);
self.generates.deinit(self.allocator);
self.allocator.destroy(self);
}
};
var gCookRegistry: *CookRegistry = undefined;
var gCooking: bool = false;
pub fn getRegistry() *CookRegistry {
return gCookRegistry;
}
pub fn startup(allocator: std.mem.Allocator) !void {
gCooking = true;
_ = try CookRegistry.create(allocator);
}
pub fn shutdown() void {
if (gCooking) {
gCookRegistry.destroy();
gCooking = false;
}
}
pub const GenerateError = error{UnableToGenerate};
pub const LoadError = error{BadFile};
pub const CookGenerateFunction = *const fn (allocator: std.mem.Allocator, filePath: []const u8, out: *std.ArrayList(u8)) GenerateError!void;
pub const CookFunction = *const fn (allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8, params: CookParams) CookResult;
pub fn loadFileAlloc(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) ![]align(8) u8 {
var file = try dir.openFile(path, .{});
defer file.close();
const filesize = (try file.stat()).size + 1; // add null byte
const buffer: []align(8) u8 = try allocator.alignedAlloc(u8, 8, filesize);
errdefer allocator.free(buffer);
try file.reader().readNoEof(buffer[0 .. buffer.len - 1]);
buffer[buffer.len - 1] = 0;
return buffer;
}
pub fn getExtension(path: []const u8) ?[]const u8 {
var extension: []const u8 = path;
var i: i32 = @intCast(path.len - 1);
while (i > 0) : (i -= 1) {
if (path[@intCast(i)] == '.') {
extension = path[@intCast(i)..];
return extension;
}
}
return null;
}
pub fn generateCookFile(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) !void {
if (getExtension(path)) |extension| {
if (gCookRegistry.typesByExt.get(extension)) |assetType| {
var cookFilePath = std.ArrayList(u8).init(allocator);
defer cookFilePath.deinit();
const generateFunction = gCookRegistry.generates.get(assetType).?;
var outPath: std.ArrayListUnmanaged(u8) = .{};
defer outPath.deinit(allocator);
try outPath.appendSlice(allocator, path);
try outPath.appendSlice(allocator, ".cook");
try generateFunction(allocator, path, &cookFilePath);
const file = try dir.createFile(outPath.items, .{});
defer file.close();
try file.writeAll(cookFilePath.items);
}
}
}
pub fn cookFile(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) !void {
std.debug.print("{s}\n", .{path});
const extension = getExtension(path).?;
if (std.mem.eql(u8, extension, ".cook")) {
return;
}
var cookFilePath = std.ArrayList(u8).init(allocator);
defer cookFilePath.deinit();
try cookFilePath.appendSlice(path);
try cookFilePath.appendSlice(".cook");
if (gCookRegistry.typesByExt.get(extension)) |assetType| {
const cookFunction = gCookRegistry.cooks.get(assetType).?;
dir.access(cookFilePath.items, .{}) catch return;
var results = cookFunction(allocator, dir, path, .{
.cookFileName = cookFilePath.items,
.fileName = path,
});
defer results.deinit();
var outPath = std.ArrayList(u8).init(allocator);
defer outPath.deinit();
try outPath.appendSlice("_cooked/");
try outPath.appendSlice(path);
try outPath.appendSlice(".");
try outPath.appendSlice(assetType);
{
var basePath: []const u8 = outPath.items;
var index: usize = basePath.len - 1;
while (basePath[index] != '/' and basePath[index] != '\\') : (index -= 1) {}
basePath = basePath[0..index];
try dir.makePath(basePath);
}
if (results.result == .Success) {
const file = try dir.createFile(outPath.items, .{});
defer file.close();
try file.writeAll(results.bytes.items);
}
return;
} else {
unreachable;
}
}
pub fn generateAllCookFiles(allocator: std.mem.Allocator, dir: std.fs.Dir) !void {
var iter = dir.iterate();
while (try iter.next()) |next| {
switch (next.kind) {
.file => {
try generateCookFile(allocator, dir, next.name);
},
.directory => {
if (!std.mem.eql(u8, "_cooked", next.name)) {
var subDir = try dir.openDir(next.name, .{ .iterate = true });
defer subDir.close();
try generateAllCookFiles(allocator, subDir);
}
},
else => {},
}
}
}
pub fn cookAllFiles(allocator: std.mem.Allocator, dir: std.fs.Dir) !void {
var walker = try dir.walk(allocator);
defer walker.deinit();
while (try walker.next()) |next| {
switch (next.kind) {
.file => {
if (std.mem.startsWith(u8, next.path, "_cooked")) {
continue;
}
if (std.mem.startsWith(u8, next.path, ".gitignore")) {
continue;
}
try cookFile(allocator, dir, next.path);
},
else => {},
}
}
}
test "testing cooking" {
const alloc = std.testing.allocator;
const Test = struct {
const TestConfig = struct {
info: CookInfo = .{ .assetType = "Texture" }, // there must always be a CookInfo field
sourceType: []const u8 = "png",
};
pub fn generateFunction(allocator: std.mem.Allocator, path: []const u8, out: *std.ArrayList(u8)) GenerateError!void {
_ = allocator;
_ = path;
out.clearRetainingCapacity();
std.json.stringify(TestConfig{}, .{ .whitespace = .indent_4 }, out.writer()) catch return GenerateError.UnableToGenerate;
}
pub fn cookFunction(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8, params: CookParams) CookResult {
_ = params;
const rawFileBytes = loadFileAlloc(allocator, dir, path) catch unreachable;
defer allocator.free(rawFileBytes);
const rv = std.ArrayList(u8).init(allocator);
return .{
.bytes = rv,
.result = .Success,
};
}
};
const dir = try std.fs.cwd().openDir("test", .{ .iterate = true });
const registry = try CookRegistry.create(alloc);
defer registry.destroy();
try registry.generates.put(registry.allocator, "Texture", Test.generateFunction);
try registry.cooks.put(registry.allocator, "Texture", Test.cookFunction);
try registry.typesByExt.put(registry.allocator, ".png", "Texture");
// scan and generate all texture files
try generateAllCookFiles(alloc, dir);
try cookAllFiles(alloc, dir);
// try generateCookFile(alloc, dir, "icon.png");
// try cookFile(alloc, dir, "icon.png");
}
const std = @import("std");

2
engine/assets/src/test/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
_cooked/
*.cook

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

36
engine/audio/build.zig Normal file
View File

@ -0,0 +1,36 @@
const std = @import("std");
const depList = [_][]const u8{
"miniaudio",
"core",
"assets",
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("audio", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/audio.zig"),
});
for (depList) |depName| {
const dep = b.dependency(depName, .{ .target = target, .optimize = optimize });
mod.addImport(depName, dep.module(depName));
}
const test_step = b.step("test", "run unit tests for audio");
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tests/tests.zig"),
});
tests.root_module.addImport("audio", mod);
const runArtifact = b.addRunArtifact(tests);
test_step.dependOn(&runArtifact.step);
b.installArtifact(tests);
}

View File

@ -0,0 +1,15 @@
.{
.name = "audio",
.version = "0.0.0",
.dependencies = .{
//
.miniaudio = .{ .path = "../../lib/miniaudio" },
// 3rd party libs
.assets = .{ .path = "../assets" },
.core = .{ .path = "../core" },
},
.paths = .{
"",
},
}

View File

@ -0,0 +1,41 @@
const core = @import("core");
const assets = @import("assets");
const std = @import("std");
const memory = core.MemoryTracker;
const soundEngine = @import("sound_engine.zig");
pub const NeonSoundEngine = soundEngine.NeonSoundEngine;
pub const sound_err = soundEngine.sound_err;
pub const sound_errs = soundEngine.sound_errs;
pub const sound_log = soundEngine.sound_log;
pub const sound_logs = soundEngine.sound_logs;
pub var gSoundEngine: *NeonSoundEngine = undefined;
pub var gSoundLoader: *soundEngine.SoundLoader = undefined;
pub fn start_module(comptime programSpec: anytype, args: anytype, allocator: std.mem.Allocator) !void {
_ = args;
_ = programSpec;
if (core.isUtility()) {
return;
}
gSoundEngine = core.gEngine.createObject(NeonSoundEngine, .{ .can_tick = true }) catch unreachable;
gSoundLoader = allocator.create(soundEngine.SoundLoader) catch unreachable;
gSoundLoader.* = soundEngine.SoundLoader.init(gSoundEngine);
assets.gAssetSys.registerLoader(gSoundLoader) catch unreachable;
gSoundEngine.loadSound(core.MakeName("s_test"), "content/sounds/engineTick.wav", .{}) catch unreachable;
core.engine_logs("sound start_module");
}
pub fn shutdown_module(allocator: std.mem.Allocator) void {
_ = allocator;
gSoundEngine.shutdown();
}
pub const Module = core.ModuleDescription{
.name = "audio",
.enabledByDefault = false,
};

View File

@ -0,0 +1,156 @@
// Implemented via miniaudio
const std = @import("std");
const core = @import("core");
const assets = @import("assets");
const Name = core.Name;
const AutoHashMapUnmanaged = std.AutoHashMapUnmanaged;
const tracy = core.tracy;
const ma = @import("miniaudio");
pub fn sound_log(comptime fmt: []const u8, args: anytype) void {
core.printInner("[SOUND ]: " ++ fmt ++ "\n", args);
}
pub fn sound_logs(comptime fmt: []const u8) void {
core.printInner("[SOUND ]: " ++ fmt ++ "\n", .{});
}
pub fn sound_err(comptime fmt: []const u8, args: anytype) void {
core.printInner("[SOUND ]: ERROR!! " ++ fmt ++ "\n", args);
}
pub fn sound_errs(comptime fmt: []const u8) void {
core.printInner("[SOUND ]: ERROR!!" ++ fmt ++ "\n", .{});
}
pub const SoundLoader = struct {
pub var LoaderInterfaceVTable: assets.AssetLoaderInterface = assets.AssetLoaderInterface.from(core.MakeName("Sound"), @This());
engine: *NeonSoundEngine,
pub fn init(engine: *NeonSoundEngine) @This() {
return @This(){
.engine = engine,
};
}
pub fn discardAll(self: *@This()) void {
_ = self;
}
pub fn destroy(self: *@This(), allocator: std.mem.Allocator) void {
allocator.destroy(self);
}
// unfortunately this one is blocking
pub fn loadAsset(self: *@This(), assetRef: assets.AssetRef, properties: ?assets.AssetPropertiesBag) assets.AssetLoaderError!void {
core.engine_log("loading sound asset {s}", .{properties.?.path});
self.engine.loadSound(assetRef.name, properties.?.path, .{
.volume = properties.?.soundVolume,
}) catch {
core.engine_log("unable to load sound {s}", .{assetRef.name.utf8()});
};
}
};
fn ma_res(value: anytype) !void {
if (value != ma.MA_SUCCESS) {
core.engine_err("miniaudio error value: {d}", .{value});
return error.MA_ERROR;
}
}
// On init, SoundEngine will spawn a
pub const NeonSoundEngine = struct {
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
engine: *ma.ma_engine,
sounds: AutoHashMapUnmanaged(u32, *ma.ma_sound),
allocator: std.mem.Allocator,
volume: f32 = 1.0,
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = @This(){
.engine = allocator.create(ma.ma_engine) catch unreachable,
.sounds = .{},
.allocator = allocator,
};
_ = ma.ma_engine_init(null, self.engine);
return self;
}
pub fn shutdown(self: *@This()) void {
ma.ma_engine_uninit(self.engine);
}
pub fn loadSound(
self: *@This(),
soundName: core.Name,
fileName: []const u8,
soundParams: struct {
looping: bool = false,
volume: f32 = 1.0,
},
) !void {
const sound = try self.allocator.create(ma.ma_sound);
errdefer self.allocator.destroy(sound);
const res = ma.ma_sound_init_from_file(self.engine, fileName.ptr, 0, null, null, sound);
if (res != ma.MA_SUCCESS) {
sound_err("tried loading sound: {s} failed", .{soundName.utf8()});
return error.MiniAudioError;
}
try self.sounds.put(self.allocator, soundName.handle(), sound);
ma.ma_sound_set_volume(sound, soundParams.volume);
}
pub fn playSound(self: *@This(), soundName: core.Name) !void {
const maybeSound = self.sounds.get(soundName.handle());
if (maybeSound == null)
return error.NoSoundError;
const sound = maybeSound.?;
if (ma.ma_sound_start(sound) != ma.MA_SUCCESS)
sound_err("unable to start sound: {s}", .{soundName.utf8()});
}
pub fn setVolume(self: *@This(), volume: f32) void {
self.volume = volume;
_ = ma.ma_engine_set_volume(self.engine, volume);
}
pub fn stopSound(self: *@This(), soundName: core.Name) void {
const sound = self.sounds.get(soundName.handle()).?;
if (ma.ma_sound_stop(sound) != ma.MA_SUCCESS) {
//
}
}
pub fn deinit(self: *@This()) void {
var iter = self.sounds.iterator();
while (iter.next()) |sound| {
self.allocator.destroy(sound.value_ptr.*);
}
self.sounds.deinit(self.allocator);
// ma.ma_engine_uninit(self.engine);
self.allocator.destroy(self.engine);
self.allocator.destroy(self);
}
pub fn tick(self: *@This(), deltaTime: f64) void {
var z = tracy.ZoneNC(@src(), "audio engine tick", 0xABBADD);
defer z.End();
_ = self;
_ = deltaTime;
}
};

View File

40
engine/core/build.zig Normal file
View File

@ -0,0 +1,40 @@
const std = @import("std");
const dependencyList = [_][]const u8{
"spng",
"nfd",
"p2",
"tracy",
"zmath",
"packer",
"zgltf",
"lua",
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("core", .{
.target = target,
.optimize = optimize,
.root_source_file = b.path("src/core.zig"),
});
for (dependencyList) |depName| {
const dep = b.dependency(depName, .{ .target = target, .optimize = optimize });
mod.addImport(depName, dep.module(depName));
}
const test_step = b.step("test", "run unit tests for core");
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tests/tests.zig"),
});
tests.root_module.addImport("core", mod);
const runArtifact = b.addRunArtifact(tests);
test_step.dependOn(&runArtifact.step);
b.installArtifact(tests);
}

17
engine/core/build.zig.zon Normal file
View File

@ -0,0 +1,17 @@
.{
.name = "core",
.version = "0.0.0",
.dependencies = .{
.spng = .{ .path = "../../lib/spng" },
.nfd = .{ .path = "../../lib/nfd" },
.p2 = .{ .path = "../../lib/p2" },
.tracy = .{ .path = "../../lib/tracy" },
.zmath = .{ .path = "../../lib/zmath" },
.packer = .{ .path = "../../lib/packer" },
.lua = .{ .path = "../../lib/lua" },
.zgltf = .{ .path = "../../lib/zgltf" },
},
.paths = .{
"",
},
}

40
engine/core/image.zig Normal file
View File

@ -0,0 +1,40 @@
// TODO --- this should become it's own engine library?
const std = @import("std");
const spng = @import("spng");
const core = @import("core.zig");
pub const PngContents = struct {
path: []const u8,
pixels: []u8,
size: core.Vector2u,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, filePath: []const u8) !@This() {
const pngFileContents = try core.loadFileAlloc(filePath, 1, allocator);
defer allocator.free(pngFileContents);
var decoder = try spng.SpngContext.newDecoder();
defer decoder.deinit();
try decoder.setBuffer(pngFileContents);
const header = try decoder.getHeader();
const imageSize = @as(usize, @intCast(header.width * header.height * 4));
core.graphics_log("loaded png {s}, dimensions={d}x{d}", .{ filePath, header.width, header.height });
const pixels: []u8 = try allocator.alloc(u8, imageSize);
const len = try decoder.decode(pixels, spng.SPNG_FMT_RGBA8, spng.SPNG_DECODE_TRNS);
try core.assertf(len == pixels.len, "decoded pixel size not buffer size {d} != {d}", .{ len, pixels.len });
return PngContents{
.path = try core.dupe(u8, allocator, filePath),
.pixels = pixels,
.size = .{ .x = header.width, .y = header.height },
.allocator = allocator,
};
}
pub fn deinit(self: *@This()) void {
self.allocator.free(self.path);
self.allocator.free(self.pixels);
}
};

View File

@ -0,0 +1,28 @@
// contains various engine callbacks for other modules to hook into.
// these are ALL called on the system thread.
allocator: std.mem.Allocator,
onFrameDebugInfoEmitted: std.ArrayListUnmanaged(OnFrameDebugInfoEmitted) = .{},
pub fn init(allocator: std.mem.Allocator) @This() {
return .{
.allocator = allocator,
};
}
pub fn deinit(self: *@This()) void {
self.onFrameDebugInfoEmitted.deinit(self.allocator);
}
const std = @import("std");
const engineObject = @import("engineObject.zig");
const EngineDataEventError = engineObject.EngineDataEventError;
// callback definition for a function which updates engine frametime.
const OnFrameDebugInfoEmitted = struct {
ctx: *anyopaque,
func: *const fn (
*anyopaque, // active context
f64, //
) EngineDataEventError!void,
};

View File

@ -0,0 +1,307 @@
// memory tracker
//
// todo.. rename this to memory.zig
const std = @import("std");
const core = @import("core.zig");
const builtin = @import("builtin");
lock: std.Thread.Mutex = .{},
backingAllocator: std.mem.Allocator,
allocationsCount: u32 = 0,
totalAllocSize: usize = 0,
eventsCount: usize = 0,
peakAllocations: u32 = 0,
peakAllocSize: usize = 0,
stackCompactor: ?*core.StackCompactor = undefined,
timeline: ?EventTimeline,
//const EventTimeline = core.algorithm.PagedVector(AllocEvent);
const EventTimeline = struct {
timelineAllocator: std.mem.Allocator,
events: core.algorithm.PagedVector(AllocEvent),
timestamps: core.algorithm.PagedVector(f64), // timestamp in milliseconds
pub fn init(timelineAllocator: std.mem.Allocator) !@This() {
return .{
.timelineAllocator = timelineAllocator,
.events = try core.algorithm.PagedVector(AllocEvent).init(timelineAllocator),
.timestamps = try core.algorithm.PagedVector(f64).init(timelineAllocator),
};
}
pub inline fn pushAlloc(self: *@This(), compactor: *core.StackCompactor, ptr: usize, size: usize) void {
const callstackId = compactor.getCallStack();
self.timestamps.append(core.getEngineTime()) catch unreachable;
self.events.append(.{
.callstackId = callstackId,
.address = ptr,
.event = .{ .alloc = .{ .size = size } },
}) catch unreachable;
}
pub inline fn pushResize(self: *@This(), compactor: *core.StackCompactor, ptr: usize, old_len: usize, new_len: usize) void {
const callstackId = compactor.getCallStack();
self.timestamps.append(core.getEngineTime()) catch unreachable;
self.events.append(.{
.callstackId = callstackId,
.address = ptr,
.event = .{ .resize = .{ .oldSize = old_len, .newSize = new_len } },
}) catch unreachable;
}
pub inline fn pushFree(self: *@This(), compactor: *core.StackCompactor, ptr: usize, size: usize) void {
const callstackId = compactor.getCallStack();
self.timestamps.append(core.getEngineTime()) catch unreachable;
self.events.append(.{
.callstackId = callstackId,
.address = ptr,
.event = .{ .free = .{ .size = size } },
}) catch unreachable;
}
};
// warning, due to laziness this uses timelineAllocator, which will leak
pub fn dumpTimeline(filename: []const u8) !void {
if (MTGet()) |tracker| {
tracker.lock.lock();
defer tracker.lock.unlock();
if (tracker.stackCompactor) |compactor| {
if (tracker.timeline) |timeline| {
const cwd = std.fs.cwd();
const ofile = try std.fmt.allocPrint(timeline.timelineAllocator, core.DefaultSavePath ++ "/{s}", .{filename});
defer timeline.timelineAllocator.free(ofile);
var obuf = std.ArrayList(u8).init(timeline.timelineAllocator);
defer obuf.deinit();
var writer = obuf.writer();
var i: usize = 0;
while (i < timeline.events.len()) : (i += 1) {
const event = timeline.events.get(i);
const timestamp = timeline.timestamps.get(i);
try writer.print("{d}: callstack: {x} @0x{x} ", .{
timestamp.*,
event.callstackId,
event.address,
});
switch (event.event) {
.alloc => |x| {
try writer.print("alloc {d} bytes\n", .{x.size});
},
.free => |x| {
try writer.print("free {d} bytes\n", .{x.size});
},
.resize => |x| {
try writer.print("resize {d} -> {d} bytes\n", .{ x.oldSize, x.newSize });
},
}
}
var iter = compactor.stackMap.iterator();
while (iter.next()) |x| {
const callstackId = x.key_ptr.*;
const stack = x.value_ptr.*;
try writer.print("{x}:\n", .{callstackId});
for (stack.debugStr) |frame| {
if (frame) |f| {
try writer.print("{s}\n", .{f});
} else {
try writer.print("no file\n", .{});
}
}
}
try cwd.makePath(core.DefaultSavePath);
try cwd.writeFile(.{
.sub_path = ofile,
.data = obuf.items,
});
}
} else {
core.engine_logs("skipping writing memory timeline as timeline is not enabled");
}
}
}
const AllocEventType = enum { alloc, free, resize };
const AllocEvent = struct {
callstackId: u32,
address: usize,
event: union(AllocEventType) {
alloc: struct { size: usize },
free: struct { size: usize },
resize: struct { oldSize: usize, newSize: usize },
},
};
pub var vtable: std.mem.Allocator.VTable = .{
.alloc = alloc,
.free = free,
.resize = resize,
};
pub fn init(backingAllocator: std.mem.Allocator, settings: SetupSettings) @This() {
// use the ansi allocator
const EnableMemoryTimeline = settings.timeline and builtin.os.tag == .windows;
if (EnableMemoryTimeline) {
core.engine_logs("[dmt] Enabling Detailed Memory Tracking");
}
const stackCompactor = if (EnableMemoryTimeline) core.StackCompactor.create(std.heap.c_allocator) catch null else null;
return .{
.backingAllocator = backingAllocator,
.stackCompactor = stackCompactor,
.timeline = if (EnableMemoryTimeline) EventTimeline.init(std.heap.c_allocator) catch null else null,
};
}
pub fn allocator(self: *@This()) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &vtable,
};
}
pub fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
var self: *@This() = @alignCast(@ptrCast(ctx));
const rv = self.backingAllocator.vtable.alloc(self.backingAllocator.ptr, len, ptr_align, ret_addr);
{
self.lock.lock();
defer self.lock.unlock();
self.allocationsCount += 1;
self.totalAllocSize += len;
self.eventsCount += 1;
if (self.totalAllocSize > self.peakAllocSize) {
self.peakAllocSize = self.totalAllocSize;
}
if (self.allocationsCount > self.peakAllocations) {
self.peakAllocations = self.allocationsCount;
}
if (self.timeline) |*timeline| {
timeline.pushAlloc(self.stackCompactor.?, @intFromPtr(rv), len);
}
}
return rv;
}
pub fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
var self: *@This() = @alignCast(@ptrCast(ctx));
{
self.lock.lock();
defer self.lock.unlock();
self.totalAllocSize = self.totalAllocSize - buf.len + new_len;
self.eventsCount += 1;
if (self.timeline) |*timeline| {
timeline.pushResize(self.stackCompactor.?, @intFromPtr(buf.ptr), buf.len, new_len);
}
}
return self.backingAllocator.vtable.resize(
self.backingAllocator.ptr,
buf,
buf_align,
new_len,
ret_addr,
);
}
pub fn free(ctx: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void {
var self: *@This() = @alignCast(@ptrCast(ctx));
{
self.lock.lock();
defer self.lock.unlock();
self.allocationsCount -= 1;
self.totalAllocSize -= buf.len;
self.eventsCount += 1;
if (self.timeline) |*timeline| {
timeline.pushFree(self.stackCompactor.?, @intFromPtr(buf.ptr), buf.len);
}
}
self.backingAllocator.vtable.free(self.backingAllocator.ptr, buf, buf_align, ret_addr);
}
pub fn printStats(self: @This()) void {
std.debug.print("allocations: {d}\n", .{self.allocationsCount});
std.debug.print("memory committed: {d}\n", .{self.totalAllocSize});
}
pub fn deinit(self: *@This()) void {
_ = self;
}
pub fn addUntrackedAllocation(self: *@This(), allocatedSize: usize) void {
self.totalAllocSize += allocatedSize;
}
pub fn removeUntrackedAllocation(self: *@This(), allocatedSize: usize) void {
self.totalAllocSize -= allocatedSize;
}
var gMemTracker: ?*@This() = null;
pub const SetupSettings = struct {
timeline: bool = false,
};
pub fn MTSetup(backingAllocator: std.mem.Allocator, settings: SetupSettings) void {
gMemTracker = backingAllocator.create(@This()) catch unreachable;
gMemTracker.?.* = @This().init(backingAllocator, settings);
}
pub fn MTShutdown() void {
var backingAllocator = gMemTracker.?.backingAllocator;
gMemTracker.?.deinit();
backingAllocator.destroy(gMemTracker.?);
gMemTracker = null;
}
pub fn MTGet() ?*@This() {
return gMemTracker;
}
// todo replace all these functions with a virtual table
pub fn MTAddUntrackedAllocation(allocatedSize: usize) void {
if (gMemTracker) |mt|
mt.addUntrackedAllocation(allocatedSize);
}
pub fn MTRemoveAllocation(allocatedSize: usize) void {
if (gMemTracker) |mt|
mt.removeUntrackedAllocation(allocatedSize);
}
pub fn MTPrintStatsDelta() void {
if (gMemTracker) |mt| {
core.engine_log("allocated size: {d} ({d:.3} MiB)", .{
mt.totalAllocSize,
@as(f64, @floatFromInt(mt.totalAllocSize)) / 1024 / 1024,
});
core.engine_log("peak allocated size size: {d} ({d:.3} MiB) ({d} peak allocations)", .{
mt.peakAllocSize,
@as(f64, @floatFromInt(mt.peakAllocSize)) / 1024 / 1024,
mt.peakAllocations,
});
}
}
pub fn PrintStatsWithTag(comptime tag: []const u8) void {
core.engine_logs(tag);
MTPrintStatsDelta();
}

132
engine/core/src/args.zig Normal file
View File

@ -0,0 +1,132 @@
// CLI argument parsing library
const std = @import("std");
pub fn ParseArgs(comptime T: type) !T {
// I think i am actually totally cool with leaking this one and leaving it around... just dont spam call this function
var iter = try std.process.argsWithAllocator(std.heap.c_allocator);
var args: T = .{};
const shortBuf: [32]u8 = undefined;
_ = shortBuf;
var longBuf: [256]u8 = undefined;
while (iter.next()) |a| {
inline for (@typeInfo(T).Struct.fields) |field| {
const longRef = try std.fmt.bufPrint(&longBuf, "--{s}", .{field.name});
if (std.mem.startsWith(u8, a, longRef)) {
// check if it has a value argument
if (a.len > longRef.len) {
if (a[longRef.len] == '=' and a.len > longRef.len + 1) {
const right = a[longRef.len + 1 ..];
switch (@typeInfo(field.type)) {
.Bool => {
if (std.mem.eql(u8, right, "true") or
std.mem.eql(u8, right, "t") or
std.mem.eql(u8, right, "True") or
std.mem.eql(u8, right, "TRUE") or
std.mem.eql(u8, right, "T") or
std.mem.eql(u8, right, "1"))
{
@field(args, field.name) = true;
} else if (std.mem.eql(u8, right, "false") or
std.mem.eql(u8, right, "f") or
std.mem.eql(u8, right, "False") or
std.mem.eql(u8, right, "FALSE") or
std.mem.eql(u8, right, "F") or
std.mem.eql(u8, right, "0"))
{
@field(args, field.name) = false;
} else {
std.debug.print("Error parsing: non-boolean argument", .{});
@field(args, field.name) = false;
}
},
.Int => {
@field(args, field.name) = try std.fmt.parseInt(field.type, right, 10);
},
.Float => {
@field(args, field.name) = try std.fmt.parseFloat(field.type, right);
},
.Pointer => |pointer| {
switch (pointer.size) {
.Slice => {
@field(args, field.name) = right;
},
else => {
@compileError("only u8 slices are supported");
},
}
},
else => {
@compileError("unable to generate argparse, unsupported struct type in field: " ++ field.name);
},
}
} else {
std.debug.print("Error parsing arguments: Expected value after argument assignment '{s}'\n", .{a});
return error.MalformedArgument;
}
} else {
switch (@typeInfo(field.type)) {
.Bool => {
@field(args, field.name) = true;
},
.Int, .Float => {
std.debug.print("Error parsing arguments: Expected value '{s}'\n", .{a});
return error.MalformedArgument;
},
.Pointer => |pointer| {
switch (pointer.size) {
.Slice => {
std.debug.print("Error parsing arguments: Expected value '{s}'\n", .{a});
return error.MalformedArgument;
},
else => {
@compileError("only u8 slices are supported");
},
}
},
else => {
@compileError("unable to generate argparse, unsupported struct type in field: " ++ field.name);
},
}
}
break;
}
}
}
return args;
}
pub fn main() !void {
const A = struct {
foo: f32 = 0,
bar: i32 = 0,
baz: bool = false,
test_file: []const u8 = "",
};
var iter = std.process.args();
const args = try ParseArgs(A, &iter);
std.debug.print("A: {any}", .{args});
}
test "test args" {
const A = struct {
foo: f32 = 0,
bar: i32 = 0,
baz: bool = false,
test_file: []const u8 = "",
};
const args = try ParseArgs(A, &.{ "--foo=32", "--bar=22", "--baz", "--test_file=lmao2nova" });
std.debug.assert(args.foo == 32);
std.debug.assert(args.bar == 22);
std.debug.assert(args.baz == true);
std.debug.assert(std.mem.eql(u8, args.test_file, "lmao2nova"));
}

View File

@ -0,0 +1,79 @@
const std = @import("std");
// RGBA format for color
pub const Color32 = u32;
pub const ColorRGBA8 = extern struct {
r: u8 = 0x0,
g: u8 = 0x0,
b: u8 = 0x0,
a: u8 = 0xff,
pub fn fromHex(hex: u32) @This() {
return .{
.r = @as(u8, @intCast((hex >> 24) & 0xFF)),
.g = @as(u8, @intCast((hex >> 16) & 0xFF)),
.b = @as(u8, @intCast((hex >> 8) & 0xFF)),
.a = @as(u8, @intCast((hex) & 0xFF)),
};
}
pub fn fromColor(o: Color) @This() {
return .{
.r = @as(u8, @intFromFloat(std.math.clamp(o.r, 0, 1.0) * 255)),
.g = @as(u8, @intFromFloat(std.math.clamp(o.g, 0, 1.0) * 255)),
.b = @as(u8, @intFromFloat(std.math.clamp(o.b, 0, 1.0) * 255)),
.a = @as(u8, @intFromFloat(std.math.clamp(o.a, 0, 1.0) * 255)),
};
}
};
pub const Color = extern struct {
r: f32 = 0,
g: f32 = 0,
b: f32 = 0,
a: f32 = 1.0,
pub const White = fromRGB(0xFFFFFF);
pub const Red = fromRGB(0xFF0000);
pub const Yellow = fromRGB(0xFFFF00);
pub const Orange = fromRGB(0xFF5500);
pub const Green = fromRGB(0x00FF00);
pub const Blue = fromRGB(0x0000FF);
pub const Cyan = fromRGB(0x00FFFF);
pub const Magenta = fromRGB(0xFF00FF);
pub const Black = fromRGB(0x000000);
pub fn intoRGBA(self: @This()) Color32 {
return ((@as(u32, @intFromFloat(self.r)) * 0xFF) << 24) |
((@as(u32, @intFromFloat(self.g)) & 0xFF) << 16) |
((@as(u32, @intFromFloat(self.b)) & 0xFF) << 8) |
((@as(u32, @intFromFloat(self.a)) & 0xFF));
}
pub fn fromRGB2(r: anytype, g: anytype, b: anytype) @This() {
return @This(){ .r = r, .g = g, .b = b, .a = 1.0 };
}
pub fn fromRGBA2(r: anytype, g: anytype, b: anytype, a: anytype) @This() {
return @This(){ .r = r, .g = g, .b = b, .a = a };
}
pub fn fromRGB(rgb: u32) @This() {
return @This(){
.r = @as(f32, @floatFromInt((rgb >> 16) & 0xFF)) / 255,
.g = @as(f32, @floatFromInt((rgb >> 8) & 0xFF)) / 255,
.b = @as(f32, @floatFromInt((rgb) & 0xFF)) / 255,
.a = 1.0,
};
}
pub fn fromRGBA(rgba: u32) @This() {
return @This(){
.r = @as(f32, @floatFromInt((rgba >> 24) & 0xFF)) / 255,
.g = @as(f32, @floatFromInt((rgba >> 16) & 0xFF)) / 255,
.b = @as(f32, @floatFromInt((rgba >> 8) & 0xFF)) / 255,
.a = @as(f32, @floatFromInt((rgba) & 0xFF)) / 255,
};
}
};

226
engine/core/src/core.zig Normal file
View File

@ -0,0 +1,226 @@
// main public facing root file for core
const std = @import("std");
pub const inputs = @import("inputs/inputStack.zig");
pub const getInputStack = inputs.getInputStack;
pub const ActionEvent = inputs.ActionEvent;
pub const Axis1dBinding = inputs.Axis1dBinding;
pub const Axis2dBinding = inputs.Axis2dBinding;
pub const ActionBinding = inputs.ActionBinding;
pub const IOEvent = inputs.IOEvent;
pub const debug_draw = @import("debug_draw.zig");
pub const DebugDrawParams = debug_draw.DebugDrawParams;
pub const DebugDrawInterface = debug_draw.DebugDrawInterface;
pub const installDebugDrawInterface = debug_draw.installDebugDrawInterface;
pub const debugSphere = debug_draw.debugSphere;
pub const debugSphereTransform = debug_draw.debugSphereTransform;
pub const debugBox = debug_draw.debugBox;
pub const debugLine = debug_draw.debugLine;
pub const script_bindings = script.script_bindings;
pub usingnamespace @import("misc.zig");
pub usingnamespace @import("logging.zig");
pub usingnamespace @import("engineTime.zig");
pub usingnamespace @import("engineObject.zig");
pub usingnamespace @import("jobs.zig");
pub usingnamespace @import("file_utils.zig");
pub usingnamespace @import("string_utils.zig");
pub usingnamespace @import("type_utils.zig");
pub const engine = @import("engine.zig");
pub const tracy = @import("tracy");
pub const png = @import("png.zig");
pub const colors = @import("colors.zig");
pub const zgltf = @import("zgltf");
pub const zm = @import("zmath");
pub usingnamespace @import("p2");
pub const algorithm = @import("p2");
pub const nfd = @import("nfd");
pub usingnamespace @import("math.zig");
pub usingnamespace @import("args.zig");
pub usingnamespace @import("file_dialogue.zig");
pub const panickers = @import("panickers.zig");
pub const scene = @import("scene.zig");
pub const SceneSystem = scene.SceneSystem;
pub const Engine = engine.Engine;
const Name = algorithm.Name;
pub const spng = @import("spng");
pub const MemoryTracker = @import("MemoryTracker.zig");
pub const DefaultSavePath = "Saved";
const logging = @import("logging.zig");
const c = @This();
const logs = logging.engine_logs;
const log = logging.engine_log;
pub const packer = @import("packer");
pub const FileSystem = packer.PackerFS;
const PackerFS = packer.PackerFS;
pub const StackCompactor = stacks.StackCompactor;
var gPackerFS: *PackerFS = undefined;
pub var gScene: *SceneSystem = undefined;
pub const Scene = scene.Scene;
pub const ecs = @import("ecs.zig");
pub usingnamespace ecs;
pub const script = @import("script.zig");
pub const stacks = @import("stacks.zig");
pub const walkAndPrintStack = stacks.walkAndPrintStack;
pub fn fs() *PackerFS {
return gPackerFS;
}
pub const Module = ModuleDescription{
.name = "core",
.enabledByDefault = true,
};
var gIsUtility: bool = false;
pub fn isUtility() bool {
return gIsUtility;
}
pub fn checkArgBool(args: anytype, comptime field: []const u8) bool {
if (@hasField(@TypeOf(args), "fatDump")) {
return @field(args, field);
}
return false;
}
pub fn start_module(comptime programSpec: anytype, args: anytype, allocator: std.mem.Allocator) !void {
if (@hasField(@TypeOf(programSpec), "utility")) {
logs("utility mode - no gui");
gIsUtility = true;
}
var fatDump: bool = false;
if (checkArgBool(args, "fatDump")) {
fatDump = true;
}
script.lua.setupMiniDump(fatDump);
_ = try algorithm.createNameRegistry(allocator);
// LUA BEGIN -- what if i want to make the scripting integration optional?
try script.start_lua(allocator);
// LUA END
gPackerFS = try PackerFS.init(allocator, .{});
gEngine = try allocator.create(Engine);
gEngine.* = try Engine.init(allocator);
if (@hasField(@TypeOf(args), "unitTest") and args.unitTest) {} else {
try logging.setupLogging(gEngine);
}
try ecs.setup(allocator);
gScene = try gEngine.createObject(scene.SceneSystem, .{ .can_tick = true });
try algorithm.string_pool.setup(allocator);
_ = try gEngine.createObject(script_bindings.ScriptTicks, .{ .can_tick = true });
_ = try inputs.initInputStack();
logs("core module starting up... ");
return;
}
pub fn shutdown_module(_: std.mem.Allocator) void {
logs("core module shutting down...");
MemoryTracker.MTPrintStatsDelta();
logging.shutdownLogging();
ecs.shutdown();
debug_draw.shutdownDrawInterface();
algorithm.destroyNameRegistry();
gEngine.deinit();
gPackerFS.destroy();
// LUA BEGIN
script.shutdown_lua();
// LUA END
algorithm.string_pool.shutdown();
return;
}
pub fn dispatchJob(capture: anytype) !void {
try gEngine.jobManager.newJob(capture);
}
pub var gEngine: *Engine = undefined;
pub fn createObject(comptime T: type, params: engine.NeonObjectParams) !*T {
return gEngine.createObject(T, params);
}
pub fn setupEnginePlatform(ctx: *anyopaque, poll: engine.PollFuncFn, proc: engine.ProcEventsFn) void {
gEngine.platformPollFunc = poll;
gEngine.platformCtx = ctx;
gEngine.platformProcEventsFunc = proc;
}
pub fn exitNow() void {
signalShutdown();
}
pub fn signalShutdown() void {
gEngine.exit();
}
pub fn getEngine() *Engine {
return gEngine;
}
pub fn BuildOption(comptime option: []const u8) bool {
if (@hasDecl(@import("root"), "options")) {
const r = @import("root").options;
if (@hasDecl(r, option)) {
return @field(r, option);
} else {
return false;
}
}
return false;
}
const EngineDelegates = @import("EngineDelegates.zig");
// binds a function + a context to
pub fn addEngineDelegateBinding(comptime event: []const u8, func: anytype, ctx: *anyopaque) !usize {
if (!@hasField(EngineDelegates, event)) {
@compileError("Engine does not have delegate " ++ event);
}
const handle = @field(gEngine.delegates, event).items.len;
try @field(gEngine.delegates, event).append(gEngine.delegates.allocator, .{ .func = func, .ctx = ctx });
return handle;
}
const getEngineTime = @import("engineTime.zig").getEngineTime;
pub fn getEngineUptime() f64 {
return getEngineTime() - gEngine.engineStartTime;
}
pub const modules = @import("modules.zig");
pub const isModuleEnabled = modules.isModuleEnabled;
pub const ModuleDescription = modules.ModuleDescription;

View File

@ -0,0 +1,56 @@
const std = @import("std");
const core = @import("core.zig");
pub const DebugDrawParams = struct {
color: core.Vectorf = .{ .x = 0, .y = 1.0, .z = 0 },
duration: f32 = 0,
rotation: core.Quat = .{ 0, 0, 0, 1 },
};
const DebugDrawInterface = struct {
debugSphereFn: *const fn (pos: core.Vectorf, radius: f32, DebugDrawParams) void,
debugBoxFn: *const fn (pos: core.Vectorf, extents: core.Vectorf, DebugDrawParams) void,
debugLineFn: *const fn (start: core.Vectorf, end: core.Vectorf, DebugDrawParams) void,
};
var gDebugDrawInterface: ?*DebugDrawInterface = null;
var gDebugDrawAllocator: ?std.mem.Allocator = null;
pub fn debugSphereTransform(t: core.Mat, radius: f32, params: DebugDrawParams) void {
if (gDebugDrawInterface) |i| {
i.debugSphereFn(core.Vectorf.fromZm(core.zm.mul(core.zm.Vec{ 0, 0, 0, 1 }, t)), radius, params);
}
}
pub fn debugSphere(position: core.Vectorf, radius: f32, params: DebugDrawParams) void {
if (gDebugDrawInterface) |i| {
i.debugSphereFn(position, radius, params);
}
}
pub fn debugLine(start: core.Vectorf, end: core.Vectorf, params: DebugDrawParams) void {
if (gDebugDrawInterface) |i| {
i.debugLineFn(start, end, params);
}
}
pub fn debugBox(pos: core.Vectorf, extents: core.Vectorf, params: DebugDrawParams) void {
if (gDebugDrawInterface) |i| {
i.debugBoxFn(pos, extents, params);
}
}
pub fn installDebugDrawInterface(allocator: std.mem.Allocator, newInterface: DebugDrawInterface) !void {
gDebugDrawInterface = try allocator.create(DebugDrawInterface);
gDebugDrawAllocator = allocator;
if (gDebugDrawInterface) |interface| {
interface.* = newInterface;
}
}
pub fn shutdownDrawInterface() void {
if (gDebugDrawInterface) |interface| {
gDebugDrawAllocator.?.destroy(interface);
}
}

411
engine/core/src/ecs.zig Normal file
View File

@ -0,0 +1,411 @@
var gEcsRegistry: *EcsRegistry = undefined;
// wew lad. this is definitely an exploratory implementation I think.
//
// todo: cruft
// - componentregistration.zig should contain all the code having to do with lua registration
// - entities should be under ecs/Entities.zig
//
// ECS: Entities, Containers, Systems
//
// So this is going to be a bit different thatn flecs or other more mainstream systems. You should think of
// this ecs system as more of a way to implement systems which can talk to each other on a level playing ground.
//
// My world view of how game systems should talk to each other is more like small processes, and the game state
// should be a collection of tiny databases that each contain discrete bits of information that piece together
// to produce a coherent vision of the world.
//
// Everything that should be tracked through ECS should be something that is gameplay significant.
// To this end, ecs has a maximum entity count of 1 million unique entities.
//
// This should be decent enough to pretty much implement 99% of features in th ecs features.
//
// However features which require a large amount of entities is likely a bad idea and not something that
// should be done in ecs.
//
// Eg. you wouldn't do a particle system in ecs and have each transform be a unique entry in the transforms
// sparse set.
//
// But you could conceivably do projectiles.
//
// Overall though you need to ask yourself. If something needs to be an entity or not. entities exist to
// recieve messages. Not nessecarily even send messages.
//
// The base implementation ideas are mostly solid but the level of cruft involed in scripting and ecs is a bit
// nasty at this time bear with me.
//
// Ok one mental model I think I can with entities is that each entity can be thought of as an independent object
// that contains no state.
//
// When it needs to actually do something, it gets added to a container and one or more systems now have a way
// to address the entity.
//
// systems that would be present for a character in a first person shooter for example
//
// --- game systems --- ones that the game would write and implement
//
// ( one or none of these three )
// player: recieves input messages from local input, forwards it to controller
// netplayer: recieves input messages from the network driver, forwards it to controller
// botplayer: generates control messages which drive bots and gameplay AI. forwards it to an controller
// - could use a behaviour tree under the hood
// arms: takes input actions and runs scripts to implement things that would be in front of the camera.
// - arms, weapons, etc...
// controller: converts raw player input to movement input for the character movement and other subsystems
// - eg. converts input axis to a vector movement
// - converts 'mouse_click_1' to a weapon fire call on the weapon component
// - also converts actions from the botplayer component which may send special components
// character: stores general gameplay relevant information about a given character.
// - gameplay stuff, updates values on the character_movement etc.. controls states, such as ragdolling
// character_movement: reconciles physics component data and movement coming in from controls
//
// --- engine systems --- ones that would come with the engine
// camera: Can be set as active. has a global state which tracks which camera is active
// physics: integration with the physics engine.
// animationGraph: animation graph drives skeletons inside the rendering_component
// renderScene: contains a list of subobjects which describe the visual-only scene for this character
// - renderScene will be the most complex one, lighter weight variants will include
// - staticMesh
// transform: final position of the character
// netAddress: acts as an endpoint to recieve network messages to this specific entity
//
// --- another thought experiment ----
// systems present in a mazing tower defense that is networked.
//
// game:
// mobs:
// mr/mazing:
// I think just this one system can handle the whole damn thing for enemies
// has a reference to the pathfinder entity. Which finds the best path for each enemy type to reach the
// main tower.
// mr/attributes:
// lightweight attributes which includes a buff/debuff system internally
// mr/pathfinder (singleton:):
// system which calls the pathfinding system to generate path segments for each of the segments of the map.
// can be queried based on position to tell the entity where to go next. includes an internal set of waypoints
//
// engine: systems that come with the engine
// p3/: peter's gameplay implementation toolbox.
// p3/grid_pathfinding: A* based pathfinding system, can create multiple grids and stitch them together.
//
// this is the kind of workflows I want to enable with ecs.
//
// - creating entities and associating that entity with any arbitrary number of systems
// - entities
//
// entities are globally unique ID handles. when you create an entity. you do so by basically asking
// the core system what set handle is free. this also adds that entity to the central registry
// which tracks stuff like if the object is completely free'd or not
//
// components which are a part of an entity are nothing more than an entry in another data structure called a container
// Any data structure can be used but the main data structures that are available for this purpose are all in
// p2/sparse-set.zig
//
// A few notes about this:
// Any data structure can be used as container storage it just needs a wrapper and implement
// the EcsContainerInterface found in sparse-set.zig
//
// current containers are:
//
// SparseMap (general purpose default, good enough for pretty much everything)
// SparseSet (only used for systsems where every entry gets iterated over every single frame)
// SparseMultiSet (AOS version of sparseSet Really specialized, only used for core engine systems)
pub fn createEntity() !Entity {
return .{ .handle = try gEcsRegistry.baseSet.createObject(.{}) };
}
pub fn CreateEntity_Lua(state: lua.LuaState) i32 {
const ud = state.newZigUserdata(Entity) catch return 0;
ud.* = createEntity() catch {
std.debug.print("failed to create entity", .{});
return 0;
};
_ = state.getGlobal("__RegisterEntityProperty");
_ = state.pushValue(-2) catch return 0;
core.engine_log("Entity created: 0x{x}", .{ud.handle.index});
_ = state.pcallStack(1) catch {
std.debug.print("faield to exectute registration function", .{});
return 0;
};
return 1;
}
pub fn setup(allocator: std.mem.Allocator) !void {
try ComponentRef.setupFormatBuffer(allocator);
gEcsRegistry = try core.createObject(EcsRegistry, .{ .can_tick = true });
}
pub fn shutdown() void {
ComponentRef.shutdownFormatBuffer();
}
pub fn getRegistry() *EcsRegistry {
return gEcsRegistry;
}
pub fn registerEcsContainer(ref: EcsContainerRef, name: core.Name) !void {
try gEcsRegistry.registerContainer(ref, name);
}
pub fn deregisterEcsContainer(ref: EcsContainerRef) void {
_ = ref;
@panic("not yet implemented");
}
pub fn createSystem(comptime System: type, allocator: std.mem.Allocator) !*System {
const system = try System.create(allocator);
const ref = p2.refFromPtr(EcsSystemInterface, system);
core.engine_log("ptr = {any}", .{ref.vtable.tick});
try gEcsRegistry.systems.append(gEcsRegistry.allocator, ref);
if (ref.vtable.tick != null) {
try gEcsRegistry.tickableSystems.append(gEcsRegistry.allocator, ref);
}
return system;
}
// only thing this is meant to do is to provide a central place to construct and destroy objects
pub const EcsRegistry = struct {
allocator: std.mem.Allocator,
baseSet: BaseSet,
systems: std.ArrayListUnmanaged(EcsSystemRef) = .{},
tickableSystems: std.ArrayListUnmanaged(EcsSystemRef) = .{},
containers: std.ArrayListUnmanaged(EcsContainerRef) = .{},
containerNames: std.ArrayListUnmanaged(core.Name) = .{},
containersByName: std.AutoHashMapUnmanaged(u32, u32) = .{},
pub const NeonObjectTable = core.EngineObjectVTable.from(@This());
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
.baseSet = BaseSet.init(allocator),
};
return self;
}
pub fn registerContainer(self: *@This(), ref: EcsContainerRef, _containerName: core.Name) !void {
var containerName = _containerName;
const newid = self.containers.items.len;
try self.containers.append(self.allocator, ref);
try self.containerNames.append(self.allocator, containerName);
try self.containersByName.put(self.allocator, containerName.handle(), @intCast(newid));
ref.vtable.onRegister(ref.ptr, @intCast(newid), .{
.ptr = @ptrCast(self),
.onHandleRemoved = onHandleRemoved,
.onHandleAdded = onHandleAdded,
});
}
pub fn onHandleRemoved(p: *anyopaque, containerID: u32, handle: core.ObjectHandle) void {
const self: *@This() = @ptrCast(@alignCast(p));
_ = containerID;
//core.engine_log("ECS:: object removed id={d} from container={s}({d}) 0x{x}", .{
// handle.index,
// self.containerNames.items[containerID].utf8(),
// containerID,
// @intFromPtr(self.containers.items[containerID].ptr),
//});
self.baseSet.get(handle).?.containersCount -= 1;
}
pub fn onHandleAdded(p: *anyopaque, containerID: u32, handle: core.ObjectHandle) void {
const self: *@This() = @ptrCast(@alignCast(p));
_ = containerID;
// core.engine_log("ECS:: object added id=0x{x} from container={s}({d}) 0x{x}", .{
// handle.index,
// self.containerNames.items[containerID].utf8(),
// containerID,
// @intFromPtr(self.containers.items[containerID].ptr),
// });
//
if (self.baseSet.get(handle)) |obj| {
obj.containersCount += 1;
} else {
_ = self.baseSet.createWithHandle(handle, .{ .containersCount = 1 }) catch unreachable;
}
}
pub fn tick(self: *@This(), deltaTime: f64) void {
if (core.getEngine().isShuttingDown())
return;
for (self.tickableSystems.items) |ref| {
ref.vtable.tick.?(ref.ptr, deltaTime);
}
}
pub fn deinit(self: *@This()) void {
self.destroy();
}
pub fn destroy(self: *@This()) void {
for (self.containers.items) |ref| {
ref.vtable.evictFromRegistry(ref.ptr);
}
for (self.systems.items) |ref| {
ref.vtable.destroy(ref.ptr);
}
self.systems.deinit(self.allocator);
self.tickableSystems.deinit(self.allocator);
self.baseSet.deinit();
self.containers.deinit(self.allocator);
self.containerNames.deinit(self.allocator);
self.containersByName.deinit(self.allocator);
self.allocator.destroy(self);
}
};
pub fn defineComponent(comptime Component: type, allocator: std.mem.Allocator) !void {
const ContainerType = @TypeOf(Component.BaseContainer.*);
Component.BaseContainer = try ContainerType.create(allocator);
core.engine_log("Component container created " ++ @typeName(Component) ++ " @{x}", .{@intFromPtr(Component.BaseContainer)});
const container = makeEcsContainerRef(Component.BaseContainer);
try registerEcsContainer(container, core.MakeName(@typeName(Component)));
try script.registerComponent(Component, container);
}
pub fn undefineComponent(comptime Component: type) void {
Component.BaseContainer.destroy();
}
pub const Entity = struct {
handle: core.ObjectHandle,
pub const PodDataTable: pod.DataTable = .{
.name = "Entity",
.newFuncOverride = lua.CWrap(CreateEntity_Lua),
.luaDirectFuncs = &.{
.{ .name = "addComponent", .func = "luaAddComponent" },
//.{ .name = "get", .func = "luaAddComponent" },
},
};
pub fn fromHandle(handle: core.ObjectHandle) @This() {
return .{ .handle = handle };
}
pub fn addComponent(self: @This(), comptime Component: type) ?*Component {
const rv = Component.BaseContainer.createWithHandleECS(self.handle);
// if (@hasDecl(Component, "init")) {
// rv.init(self.handle);
// }
return rv;
}
pub fn get(self: @This(), comptime Component: type) ?*Component {
const rv = Component.BaseContainer.get(self.handle);
return if (rv == null) null else @ptrCast(@alignCast(rv.?));
}
pub fn removeComponent(self: @This(), comptime Component: type) void {
Component.BaseContainer.remove(self.handle);
}
pub fn luaAddComponent(state: lua.LuaState) i32 {
const argc = state.getTop();
if (argc != 2) {
core.engine_log("Add Component Error, expected 2 arguments got {d}", .{argc});
return 0;
}
if (state.toUserdata(@This(), 1)) |self| {
if (state.toUserdata(ComponentRegistration, 2)) |componentRegistration| {
// core.engine_log("component Registration: name: {s}", .{componentRegistration.name});
// create the zig version of the component
const comp = componentRegistration.createComponent(self.handle);
// call luaNew on ComponentReferenceType
// create the lua binding for the component
// this pushes one onto the stack
componentRegistration.luaNew(state, self.handle, comp);
}
}
return 1;
}
};
pub const EcsComponentInterface = p2.MakeInterface("EcsComponentInterfaceVTable", struct {
container: ?EcsContainerRef = null,
pub fn Implement(comptime T: type) @This() {
_ = T;
const Impl = struct {};
_ = Impl;
return .{};
}
});
pub const EcsContainerInterface = p2.EcsContainerInterface;
pub const EcsContainerRef = p2.Reference(EcsContainerInterface);
pub fn makeEcsContainerRef(ptr: anytype) EcsContainerRef {
return p2.refFromPtr(EcsContainerInterface, ptr);
}
pub fn getTypeContainer(comptime T: type) EcsContainerRef {
return makeEcsContainerRef(T.BaseContainer);
}
pub const EcsEntry = struct {
containersCount: u32 = 0,
};
pub const BaseSet = p2.SparseSet(EcsEntry);
pub const EcsSystemRef = p2.Reference(EcsSystemInterface);
pub const EcsSystemInterface = p2.MakeInterface("EcsSystemVTable", struct {
create: *const fn (std.mem.Allocator) core.EngineDataEventError!*anyopaque,
destroy: *const fn (*anyopaque) void,
tick: ?*const fn (*anyopaque, f64) void = null,
pub fn Implement(comptime TargetType: type) @This() {
const Wrap = struct {
pub fn create(allocator: std.mem.Allocator) core.EngineDataEventError!*anyopaque {
const new = try TargetType.create(allocator);
return new;
}
pub fn destroy(p: *anyopaque) void {
const ptr: *TargetType = @ptrCast(@alignCast(p));
ptr.destroy();
}
pub fn tick(p: *anyopaque, dt: f64) void {
const ptr: *TargetType = @ptrCast(@alignCast(p));
ptr.tick(dt);
}
};
return .{
.destroy = Wrap.destroy,
.create = Wrap.create,
.tick = if (@hasDecl(TargetType, "tick")) Wrap.tick else null,
};
}
});
const ComponentRegistration = @import("script/ComponentRegistration.zig");
const ComponentRef = @import("script/ComponentRef.zig");
const script = @import("script.zig");
const std = @import("std");
const p2 = @import("p2");
const core = @import("core.zig");
const lua = @import("lua");
const pod = lua.pod;
const scene = @import("scene.zig");

360
engine/core/src/engine.zig Normal file
View File

@ -0,0 +1,360 @@
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" {}

View File

@ -0,0 +1,162 @@
const std = @import("std");
const logging = @import("logging.zig");
const input = @import("input.zig");
const p2 = @import("p2");
const engine_logs = logging.engine_logs;
const ObjectHandle = p2.ObjectHandle;
const Name = p2.Name;
const MakeName = p2.MakeName;
const ArrayList = std.ArrayList;
const AutoHashMap = std.AutoHashMap;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const AutoHashMapUnmanaged = std.AutoHashMapUnmanaged;
pub fn InterfaceRef(comptime Vtable: type) type {
return struct {
ptr: *anyopaque,
vtable: *const Vtable,
};
}
pub fn MakeTypeName(comptime TargetType: type) Name {
const hashedName = comptime std.fmt.comptimePrint("{s}_{d}", .{ @typeName(TargetType), @sizeOf(TargetType) });
return MakeName(hashedName);
}
// todo.. engineObject is not the right word for this
pub const EngineDataEventError = error{
UnknownStatePanic,
BadInit,
UnknownError,
OutOfMemory,
};
pub const EngineObjectVTable = struct {
typeName: []const u8,
typeSize: usize,
typeAlign: usize,
init_func: *const fn (std.mem.Allocator) EngineDataEventError!*anyopaque,
tick_func: ?*const fn (*anyopaque, f64) void = null,
engineDraw_func: ?*const fn (*anyopaque, f64) void = null,
preTick_func: ?*const fn (*anyopaque, f64) EngineDataEventError!void = null,
deinit_func: ?*const fn (*anyopaque) void = null,
postInit_func: ?*const fn (*anyopaque) EngineDataEventError!void = null,
processEvents: ?*const fn (*anyopaque, u64) EngineDataEventError!void = null,
exitSignal_func: ?*const fn (*anyopaque) EngineDataEventError!void = null,
readyToExit_func: ?*const fn (*anyopaque) bool = null,
pub fn from(comptime TargetType: type) EngineObjectVTable {
const wrappedInit = struct {
const funcFind: @TypeOf(@field(TargetType, "init")) = @field(TargetType, "init");
pub fn func(allocator: std.mem.Allocator) EngineDataEventError!*anyopaque {
const newObject = funcFind(allocator) catch return error.BadInit;
return @as(*anyopaque, @ptrCast(newObject));
}
};
var self = EngineObjectVTable{
.typeName = @typeName(TargetType),
.typeSize = @sizeOf(TargetType),
.typeAlign = @alignOf(TargetType),
.init_func = wrappedInit.func,
};
if (@hasDecl(TargetType, "postInit")) {
const wrappedPostInit = struct {
pub fn func(pointer: *anyopaque) EngineDataEventError!void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
try ptr.postInit();
}
};
self.postInit_func = wrappedPostInit.func;
}
if (@hasDecl(TargetType, "preTick")) {
const Wrapped = struct {
pub fn func(pointer: *anyopaque, deltaTime: f64) EngineDataEventError!void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
ptr.preTick(deltaTime) catch return error.UnknownStatePanic;
}
};
self.preTick_func = Wrapped.func;
}
if (@hasDecl(TargetType, "engineDraw")) {
const Wrapped = struct {
pub fn func(pointer: *anyopaque, deltaTime: f64) void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
ptr.engineDraw(deltaTime);
}
};
self.engineDraw_func = Wrapped.func;
}
if (@hasDecl(TargetType, "tick")) {
const Wrapped = struct {
pub fn func(pointer: *anyopaque, deltaTime: f64) void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
ptr.tick(deltaTime);
}
};
self.tick_func = Wrapped.func;
}
if (@hasDecl(TargetType, "processEvents")) {
const wrappedProcessEvents = struct {
pub fn func(pointer: *anyopaque, frameNumber: u64) EngineDataEventError!void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
try ptr.processEvents(frameNumber);
}
};
self.processEvents = wrappedProcessEvents.func;
}
if (@hasDecl(TargetType, "onExitSignal")) {
if (!@hasDecl(TargetType, "readyToExit")) {
@compileError("engine object implemented onExitSignal but did not implement fn readyToExit() bool");
}
const wrappedProcessEvents = struct {
pub fn onExitSignal(pointer: *anyopaque) EngineDataEventError!void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
try ptr.onExitSignal();
}
pub fn readyToExit(pointer: *anyopaque) bool {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
return ptr.readyToExit();
}
};
self.exitSignal_func = wrappedProcessEvents.onExitSignal;
self.readyToExit_func = wrappedProcessEvents.readyToExit;
}
if (@hasDecl(TargetType, "deinit")) {
const wrappedDeinit = struct {
pub fn func(pointer: *anyopaque) void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(pointer)));
ptr.deinit();
}
};
self.deinit_func = wrappedDeinit.func;
}
return self;
}
};
pub const EngineObjectRef = InterfaceRef(EngineObjectVTable);

View File

@ -0,0 +1,11 @@
const std = @import("std");
// todo need a better timing system than this
pub fn getEngineTime() f64 {
return @as(f64, @floatFromInt(std.time.milliTimestamp())) / 1000;
}
// return the current system timestamp in nanoseconds
pub fn getEngineTimeStamp() i128 {
return std.time.nanoTimestamp();
}

View File

@ -0,0 +1,21 @@
const std = @import("std");
const core = @import("core.zig");
const nfd = @import("nfd");
pub const AsyncOpenFileDialogArgs = nfd.AsyncOpenFileDialogArgs;
pub fn openFileDialog(defaultPath: []const u8, filters: []const u8) ![*c]const u8 {
return try core.getEngine().nfdRuntime.openFileDialog(defaultPath, filters);
}
pub fn openFolderDialog(defaultPath: []const u8, context: ?*anyopaque) ![*c]const u8 {
return try core.getEngine().nfdRuntime.openFolderDialog(defaultPath, context);
}
pub fn asyncOpenFile(args: AsyncOpenFileDialogArgs) !void {
return try core.getEngine().nfdRuntime.asyncOpenFileDialog(args);
}
pub fn asyncOpenFolder(args: AsyncOpenFileDialogArgs) !void {
return try core.getEngine().nfdRuntime.asyncOpenFolderDialog(args);
}

View File

@ -0,0 +1,34 @@
const std = @import("std");
const logging = @import("logging.zig");
pub fn writeToFile(data: []const u8, path: []const u8) !void {
const file = try std.fs.cwd().createFile(
path,
.{
.read = true,
},
);
const bytes_written = try file.writeAll(data);
_ = bytes_written;
logging.engine_log("written: bytes to {s}", .{path});
}
pub fn splitIntoLines(file_contents: []const u8) std.mem.SplitIterator(u8) {
// find a \n and see if it has \r\n
var index: u32 = 0;
while (index < file_contents.len) : (index += 1) {
if (file_contents[index] == '\n') {
if (index > 0) {
if (file_contents[index - 1] == '\r') {
return std.mem.split(u8, file_contents, "\r\n");
} else {
return std.mem.split(u8, file_contents, "\n");
}
} else {
return std.mem.split(u8, file_contents, "\n");
}
}
}
return std.mem.split(u8, file_contents, "\n");
}

20
engine/core/src/input.zig Normal file
View File

@ -0,0 +1,20 @@
// procedural functions to describe input primitives with which
// we can build on the rest of the engine.
const std = @import("std");
const engineObject = @import("engineObject.zig");
pub const InputSubsystem = struct {
const Self = @This();
pub var NeonObjectTable: engineObject.EngineObjectVTable = engineObject.EngineObjectVTable.from(Self);
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Self {
const self = Self{
.allocator = allocator,
};
return self;
}
};

View File

@ -0,0 +1,619 @@
pub const BindingType = enum(u8) {
action,
axis1d,
axis2d,
};
const glfw_release = 0;
pub const IOEvent = union(enum(u8)) {
windowFocused: struct { focused: c_int },
mousePosition: struct { x: f64, y: f64 },
mouseButton: struct { button: c_int, action: c_int, mods: c_int },
scroll: struct { xoffset: f64, yoffset: f64 },
key: struct { key: c_int, scancode: c_int, action: c_int, mods: c_int },
windowResize: struct { newSize: core.Vector2f },
codepoint: c_uint,
};
pub const ActionEvent = enum(u8) {
keyUp,
keyDown,
keyHeld,
};
pub const ActionBindingKey = struct {
key: Key,
event: ActionEvent = .keyDown,
};
fn arenaAlloc() std.mem.Allocator {
return gInputStack.arena.allocator();
}
fn BindingData(Listener: type, Func: type) type {
return struct {
keysDown: KeysDownQueue,
keysUp: std.ArrayListUnmanaged(Key) = .{},
listeners: std.ArrayListUnmanaged(Listener) = .{},
consumed: bool = false,
routed: bool = false,
count: u32 = 0,
name: core.Name,
pub const KeysDownQueue = core.RingQueueU(struct { key: Key, first: bool = true });
pub fn init(name: core.Name) @This() {
return .{
.name = name,
.keysDown = KeysDownQueue.init(arenaAlloc(), 256) catch unreachable,
};
}
pub fn addListener(self: *@This(), ctx: ?*anyopaque, func: Func) u32 {
const allocator = arenaAlloc();
self.listeners.append(allocator, .{ .id = self.count, .ctx = ctx, .func = func }) catch unreachable;
self.count +%= 1;
return self.count;
}
pub fn removeListener(self: *@This(), id: u32) void {
for (self.listeners.items.len, 0..) |listener, i| {
if (listener.id == id) {
self.listeners.swapRemove(i);
return;
}
}
}
pub fn setKeyDown(self: *@This(), key: Key) void {
self.keysDown.push(.{ .key = key, .first = true }) catch unreachable;
}
pub fn setKeyHeld(self: *@This(), key: Key) void {
// core.engine_log("repeating key {any}", .{key});
self.keysDown.push(.{ .key = key, .first = false }) catch unreachable;
}
pub fn setKeyUp(self: *@This(), key: Key) void {
self.keysUp.append(arenaAlloc(), key) catch unreachable;
}
pub fn deinit(self: *@This()) void {
self.keysDown.deinit(arenaAlloc());
self.listeners.deinit(arenaAlloc());
}
};
}
pub const ActionBinding = struct {
data: BindingData(Listener, ActionFunc),
keys: std.ArrayListUnmanaged(ActionBindingKey) = .{},
pub const Listener = struct {
id: u32,
ctx: ?*anyopaque,
func: ActionFunc,
};
pub fn create(name: core.Name) !*@This() {
const allocator = arenaAlloc();
const self = try allocator.create(@This());
self.* = .{
.data = BindingData(Listener, ActionFunc).init(name),
};
return self;
}
// pushes this binding to the active layer
pub fn activate(self: *@This()) void {
gInputStack.active.addBindingByName(self.data.name, self) catch unreachable;
}
pub fn deactivate(self: *@This()) void {
gInputStack.active.removeBindingByName(self.data.name);
}
pub fn addKey(self: *@This(), key: Key, event: ActionEvent) void {
const allocator = arenaAlloc();
self.keys.append(allocator, .{ .key = key, .event = event }) catch unreachable;
}
pub fn deinit(self: *@This()) void {
const allocator = arenaAlloc();
self.data.deinit();
self.keys.deinit(allocator);
}
};
pub const ActionFunc = *const fn (?*anyopaque, ActionEvent) void;
pub const AxisBindingKeyMagnitude = struct {
key: Key,
magnitude: f32 = 1.0,
};
pub const Axis1dFunc = *const fn (?*anyopaque, f32) void;
pub const Axis1dBinding = struct {
data: BindingData(Listener, Axis1dFunc),
keys: std.ArrayListUnmanaged(AxisBindingKeyMagnitude) = .{},
magnitude: f32 = 0.0,
pub const Listener = struct {
id: u32,
ctx: ?*anyopaque,
func: Axis1dFunc,
};
pub fn create(name: core.Name) !*@This() {
const allocator = arenaAlloc();
const self = try allocator.create(@This());
self.* = .{
.data = BindingData(Listener, Axis1dFunc).init(name),
};
return self;
}
// pushes this binding to the active layer
pub fn activate(self: *@This()) void {
gInputStack.active.addBindingByName(self.data.name, self) catch unreachable;
}
pub fn deactivate(self: *@This()) void {
gInputStack.active.removeBindingByName(self.data.name);
}
pub fn addKey(self: *@This(), key: Key, magnitude: f32) void {
const allocator = arenaAlloc();
self.keys.append(allocator, .{ .key = key, .magnitude = magnitude }) catch unreachable;
}
pub fn deinit(self: *@This()) void {
const allocator = arenaAlloc();
self.data.deinit();
self.keys.deinit(allocator);
self.listeners.deinit(allocator);
}
};
pub const Axis2dFunc = *const fn (?*anyopaque, core.Vector2f) void;
pub const Axis2dBinding = struct {
data: BindingData(Listener, Axis2dFunc),
yKeys: std.ArrayListUnmanaged(AxisBindingKeyMagnitude) = .{},
xKeys: std.ArrayListUnmanaged(AxisBindingKeyMagnitude) = .{},
magnitude: core.Vector2f = core.Vector2f.Zeroes,
pub const Listener = struct {
id: u32,
ctx: ?*anyopaque,
func: Axis2dFunc,
};
pub fn create(name: core.Name) !*@This() {
const allocator = arenaAlloc();
const self = try allocator.create(@This());
self.* = .{
.data = BindingData(Listener, Axis2dFunc).init(name),
};
return self;
}
// pushes this binding to the active layer
pub fn activate(self: *@This()) void {
gInputStack.active.addBindingByName(self.data.name, self) catch unreachable;
}
pub fn deactivate(self: *@This()) void {
gInputStack.active.removeBindingByName(self.data.name);
}
pub fn addKey(self: *@This(), key: Key, magnitude: f32, axis: enum { x, y }) void {
const allocator = arenaAlloc();
switch (axis) {
.x => {
self.xKeys.append(allocator, .{ .key = key, .magnitude = magnitude }) catch unreachable;
},
.y => {
self.yKeys.append(allocator, .{ .key = key, .magnitude = magnitude }) catch unreachable;
},
}
}
pub fn deinit(self: *@This()) void {
const allocator = arenaAlloc();
self.xKeys.deinit(allocator);
self.yKeys.deinit(allocator);
self.data.deinit();
}
};
pub const Binding = union(BindingType) {
action: *ActionBinding,
axis1d: *Axis1dBinding,
axis2d: *Axis2dBinding,
pub fn setKeyDown(self: @This(), key: Key) void {
switch (self) {
.action => |p| {
p.data.setKeyDown(key);
},
.axis1d => |p| {
p.data.setKeyDown(key);
},
.axis2d => |p| {
p.data.setKeyDown(key);
},
}
}
pub fn setKeyHeld(self: @This(), key: Key) void {
switch (self) {
.action => |p| {
p.data.setKeyHeld(key);
},
.axis1d => |p| {
p.data.setKeyHeld(key);
},
.axis2d => |p| {
p.data.setKeyHeld(key);
},
}
}
pub fn setKeyUp(self: @This(), key: Key) void {
switch (self) {
.action => |p| {
p.data.setKeyUp(key);
},
.axis1d => |p| {
p.data.setKeyUp(key);
},
.axis2d => |p| {
p.data.setKeyUp(key);
},
}
}
pub fn sendAxisUpdates(self: @This()) void {
switch (self) {
.action => {},
.axis1d => |p| {
p.data.routed = false;
for (p.data.listeners.items) |listener| {
listener.func(listener.ctx, p.magnitude);
}
p.magnitude = 0.0;
},
.axis2d => |p| {
p.data.routed = false;
for (p.data.listeners.items) |listener| {
listener.func(listener.ctx, p.magnitude);
}
p.magnitude = core.Vector2f.Zeroes;
},
}
}
pub fn routeBindingEvent(self: @This(), key: Key, event: ActionEvent, consumedOut: *bool) bool {
var consumed: bool = false;
var routed: bool = false;
switch (self) {
.action => |p| {
for (p.keys.items) |keyBinding| {
if (keyBinding.key == key and event == keyBinding.event) {
consumed = p.data.consumed;
routed = true;
}
}
if (routed) {
for (p.data.listeners.items) |listener| {
listener.func(listener.ctx, event);
}
}
p.data.routed = routed;
},
.axis1d => |p| {
for (p.keys.items) |keyBinding| {
if (keyBinding.key == key and (event == .keyDown or event == .keyHeld)) {
p.magnitude += keyBinding.magnitude;
consumed = p.data.consumed;
routed = true;
}
}
p.magnitude = std.math.clamp(p.magnitude, -1.0, 1.0);
p.data.routed = routed;
// for (p.data.listeners.items) |listener| {
// listener.func(listener.ctx, magnitude);
// }
},
.axis2d => |p| {
for (p.xKeys.items) |keyBinding| {
if (keyBinding.key == key and (event == .keyDown or event == .keyHeld)) {
p.magnitude.x += keyBinding.magnitude;
consumed = p.data.consumed;
routed = true;
}
}
for (p.yKeys.items) |keyBinding| {
if (keyBinding.key == key and (event == .keyDown or event == .keyHeld)) {
p.magnitude.y += keyBinding.magnitude;
consumed = p.data.consumed;
routed = true;
}
}
p.magnitude.x = std.math.clamp(p.magnitude.x, -1.0, 1.0);
p.magnitude.y = std.math.clamp(p.magnitude.y, -1.0, 1.0);
// for (p.data.listeners.items) |listener| {
// listener.func(listener.ctx, p.magnitude);
// }
p.data.routed = routed;
},
}
consumedOut.* = consumed;
return routed;
}
fn processKeysHeldDown(self: @This(), p: anytype) void {
const keysCount = p.data.keysDown.count();
var i: usize = 0;
while (i < keysCount) : (i += 1) {
const keydown = p.data.keysDown.pop().?;
var isKeyUp: bool = false;
for (p.data.keysUp.items) |up| {
if (up == keydown.key) {
isKeyUp = true;
break;
}
}
var consumed: bool = false;
if (!isKeyUp and !keydown.first) {
_ = self.routeBindingEvent(keydown.key, .keyHeld, &consumed);
}
if (!isKeyUp) {
p.data.setKeyHeld(keydown.key);
}
}
p.data.keysUp.clearRetainingCapacity();
}
pub fn processHeldKeys(self: @This()) void {
switch (self) {
.action => |p| {
self.processKeysHeldDown(p);
},
.axis1d => |p| {
self.processKeysHeldDown(p);
},
.axis2d => |p| {
self.processKeysHeldDown(p);
},
}
}
pub fn releaseKeys(self: @This()) void {
switch (self) {
.action => |p| {
p.keysDown.clearRetainingCapacity();
},
.axis1d => |p| {
p.keysDown.clearRetainingCapacity();
},
.axis2d => |p| {
p.keysDown.clearRetainingCapacity();
},
}
}
};
pub const BindingLayer = struct {
allocator: std.mem.Allocator,
bindingsByName: std.AutoHashMapUnmanaged(u32, u32) = .{},
bindingStack: std.ArrayListUnmanaged(Binding) = .{},
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
};
return self;
}
pub fn addBindingByName(self: *@This(), _name: core.Name, new: anytype) !void {
var name = _name;
if (self.bindingsByName.contains(name.handle())) {
self.removeBindingByName(name);
}
try self.bindingsByName.put(self.allocator, name.handle(), @intCast(self.bindingStack.items.len));
var newBinding: Binding = undefined;
switch (@TypeOf(new)) {
*ActionBinding => {
newBinding = .{ .action = new };
},
*Axis1dBinding => {
newBinding = .{ .axis1d = new };
},
*Axis2dBinding => {
newBinding = .{ .axis2d = new };
},
else => {
@panic("uh oh");
},
}
try self.bindingStack.append(self.allocator, newBinding);
}
pub fn removeBindingByName(self: *@This(), _name: core.Name) void {
var name = _name;
_ = self.bindingStack.orderedRemove(self.bindingsByName.get(name.handle()).?);
_ = self.bindingsByName.remove(name.handle());
}
pub fn destroy(self: *@This()) void {
self.bindingsByName.deinit(self.allocator);
self.bindingStack.deinit(self.allocator);
self.allocator.destroy(self);
}
};
// not gonna actually deal with layers right now
pub const InputStack = struct {
allocator: std.mem.Allocator,
active: *BindingLayer,
arena: std.heap.ArenaAllocator,
keysDown: std.AutoHashMapUnmanaged(Key, bool) = .{},
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
.arena = std.heap.ArenaAllocator.init(allocator),
.active = try BindingLayer.create(allocator),
};
gInputStack = self;
return self;
}
pub fn updatePreviousInputs(self: *@This()) void {
if (self.active.bindingStack.items.len == 0) {
return;
}
var i: i32 = @intCast(self.active.bindingStack.items.len - 1);
while (i >= 0) : (i -= 1) {
const binding = &self.active.bindingStack.items[@intCast(i)];
binding.processHeldKeys();
}
}
pub fn sendAxisUpdates(self: *@This()) void {
for (self.active.bindingStack.items) |binding| {
binding.sendAxisUpdates();
}
}
fn routeKeyEvent(self: *@This(), key: Key, event: ActionEvent) void {
// for all bindings in reverse order from this layer, route the input, stop the
// route if the input was consumed
if (self.active.bindingStack.items.len == 0) {
return;
}
var i: i32 = @intCast(self.active.bindingStack.items.len - 1);
while (i >= 0) : (i -= 1) {
const binding = self.active.bindingStack.items[@intCast(i)];
var consumed: bool = false;
var routed: bool = false;
if (binding.routeBindingEvent(key, event, &consumed)) {
routed = true;
}
if (event == .keyDown) {
binding.setKeyDown(key);
}
if (event == .keyHeld) {
binding.setKeyHeld(key);
}
if (event == .keyUp) {
binding.setKeyUp(key);
}
if (consumed) {
break;
}
}
}
pub fn routeEvent(self: *@This(), eventToRoute: IOEvent) void {
switch (eventToRoute) {
.key => |key| {
const keyEvent: ActionEvent = @enumFromInt(@as(u8, @intCast(key.action)));
if (keyEvent != .keyHeld) {
self.routeKeyEvent(@enumFromInt(key.key), keyEvent);
}
},
.mouseButton => |button| {
const keyEvent: ActionEvent = @enumFromInt(@as(u8, @intCast(button.action)));
if (keyEvent != .keyHeld) {
self.routeKeyEvent(@enumFromInt(button.button + 10001), keyEvent);
}
},
else => {},
}
}
pub fn deinit(self: *@This()) void {
self.arena.deinit();
self.active.destroy();
self.allocator.destroy(self);
}
};
var gInputStack: *InputStack = undefined;
pub fn getInputStack() *InputStack {
return gInputStack;
}
pub fn initInputStack() !void {
gInputStack = try core.createObject(InputStack, .{});
}
// creates a binding for a lua type
// TODO
// pub fn LuaBinding(comptime T: type, comptime typeName: []const u8) type {
// return struct {
// p: ?*T,
//
// pub const PodDataTable = pod.DataTable{
// .name = typeName,
// .funcs = &.{},
// };
//
// pub fn create() @This() {
// return .{};
// }
// };
// }
const lua = core.lua;
const pod = lua.pod;
const Key = @import("keys.zig").Key;
const std = @import("std");
const core = @import("../core.zig");

View File

@ -0,0 +1,128 @@
pub const Key = enum(i32) {
Unknown = -1,
Space = 32,
Apostrophe = 39,
Comma = 44,
Period = 45,
Slash = 47,
@"0" = 48,
@"1" = 49,
@"2" = 50,
@"3" = 51,
@"4" = 52,
@"5" = 53,
@"6" = 54,
@"7" = 55,
@"8" = 56,
@"9" = 57,
Semicolon = 59,
Equal = 61,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
LeftBrackSq = 91,
BackSlash = 92,
RightBrackSq = 93,
Grave = 96,
World1 = 161,
World2 = 162,
Escape = 256,
Enter = 257,
Tab = 258,
Backspace = 259,
Insert = 260,
Delete = 261,
Right = 262,
Left = 263,
Down = 264,
Up = 265,
PageUp = 266,
PageDown = 267,
Home = 268,
End = 269,
CapsLock = 280,
ScrollLock = 281,
NumLock = 282,
PrintScreen = 283,
Pause = 284,
F1 = 290,
F2 = 291,
F3 = 292,
F4 = 293,
F5 = 294,
F6 = 295,
F7 = 296,
F8 = 297,
F9 = 298,
F10 = 299,
F11 = 300,
F12 = 301,
F13 = 302,
F14 = 303,
F15 = 304,
F16 = 305,
F17 = 306,
F18 = 307,
F19 = 308,
F20 = 309,
F21 = 310,
F22 = 311,
F23 = 312,
F24 = 313,
F25 = 314,
Kp0 = 320,
Kp1 = 321,
Kp2 = 322,
Kp3 = 323,
Kp4 = 324,
Kp5 = 325,
Kp6 = 326,
Kp7 = 327,
Kp8 = 328,
Kp9 = 329,
KpDecimal = 330,
KpDivide = 331,
KpMultiply = 332,
KpSubtract = 333,
KpAdd = 334,
KpEnter = 335,
KpEqual = 336,
LeftShift = 340,
LeftControl = 341,
LeftAlt = 342,
LeftSuper = 343,
RightShift = 344,
RightControl = 345,
RightAlt = 346,
RightSuper = 347,
Menu = 348,
Mouse1 = 10001,
Mouse2 = 10002,
Mouse3 = 10003,
Mouse4 = 10004,
Mouse5 = 10005,
_,
};

View File

@ -0,0 +1,14 @@
moveAxis = CreateInput2DAxis()
moveAxis:addKey(Keys.W, 1.0, Axis.Y)
moveAxis:addKey(Keys.S, -1.0, Axis.Y)
moveAxis:addKey(Keys.A, -1.0, Axis.X)
moveAxis:addKey(Keys.D, 1.0, Axis.X)
moveAxis:addListener(balls, )
Input.addBinding("move", moveAxis, true)
Input.removeBinding("move")

247
engine/core/src/jobs.zig Normal file
View File

@ -0,0 +1,247 @@
const std = @import("std");
const Atomic = std.atomic.Value;
const core = @import("core.zig");
const tracy = core.tracy;
const RingQueueU = core.RingQueueU;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const mutex_job_queue = core.BuildOption("mutex_job_queue");
pub const JobManager = struct {
allocator: std.mem.Allocator,
// need a mutex for the jobQueue... todo later
jobQueue: RingQueueU(JobContext),
mutex: std.Thread.Mutex = .{},
jobQueueConcurrent: core.ConcurrentQueueU(JobContext),
workers: []*JobWorker,
numCpus: usize,
pub fn create(allocator: std.mem.Allocator) !*@This() {
var self = try allocator.create(@This());
self.* = JobManager{
.allocator = allocator,
.numCpus = std.Thread.getCpuCount() catch 4,
.jobQueue = RingQueueU(JobContext).init(allocator, 4096) catch unreachable,
.jobQueueConcurrent = core.ConcurrentQueueU(JobContext).initCapacity(allocator, 4096) catch unreachable,
.workers = undefined,
};
self.workers = self.allocator.alloc(*JobWorker, @max(self.numCpus - 2, 4)) catch unreachable;
core.engine_log("allocating worker count : {d}", .{self.workers.len});
var i: usize = 0;
while (i < self.workers.len) : (i += 1) {
self.workers[i] = JobWorker.init(self.allocator, i) catch unreachable;
self.workers[i].workerId = i;
self.workers[i].manager = self;
}
return self;
}
pub fn newJob(self: *@This(), capture: anytype) !void {
const Lambda = @TypeOf(capture);
const ctx = try JobContext.new(self.allocator, Lambda, capture);
if (mutex_job_queue) {
self.mutex.lock();
try self.jobQueue.push(ctx);
self.mutex.unlock();
} else {
try self.jobQueueConcurrent.push(ctx);
}
self.bump();
}
pub fn bump(self: *@This()) void {
var shouldBump: bool = false;
if (mutex_job_queue) {
shouldBump = self.jobQueue.count() > 0;
} else {
shouldBump = self.jobQueueConcurrent.count() > 0;
}
if (shouldBump) {
for (self.workers) |worker| {
if (!worker.isBusy()) {
worker.wake();
break;
}
}
}
}
pub fn destroy(self: *@This()) void {
for (self.workers) |worker| {
worker.deinit();
}
self.allocator.free(self.workers);
self.clearJobs();
self.jobQueue.deinit(self.allocator);
self.jobQueueConcurrent.deinit(self.allocator);
self.allocator.destroy(self);
}
pub fn clearJobs(self: *@This()) void {
var jobCtx: ?JobContext = null;
if (mutex_job_queue) {
self.mutex.lock();
jobCtx = self.jobQueue.pop();
self.mutex.unlock();
} else {
jobCtx = self.jobQueueConcurrent.pop();
}
while (jobCtx) |*c| {
c.deinit();
if (mutex_job_queue) {
self.mutex.lock();
jobCtx = self.jobQueue.pop();
self.mutex.unlock();
} else {
jobCtx = self.jobQueueConcurrent.pop();
}
}
}
};
pub const JobWorker = struct {
detached: bool = true, // most threads are detached, completions are handled via callbacks.
currentJobContext: ?JobContext = null,
workerThread: std.Thread,
futex: Atomic(u32) = Atomic(u32).init(0),
current: u32 = 0,
shouldDie: Atomic(bool) = Atomic(bool).init(false),
busy: Atomic(bool) = Atomic(bool).init(false),
allocator: std.mem.Allocator,
workerId: usize = 0,
manager: ?*JobManager = null,
pub fn wake(self: *JobWorker) void {
// this needs to be re-evaluated, it's nice for system performance but
// a 1-2ms worst case wake time is kind of unacceptable.
std.Thread.Futex.wake(&self.futex, 1);
}
pub fn init(allocator: std.mem.Allocator, workerNumber: usize) !*@This() {
const self = try allocator.create(JobWorker);
self.* = .{
.allocator = allocator,
.workerThread = try std.Thread.spawn(.{}, @This().workerThreadFunc, .{self}),
.workerId = workerNumber,
};
return self;
}
pub fn isBusy(self: *@This()) bool {
return self.busy.load(.acquire);
}
pub fn workerThreadFunc(self: *@This()) void {
const printed = std.fmt.allocPrintZ(self.allocator, "WorkerThread_{d}", .{self.workerId}) catch unreachable;
tracy.InitThread();
tracy.SetThreadName(@as([*:0]u8, @ptrCast(printed.ptr)));
self.allocator.free(printed);
var wakeGrabCount: u32 = 1;
while (!self.shouldDie.load(.acquire)) {
if (self.currentJobContext != null) {
wakeGrabCount = 0;
self.busy.store(true, .seq_cst);
var ctx = self.currentJobContext.?;
ctx.func(ctx.capture, &ctx);
self.busy.store(false, .seq_cst);
ctx.deinit();
self.currentJobContext = null;
} else {
std.Thread.Futex.wait(&self.futex, self.current);
}
if (self.manager) |manager| {
if (mutex_job_queue) {
self.manager.?.mutex.lock();
if (self.manager.?.jobQueue.count() > 0) {
self.currentJobContext = self.manager.?.jobQueue.pop().?;
}
self.manager.?.mutex.unlock();
} else {
self.currentJobContext = manager.jobQueueConcurrent.pop();
//while (wakeGrabCount > 0) : (wakeGrabCount -= 1) {
// self.currentJobContext = manager.jobQueueConcurrent.pop();
// if (self.currentJobContext != null) {
// break;
// }
// std.time.sleep(1000 * 1000);
//}
wakeGrabCount = 1;
}
}
}
if (self.currentJobContext) |*ctx| {
ctx.deinit();
}
}
pub fn deinit(self: *@This()) void {
self.shouldDie.store(true, .seq_cst);
self.wake();
self.workerThread.join();
self.allocator.destroy(self);
}
};
pub const JobContext = struct {
const Self = @This();
allocator: std.mem.Allocator, //todo, backed arena allocator would be sick for this.
func: *const fn (*anyopaque, *JobContext) void, // todo, add an error for job funcs
capture: *anyopaque = undefined,
destroyFunc: *const fn (*anyopaque, std.mem.Allocator) void,
const align8_struct = struct { size: u64 };
pub fn new(allocator: std.mem.Allocator, comptime CaptureType: type, capture: CaptureType) !JobContext {
if (!@hasDecl(CaptureType, "func")) {
return error.NoValidLambda;
}
const Wrap = struct {
pub fn wrappedFunc(pointer: *anyopaque, context: *JobContext) void {
var ptr = @as(*CaptureType, @ptrCast(@alignCast(pointer)));
ptr.func(context);
}
pub fn wrappedDestroy(ptr: *anyopaque, alloc: std.mem.Allocator) void {
const p = @as(*CaptureType, @ptrCast(@alignCast(ptr)));
alloc.destroy(p);
}
};
const self = Self{
.allocator = allocator,
.func = Wrap.wrappedFunc,
.destroyFunc = Wrap.wrappedDestroy,
.capture = try allocator.create(CaptureType),
};
const ptr = @as(*CaptureType, @ptrCast(@alignCast(self.capture)));
ptr.* = capture;
return self;
}
pub fn deinit(self: *Self) void {
self.destroyFunc(self.capture, self.allocator);
}
};

307
engine/core/src/logging.zig Normal file
View File

@ -0,0 +1,307 @@
const std = @import("std");
const core = @import("core.zig");
const tracy = @import("tracy");
const builtin = @import("builtin");
const slow_logging = core.BuildOption("slow_logging");
const zero_logging = core.BuildOption("zero_logging");
const LogBufferSize = 1 * 1024 * 1024; // 1Mb buffer log
var gLoggerSys: ?*LoggerSys = null;
pub fn printRaw(comptime fmt: []const u8, args: anytype) void {
if (zero_logging) {
return;
}
if (slow_logging) {
std.debug.print(fmt, args);
} else {
if (gLoggerSys) |loggerSys| {
loggerSys.print(fmt, args) catch std.debug.print("!> " ++ fmt, args);
} else {
std.debug.print(fmt, args);
}
}
}
pub fn printInner(comptime fmt: []const u8, args: anytype) void {
if (zero_logging) {
return;
}
if (slow_logging) {
std.debug.print("> " ++ fmt, args);
} else {
if (gLoggerSys) |loggerSys| {
loggerSys.print(fmt, args) catch std.debug.print("!> " ++ fmt, args);
} else {
std.debug.print("> " ++ fmt, args);
}
}
}
pub fn game_log(comptime fmt: []const u8, args: anytype) void {
printInner("[GAME ]: " ++ fmt ++ "\n", args);
printInner("[SCRIPT ]: " ++ fmt ++ "\n", args);
}
pub fn game_logs(comptime fmt: []const u8) void {
printInner("[GAME ]: " ++ fmt ++ "\n", .{});
}
pub fn ui_log(comptime fmt: []const u8, args: anytype) void {
printInner("[UI ]: " ++ fmt ++ "\n", args);
}
pub fn ui_logs(comptime fmt: []const u8) void {
printInner("[UI ]: " ++ fmt ++ "\n", .{});
}
pub fn engine_log(comptime fmt: []const u8, args: anytype) void {
printInner("[ENGINE ]: " ++ fmt ++ "\n", args);
}
pub fn engine_logs(comptime fmt: []const u8) void {
printInner("[ENGINE ]: " ++ fmt ++ "\n", .{});
}
pub fn engine_err(comptime fmt: []const u8, args: anytype) void {
printInner("[ENGINE ]: ERROR!! " ++ fmt ++ "\n", args);
}
pub fn engine_errs(comptime fmt: []const u8) void {
printInner("[ENGINE ]: ERROR!! " ++ fmt ++ "\n", .{});
}
pub fn test_log(comptime fmt: []const u8, args: anytype) void {
printInner("[TEST ]: " ++ fmt ++ "\n", args);
}
pub fn test_logs(comptime fmt: []const u8) void {
printInner("[TEST ]: " ++ fmt ++ "\n", .{});
}
pub fn graphics_log(comptime fmt: []const u8, args: anytype) void {
printInner("[GRAPHICS ]: " ++ fmt ++ "\n", args);
}
pub fn graphics_logs(comptime fmt: []const u8) void {
printInner("[GRAPHICS ]: " ++ fmt ++ "\n", .{});
}
pub const FileLog = struct {
buffer: std.ArrayList(u8),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) !@This() {
const self = @This(){
.buffer = std.ArrayList(u8).init(allocator),
.allocator = allocator,
};
return self;
}
pub fn write(self: *@This(), comptime fmt: []const u8, args: anytype) !void {
var writer = self.buffer.writer();
try writer.print(fmt, args);
}
pub fn writeGraphvizHeader(self: *@This()) !void {
try self.write("digraph G {{", .{});
}
pub fn writeOutGraphViz(self: *@This(), fileName: []const u8) !void {
const obuf = try std.fmt.allocPrint(
self.allocator,
"{s}{s}",
.{ self.buffer.items, "}}\n" },
);
defer self.free(obuf);
const cwd = std.fs.cwd();
const ofile = try std.fmt.allocPrint(self.allocator, core.DefaultSavePath ++ "/{s}", .{fileName});
defer self.allocator.free(ofile);
try cwd.makePath(core.DefaultSavePath);
try cwd.writeFile(.{
.sub_path = ofile,
.data = obuf,
});
}
pub fn writeOut(self: @This(), fileName: []const u8) !void {
const cwd = std.fs.cwd();
const ofile = try std.fmt.allocPrint(self.allocator, core.DefaultSavePath ++ "/{s}", .{fileName});
defer self.allocator.free(ofile);
try cwd.makePath(core.DefaultSavePath);
try cwd.writeFile(.{
.sub_path = ofile,
.data = self.buffer.items,
});
}
pub fn deinit(self: *@This()) void {
self.buffer.deinit();
}
};
pub const LoggerSys = struct {
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
writeOutBuffer: std.ArrayList(u8),
flushBuffer: std.ArrayList(u8),
allocator: std.mem.Allocator,
logFilePath: []const u8,
logFile: std.fs.File,
consoleFile: std.fs.File,
lock: std.Thread.Mutex = .{},
flushing: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
pub fn flush(self: *@This()) !void {
var z = tracy.ZoneN(@src(), "Trying to flush");
defer z.End();
self.lock.lock();
if (!self.flushing.load(.acquire)) {
self.flushing.store(true, .seq_cst);
const swap = self.writeOutBuffer;
self.writeOutBuffer = self.flushBuffer;
self.flushBuffer = swap;
const L = struct {
loggerSys: *LoggerSys,
pub fn func(ctx: @This(), _: *core.JobContext) void {
var z1 = tracy.ZoneN(@src(), "flushing output buffer");
defer z1.End();
ctx.loggerSys.flushFromJob() catch unreachable;
ctx.loggerSys.flushing.store(false, .seq_cst);
}
};
try core.dispatchJob(L{ .loggerSys = self });
} else {
//self.flushWriteBuffer();
}
self.lock.unlock();
}
pub fn shutdownFlush(self: *@This()) !void {
while (self.flushing.load(.seq_cst)) {}
// try self.logFile.writer().writeAll(self.writeOutBuffer.items);
// try self.consoleFile.writer().writeAll(self.writeOutBuffer.items);
}
pub fn flushWriteBuffer(self: *@This()) !void {
self.lock.lock();
try self.logFile.writer().writeAll(self.writeOutBuffer.items);
if (builtin.is_test) {
std.debug.print("{s}", .{self.flushBuffer.items});
} else {
try self.consoleFile.writer().writeAll(self.writeOutBuffer.items);
}
self.writeOutBuffer.clearRetainingCapacity();
self.lock.unlock();
}
pub fn flushFromJob(self: *@This()) !void {
self.lock.lock();
try self.logFile.writer().writeAll(self.flushBuffer.items);
if (builtin.is_test) {
std.debug.print("{s}", .{self.flushBuffer.items});
} else {
try self.consoleFile.writer().writeAll(self.flushBuffer.items);
}
self.flushBuffer.clearRetainingCapacity();
self.lock.unlock();
}
pub fn print(self: *@This(), comptime fmt: []const u8, args: anytype) !void {
self.lock.lock();
try self.writeOutBuffer.writer().print(fmt, args);
self.lock.unlock();
if (self.writeOutBuffer.items.len > LogBufferSize) {
if (self.flushBuffer.items.len == 0) {
try self.flush();
} else {
try self.flushWriteBuffer();
}
}
}
pub fn init(allocator: std.mem.Allocator) !*@This() {
const cwd = std.fs.cwd();
const ofile = std.fmt.allocPrint(allocator, core.DefaultSavePath ++ "/{s}", .{"Session_Log.txt"}) catch unreachable;
cwd.makePath(core.DefaultSavePath) catch unreachable;
const self = try allocator.create(@This());
self.* = @This(){
.allocator = allocator,
.writeOutBuffer = std.ArrayList(u8).initCapacity(allocator, LogBufferSize) catch unreachable,
.flushBuffer = std.ArrayList(u8).initCapacity(allocator, LogBufferSize) catch unreachable,
.logFilePath = ofile,
.logFile = cwd.createFile(ofile, .{}) catch unreachable,
.consoleFile = std.io.getStdOut(),
};
return self;
}
pub fn deinit(self: *@This()) void {
self.flushWriteBuffer() catch {};
self.allocator.free(self.logFilePath);
self.logFile.close();
self.writeOutBuffer.deinit();
self.flushBuffer.deinit();
self.allocator.destroy(self);
}
pub fn processEvents(self: *@This(), frameNumber: u64) core.EngineDataEventError!void {
_ = frameNumber;
if (self.writeOutBuffer.items.len == 0) {
return;
}
self.flush() catch return error.UnknownStatePanic;
}
};
var gFlushForcing: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
pub fn forceFlush() void {
if (gFlushForcing.load(.seq_cst)) {
return;
}
gFlushForcing.store(true, .seq_cst);
if (gLoggerSys != null) {
gLoggerSys.?.flushWriteBuffer() catch {};
}
}
pub fn setupLogging(engine: *core.Engine) !void {
gLoggerSys = try engine.createObject(LoggerSys, .{
.responds_to_events = true,
.isCore = true,
});
}
pub fn shutdownLogging() void {
if (gLoggerSys) |g| {
while (g.flushing.load(.acquire)) {}
forceFlush();
}
gLoggerSys = null;
}
pub fn getLogger() ?*LoggerSys {
return gLoggerSys;
}

View File

@ -0,0 +1,33 @@
tickables = {}
properties = {}
local function registerTick(object, f)
table.insert(tickables, {obj = object, func = f})
end
function __TickScripts(deltaTime)
for index, entry in ipairs(tickables) do
entry.func(entry.obj, deltaTime)
end
end
function __RegisterEntityProperty(userdata)
properties[userdata] = {}
end
function SetProperty(entity, property)
properties[entity] = property
end
function GetProperty(entity)
return properties[entity]
end
function GetObject(entity)
return properties[entity]
end
Core = {
registerTick = registerTick;
}

View File

@ -0,0 +1,2 @@
print("Hello from lua.")

611
engine/core/src/math.zig Normal file
View File

@ -0,0 +1,611 @@
const std = @import("std");
const misc = @import("misc.zig");
const zm = @import("zmath");
const math = std.math;
// LUA BEGIN feel like I should mark up the parts of the engine that need to be
// sliced out should i ever decide I'm sick of lua
const lua = @import("lua");
const pod = lua.pod;
// LUA END
pub const Rayf = RayType(f32);
pub fn matToScalef(mat: anytype) Vectorf {
const x = Vectorf.new(mat[0][0], mat[0][1], mat[0][2]).length();
const y = Vectorf.new(mat[1][0], mat[1][1], mat[1][2]).length();
const z = Vectorf.new(mat[2][0], mat[2][1], mat[2][2]).length();
return Vectorf.new(x, y, z);
}
pub fn RayType(comptime T: type) type {
return struct {
start: Vector3Type(T),
dir: Vector3Type(T),
};
}
pub fn clamp(x: anytype, min: anytype, max: anytype) @TypeOf(x) {
if (x < min)
return min;
if (x > max)
return max;
return x;
}
pub fn matFromEulerAngles(x: f32, y: f32, z: f32) Mat {
return zm.matFromRollPitchYaw(y, z, x);
}
pub fn Radians(comptime T: type) type {
return struct {
value: T,
pub fn fromDegrees(f: anytype) @This() {
return .{ .value = f };
}
};
}
pub fn radians(f: anytype) @TypeOf(f) {
return f * math.pi / 180.0;
}
pub fn fabs(x: anytype) @TypeOf(x) {
if (x < 0)
return -x;
return x;
}
pub fn Vector2Type(comptime T: type, comptime typeName: []const u8) type {
return extern struct {
x: T = 0,
y: T = 0,
// LUA BEGIN
pub const PodDataTable: pod.DataTable = .{
.name = typeName,
.funcs = &.{
"fmul",
"dot",
"length",
"normalize",
},
.operators = .{
.add = "add",
.sub = "sub",
.mul = "vmul",
.eq = "equals",
},
};
// LUA END
pub const Ones = @This(){ .x = 1, .y = 1 };
pub const Zeroes = @This(){ .x = 0, .y = 0 };
pub inline fn new(x: T, y: T) @This() {
return .{
.x = x,
.y = y,
};
}
pub inline fn add(self: @This(), other: @This()) @This() {
return .{
.x = self.x + other.x,
.y = self.y + other.y,
};
}
pub inline fn sub(self: @This(), other: @This()) @This() {
return .{
.x = self.x - other.x,
.y = self.y - other.y,
};
}
pub inline fn vmul(self: @This(), other: @This()) @This() {
return .{
.x = self.x * other.x,
.y = self.y * other.y,
};
}
pub inline fn fmul(self: @This(), other: T) @This() {
return .{
.x = self.x * other,
.y = self.y * other,
};
}
pub inline fn dot(self: @This(), other: @This()) T {
return self.x * other.x + self.y * other.y;
}
pub inline fn equals(self: @This(), other: @This()) bool {
return self.x == other.x and self.y == other.y;
}
pub inline fn length(self: @This()) T {
return std.math.sqrt(self.x * self.x + self.y * self.y);
}
pub inline fn swizzleYX(self: @This()) @This() {
return .{ .x = self.y, .y = self.x };
}
pub inline fn normalize(self: @This()) @This() {
if (fabs(self.x) <= 0.0001 and fabs(self.y) <= 0.0001) {
return .{ .x = 0, .y = 0 };
}
const len = std.math.sqrt(self.x * self.x + self.y * self.y);
if (len < 0.00001)
return .{ .x = 0, .y = 0 };
return .{
.x = self.x / len,
.y = self.y / len,
};
}
pub inline fn from(o: anytype) @This() {
const OType: std.builtin.Type = @typeInfo(@TypeOf(o.x));
switch (@typeInfo(T)) {
.Int => {
switch (OType) {
.Int => {
// convert integer into integer
return .{
.x = @intCast(o.x),
.y = @intCast(o.y),
};
},
.Float => {
// convert float into float
return .{
.x = @intFromFloat(o.x),
.y = @intFromFloat(o.y),
};
},
else => {
@compileError("Invalid vector type conversion");
},
}
},
.Float => {
switch (OType) {
.Int => {
// convert integer into float
return .{
.x = @floatFromInt(o.x),
.y = @floatFromInt(o.y),
};
},
.Float => {
// convert float
return .{
.x = @floatCast(o.x),
.y = @floatCast(o.y),
};
},
else => {
// convert float into float
@compileError("Invalid vector type conversion");
},
}
},
else => {
@compileError("Invalid vector type conversion");
},
}
@compileError("Invalid vector conversion");
}
};
}
pub fn Vector3Type(comptime T: type, comptime typeName: []const u8) type {
return extern struct {
x: T = 0,
y: T = 0,
z: T = 0,
pub const Ones = @This(){ .x = 1, .y = 1, .z = 1 };
pub const Zeroes = @This(){ .x = 0, .y = 0, .z = 0 };
pub const Up = @This(){ .y = 1 };
pub const Right = @This(){ .x = 1 };
pub const Forward = @This(){ .z = 1 };
// LUA BEGIN
pub const PodDataTable: pod.DataTable = .{
.name = typeName,
.funcs = &.{
"fmul",
"dot",
"length",
"normalize",
},
.operators = .{
.add = "add",
.sub = "sub",
.mul = "vmul",
.eq = "equals",
},
};
// LUA END
pub inline fn new(x: T, y: T, z: T) @This() {
return .{
.x = x,
.y = y,
.z = z,
};
}
pub inline fn fromInt(t: anytype) @This() {
return .{ .x = t, .y = t, .z = t };
}
// easing functions
pub inline fn drainToZero(self: @This(), o: anytype) @This() {
return self.drain(o, Zeroes);
}
pub inline fn drain(self: @This(), o: anytype, target: anytype) @This() {
return .{
.x = drainElement(self.x, o.x, target.x),
.y = drainElement(self.y, o.y, target.y),
.z = drainElement(self.z, o.z, target.z),
};
}
inline fn drainElement(a: anytype, v: anytype, t: anytype) @TypeOf(a) {
if (a > t) {
return @max(t, a - v);
} else {
return @min(t, a + v);
}
}
// array functions.
pub inline fn toArr3(self: @This()) [3]T {
return .{ self.x, self.y, self.z };
}
pub inline fn fromArr3(v: [3]f32) @This() {
return .{ .x = v[0], .y = v[1], .z = v[2] };
}
pub inline fn toZm(self: @This()) zm.Vec {
return .{ self.x, self.y, self.z, 1.0 };
}
pub inline fn fromZm(vec: zm.Vec) @This() {
return @This().new(vec[0], vec[1], vec[2]);
}
pub inline fn add(self: @This(), other: @This()) @This() {
return .{
.x = self.x + other.x,
.y = self.y + other.y,
.z = self.z + other.z,
};
}
pub inline fn sub(self: @This(), other: @This()) @This() {
return .{
.x = self.x - other.x,
.y = self.y - other.y,
.z = self.z - other.z,
};
}
pub inline fn vmul(self: @This(), other: @This()) @This() {
return .{
.x = self.x * other.x,
.y = self.y * other.y,
.z = self.z * other.z,
};
}
pub inline fn fmul(self: @This(), other: T) @This() {
return .{
.x = self.x * other,
.y = self.y * other,
.z = self.z * other,
};
}
pub inline fn dot(self: @This(), other: @This()) T {
return self.x * other.x + self.y * other.y + self.z * other.z;
}
pub inline fn equals(self: @This(), other: @This()) bool {
return self.x == other.x and self.y == self.y and self.z == self.z;
}
pub inline fn length(self: @This()) T {
return std.math.sqrt(self.x * self.x + self.z * self.z + self.y * self.y);
}
pub inline fn swizzleYZX(self: @This()) @This() {
return .{ .x = self.y, .y = self.z, .z = self.x };
}
pub inline fn swizzleYXZ(self: @This()) @This() {
return .{ .x = self.y, .y = self.x, .z = self.z };
}
pub inline fn swizzleZXY(self: @This()) @This() {
return .{ .x = self.z, .y = self.x, .z = self.y };
}
pub inline fn swizzleZYX(self: @This()) @This() {
return .{ .x = self.z, .y = self.y, .z = self.x };
}
pub inline fn swizzleXZY(self: @This()) @This() {
return .{ .x = self.x, .y = self.z, .z = self.y };
}
pub inline fn lengthXZ(self: @This()) T {
return std.math.sqrt(self.x * self.x + self.z * self.z);
}
pub inline fn normalize(self: @This()) @This() {
if (fabs(self.x) <= 0.0001 and fabs(self.y) <= 0.0001 and fabs(self.z) <= 0.0001) {
return .{ .x = 0, .y = 0, .z = 0 };
}
const len = std.math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
if (len < 0.00001)
return .{ .x = 0, .y = 0, .z = 0 };
return .{
.x = self.x / len,
.y = self.y / len,
.z = self.z / len,
};
}
pub inline fn fromArray(o: anytype) @This() {
return .{
.x = o[0],
.y = o[1],
.z = o[2],
};
}
pub inline fn clampAllAbs(self: @This(), abs: anytype) @This() {
return .{
.x = std.math.clamp(self.x, -abs, abs),
.y = std.math.clamp(self.y, -abs, abs),
.z = std.math.clamp(self.z, -abs, abs),
};
}
pub inline fn cross(self: @This(), other: @This()) @This() {
return .{
.x = self.y * other.z - self.z * other.y,
.y = self.z * other.x - self.x * other.z,
.z = self.x * other.y - self.y * other.x,
};
}
pub fn lerp(self: @This(), other: @This(), alpha: anytype) @This() {
return .{
.x = std.math.lerp(self.x, other.x, alpha),
.y = std.math.lerp(self.y, other.y, alpha),
.z = std.math.lerp(self.z, other.z, alpha),
};
}
pub inline fn clampAll(self: @This(), lower: anytype, upper: anytype) @This() {
return .{
.x = std.math.clamp(self.x, -lower, upper),
.y = std.math.clamp(self.y, -lower, upper),
.z = std.math.clamp(self.z, -lower, upper),
};
}
pub inline fn from(o: anytype) @This() {
const OType: std.builtin.Type = @typeInfo(@TypeOf(o.x));
switch (@typeInfo(T)) {
.Int => {
switch (OType) {
.Int => {
// convert integer into integer
return .{
.x = @intCast(o.x),
.y = @intCast(o.y),
.z = @intCast(o.z),
};
},
.Float => {
// convert float into float
return .{
.x = @intFromFloat(o.x),
.y = @intFromFloat(o.y),
.z = @intFromFloat(o.z),
};
},
else => {
@compileError("Invalid vector type conversion");
},
}
},
.Float => {
switch (OType) {
.Int => {
// convert integer into float
return .{
.x = @floatFromInt(o.x),
.y = @floatFromInt(o.y),
.z = @floatFromInt(o.z),
};
},
.Float => {
// convert float
return .{
.x = @floatCast(o.x),
.y = @floatCast(o.y),
.z = @floatCast(o.z),
};
},
else => {
// convert float into float
@compileError("Invalid vector type conversion");
},
}
},
else => {
@compileError("Invalid vector type conversion");
},
}
@compileError("Invalid vector conversion");
}
};
}
pub fn Vector4Type(comptime T: type) type {
return extern struct {
x: T = 0,
y: T = 0,
z: T = 0,
w: T = 0,
pub const Ones = @This(){ .x = 1, .y = 1, .z = 1 };
pub const Zeroes = @This(){ .x = 0, .y = 0, .z = 0 };
pub inline fn from(o: anytype) @This() {
return .{ .x = o.x, .y = o.y, .z = o.z, .w = o.w };
}
pub inline fn new(x: T, y: T, z: T, w: T) @This() {
return .{
.x = x,
.y = y,
.z = z,
.w = w,
};
}
pub inline fn add(self: @This(), other: @This()) @This() {
return .{
.x = self.x + other.x,
.y = self.y + other.y,
.z = self.z + other.z,
.w = self.w + other.w,
};
}
pub inline fn sub(self: @This(), other: @This()) @This() {
return .{
.x = self.x - other.x,
.y = self.y - other.y,
.z = self.z - other.z,
.w = self.w - other.w,
};
}
pub inline fn normalize(self: @This()) @This() {
const len = std.math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w);
return .{
.x = self.x / len,
.y = self.y / len,
.z = self.z / len,
.w = self.w / len,
};
}
};
}
pub const Vector4 = Vector4Type(f64);
pub const Vector4f = Vector4Type(f32);
pub const Vector = Vector3Type(f64, "Vector");
pub const Vectorf = Vector3Type(f32, "Vectorf");
pub const Vector2f = Vector2Type(f32, "Vector2f");
pub const Vector2 = Vector2Type(f64, "Vector2");
pub const Vector2i = Vector2Type(i32, "Vector2i");
pub const Vector2c = Vector2Type(c_int, "Vector2c");
pub const Vector2u = Vector2Type(u32, "Vector2u");
pub const Vector2l = Vector2Type(i64, "Vector2l");
pub const EulerAngles = Vectorf;
pub const Quat = zm.Quat;
pub const Mat = zm.Mat;
pub const Transform = zm.Mat;
pub fn matGetPosition(m: Mat) Vectorf {
const vec: Vectorf = .{};
return Vectorf.fromZm(zm.mul(vec.toZm(), m));
}
pub fn matToRotation(m: Mat) Rotation {
const r: Rotation = .{ .quat = zm.matToQuat(m) };
return r;
}
pub const Rotation = struct {
quat: Quat = zm.qidentity(),
pub fn init() @This() {
return .{};
}
pub fn eulerX(o: f32) @This() {
return .{
.quat = zm.matToQuat(zm.rotationX((o))),
};
}
pub fn eulerY(o: f32) @This() {
return .{
.quat = zm.matToQuat(zm.rotationY((o))),
};
}
pub fn eulerZ(o: f32) @This() {
return .{
.quat = zm.matToQuat(zm.rotationZ((o))),
};
}
pub fn rotateVector(self: @This(), other: Vectorf) Vectorf {
return Vectorf.fromZm(
zm.mul(
other.toZm(),
zm.quatToMat(self.quat),
),
);
}
};
pub fn simdVec4ToVec(vec: zm.Vec) Vector4f {
return .{
.x = vec[0],
.y = vec[1],
.z = vec[2],
.w = vec[3],
};
}
pub fn easeLinear(c: f32, t: f32, dt: f64, rate: f32) f32 {
const d = rate * @as(f32, @floatCast(dt));
if (c > t) {
return @max(c - d, t);
} else if (c < t) {
return @min(c + d, t);
} else {
return t;
}
}
pub fn rollingAverage(average: *f64, newValue: f64, sampleCount: f64) void {
average.* = average.* - (average.* / sampleCount) + newValue / sampleCount;
}

41
engine/core/src/misc.zig Normal file
View File

@ -0,0 +1,41 @@
// Random misc. utilities that aren't /totally/ categorized right now.
const logging = @import("logging.zig");
// Creates a for-loopable range between [0, n)
pub fn count(comptime n: anytype) [n]u0 {
return comptime [1]u0{0} ** n;
}
// Creates a for-loopable range between [start, end).
// reccomended for use with inline FORs
pub fn range(comptime start: usize, comptime end: anytype) [end - start]@TypeOf(start) {
comptime var r = [1]@TypeOf(start){start} ** (end - start);
comptime {
for (r, 0..) |val, i| {
r[i] = val + i;
}
}
return comptime r;
}
pub fn slice_to_cstr(str: []const u8) ?[*:0]const u8 {
return @as(?[*:0]const u8, @ptrCast(str.ptr));
}
pub fn buf_to_cstr(str: anytype) ?[*:0]const u8 {
return @as(?[*:0]const u8, @ptrCast(&str[0]));
}
pub const CStr = [*:0]const u8;
pub fn debug_struct(preamble: []const u8, s: anytype) void {
logging.graphics_log("{s}:", .{preamble});
logging.graphics_log(" {any}", .{s});
}
pub fn p_to_av(a: anytype) [*]@TypeOf(a.*) {
return @as([*]@TypeOf(a.*), @ptrCast(a));
}

View File

@ -0,0 +1,36 @@
// the module startup system.
const std = @import("std");
// a series of comptime functions for letting you select which features are compiled and brought
// into the engine.
pub const ModuleDescription = struct {
name: []const u8,
enabledByDefault: bool,
};
pub fn isModuleEnabled(comptime module: ModuleDescription, comptime buildDescription: anytype) bool {
if (!@hasField(@TypeOf(buildDescription), "enabledModules")) {
return module.enabledByDefault;
} else if (@hasField(@TypeOf(buildDescription.enabledModules), module.name)) {
return @field(buildDescription.enabledModules, module.name);
} else {
return module.enabledByDefault;
}
}
test "isModule in build test" {
const ModA = ModuleDescription{
.name = "featureA",
.enabledByDefault = false,
};
const buildDesc: struct {
enabledModules: struct {
featureA: bool = true,
} = .{},
} = .{};
std.debug.print("modA enabled = {any}\n", .{isModuleEnabled(ModA, buildDesc)});
}

View File

@ -0,0 +1,183 @@
const std = @import("std");
const builtin = @import("builtin");
const os = std.os;
const windows = std.os.windows;
const core = @import("core.zig");
const lua = @import("lua");
fn handleSegfaultWindows(info: *windows.EXCEPTION_POINTERS) callconv(windows.WINAPI) c_long {
switch (info.ExceptionRecord.ExceptionCode) {
windows.EXCEPTION_DATATYPE_MISALIGNMENT => handleSegfaultWindowsExtra(info, 0, "Unaligned Memory Access"),
windows.EXCEPTION_ACCESS_VIOLATION => handleSegfaultWindowsExtra(info, 1, null),
windows.EXCEPTION_ILLEGAL_INSTRUCTION => handleSegfaultWindowsExtra(info, 2, null),
windows.EXCEPTION_STACK_OVERFLOW => handleSegfaultWindowsExtra(info, 0, "Stack Overflow"),
else => return windows.EXCEPTION_CONTINUE_SEARCH,
}
}
var panic_mutex = std.Thread.Mutex{};
var panicking = std.atomic.Value(u8).init(0);
threadlocal var panic_stage: usize = 0;
fn waitForOtherThreadToFinishPanicking() void {
if (panicking.fetchSub(1, .seq_cst) != 1) {
// Another thread is panicking, wait for the last one to finish
// and call abort()
if (builtin.single_threaded) unreachable;
// Sleep forever without hammering the CPU
var futex = std.atomic.Value(u32).init(0);
while (true) std.Thread.Futex.wait(&futex, 0);
unreachable;
}
}
fn handleSegfaultWindowsExtra(
info: *windows.EXCEPTION_POINTERS,
msg: u8,
label: ?[]const u8,
) noreturn {
const exception_address = @intFromPtr(info.ExceptionRecord.ExceptionAddress);
core.engine_logs("PANIC!!");
core.forceFlush();
if (!@hasDecl(windows, "CONTEXT")) {
switch (msg) {
0 => {
dumpSegfaultInfoWindows(info, msg, label);
std.posix.abort();
},
1 => {
const format_item = "Segmentation fault at address 0x{x}";
var buf: [format_item.len + 64]u8 = undefined; // 64 is arbitrary, but sufficiently large
const to_print = std.fmt.bufPrint(buf[0..buf.len], format_item, .{info.ExceptionRecord.ExceptionInformation[1]}) catch unreachable;
std.debug.panicImpl(null, exception_address, to_print);
},
2 => std.debug.panicImpl(null, exception_address, "Illegal Instruction"),
else => unreachable,
}
} else {
dumpSegfaultInfoWindows(info, msg, label);
std.posix.abort();
}
}
fn dumpSegfaultInfoWindows(info: *windows.EXCEPTION_POINTERS, msg: u8, label: ?[]const u8) void {
const stderr = std.io.getStdErr().writer();
_ = switch (msg) {
0 => stderr.print("{s}\n", .{label.?}),
1 => stderr.print("Segmentation fault at address 0x{x}\n", .{info.ExceptionRecord.ExceptionInformation[1]}),
2 => stderr.print("Illegal instruction at address 0x{x}\n", .{info.ContextRecord.getRegs().ip}),
else => unreachable,
} catch std.posix.abort();
std.debug.dumpStackTraceFromBase(info.ContextRecord);
}
pub fn dumpStackPointerAddr(prefix: []const u8) void {
const sp = asm (""
: [argc] "={rsp}" (-> usize),
);
std.debug.print("{} sp = 0x{x}\n", .{ prefix, sp });
}
fn dumpSegfaultInfoPosix(sig: i32, addr: usize, ctx_ptr: ?*const anyopaque) void {
const stderr = std.io.getStdErr().writer();
_ = switch (sig) {
std.posix.SIG.SEGV => stderr.print("Segmentation fault at address 0x{x}\n", .{addr}),
std.posix.SIG.ILL => stderr.print("Illegal instruction at address 0x{x}\n", .{addr}),
std.posix.SIG.BUS => stderr.print("Bus error at address 0x{x}\n", .{addr}),
std.posix.SIG.FPE => stderr.print("Arithmetic exception at address 0x{x}\n", .{addr}),
else => unreachable,
} catch std.posix.abort();
switch (builtin.cpu.arch) {
.x86,
.x86_64,
.arm,
.aarch64,
=> {
const ctx: *const std.c.ucontext_t = @ptrCast(@alignCast(ctx_ptr));
std.debug.dumpStackTraceFromBase(ctx);
},
else => {},
}
}
fn handleSegfaultPosix(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*const anyopaque) callconv(.C) noreturn {
core.engine_logs("PANIC!!");
core.forceFlush();
resetSegfaultHandler();
const addr = switch (builtin.os.tag) {
.linux => @intFromPtr(info.fields.sigfault.addr),
.freebsd, .macos => @intFromPtr(info.addr),
.netbsd => @intFromPtr(info.info.reason.fault.addr),
.openbsd => @intFromPtr(info.data.fault.addr),
.solaris => @intFromPtr(info.reason.fault.addr),
else => unreachable,
};
nosuspend switch (panic_stage) {
0 => {
panic_stage = 1;
_ = panicking.fetchAdd(1, .seq_cst);
{
panic_mutex.lock();
defer panic_mutex.unlock();
dumpSegfaultInfoPosix(sig, addr, ctx_ptr);
}
waitForOtherThreadToFinishPanicking();
},
else => {
// panic mutex already locked
dumpSegfaultInfoPosix(sig, addr, ctx_ptr);
},
};
// We cannot allow the signal handler to return because when it runs the original instruction
// again, the memory may be mapped and undefined behavior would occur rather than repeating
// the segfault. So we simply abort here.
std.posix.abort();
}
//
//
fn resetSegfaultHandler() void {
if (builtin.os.tag == .windows) {
if (windows_segfault_handle) |handle| {
_ = windows.kernel32.RemoveVectoredExceptionHandler(handle);
windows_segfault_handle = null;
}
return;
}
var act = std.c.Sigaction{
.handler = .{ .handler = std.posix.SIG.DFL },
.mask = std.posix.empty_sigset,
.flags = 0,
};
// To avoid a double-panic, do nothing if an error happens here.
std.debug.updateSegfaultHandler(&act) catch {};
}
// my custom version of segfault handler.
var windows_segfault_handle: ?windows.HANDLE = null;
pub fn attachSegfaultHandler() void {
if (builtin.os.tag == .windows) {
windows_segfault_handle = windows.kernel32.AddVectoredExceptionHandler(0, handleSegfaultWindows);
return;
}
var act = std.c.Sigaction{
.handler = .{ .sigaction = handleSegfaultPosix },
.mask = std.posix.empty_sigset,
.flags = (std.posix.SA.SIGINFO | std.posix.SA.RESTART | std.posix.SA.RESETHAND),
};
std.debug.updateSegfaultHandler(&act) catch {
@panic("unable to install segfault handler, maybe adjust have_segfault_handling_support in std/debug.zig");
};
}

93
engine/core/src/png.zig Normal file
View File

@ -0,0 +1,93 @@
// TODO --- this should become it's own engine module
const std = @import("std");
const spng = @import("spng");
const core = @import("core.zig");
pub const PngContents = struct {
path: []const u8,
pixels: []u8,
size: core.Vector2u,
allocator: std.mem.Allocator,
pub fn initFromPathSpec(path: []const u8, allocator: std.mem.Allocator) !@This() {
var pngContents: PngContents = undefined;
var cookedPath = std.ArrayList(u8).init(allocator);
defer cookedPath.deinit();
try cookedPath.appendSlice("_cooked/");
try cookedPath.appendSlice(path);
try cookedPath.appendSlice(".Texture");
if (core.fs().fileExists(cookedPath.items)) {
pngContents = try PngContents.initFromFSCooked(core.fs(), allocator, cookedPath.items);
} else {
pngContents = try PngContents.initFromFS(core.fs(), allocator, path);
}
return pngContents;
}
pub fn initFromFSCooked(fs: *core.FileSystem, allocator: std.mem.Allocator, path: []const u8) !@This() {
const mapping = try fs.loadFile(path);
defer fs.unmap(mapping);
core.engine_log("loading cooked version of file: {s}", .{path});
const rv: @This() = .{
.size = .{
.x = @as(u32, @bitCast(mapping.bytes[0..4].*)),
.y = @as(u32, @bitCast(mapping.bytes[4..8].*)),
},
.path = try allocator.dupe(u8, path),
.pixels = try allocator.dupe(u8, mapping.bytes[8..]),
.allocator = allocator,
};
return rv;
}
pub fn toBuffer(self: @This()) !std.ArrayList(u8) {
var buffer = std.ArrayList(u8).init(self.allocator);
try buffer.appendSlice(&@as([4]u8, @bitCast(self.size.x)));
try buffer.appendSlice(&@as([4]u8, @bitCast(self.size.y)));
try buffer.appendSlice(self.pixels);
return buffer;
}
pub fn initFromFS(fs: *core.FileSystem, allocator: std.mem.Allocator, path: []const u8) !@This() {
// 1. check the fs to see if a cooked version of the file exists
// 2. load that one if possible
// 3. otherwise, load the other one.
const mapping = try fs.loadFile(path);
defer fs.unmap(mapping);
return try initFromBytes(allocator, path, mapping.bytes);
}
pub fn initFromBytes(allocator: std.mem.Allocator, pathName: []const u8, pngFileContents: []const u8) !@This() {
var decoder = try spng.SpngContext.newDecoder();
defer decoder.deinit();
try decoder.setBuffer(pngFileContents);
const header = try decoder.getHeader();
const imageSize = @as(usize, @intCast(header.width * header.height * 4));
const pixels: []u8 = try allocator.alloc(u8, imageSize);
const len = try decoder.decode(pixels, spng.SPNG_FMT_RGBA8, spng.SPNG_DECODE_TRNS);
try core.assertf(len == pixels.len, "decoded pixel size not buffer size {d} != {d}", .{ len, pixels.len });
return PngContents{
.path = try core.dupe(u8, allocator, pathName),
.pixels = pixels,
.size = .{ .x = header.width, .y = header.height },
.allocator = allocator,
};
}
pub fn deinit(self: *@This()) void {
self.allocator.free(self.path);
self.allocator.free(self.pixels);
}
};

407
engine/core/src/scene.zig Normal file
View File

@ -0,0 +1,407 @@
const std = @import("std");
const core = @import("core.zig");
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const tracy = core.tracy;
pub const Transform = core.Mat;
const SceneObjectSet = core.SparseMultiSet(SceneObject);
const SceneSet = core.SparseSet(Scene);
pub const SceneAttachMode = enum {
none, // default, parent attachment is irrelevant and not used
relativePositionOnly, // only position is relative to parent transform
relativeRotationOnly, // only rotation is relative to parent transform
relative, // position and rotation are relative to parent transform
snapToParentPositionOnly, // snaps to the parent's position, maintaining independent rotation (not implemented)
snapToParentRotationOnly, // copys the parent's rotation, maintaining independent rotation (not implemented)
snapToParent, // snaps to parent rotation and position
};
pub const SceneMobilityMode = enum {
static, // scene is updated once and never again
moveable, // sceneobject is moveable and has it's final transform updated
};
pub const SceneObjectPosRot = struct {
position: core.Vectorf = .{ .x = 0, .y = 0, .z = 0 },
rotation: core.Rotation = core.Rotation.init(),
scale: core.Vectorf = core.Vectorf.new(1.0, 1.0, 1.0),
pub inline fn toTransform(self: @This()) core.Transform {
var transform = core.zm.mul(
core.zm.scalingV(self.scale.toZm()),
core.zm.matFromQuat(self.rotation.quat),
);
transform = core.zm.mul(
transform,
core.zm.translationV(self.position.toZm()),
);
return transform;
}
};
pub const SceneObjectSettings = struct {
attachmentMode: SceneAttachMode = .none,
sceneMode: SceneMobilityMode = .static,
};
pub const SceneObjectRepr = struct {
// fields intended to be internally used, don't touch them
// unless you know what you're doing
transform: core.Mat = core.zm.identity(), // relative transform against parent, if no parent, then this is world transform
parent: ?core.ObjectHandle = null,
attachmentMode: SceneAttachMode = .relative, // doesn't do anything yet, only support relative right now
transformOverride: ?*core.Transform = null,
lastUpdate: u32 = 0,
};
pub const SceneObject = struct {
_repr: SceneObjectRepr = .{}, // not public
posRot: SceneObjectPosRot = .{}, // position and rotation
settings: SceneObjectSettings = .{}, //
children: ArrayListUnmanaged(core.ObjectHandle) = .{},
pub fn init(params: SceneObjectInitParams) @This() {
// Hmm thinking in the future we could have scene objects be f64s then crunch them down to f32s when we are submiting to gpu
var self = @This(){
.posRot = .{},
._repr = .{},
.settings = .{},
.children = .{},
};
const shouldUpdate: bool = false; // should mutate
switch (params) {
.transform => {
self._repr.transform = params.transform;
self.posRot.position = core.Vectorf.fromZm(core.zm.mul(params.transform, core.Vectorf.zero().toZm()));
self.posRot.rotation = .{ .quat = core.zm.matToQuat(params.transform) };
},
.position => {
@panic("todo: implement position only initialization");
},
.rotation => {
@panic("todo: implement rotation only initialization");
},
.positionRotAngles => {
@panic("todo: implement position + rotation initialization (angles) ");
},
.positionRot => {
@panic("todo: implement position + rotation initialization");
},
}
if (shouldUpdate) {
self.update();
}
return self;
}
};
pub const Scene = struct {
handle: core.ObjectHandle = .{ .generation = 0, .index = 0, .alive = false },
pub var BaseContainer: *SceneSet = undefined;
pub var SceneObjectContainer: *SceneObjectSet = undefined;
pub const ComponentName = "Scene";
pub const ScriptExports: []const []const u8 = &.{
"setPosition",
"setRotation",
"setScale",
"setScaleV",
"getPosition",
"getRotation",
"getParent",
"printHandleIndex",
// "getTransform", not implemented yet
// "setMobility", gonna need special setup for this one
};
pub fn initECS(self: *@This(), handle: core.ObjectHandle) void {
self.handle = handle;
_ = SceneObjectContainer.createWithHandleECS(handle);
}
pub fn printHandleIndex(self: @This()) void {
core.engine_log("handle.index = 0x{x} generation = {d} alive={any}", .{ self.handle.index, self.handle.generation, self.handle.alive });
}
pub fn setPosition(self: @This(), position: core.Vectorf) void {
if (SceneObjectContainer.get(self.handle, .posRot)) |posRot| {
posRot.*.position = position;
// core.engine_log("setposition scucess handle.index = 0x{x}", .{self.handle.index});
} else {
core.engine_log("setposition failed handle.index = 0x{x} generation = {d} alive={any}", .{ self.handle.index, self.handle.generation, self.handle.alive });
}
}
pub fn getPosRot(self: *@This()) *SceneObjectPosRot {
return SceneObjectContainer.get(self.handle, .posRot).?;
}
pub fn setRotation(self: @This(), rotation: core.Rotation) void {
SceneObjectContainer.get(self.handle, .posRot).?.*.rotation = rotation;
}
pub fn setScale(self: @This(), x: f32, y: f32, z: f32) void {
SceneObjectContainer.get(self.handle, .posRot).?.*.scale = .{ .x = x, .y = y, .z = z };
}
pub fn setScaleV(self: @This(), scale: core.Vectorf) void {
SceneObjectContainer.get(self.handle, .posRot).?.*.scale = scale;
}
pub fn getPosition(self: @This()) core.Vectorf {
// core.engine_logs("getPosition called");
return SceneObjectContainer.get(self.handle, .posRot).?.position;
}
pub fn getRotation(self: @This()) core.Rotation {
return SceneObjectContainer.get(self.handle, .posRot).?.rotation;
}
pub fn getScaleV(self: @This()) core.Vectorf {
return SceneObjectContainer.get(self.handle, .posRot).?.scale;
}
pub fn getParent(self: @This()) core.Entity {
return core.Entity{ .handle = SceneObjectContainer.get(self.handle, ._repr).?.parent orelse .{} };
}
pub fn setParent(self: @This(), newParent: core.Entity) void {
core.engine_log("parenting entity {d} -> {d}", .{ self.handle.index, newParent.handle.index });
const repr: *SceneObjectRepr = SceneObjectContainer.get(self.handle, ._repr).?;
const thisParent = repr.parent;
if (thisParent) |p| {
const parentRef = @This(){ .handle = p };
parentRef.removeChild(self.handle);
}
repr.parent = newParent.handle;
const children = SceneObjectContainer.get(newParent.handle, .children).?;
children.append(childAllocator(), newParent.handle) catch unreachable;
}
pub fn clearParent(self: @This()) void {
const repr = SceneObjectContainer.get(self.handle, ._repr).?;
const thisParent = &repr.parent;
if (thisParent) |p| {
const parentRef = @This(){ .handle = p };
parentRef.removeChild(self.handle);
}
repr.parent = null;
}
pub fn removeChild(self: @This(), child: core.ObjectHandle) void {
const children = SceneObjectContainer.get(self.handle, .children).?;
for (children.items, 0..) |search, i| {
if (child.eql(search)) {
_ = children.swapRemove(i);
break;
}
}
}
pub fn getAndResolveTransform(self: @This()) core.Transform {
const repr: *SceneObjectRepr = SceneObjectContainer.get(self.handle, ._repr).?;
if (repr.lastUpdate != gSceneSystem.tickCount) {
gSceneSystem.updateTransform(repr, SceneObjectContainer.get(self.handle, .posRot).?);
}
return repr.transform;
}
pub fn getTransform(self: @This()) core.Transform {
return SceneObjectContainer.get(self.handle, ._repr).?.transform;
}
// you MUST clearTransfomRefUnsafe() before destroying this transform
pub fn setTransformRefUnsafe(self: @This(), ref: *core.Transform) void {
SceneObjectContainer.get(self.handle, ._repr).?.transformOverride = ref;
}
pub fn clearTransformRefUnsafe(self: @This(), ref: *core.Transform) void {
SceneObjectContainer.get(self.handle, ._repr).?.transformOverride = ref;
}
pub fn setMobility(self: @This(), mobility: SceneMobilityMode) void {
const settings = Scene.SceneObjectContainer.get(self.handle, .settings).?;
if (settings.sceneMode == .static) {
if (mobility == .moveable) {
core.gScene.dynamicObjects.append(core.gScene.allocator, self.handle) catch unreachable;
}
}
if (settings.sceneMode == .moveable) {
if (mobility == .static) {
@panic("todo unable to change scene mobility back to static");
}
}
settings.*.sceneMode = mobility;
}
};
pub const SceneObjectInitParams = union(enum) {
transform: core.Transform,
position: core.Vectorf,
rotation: core.Quat,
positionRotAngles: struct {
position: core.Vectorf = .{ .x = 0.0, .y = 0.0, .z = 0.0 },
angles: core.Vectorf = .{ .x = 0.0, .y = 0.0, .z = 0.0 },
},
positionRot: struct {
position: core.Vectorf = .{ .x = 0.0, .y = 0.0, .z = 0.0 },
angles: core.Quat = core.zm.qidentity(),
},
};
pub var gSceneSystem: *SceneSystem = undefined;
fn childAllocator() std.mem.Allocator {
return gSceneSystem.childrenArena.allocator();
}
pub const SceneSystem = struct {
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
allocator: std.mem.Allocator,
dynamicObjects: ArrayListUnmanaged(core.ObjectHandle) = .{},
childrenArena: std.heap.ArenaAllocator,
tickCount: u32 = 0,
pub const Field = SceneObjectSet.Field;
pub const FieldType = SceneObjectSet.FieldType;
// internal update transform function
fn updateTransform(self: *@This(), repr: *SceneObjectRepr, posRot: *const SceneObjectPosRot) void {
if (repr.lastUpdate == self.tickCount) {
return;
}
if (repr.transformOverride) |override| {
repr.lastUpdate = self.tickCount;
repr.transform = override.*;
return;
}
var final: core.Transform = core.zm.identity();
if (repr.parent) |parent| {
if (Scene.SceneObjectContainer.get(parent, ._repr)) |parentRepr| {
const parentPosRot = Scene.SceneObjectContainer.get(parent, .posRot).?;
self.updateTransform(parentRepr, parentPosRot);
const parentTransform = parentRepr.transform;
final = core.zm.mul(parentTransform, final);
} else {}
}
// core.engine_log("final\n{d} {d} {d} {d}\n{d} {d} {d} {d}", .{
// final[0][0],
// final[0][1],
// final[0][2],
// final[0][3],
// final[1][0],
// final[1][1],
// final[1][2],
// final[1][3],
// });
repr.transform = core.zm.mul(
core.zm.mul(
core.zm.mul(
core.zm.scalingV(posRot.scale.toZm()),
core.zm.matFromQuat(posRot.rotation.quat),
),
core.zm.translationV(posRot.position.toZm()),
),
final,
);
// core.engine_log("final\n{d} {d} {d} {d}\n{d} {d} {d} {d}", .{
// repr.transform[0][0],
// repr.transform[0][1],
// repr.transform[0][2],
// repr.transform[0][3],
// repr.transform[1][0],
// repr.transform[1][1],
// repr.transform[1][2],
// repr.transform[1][3],
// });
repr.lastUpdate = self.tickCount;
}
pub fn updateTransforms(self: *@This()) void {
// todo. calculate a running load factor for the number of movable objects
// vs static objects
// if we have a small amount of movable vs static AND if we have > 1000 objects,
// then iterate over dynamicObjects array instead
for (Scene.SceneObjectContainer.denseItems(._repr), 0..) |*repr, i| {
const settings = Scene.SceneObjectContainer.readDense(i, .settings);
if (settings.sceneMode == .moveable or repr.lastUpdate == 0) {
const posRot = Scene.SceneObjectContainer.readDense(i, .posRot);
self.updateTransform(repr, posRot);
}
}
}
// ----- NeonObject interace ----
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
.childrenArena = std.heap.ArenaAllocator.init(allocator),
};
gSceneSystem = self;
try core.defineComponent(Scene, allocator);
Scene.SceneObjectContainer = try SceneObjectSet.create(allocator);
return self;
}
pub fn preTick(self: *@This(), dt: f64) !void {
_ = dt;
self.tickCount +%= 1;
if (self.tickCount == 0) {
self.tickCount += 1;
}
}
pub fn tick(self: *@This(), deltaTime: f64) void {
var z = tracy.ZoneNC(@src(), "Scene System Tick", 0xAABBDD);
defer z.End();
self.updateTransforms();
_ = deltaTime;
}
pub fn deinit(self: *@This()) void {
self.dynamicObjects.deinit(self.allocator);
self.childrenArena.deinit();
core.undefineComponent(Scene);
Scene.SceneObjectContainer.destroy();
self.allocator.destroy(self);
}
};
// LUA_BEGIN
// because scene objects are a special sparse-multiset type,
// they do not have a fixed representation in the sparse set.
// as a result this type requires a special implementation to operate properly.
// multi-set systems should only ever modify values via functions
// we really need a way to deal with multi-set handles.
// idea - in the component registration. if the container type is a sparse multiset
// then the pointer type shall be a pointer to the set handle.
// and the component acquisition shall do absolutely nothing but grab the sparse index of the set handle
//
// man... that shit sounds like so much work...
// LUA_END

145
engine/core/src/script.zig Normal file
View File

@ -0,0 +1,145 @@
// scripting integration using lua
const std = @import("std");
pub const lua = @import("lua");
const core = @import("core.zig");
const startup_script = @embedFile("lua/startup.lua");
const core_script = @embedFile("lua/core.lua");
const ecs = @import("ecs.zig");
const ComponentRef = @import("script/ComponentRef.zig");
const ComponentRegistration = @import("script/ComponentRegistration.zig");
pub const script_bindings = @import("script_bindings.zig");
const c = lua.c;
var gLuaState: lua.LuaState = undefined;
var gLuaAllocator: std.mem.Allocator = undefined;
const luaRegLibs: []const c.luaL_Reg = &.{
.{ .name = "print", .func = printWrapper },
.{ .name = null, .func = null },
};
// binds the default print() function in lua to print to the console
fn printWrapper(l: ?*lua.c.lua_State) callconv(.C) i32 {
const state: lua.LuaState = .{ .l = l };
const argc = state.getTop();
if (core.getLogger() == null) {
core.printRaw("> ", .{});
}
if (argc >= 1) {
core.printRaw("[SCRIPT ]: ", .{});
}
var i: i32 = 1;
while (i <= argc) : (i += 1) {
if (state.isString(i)) {
if (i > 1) {
core.printRaw(" ", .{});
}
core.printRaw("{s}", .{state.toString(i)});
} else if (state.isUserdata(i)) {
const s = state.toStringL(i);
state.pop(1);
core.printRaw("{s}", .{s});
}
}
core.printRaw("\n", .{});
state.pop(argc);
return 0;
}
// initialization of the lua scripting interface.
// this interface is only threadsafe to operate on from the main systems thread (at this time).
pub fn start_lua(allocator: std.mem.Allocator) !void {
gLuaAllocator = allocator;
gLuaState = try lua.LuaState.init(.{});
try lua.pod.setupFormatBuffer(allocator);
// overload and hook into global functions
_ = gLuaState.getGlobal("_G");
c.luaL_setfuncs(gLuaState.l, luaRegLibs.ptr, 0);
gLuaState.pop(1);
try lua.pod.registerPodType(&gLuaState, ecs.Entity);
try lua.pod.registerPodType(&gLuaState, ComponentRegistration);
try script_bindings.registerTypes();
try gLuaState.loadString(startup_script);
try gLuaState.pcall();
try gLuaState.loadString(core_script);
try gLuaState.pcall();
}
pub fn createLuaComponentDefinitions(globalName: []const u8, container: ecs.EcsContainerRef, luaNew: anytype) !void {
const reg = try gLuaState.newZigUserdata(ComponentRegistration);
reg.ref = container;
reg.name = globalName;
reg.luaNew = luaNew;
try gLuaState.setGlobal(globalName);
}
pub fn registerComponent(comptime Component: type, container: ecs.EcsContainerRef) !void {
const ReferenceType = ComponentRef.ComponentReferenceType(Component);
try createLuaComponentDefinitions(@ptrCast(Component.ComponentName), container, ReferenceType.luaNew);
try ReferenceType.registerType(getState());
}
pub fn shutdown_lua() void {
lua.pod.shutdownFormatBuffer();
gLuaState.deinit();
}
pub fn getState() *lua.LuaState {
return &gLuaState;
}
fn findScriptBasepath(s: []const u8) []const u8 {
var i: usize = s.len - 1;
while (i > 0) : (i -= 1) {
if (s[i] == '\\' or s[i] == '/') {
break;
}
}
if (s[i] == '\\' or s[i] == '/') {
return s[i + 1 ..];
}
return s;
}
pub fn loadTypes(scriptPath: []const u8) !void {
// 1. scan filesystem for all files under the script path
var fileList = try core.fs().listAllSubpaths(gLuaAllocator, scriptPath);
defer fileList.deinit();
for (fileList.data.items, 0..) |f, i| {
// 2. enforce naming scheme for each script.
const basePath = findScriptBasepath(f);
// core.engine_log("checking script file: {s} path:{s} base {s}", .{ f, fileList.sources.items[i], basePath });
// 3. for each one, load the script and assign them based on name.
if (std.ascii.isUpper(basePath[0])) {
core.engine_log("loading script file: {s} path:{s}", .{ f, fileList.sources.items[i] });
const scriptFile = try core.fs().loadFile(f);
defer core.fs().unmap(scriptFile);
try gLuaState.loadString(scriptFile.bytes);
try gLuaState.pcall();
}
}
}
pub fn runScriptFile(scriptPath: []const u8) !void {
const scriptFile = try core.fs().loadFile(scriptPath);
defer core.fs().unmap(scriptFile);
try gLuaState.loadString(scriptFile.bytes);
try gLuaState.pcall();
}

View File

@ -0,0 +1,258 @@
// lwdt can't have meta tables so I'm going to create component references
//
// the way this works is
// - entities are created as POD types
// - entities can get components added to them via entity:addComponent
// - this returns a ComponentReference
// - you can also get components from an entity via enity:get()
// - this also returns a ComponentReference
//
// - ComponentReferences allow you to modify data on a component or call functions on them
//
//
// How the registration works
//
// - define component
// - ecs.zig defineComponent
// - ComponentRef.zig - ReferenceType
// - addComponentRegistration - script.zig
// - ComponentRef - ReferenceType.registerType
// this represents the lua side of the object
pub fn ComponentReferenceType(comptime T: type) type {
return struct {
ptr: *T = undefined,
// used for resolving deltas.
handle: core.ObjectHandle = undefined,
stateCount: u32 = 0,
containerRef: ecs.EcsContainerRef = undefined,
pub const MetatableName = T.ComponentName;
// argc = 1,
// 1. a componentRegistration userdata
// can only be called from entity.luaAddComponent
pub fn luaNew(state: lua.LuaState, handle: core.ObjectHandle, ptr: ?*anyopaque) void {
const ud = state.newZigUserdata(@This()) catch return;
const containerRef = ecs.getTypeContainer(T);
ud.* = .{
.handle = handle,
.containerRef = containerRef,
.stateCount = 0,
};
// std.debug.print("luaNew ComponentReferenceType: {x}\n", .{@intFromPtr(ud)});
// std.debug.print("luaNew ContainerRef: {x}\n", .{@intFromPtr(containerRef.ptr)});
// std.debug.print("luaNew " ++ @typeName(T) ++ " ud.ptr = {x}\n", .{@intFromPtr(ptr)});
if (ptr) |p| {
ud.ptr = @ptrCast(@alignCast(p));
ud.stateCount = containerRef.vtable.getStateCount(containerRef.ptr);
}
if (@hasDecl(@TypeOf(T.BaseContainer.*), "IsMultiset")) {
// ... there are several things that need to be reworked here...
ud.ptr = @ptrCast(@alignCast(@as(*anyopaque, @ptrCast(&ud.handle))));
// std.debug.print("luaNew " ++ @typeName(T) ++ " v = {any}\n", .{ud.ptr.getPosition()});
}
}
pub fn resolve(self: *@This()) void {
const ref = self.containerRef;
// std.debug.print("self: {x}\n", .{@intFromPtr(self)});
// std.debug.print("ptr: {x}\n", .{@intFromPtr(ref.ptr)});
if (@hasDecl(@TypeOf(T.BaseContainer.*), "IsMultiset")) {
//
}
if (ref.vtable.getStateCount(ref.ptr) != self.stateCount) {
self.ptr = @ptrCast(@alignCast(ref.vtable.get(ref.ptr, self.handle)));
}
}
pub fn get(self: *@This()) *T {
self.resolve();
return self.ptr;
}
pub fn luaToString(state: lua.LuaState) i32 {
// oh god... this isn't good
// I think i've been treating this component ref as the user type
// huge failure of type resolution
if (state.toUserdata(@This(), 1)) |self| {
self.resolve();
Buffer.clearRetainingCapacity();
var writer = Buffer.writer();
writer.print("{s}{{", .{MetatableName}) catch return 0;
inline for (std.meta.fields(T), 0..) |field, i| {
if (i == 0) {
writer.print(" {s} = ", .{field.name}) catch return 0;
} else {
writer.print(", {s} = ", .{field.name}) catch return 0;
}
switch (field.type) {
f32, i32, u32, f64, i64, u64 => {
writer.print("{d}", .{@field(self.ptr, field.name)}) catch return 0;
},
[]const u8 => {
writer.print("\"{s}\"", .{@field(self.ptr, field.name)}) catch return 0;
},
else => {
writer.print("<unknown type {s}>", .{@typeName(field.type)}) catch return 0;
},
}
}
writer.print(" }}" ++ "\x00", .{}) catch return 0;
state.pop(1);
state.pushString(Buffer.items) catch return 0;
return 1;
}
return 0;
}
pub fn luaIndex(state: lua.LuaState) i32 {
if (state.toUserdata(@This(), 1)) |self| {
if (state.isString(2)) {
_ = self;
const argument = state.toString(2);
// core.engine_log(@typeName(@This()) ++ " got indexed. 0x{x}", .{self.handle.index});
// check metatable
if (state.getMetafield(1, @ptrCast(argument))) {
return 1;
}
}
}
return 0;
}
fn makeTypeTable() lua.LibSpec {
const methods = blk: {
comptime var m: lua.LibSpec = &.{};
m = m ++ .{.{ .name = "__index", .func = lua.CWrap(luaIndex) }};
m = m ++ .{.{ .name = "__tostring", .func = lua.CWrap(luaToString) }};
inline for (T.ScriptExports) |name| {
m = m ++ .{.{ .name = @as([*c]const u8, @ptrCast(name)), .func = ComponentFuncWrapper(@field(T, name), T) }};
}
break :blk m ++ .{.{ .name = null, .func = null }};
};
return methods;
}
pub fn registerType(state: *lua.LuaState) !void {
core.engine_log("creating lua metatable {s}", .{MetatableName});
const methods = comptime makeTypeTable();
try state.newMetatable(@ptrCast(MetatableName));
try state.setFuncs(methods, 0);
}
};
}
pub fn ComponentFuncWrapper(comptime baseFunc: anytype, comptime baseType: type) lua.LuaCFunc {
return lua.CWrap(FuncWrapper(baseFunc, baseType).wrapper);
}
pub fn FuncWrapper(comptime baseFunc: anytype, comptime baseType: type) type {
return struct {
pub fn wrapper(state: lua.LuaState) i32 {
const Args = std.meta.ArgsTuple(@TypeOf(baseFunc));
var args: Args = undefined;
inline for (std.meta.fields(Args), 0..) |field, index| {
// std.debug.print("typename = {s}\n", .{@typeName(field.type)});
switch (field.type) {
f32 => {
args[index] = @as(f32, @floatCast(state.toNumber(index + 1)));
},
f64 => {
args[index] = state.toNumber(index + 1);
},
i32 => {
args[index] = @intFromFloat(state.toNumber(index + 1));
},
[]const u8 => {
args[index] = state.toString(index + 1);
},
// I'll be honest how the hell does this work?
//
// the pointer being passed in here isn't the actual resulting type...
// it's the reference type
*baseType => {
const ref = state.toUserdata(ComponentReferenceType(baseType), index + 1).?;
ref.resolve();
args[index] = ref.ptr;
},
baseType => {
const ref = state.toUserdata(ComponentReferenceType(baseType), index + 1).?;
ref.resolve();
args[index] = ref.ptr.*;
},
else => {
// if(isComponentType(field.type)) {
// const ref = state.toUserdata(field.type, index + 1).?;
// ref.resolve();
// args[index] = ref.ptr.*;
// continue;
// }
//
lua.debugPrints(true);
args[index] = (state.toUserdata(field.type, index + 1) orelse {
std.debug.print("argument error in index: {d}\n", .{index});
state.emitError("something's weird with this argument\n");
@panic("lmao");
}).*;
lua.debugPrints(false);
},
}
}
state.pop(@intCast(args.len));
//const rv = @call(.always_inline, baseFunc, args);
const rv = @call(.auto, baseFunc, args);
switch (@TypeOf(rv)) {
i32, u32, i64, u64 => {
state.pushNumber(@floatFromInt(rv));
},
f32, f64 => {
state.pushNumber(@floatCast(rv));
},
void => {
return 0;
},
else => {
const ud = state.newZigUserdata(@TypeOf(rv)) catch @panic("not implemented");
ud.* = rv;
},
}
return 1;
}
};
}
var Buffer: std.ArrayList(u8) = undefined;
pub fn setupFormatBuffer(allocator: std.mem.Allocator) !void {
Buffer = std.ArrayList(u8).init(allocator);
}
pub fn shutdownFormatBuffer() void {
Buffer.deinit();
}
const std = @import("std");
const core = @import("../core.zig");
const ecs = @import("../ecs.zig");
const lua = @import("lua");
const pod = lua.pod;
const scene = @import("../scene.zig");

View File

@ -0,0 +1,33 @@
ref: ecs.EcsContainerRef = undefined,
name: []const u8 = undefined,
luaNew: *const fn (state: lua.LuaState, core.ObjectHandle, ?*anyopaque) void = undefined,
pub const PodDataTable: pod.DataTable = .{
.name = "ComponentRegistration",
.banInstantiation = true,
.toStringOverride = lua.CWrap(toString),
};
// I should really move all the registration to this file.
pub fn toString(state: lua.LuaState) i32 {
var workBuffer: [256]u8 = undefined;
const ud = state.toUserdata(@This(), 1).?;
state.pop(1);
const str = std.fmt.bufPrintZ(&workBuffer, "Component Data Table: {s} 0x{x}", .{ ud.name, ud.ref.ptr }) catch return 0;
state.pushString(str) catch return 0;
return 1;
}
pub fn createComponent(self: @This(), handle: core.ObjectHandle) ?*anyopaque {
const ref = self.ref;
const rv = ref.vtable.createWithHandle(ref.ptr, handle);
// std.debug.print("createComponent: {p}\n", .{rv});
return rv;
}
const lua = @import("lua");
const ecs = @import("../ecs.zig");
const std = @import("std");
const core = @import("../core.zig");
const pod = lua.pod;

View File

@ -0,0 +1,59 @@
const luaRegLibs: []const lua.c.luaL_Reg = &.{
.{ .name = "registerTick", .func = lua.CWrap(registerTick) },
.{ .name = null, .func = null },
};
// register core types and subsystems into the scripting engine
pub fn registerTypes() !void {
// transform POD type
const state = script.getState();
try lua.pod.registerPodType(state, core.Vector);
try lua.pod.registerPodType(state, core.Vectorf);
try lua.pod.registerPodType(state, core.Vector2);
try lua.pod.registerPodType(state, core.Vector2f);
// try lua.pod.registerPodType(state, core.Transform);
// lua.pod.registerPodType(state, core.Vector4, "Vector4");
try state.createLibrary("Systems", luaRegLibs);
}
pub fn registerTick(l: lua.LuaState) i32 {
// two arguments first one is going to be userdata entity
// second one is going to be a lua function.
_ = l;
return 0;
}
pub const ScriptTicks = struct {
allocator: std.mem.Allocator,
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
};
return self;
}
pub fn tick(self: *@This(), deltaTime: f64) void {
_ = self;
const state = script.getState();
_ = state.getGlobal("__TickScripts");
state.pushNumber(deltaTime);
state.pcallStack(1) catch {
core.engine_logs("could not execute lua script");
};
}
pub fn deinit(self: *@This()) void {
self.allocator.destroy(self);
}
};
const lua = @import("lua");
const std = @import("std");
const script = @import("script.zig");
const core = @import("core.zig");

197
engine/core/src/stacks.zig Normal file
View File

@ -0,0 +1,197 @@
const std = @import("std");
const core = @import("core.zig");
const BumpArena = core.BumpArena;
const builtin = @import("builtin");
pub fn hashStackList(stack: []const usize) u32 {
var c: u32 = 0;
for (stack) |l| {
c = combineHash32(c, hashPtr(l));
}
return c;
}
pub inline fn combineHash32(lhs: u32, rhs: u32) u32 {
return lhs ^ (rhs +% 0x9e3779b9 +% (lhs << 6) +% (lhs >> 2));
}
pub inline fn hashPtr(value: usize) u32 {
var k = value +% 1;
if (@sizeOf(@TypeOf(value)) == 8) {
k = (~k) +% (k << 18);
k = k ^ std.math.rotr(usize, k, 31);
k = @mulWithOverflow(k, 21)[0];
k = k ^ std.math.rotr(usize, k, 11);
k = k +% (k << 6);
k = k ^ std.math.rotr(usize, k, 22);
return @truncate(k);
} else {}
}
test "1 million stacks." {
// see how fast i can walk 1 million stacks
var timer = std.time.Timer.start() catch unreachable;
const startTime = timer.read();
for (0..100000) |_| {
core.stacks.pushCallStack();
}
const endTime = timer.read();
std.debug.print("timeElapsed 100k callstacks {d}s", .{@as(f64, @floatFromInt(endTime - startTime)) / 1000000000});
}
test "test hash64s" {
const pointers: []const usize = &.{
0x0,
0x1,
0xffffffff1eed1234,
0xffffffff_ffffffff,
0xffffffff_ffffffff - 1,
};
var combined: u32 = 0;
for (pointers) |l| {
const h = hashPtr(l);
std.debug.print("l = {x}, hash = {x}\n", .{ l, h });
combined = combineHash32(combined, h);
}
std.debug.print("combined hash = {x} ({x})\n", .{ combined, hashStackList(pointers) });
}
pub const StackCompactor = struct {
allocator: std.mem.Allocator,
stackArena: BumpArena,
stackMap: std.AutoHashMapUnmanaged(u32, *CallStack) = .{},
const CallStack = struct {
pointers: []usize,
debugStr: []?[]u8,
};
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
.stackArena = try BumpArena.init(allocator),
};
return self;
}
pub fn addNewCallstack(self: *@This(), stack: []const usize) !u32 {
const hash = hashStackList(stack);
const bumpAllocator = self.stackArena.allocator();
if (!self.stackMap.contains(hash)) {
const ownedStack: *CallStack = try bumpAllocator.create(CallStack);
ownedStack.pointers = try bumpAllocator.dupe(usize, stack);
ownedStack.debugStr = try bumpAllocator.alloc(?[]u8, stack.len);
const debug_info = std.debug.getSelfDebugInfo() catch {
try self.stackMap.put(self.allocator, hash, ownedStack);
return hash;
};
for (stack, 0..) |address, i| {
ownedStack.debugStr[i] = null;
const module = debug_info.getModuleForAddress(address) catch continue;
const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch continue;
if (symbol_info.line_info) |line_info| {
ownedStack.debugStr[i] = try std.fmt.allocPrintZ(
bumpAllocator,
"{d}> @0x{x} symbol_name: {s} {s} > {s}: {d}",
.{ i, address, symbol_info.symbol_name, symbol_info.compile_unit_name, line_info.file_name, line_info.line },
);
} else {
ownedStack.debugStr[i] = try std.fmt.allocPrintZ(
bumpAllocator,
"{d}> @0x{x} symbol_name: {s} {s} > no file info",
.{ i, address, symbol_info.symbol_name, symbol_info.compile_unit_name },
);
}
}
try self.stackMap.put(self.allocator, hash, ownedStack);
}
return hash;
}
pub fn deinit(self: @This()) void {
self.stackList.deinit(self.allocator);
self.allocator.destroy(self);
}
pub fn getCallStack(self: *@This()) u32 {
if (builtin.os.tag != .windows) {
return 0;
}
var context: std.debug.ThreadContext = undefined;
const has_context = std.debug.getContext(&context);
if (!has_context) {
return 0;
}
var addr_buf: [1024]usize = undefined;
const n = std.debug.walkStackWindows(addr_buf[0..], &context);
return self.addNewCallstack(addr_buf[0..n]) catch unreachable;
}
};
var stackCompactor: *core.StackCompactor = undefined;
pub fn initStackCompactor() void {
stackCompactor = core.StackCompactor.create(std.heap.c_allocator) catch unreachable;
}
pub inline fn pushCallStack() void {
var context: std.debug.ThreadContext = undefined;
const has_context = std.debug.getContext(&context);
if (!has_context) {
return;
}
var addr_buf: [1024]usize = undefined;
const n = std.debug.walkStackWindows(addr_buf[0..], &context);
stackCompactor.addNewCallstack(addr_buf[0..n]) catch unreachable;
}
pub fn getStackCompactor() *StackCompactor {
return stackCompactor;
}
pub fn walkAndPrintStack() void {
var context: std.debug.ThreadContext = undefined;
const has_context = std.debug.getContext(&context);
if (!has_context) {
return;
}
var addr_buf: [1024]usize = undefined;
const n = std.debug.walkStackWindows(addr_buf[0..], &context);
const debug_info = std.debug.getSelfDebugInfo() catch {
return;
};
var i: u32 = 0;
while (i < n) : (i += 1) {
const address = addr_buf[i];
const module = debug_info.getModuleForAddress(address) catch continue;
const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch continue;
std.debug.print("@0x{x} symbol_name: {s} {s} > ", .{ address, symbol_info.symbol_name, symbol_info.compile_unit_name });
if (symbol_info.line_info) |line_info| {
std.debug.print(" {s}:{d}\n", .{ line_info.file_name, line_info.line });
} else {
std.debug.print(" no file info\n", .{});
}
}
}

View File

@ -0,0 +1,18 @@
const std = @import("std");
pub fn dupeZ(comptime T: type, allocator: std.mem.Allocator, source: []const T) ![]T {
var buff: []T = try allocator.alloc(T, source.len + 1);
for (source, 0..source.len) |s, i| {
buff[i] = s;
}
buff[source.len] = 0;
return buff;
}
pub fn dupe(comptime T: type, allocator: std.mem.Allocator, source: []const T) ![]T {
var buff: []T = try allocator.alloc(T, source.len);
for (source, 0..) |s, i| {
buff[i] = s;
}
return buff;
}

View File

View File

@ -0,0 +1,19 @@
const std = @import("std");
pub fn implement_func_for_tagged_union_nonull(
self: anytype,
comptime funcName: []const u8,
comptime returnType: type,
args: anytype,
) returnType {
const Self = @TypeOf(self);
inline for (@typeInfo(std.meta.Tag(Self)).Enum.fields) |field| {
if (@as(std.meta.Tag(Self), @enumFromInt(field.value)) == self) {
if (@hasDecl(@TypeOf(@field(self, field.name)), funcName)) {
return @field(@field(self, field.name), funcName)(args);
}
}
}
unreachable;
}

View File

@ -0,0 +1,25 @@
const core = @import("core");
const std = @import("std");
const memory = core.MemoryTracker;
const engine_log = core.engine_log;
const engine_logs = core.engine_logs;
test "simple systems setup for core" {
std.testing.refAllDecls(core.algorithm);
memory.MTSetup(std.testing.allocator, .{ .timeline = false });
defer memory.MTShutdown();
const tracker = memory.MTGet().?;
const allocator = tracker.allocator();
std.debug.print("Starting up \n", .{});
engine_logs("systems starting");
try core.start_module(.{}, .{ .unitTest = true }, allocator);
defer core.shutdown_module(allocator);
engine_logs("systems started, shutting down");
memory.MTPrintStatsDelta();
try memory.dumpTimeline("test-core-timeline.txt");
}

View File

View File

View File

@ -0,0 +1 @@
const core = @import("core.zig");

View File

@ -0,0 +1,9 @@
// first person camera...
// god... modularity with user inputs are fucking crazy man...
//
// I like the idea of an input stack.
//
// inputs go through a list of stacked listeners.
//
// listeners can choose to either consume or pass a given input event.
// but listeners always listen.

View File

71
engine/graphics/build.zig Normal file
View File

@ -0,0 +1,71 @@
const std = @import("std");
const SpirvReflect = @import("SpirvReflect");
const dependencyList = [_][]const u8{
"vulkan",
"vma",
"glfw3",
"core",
"assets",
"platform",
"objLoader",
"ozz",
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("graphics", .{
.target = target,
.optimize = optimize,
.link_libc = true,
.root_source_file = b.path("src/graphics.zig"),
});
mod.addAnonymousImport("texture_sample.png", .{ .root_source_file = b.path("defaults/texture_sample.png") });
const options = b.addOptions();
options.addOption(bool, "force_mailbox", b.option(bool, "force_mailbox", "forces mailbox mode for present mode. unlocks framerate to irresponsible levels") orelse false);
mod.addOptions("game_build_opts", options);
for (dependencyList) |depName| {
const dep = b.dependency(depName, .{ .target = target, .optimize = optimize });
mod.addImport(depName, dep.module(depName));
if (std.mem.eql(u8, depName, "ozz")) {
mod.linkLibrary(dep.artifact("ozz_cpp"));
}
}
const spirvGen = SpirvReflect.SpirvGenerator2.init(b, .{ .optimize = optimize });
spirvGen.addShader(mod, b.path("shaders/triangle_mesh.vert"), "triangle_mesh_vert");
spirvGen.addShader(mod, b.path("shaders/default_lit.frag"), "default_lit");
spirvGen.addShader(mod, b.path("shaders/skybox/skybox.vert"), "skybox_vert");
spirvGen.addShader(mod, b.path("shaders/skybox/skybox.frag"), "skybox_frag");
spirvGen.addShader(mod, b.path("shaders/debug.vert"), "debug_vert");
spirvGen.addShader(mod, b.path("shaders/debug.frag"), "debug_frag");
// === simple little integration test ===
//
// this doesn't really do anything other than call a few functions
// to make sure that we properly linked everything
const test_step = b.step("test", "");
const tests = b.addTest(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tests/tests.zig"),
.link_libc = true,
});
tests.root_module.addImport("graphics", mod);
for (dependencyList) |depName| {
const dep = b.dependency(depName, .{ .target = target, .optimize = optimize });
tests.root_module.addImport(depName, dep.module(depName));
}
const runArtifact = b.addRunArtifact(tests);
test_step.dependOn(&runArtifact.step);
}

View File

@ -0,0 +1,21 @@
.{
.name = "graphics",
.version = "0.0.0",
.dependencies = .{
//
.vulkan = .{ .path = "../../lib/vulkan" },
.vma = .{ .path = "../../lib/vma" },
.glfw3 = .{ .path = "../../lib/glfw3" },
.cgltf = .{ .path = "../../lib/cgltf" },
.objLoader = .{ .path = "../../lib/objLoader" },
.SpirvReflect = .{ .path = "../../lib/spirv-reflect-zig" },
.ozz = .{ .path = "../../lib/ozz" },
// core
.core = .{ .path = "../core" },
.assets = .{ .path = "../assets" },
.platform = .{ .path = "../platform" },
},
.paths = .{
"",
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,32 @@
//glsl version 4.5
#version 450
layout (location = 0) in vec3 in_color;
layout (location = 1) in vec2 texCoord;
layout (location = 2) in vec3 worldPosition;
layout (location = 0) out vec4 outFragColor;
layout (set = 0, binding = 0) uniform CameraBuffer{
mat4 view;
mat4 proj;
mat4 viewproj;
vec4 position;
} cameraData;
layout(set = 0, binding = 1) uniform SceneData{
vec4 fogColor; // w is for exponent
vec4 fogDistances; //x for min, y for max, zw unused.
vec4 ambientColor;
vec4 sunlightDirection; //w for sun power
vec4 sunlightColor;
} sceneData;
void main()
{
vec3 color = in_color.rgb;
float cameraDist = length(cameraData.position.xyz - worldPosition);
float opacity = (1.0) - (cameraDist / 300);
outFragColor = vec4(color, opacity * 1.0);
}

View File

@ -0,0 +1,40 @@
#version 460
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 2) in vec4 vColor;
layout (location = 3) in vec2 vTexCoord;
layout (location = 0) out vec3 outColor;
layout (location = 1) out vec2 texCoord;
layout (location = 2) out vec3 worldPosition;
layout (set = 0, binding = 0) uniform CameraBuffer{
mat4 view;
mat4 proj;
mat4 viewproj;
vec4 position;
} cameraData;
// size: 16 x 4 + 3 x 4 = 76 => 128 bytes per object per alignment
struct ObjectData {
mat4 model;
vec4 color;
};
layout(std140, set = 1, binding = 0) readonly buffer ObjectBuffer{
ObjectData objects[];
} objectBuffer;
void main()
{
ObjectData object = objectBuffer.objects[gl_BaseInstance];
mat4 modelMatrix = object.model;
mat4 final = (cameraData.viewproj * modelMatrix);
vec4 position = final * vec4(vPosition, 1.0f);
gl_Position = position;
outColor = object.color.xyz;
texCoord = vTexCoord;
vec4 modelPos = modelMatrix * vec4(vPosition, 1.0f);
worldPosition = modelPos.xyz;
}

View File

@ -0,0 +1,47 @@
//glsl version 4.5
#version 450
#extension GL_EXT_nonuniform_qualifier : require
layout (location = 0) in vec3 in_color;
layout (location = 1) in vec2 texCoord;
layout (location = 2) in vec3 worldPosition;
layout (location = 3) flat in uint textureId;
layout (location = 4) flat in uint baseInstance;
layout (location = 0) out vec4 outFragColor;
#include "globalSet.glsl"
#include "sharedSsbo.glsl"
void main()
{
// outFragColor = vec4(in_color + 0.25 * sceneData.ambientColor.xyz,1.0f);
// outFragColor = vec4(texCoord.x, texCoord.y, 0.5f, 1.0f);
// vec4 color = texture(tex1, texCoord).xyzw;
vec4 color = texture(gTex[textureId], texCoord).xyzw;
if(color.w < 0.05f)
{
discard;
}
float cameraDist = length(cameraData.position.xyz - worldPosition);
float opacity = clamp((1.f) - (clamp(cameraDist - 300, 0, 300) / 300.f), 0.f, 1.f);
//float opacity = 1.0;
if(opacity < 0.05f)
{
discard;
}
// outFragColor = vec4(mix(sceneData.fogColor.xyz, color.xyz, opacity), color.w);
outFragColor = vec4(color.xyz, opacity);
//vec3 mixed = mix(normalize(vec3(0.5, 0.3, 0.2)) * 3, vec3(0.2, 0.2, 1) * 3, texCoord.y * 2);
//outFragColor = vec4(color.xyz, 1.0f);
//outFragColor = vec4(0.0, 1.0, 0.0, 1.0f);
}

View File

@ -0,0 +1,18 @@
layout (set = 0, binding = 0) uniform CameraBuffer{
mat4 view;
mat4 proj;
mat4 viewproj;
mat4 viewprojAlt;
vec4 position;
} cameraData;
layout(set = 0, binding = 1) uniform SceneData{
vec4 fogColor; // w is for exponent
vec4 fogDistances; //x for min, y for max, zw unused.
vec4 ambientColor;
vec4 sunlightDirection; //w for sun power
vec4 sunlightColor;
} sceneData;
layout(set = 0, binding = 2) uniform sampler2D[] gTex;

View File

@ -0,0 +1,25 @@
struct ObjectData {
mat4 model;
uint textureId;
int animation; // this is the index offset of the first matrix in the animation finals buffer.
uint flags0;
// packed flags0 flags;
// [0,0]: alwaysInFront
// [1,1]: useAltCamera
uint pad1;
};
layout(std140, set = 1, binding = 0) readonly buffer ObjectBuffer{
ObjectData objects[];
} objectBuffer;
uint flag0_AlwaysInFront(uint flags)
{
return flags & 0x1;
}
uint flag0_useAltFov(uint flags)
{
return (flags >> 1) & 0x1;
}

View File

@ -0,0 +1,3 @@
layout(std140, set = 1, binding = 1) readonly buffer BoneBuffer{
mat4 finals[];
} animationBuffer;

View File

@ -0,0 +1,15 @@
#version 450
#include "../globalSet.glsl"
layout(set = 1, binding = 0) uniform samplerCube cubemap;
layout (location = 0) in vec3 inUVW;
layout (location = 0) out vec4 outFragColor;
void main()
{
outFragColor = texture(cubemap, inUVW);
}

View File

@ -0,0 +1,17 @@
#version 450
#include "../vertexInput.glsl"
#include "../globalSet.glsl"
layout (location = 0) out vec3 outUVW;
void main()
{
outUVW = vPosition;
// Convert cubemap coordinates into Vulkan coordinate space
// Remove translation from view matrix
mat4 viewMat = mat4(mat3(cameraData.view));
gl_Position = cameraData.proj * viewMat * vec4(vPosition.xyz, 1.0);
}

View File

@ -0,0 +1,66 @@
#version 460
#include "vertexInput.glsl"
layout (location = 0) out vec3 outColor;
layout (location = 1) out vec2 texCoord;
layout (location = 2) out vec3 worldPosition;
layout (location = 3) flat out uint textureId;
layout (location = 4) flat out uint baseInstance;
#include "globalSet.glsl"
#include "sharedSsbo.glsl"
#include "skeletalBuffers.glsl"
void main()
{
vec3 vertexPos = vec3(0.0);
int animation = objectBuffer.objects[gl_BaseInstance].animation;
if(animation == -1)
{
vertexPos = vPosition;
}
else
{
uint animation = objectBuffer.objects[gl_BaseInstance].animation;
for(int i = 0; i < 4; i += 1)
{
uint boneIndex = bones[i];
float weight = float(weights[i]) / 255;
// this will depend on ozz's finals format
mat4 boneTransform = animationBuffer.finals[animation + boneIndex];
vertexPos += weight * ( boneTransform * vec4(vPosition, 1.0) ).xyz;
}
}
mat4 modelMatrix = objectBuffer.objects[gl_BaseInstance].model;
mat4 final;
if(flag0_useAltFov(objectBuffer.objects[gl_BaseInstance].flags0) == 1)
{
final = (cameraData.viewprojAlt * modelMatrix);
}
else
{
final = (cameraData.viewproj * modelMatrix);
}
vec4 position = final * vec4(vertexPos, 1.0f);
if( flag0_AlwaysInFront(objectBuffer.objects[gl_BaseInstance].flags0) == 1)
{
position.z *= 0.0001;
}
baseInstance = gl_BaseInstance;
gl_Position = position;
textureId = objectBuffer.objects[gl_BaseInstance].textureId;
outColor = vec3(vColor.x, vColor.y, vColor.z);
texCoord = vTexCoord;
worldPosition = (modelMatrix * vec4(0,0,0,1)).xyz;
}

View File

@ -0,0 +1,7 @@
#extension GL_EXT_shader_explicit_arithmetic_types_int8 : enable
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 2) in vec4 vColor;
layout (location = 3) in vec2 vTexCoord;
layout (location = 4) in u8vec4 bones;
layout (location = 5) in u8vec4 weights;

View File

@ -0,0 +1,32 @@
pixels: []u8,
extent: core.Vector2i,
const std = @import("std");
const core = @import("core");
const colors = core.colors;
pub fn init(allocator: std.mem.Allocator, extent: core.Vector2i) !@This() {
return .{
.pixels = try allocator.alignedAlloc(u8, 8, @intCast(extent.x * extent.y * 4)),
.extent = extent,
};
}
pub fn clear(self: *@This(), clearColor: colors.ColorRGBA8) void {
var as32: []u32 = undefined;
as32.len = self.pixels.len / 4;
as32.ptr = @alignCast(@ptrCast(self.pixels.ptr));
@memset(as32, @as(u32, @bitCast(clearColor)));
}
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
allocator.free(self.pixels);
}
pub inline fn getPixel(self: *@This(), position: core.Vector2i) *colors.ColorRGBA8 {
const offset = position.x * position.y * 4;
const r: *u8 = &self.pixels[@intCast(offset)];
return @as(*colors.ColorRGBA8, @alignCast(@ptrCast(r)));
}

View File

@ -0,0 +1,232 @@
pub const AnimResolverRef = core.Reference(AnimResolverInterface);
pub const AnimResolverInterface = core.MakeInterface("AnimResolverVTable", struct {
// this tick function should evaluate the current state of the resolver
// and then update the animator's finals[] matrix list.
resolve: *const fn (*anyopaque, f64, *Animator) void,
onSkeletonSet: ?*const fn (*anyopaque, *Animator) void = null,
create: *const fn (std.mem.Allocator) core.EngineDataEventError!*anyopaque,
destroy: *const fn (*anyopaque) void,
pub fn Implement(comptime TargetType: type) @This() {
const Wrap = struct {
pub fn create(allocator: std.mem.Allocator) core.EngineDataEventError!*anyopaque {
const new = TargetType.create(allocator) catch return core.EngineDataEventError.BadInit;
return @ptrCast(new);
}
pub fn destroy(p: *anyopaque) void {
const ptr: *TargetType = @ptrCast(@alignCast(p));
ptr.destroy();
}
pub fn onSkeletonSet(p: *anyopaque, a: *Animator) void {
const ptr: *TargetType = @ptrCast(@alignCast(p));
ptr.onSkeletonSet(a) catch unreachable;
}
pub fn resolve(p: *anyopaque, dt: f64, a: *Animator) void {
const ptr: *TargetType = @ptrCast(@alignCast(p));
ptr.resolve(dt, a) catch unreachable;
}
};
return .{
.destroy = Wrap.destroy,
.create = Wrap.create,
.resolve = Wrap.resolve,
};
}
});
pub const AnimSampler = struct {
name: ?core.Name = null,
track: ?*AnimationTrack = null,
playbackRate: f32 = 1.0,
time: f32 = 0.0,
outputLocals: std.ArrayListUnmanaged(ozz.SoaTransform) = .{},
// other features
// paused: bool = false,
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
self.outputLocals.deinit(allocator);
}
pub fn getOutput(self: *@This()) []ozz.SoaTransform {
return self.outputLocals.items;
}
pub fn sampleAndAdvance(self: *@This(), allocator: std.mem.Allocator, dt: f64, animator: *Animator) void {
self.sample(allocator, animator);
self.advance(dt);
}
pub fn advance(self: *@This(), dt: f64) void {
if (self.track == null) {
return;
}
FloatHelpers.updateTrackTime(&self.time, dt, self.playbackRate, self.track.?.endTime);
}
pub fn setName(self: *@This(), name: core.Name) void {
self.name = name;
self.track = null;
}
pub fn sample(self: *@This(), allocator: std.mem.Allocator, animator: *Animator) void {
if (self.name == null) {
return;
}
if (self.track == null) {
self.track = animation_system.gAnimationSys.animTracks.get(self.name.?.handle());
}
self.outputLocals.resize(allocator, animator.jointLength) catch return;
if (self.track) |track| {
if (track.endTime < 0.01) {
return;
}
animator.sampleAnimation(self.time, track, self.outputLocals.items);
}
}
};
pub const BlenderList = struct {
backing: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
jobLayers: std.ArrayListUnmanaged(ozz.Layer) = .{},
jobLayersAdditive: std.ArrayListUnmanaged(ozz.Layer) = .{},
useAdditive: bool = false,
threshold: f32 = 0.01,
jointLength: usize = 0,
blendingJob: ozz.BlendingJob = .{},
pub fn create(backingAllocator: std.mem.Allocator) !*@This() {
const self = try backingAllocator.create(@This());
self.* = .{
.backing = backingAllocator,
.arena = std.heap.ArenaAllocator.init(backingAllocator),
};
return self;
}
pub fn destroy(self: *@This()) void {
self.arena.deinit();
self.backing.destroy(self);
}
pub fn updateRestPose(self: *@This(), animator: *Animator) !void {
if (animator.skeleton) |skeleton| {
self.jointLength = skeleton.sk.numJoints();
self.blendingJob.rest_pose = skeleton.sk.getRestPoseModel();
}
}
pub fn clearLayers(self: *@This()) void {
self.jobLayersAdditive.clearRetainingCapacity();
self.jobLayers.clearRetainingCapacity();
}
pub fn addLayer(self: *@This(), transform: []ozz.SoaTransform, weight: f32, settings: anytype) void {
const layer = self.jobLayers.addOne(self.arena.allocator()) catch unreachable;
layer.* = .{
.weight = weight,
.transform = ozz.makeSpan(transform),
};
_ = settings;
}
pub fn updateAndRun(self: *@This(), output: []ozz.SoaTransform) void {
self.updateBlendingJob();
self.runBlendingJob(output) catch return;
}
pub fn updateBlendingJob(self: *@This()) void {
self.blendingJob.threshold = self.threshold;
self.blendingJob.layers = ozz.makeSpan(self.jobLayers.items);
//self.blendingJob.additive_layers = if (self.useAdditive) ozz.makeSpan(self.jobLayersAdditive.items) else .{};
self.blendingJob.additive_layers = .{};
}
pub fn runBlendingJob(self: *@This(), output: []ozz.SoaTransform) !void {
self.blendingJob.output = ozz.makeSpan(output);
if (!self.blendingJob.run()) {
core.engine_logs("blending job failed");
return;
}
}
};
// resolver helpers
pub const FloatHelpers = struct {
pub inline fn updateTrackTime(target: *f32, dt: f64, rate: f32, endTime: f32) void {
target.* += @as(f32, @floatCast(dt)) * rate;
while (target.* > endTime) {
target.* -= endTime;
}
}
};
// samples a single animation, same as the default behaviour.
// used as a test for the resolver system
pub const SingleAnimationResolver = struct {
allocator: std.mem.Allocator,
locals: std.ArrayListUnmanaged(ozz.SoaTransform) = .{},
track: ?*AnimationTrack = null,
playback: f32 = 0.0,
playbackRate: f32 = 1.0,
pub const AnimResolverVTable = AnimResolverInterface.Implement(@This());
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
};
return self;
}
pub fn onSkeletonSet(self: *@This(), animator: *Animator) !void {
if (animator.skeleton) |skeleton| {
try self.locals.resize(self.allocator, skeleton.sk.numSoaJoints());
}
}
pub fn resolve(self: *@This(), dt: f64, animator: *Animator) !void {
self.track = animator.track;
if (self.track == null) {
return;
}
const track = self.track.?;
if (track.endTime < 0.01) {
return;
}
FloatHelpers.updateTrackTime(&self.playback, dt, self.playbackRate, track.endTime);
animator.sampleAnimation(self.playback, track, self.locals.items);
animator.commitLocalToModel(self.locals.items);
animator.modelToFinal();
}
pub fn destroy(self: *@This()) void {
self.locals.deinit(self.allocator);
self.allocator.destroy(self);
}
};
const animation_system = @import("animationSystem.zig");
const Animator = animation_system.Animator;
const AnimationTrack = animation_system.AnimationTrack;
const core = @import("core");
const std = @import("std");
const ozz = @import("ozz");

View File

@ -0,0 +1,480 @@
// big main sy.itemsstem for animation
const ozz = @import("ozz");
const core = @import("core");
const std = @import("std");
pub const BoneHandle = enum(u8) { _ };
pub const Skeleton = struct {
sk: *ozz.Skeleton,
inverseBinds: std.ArrayListUnmanaged(core.Mat) = .{},
jointMapping: std.StringHashMapUnmanaged(u8) = .{},
pub fn buildJointMap(self: *@This(), allocator: std.mem.Allocator) !void {
for (self.sk.getJointsList(), 0..) |jointName, i| {
// std.debug.print("jointName {d} {s}\n", .{ i, jointName });
const str = std.mem.span(jointName);
try self.jointMapping.put(allocator, str, @intCast(i));
}
}
pub fn getBoneHandleByName(self: @This(), string: []const u8) ?BoneHandle {
if (self.jointMapping.get(string)) |x| {
return @enumFromInt(x);
} else {
return null;
}
}
pub fn deinit(self: *@This()) void {
self.sk.destroy();
}
};
pub const AnimationTrack = struct {
animation: *ozz.Animation,
endTime: f32 = 1.0,
pub fn deinit(self: *@This()) void {
self.animation.destroy();
}
};
pub const PlaybackTrack = struct {
track: ?*AnimationTrack = null,
playback: f32 = 0.0,
playbackRate: f32 = 1.0,
};
pub const Animator = struct {
jointRemap: ?[]u8 = null,
animationName: ?core.Name = null,
skeleton: ?*Skeleton = null,
skeletonName: ?core.Name = null,
sjc: *ozz.SamplingJobContext = undefined,
track: ?*AnimationTrack = null,
playback: f32 = 0.0,
playbackRate: f32 = 1.0,
// todo.. implement blending
// animations: [4]*ozz.Animation = undefined,
// timelines: [4]f32 = .{ 0, 0, 0, 0 },
animationCount: u32 = 0,
locals: std.ArrayListUnmanaged(ozz.SoaTransform) = .{},
models: std.ArrayListUnmanaged(ozz.Float4x4) = .{},
finals: std.ArrayListUnmanaged(core.Mat) = .{},
finalsSpan: core.Span = undefined,
entity: core.Entity = undefined,
jointLength: usize = 0,
resolverRef: ?AnimResolverRef = null,
pub var allocator: std.mem.Allocator = undefined;
// oh god if I want to support multiple animation blending...
// maybe the kernel should contain a fixed amount of animations?
pub fn initECS(self: *@This(), handle: core.SetHandle) void {
// get the mesh component
self.entity = core.Entity{ .handle = handle };
if (self.entity.get(graphics.StaticMesh)) |mesh| {
mesh.animated = true; //todo
mesh.animator = self;
self.sjc = ozz.SamplingJobContext.createMaxTracks(256);
} else {
@panic("animator added to an entity that does not have a mesh component");
}
}
pub fn getBoneTransform(self: *@This(), handle: BoneHandle) core.Mat {
return @bitCast(self.models.items[@intFromEnum(handle)]);
}
pub fn setSkeletonByName(self: *@This(), skName: core.Name) !void {
if (self.skeleton != null) {
// return the previous span and allocate a new one.
gAnimationSys.slots.removeSpan(self.finalsSpan);
}
self.skeletonName = skName;
self.skeleton = gAnimationSys.skeletons.get(self.skeletonName.?.handle()).?;
const numJoints = self.skeleton.?.sk.numJoints();
self.jointLength = numJoints;
self.sjc.resize(@intCast(numJoints));
try self.locals.resize(allocator, self.skeleton.?.sk.numSoaJoints());
try self.models.resize(allocator, numJoints);
try self.finals.resize(allocator, numJoints);
self.finalsSpan = try gAnimationSys.slots.allocate(@intCast(numJoints));
if (self.resolverRef) |ref| {
if (ref.vtable.onSkeletonSet) |f| {
f(ref.ptr, self);
}
}
self.jointRemap = null;
}
pub fn setSkeleton(self: *@This(), skeleton: []const u8) void {
self.setSkeletonByName(core.MakeName(skeleton)) catch unreachable;
}
pub fn addResolver(self: *@This(), comptime Resolver: type) !*Resolver {
const resolver = try Resolver.create(allocator);
try resolver.onSkeletonSet(self);
self.resolverRef = core.refFromPtr(AnimResolverInterface, resolver);
return resolver;
}
pub fn removeResolver(self: *@This()) void {
if (self.resolverRef) |ref| {
ref.vtable.destroy(ref.ptr);
self.resolverRef = null;
}
}
pub fn update(self: *@This(), dt: f64) void {
if (self.skeleton == null) {
return;
}
// if a resolver is present, use that to update my the finals instead of the default function below
if (self.resolverRef) |ref| {
ref.vtable.resolve(ref.ptr, dt, self);
return;
}
if (!self.defaultSample(dt)) {
return;
}
self.modelToFinal();
}
fn defaultSample(self: *@This(), dt: f64) bool {
if (self.track == null)
return false;
const track = self.track.?;
if (track.endTime < 0.01)
return false;
const skeleton = self.skeleton.?;
self.playback += @as(f32, @floatCast(dt)) * self.playbackRate;
while (self.playback > track.endTime) {
self.playback -= track.endTime;
}
var samplingJob: ozz.SamplingJob = .{
.ratio = self.playback / track.endTime,
.animation = track.animation,
.context = self.sjc,
.output = ozz.makeSpan(self.locals.items),
};
if (!samplingJob.run()) {
core.engine_errs("sampling job failed");
return false;
}
var ltmJob: ozz.LocalToModelJob = .{
.skeleton = skeleton.sk,
.input = ozz.makeSpan(self.locals.items),
.output = ozz.makeSpan(self.models.items),
};
if (!ltmJob.run()) {
core.engine_errs("local to model job failed");
return false;
}
return true;
}
pub fn commitLocalToModel(self: *@This(), input: []ozz.SoaTransform) void {
self.localToModel(input, self.models.items);
}
pub fn localToModel(self: *@This(), input: []ozz.SoaTransform, output: []ozz.Float4x4) void {
var ltmJob: ozz.LocalToModelJob = .{
.skeleton = self.skeleton.?.sk,
.input = ozz.makeSpan(input),
.output = ozz.makeSpan(output),
};
if (!ltmJob.run()) {
core.engine_errs("local to model job failed");
return;
}
}
pub fn sampleAnimation(self: *@This(), time: f32, track: *AnimationTrack, output: []ozz.SoaTransform) void {
var samplingJob: ozz.SamplingJob = .{
.ratio = time / track.endTime,
.animation = track.animation,
.context = self.sjc,
.output = ozz.makeSpan(output),
};
if (!samplingJob.run()) {
core.engine_errs("sampling job failed");
return;
}
}
pub fn modelToFinal(self: *@This()) void {
if (self.jointRemap == null) {
if (self.entity.get(graphics.StaticMesh)) |meshComponent| {
if (meshComponent.mesh) |mesh| {
self.jointRemap = mesh.jointRemap;
}
}
}
const skeleton = self.skeleton.?;
for (self.models.items, 0..) |model, i| {
const transform: core.Mat = @bitCast(model);
// const p: core.zm.Vec = .{ 0, 0, 0, 1 };
// graphics.debugSphere(core.Vectorf.fromZm(core.zm.mul(p, transform)), 0.03, .{
// .color = if (i == 15) .{ .x = 1 } else .{ .y = 1 },
// });
const final = core.zm.mul(skeleton.inverseBinds.items[i], transform);
// joint remap ozz -> gltf
if (self.jointRemap) |jr| {
// core.engine_log("{d} xx {d}", .{ i, jr[i] });
self.finals.items[@intCast(jr[i])] = final;
} else {
self.finals.items[i] = final;
}
// core.engine_log(
// "[{d}] {d} {d} {d} {d}, {d} {d} {d} {d}",
// .{ i, transform[0][0], transform[0][1], transform[0][2], transform[0][3], transform[1][0], transform[1][1], transform[1][2], transform[1][3] },
// );
}
}
pub fn setAnimationByName(self: *@This(), _name: core.Name) !void {
var name = _name;
self.track = gAnimationSys.animTracks.get(name.handle());
}
pub fn setAnimation(self: *@This(), path: []const u8) void {
const name = core.MakeName(path);
self.setAnimationByName(name) catch unreachable;
}
pub fn deinit(self: *@This()) void {
self.sjc.destroy();
self.removeResolver();
self.finals.deinit(allocator);
self.locals.deinit(allocator);
self.models.deinit(allocator);
}
pub var BaseContainer: *core.SparseMap(@This()) = undefined;
pub const ComponentName = "Animator";
pub const ScriptExports: []const []const u8 = &.{};
};
pub const AnimationSystem = struct {
backingAllocator: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
slots: MergedSpans,
// Only AnimationTrack and Skeletons are made using the ArenaAllocator
animTracks: std.AutoHashMapUnmanaged(u32, *AnimationTrack) = .{},
skeletons: std.AutoHashMapUnmanaged(u32, *Skeleton) = .{},
sharedArena: [2]std.heap.ArenaAllocator, // could be a good usecase for a fat bump arena
shared: [2]std.ArrayListUnmanaged(MatrixUploads) = .{ .{}, .{} },
sharedLocks: [2]std.Thread.Mutex = .{ .{}, .{} }, // could be a good usecase for a fat bump arena
pub const MatrixUploads = struct {
offset: u32,
matrices: std.ArrayListUnmanaged(core.Mat) = .{},
};
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
pub const RendererInterfaceVTable = graphics.RendererInterface.from(@This());
pub fn preTick(self: *@This(), dt: f64) !void {
_ = self;
var z1 = core.tracy.ZoneN(@src(), "animation system tick");
defer z1.End();
for (Animator.BaseContainer.list.items) |animator| {
animator.update(dt);
}
}
pub fn newAnimTrack(self: *@This(), _name: core.Name, anim: *ozz.Animation) !void {
var name = _name;
const new = try self.arenaAllocator().create(AnimationTrack);
new.* = .{
.animation = anim,
.endTime = anim.getDuration(),
};
try self.animTracks.put(self.backingAllocator, name.handle(), new);
}
pub fn newSkeleton(self: *@This(), _name: core.Name, sk: *ozz.Skeleton) !void {
const new = try self.arenaAllocator().create(Skeleton);
new.* = .{
.sk = sk,
};
var name = _name;
try new.buildJointMap(self.arenaAllocator());
try new.inverseBinds.resize(self.arenaAllocator(), new.sk.numJoints());
if (new.inverseBinds.items.len > 256) {
@panic("too many bones in skeleton, not supported");
}
var bindModels = std.ArrayList(ozz.Float4x4).init(self.backingAllocator);
defer bindModels.deinit();
try bindModels.resize(new.sk.numJoints());
var ltmJob: ozz.LocalToModelJob = .{
.skeleton = new.sk,
.input = new.sk.getRestPoseModel(),
.output = ozz.makeSpan(bindModels.items),
};
core.engine_log("creating bind pose {d} joints", .{new.inverseBinds.items.len});
if (!ltmJob.run()) {
core.engine_logs("unable to get bind pose");
return error.UnableToLoad;
}
for (bindModels.items, 0..) |bind, i| {
// const p: core.zm.Vec = .{ 0, 0, 0, 1 };
// graphics.debugSphere(core.Vectorf.fromZm(core.zm.mul(p, @as(core.Mat, @bitCast(bind)))), 0.1, .{ .duration = 100 });
new.inverseBinds.items[i] = core.zm.inverse(@as(core.Mat, @bitCast(bind)));
}
try self.skeletons.put(self.backingAllocator, name.handle(), new);
}
pub fn arenaAllocator(self: *@This()) std.mem.Allocator {
return self.arena.allocator();
}
pub fn getShared(self: @This(), fi: u32) []const MatrixUploads {
return self.shared[fi].items;
}
pub fn sendShared(self: *@This(), frameIndex: u32) void {
const fi: usize = @intCast(frameIndex);
self.sharedLocks[fi].lock();
defer self.sharedLocks[fi].unlock();
_ = self.sharedArena[fi].reset(.retain_capacity);
const allocator = self.sharedArena[fi].allocator();
const shared = &self.shared[fi];
shared.* = .{};
for (Animator.BaseContainer.list.items) |animator| {
var upload: MatrixUploads = .{ .offset = animator.finalsSpan.start };
// core.engine_log(
// "finalsSpan size offset{d} {d} animator finals {d}\n",
// .{
// animator.finalsSpan.start,
// animator.finalsSpan.size,
// animator.finals.items.len
// });
upload.matrices.resize(allocator, animator.finalsSpan.size) catch unreachable;
for (animator.finals.items, 0..) |final, i| {
upload.matrices.items[i] = final;
}
shared.append(allocator, upload) catch unreachable;
}
}
pub fn init(alloc: std.mem.Allocator) !*@This() {
const self = try alloc.create(@This());
self.* = .{
.backingAllocator = alloc,
.arena = std.heap.ArenaAllocator.init(alloc),
.sharedArena = .{
std.heap.ArenaAllocator.init(alloc),
std.heap.ArenaAllocator.init(alloc),
},
.slots = try MergedSpans.init(alloc, vk_constants.MAX_SKIN_SLOTS),
};
gAnimationSys = self;
Animator.allocator = alloc;
try core.defineComponent(Animator, alloc);
return self;
}
pub fn deinit(self: *@This()) void {
core.engine_logs("deinitializing animation system");
{
core.engine_log("skeleton count {d}", .{self.skeletons.count()});
var iter = self.skeletons.iterator();
while (iter.next()) |i| {
i.value_ptr.*.deinit();
}
}
{
core.engine_log("animTracks count {d}", .{self.animTracks.count()});
var iter = self.animTracks.iterator();
while (iter.next()) |i| {
i.value_ptr.*.deinit();
}
}
for (self.sharedArena) |arena| {
arena.deinit();
}
for (Animator.BaseContainer.list.items) |animator| {
animator.deinit();
}
self.slots.deinit();
core.undefineComponent(Animator);
self.arena.deinit();
self.skeletons.deinit(self.backingAllocator);
self.animTracks.deinit(self.backingAllocator);
self.backingAllocator.destroy(self);
}
};
pub var gAnimationSys: *AnimationSystem = undefined;
pub fn getSkeletonByName(_name: core.Name) ?*Skeleton {
var name = _name;
return gAnimationSys.skeletons.get(name.handle());
}
const graphics = @import("../graphics.zig");
const MergedSpans = core.MergedSpans;
const vk_constants = @import("../vk_constants.zig");
const anim_resolver = @import("animResolver.zig");
const AnimResolverRef = anim_resolver.AnimResolverRef;
const AnimResolverInterface = anim_resolver.AnimResolverInterface;

View File

@ -0,0 +1,86 @@
pub const AnimationLoader = struct {
pub var LoaderInterfaceVTable: assets.AssetLoaderInterface = assets.AssetLoaderInterface.from("Animation", @This());
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
sys: *animation_system.AnimationSystem,
pub fn discardAll(self: *@This()) void {
_ = self;
}
pub fn loadAsset(self: *@This(), assetRef: assets.AssetRef, propertiesBag: ?assets.AssetPropertiesBag) assets.AssetLoaderError!void {
const animation = ozz.Animation.create();
const mapping = core.fs().loadFile(propertiesBag.?.path) catch return error.UnableToLoad;
defer core.fs().unmap(mapping);
animation.loadFromBytes(mapping.bytes);
self.sys.newAnimTrack(assetRef.name, animation) catch return error.UnableToLoad;
}
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.sys = animation_system.gAnimationSys,
};
return self;
}
pub fn destroy(self: *@This(), allocator: std.mem.Allocator) void {
allocator.destroy(self);
}
};
pub const SkeletonLoader = struct {
pub var LoaderInterfaceVTable: assets.AssetLoaderInterface = assets.AssetLoaderInterface.from("Skeleton", @This());
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
sys: *animation_system.AnimationSystem,
pub fn discardAll(self: *@This()) void {
_ = self;
}
pub fn loadAsset(self: *@This(), assetRef: assets.AssetRef, propertiesBag: ?assets.AssetPropertiesBag) assets.AssetLoaderError!void {
const sk = ozz.Skeleton.create();
const mapping = core.fs().loadFile(propertiesBag.?.path) catch return error.UnableToLoad;
defer core.fs().unmap(mapping);
sk.loadFromBytes(mapping.bytes);
self.sys.newSkeleton(assetRef.name, sk) catch return error.UnableToLoad;
}
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.sys = animation_system.gAnimationSys,
};
return self;
}
pub fn destroy(self: *@This(), allocator: std.mem.Allocator) void {
allocator.destroy(self);
}
};
pub var gSkeletonLoader: *SkeletonLoader = undefined;
pub var gAnimationLoader: *AnimationLoader = undefined;
pub fn initLoaders() !void {
gSkeletonLoader = try core.createObject(SkeletonLoader, .{});
gAnimationLoader = try core.createObject(AnimationLoader, .{});
try assets.gAssetSys.registerLoader(gSkeletonLoader);
try assets.gAssetSys.registerLoader(gAnimationLoader);
}
const animation_system = @import("animationSystem.zig");
const assets = @import("assets");
const core = @import("core");
const std = @import("std");
const ozz = @import("ozz");

View File

@ -0,0 +1,239 @@
const MeshConfig = struct {
info: CookInfo = .{ .assetType = "Mesh" }, // there must always be a CookInfo field
sourceType: []const u8 = "obj",
animated: bool = false,
};
const extList = [_][]const u8{ "gltf", "obj", "glb" };
pub fn generateFunction(allocator: std.mem.Allocator, path: []const u8, out: *std.ArrayList(u8)) GenerateError!void {
_ = allocator;
out.clearRetainingCapacity();
const ext = core.getFileExtension(path)[1..];
var config: MeshConfig = .{};
for (extList) |e| {
if (std.mem.eql(u8, e, ext)) {
config.sourceType = e;
if (std.mem.eql(u8, e, "glb")) {
config.sourceType = "gltf";
}
}
}
std.json.stringify(
config,
.{ .whitespace = .indent_4 },
out.writer(),
) catch return GenerateError.UnableToGenerate;
}
fn cookObj(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) cook.CookResult {
const rawFileBytes = cook.loadFileAlloc(allocator, dir, path) catch unreachable;
defer allocator.free(rawFileBytes);
var out = std.ArrayList(u8).init(allocator);
var vertices = std.ArrayList(Vertex).init(allocator);
defer vertices.deinit();
var objs = obj.loadObjBytes(rawFileBytes, allocator) catch unreachable;
defer objs.deinit();
if (objs.meshes.items.len > 0) {
mesh.loadObjMeshVertices(&vertices, objs.meshes.items[0]) catch unreachable;
for (vertices.items) |vert| {
out.appendSlice(&@as([@sizeOf(Vertex)]u8, @bitCast(vert))) catch unreachable;
}
return .{
.bytes = out,
.result = .Success,
};
} else {
return .{
.bytes = out,
.result = .Failure,
};
}
}
fn ensureGltf2ozz(allocator: std.mem.Allocator) !void {
const suffix = if (builtin.os.tag == .windows) ".exe" else "";
std.fs.cwd().access("zig-out/tools/gltf2ozz" ++ suffix, .{}) catch {
const argv: []const []const u8 = &.{ "zig", "build", "tools" };
core.engine_log("gltf2ozz missing, building it...", .{});
var child = std.process.Child.init(argv, allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Pipe;
child.cwd = ".";
switch (try child.spawnAndWait()) {
.Exited => |value| {
if (value == 0) {
core.engine_log("gltf2ozz built", .{});
} else {
core.engine_logs("unable to build gltf2ozz");
}
},
.Signal => {
core.engine_logs("unable to build gltf2ozz");
},
.Stopped => {},
.Unknown => {
unreachable;
},
}
return;
};
core.engine_log("gltf2ozz found", .{});
}
fn cookAnimations(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8, config: MeshConfig) !void {
_ = config;
// 1. check if it has an associated .ozzconfig file.
const gltf2OzzAbs = try std.fs.cwd().realpathAlloc(allocator, "zig-out/tools/gltf2ozz.exe");
defer allocator.free(gltf2OzzAbs);
const ozzconfig = try std.fmt.allocPrint(allocator, "{s}.ozzconfig", .{path});
defer allocator.free(ozzconfig);
const fileArg = try std.fmt.allocPrint(allocator, "--file={s}", .{core.getBasePath(path)});
defer allocator.free(fileArg);
const configArg = try std.fmt.allocPrint(allocator, "--config_file={s}", .{core.getBasePath(ozzconfig)});
defer allocator.free(configArg);
// todo, fix this later, idrc right now.
const newConfigArg = try std.fmt.allocPrint(allocator, "--config_dump_reference={s}", .{core.getBasePath(ozzconfig)});
defer allocator.free(newConfigArg);
const absFile = try dir.realpathAlloc(allocator, path);
defer allocator.free(absFile);
var argv: []const []const u8 = &.{
gltf2OzzAbs,
fileArg,
configArg,
};
dir.access(ozzconfig, .{}) catch {
argv = &.{
gltf2OzzAbs,
fileArg,
newConfigArg,
};
core.engine_log("creating ozz config for file, marked as animated but no animation data", .{});
};
// std.debug.print("{s} {s} {s} cwd = {s}\n", .{ argv[0], argv[1], argv[2], core.getFolder(absFile) });
const result = try std.process.Child.run(.{
.argv = argv,
.allocator = allocator,
.cwd = core.getFolder(absFile),
.max_output_bytes = 150 * 1024 * 1024,
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
var success: bool = true;
switch (result.term) {
.Exited => |value| {
if (value != 0) {
success = false;
}
},
.Signal => {
success = false;
},
.Stopped => {
success = false;
// no-op should be ok?
},
.Unknown => {
unreachable;
},
}
if (success) {
core.engine_log("generated animations for {s}", .{path});
} else {
core.engine_log("error generating animations {s} stdout:\n{s}\n stderr:{s}\n", .{ path, result.stdout, result.stderr });
}
// 2. if so, run it through gltf2ozz with that file.
}
fn cookGltf(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8, config: MeshConfig) cook.CookResult {
core.engine_logs("gltf cooking not implemeted");
const out = std.ArrayList(u8).init(allocator);
// check if it's animated. if it's animated, then invoke gltf2ozz and create a .ozzconfig file and
// make a subfolder called
if (config.animated) {
// if gltf2ozz isn't there then we have to call zig build tools
ensureGltf2ozz(allocator) catch unreachable;
cookAnimations(allocator, dir, path, config) catch unreachable;
}
return .{ .bytes = out, .result = .Failure };
}
pub fn cookFunction(
allocator: std.mem.Allocator,
dir: std.fs.Dir,
path: []const u8,
params: cook.CookParams,
) cook.CookResult {
core.engine_log("{s}", .{params.cookFileName});
const fc = cook.loadFileAlloc(allocator, dir, params.cookFileName) catch unreachable;
defer allocator.free(fc);
const config = std.json.parseFromSlice(MeshConfig, allocator, fc[0 .. fc.len - 1], .{}) catch unreachable;
defer config.deinit();
if (std.mem.eql(u8, config.value.sourceType, "obj")) {
return cookObj(allocator, dir, path);
} else {
return cookGltf(allocator, dir, path, config.value);
}
}
pub fn initCooker(allocator: std.mem.Allocator) !void {
_ = allocator;
const registry = assets.cook.getRegistry();
try registry.install("Mesh", generateFunction, cookFunction, &.{
".obj",
".gltf",
".glb",
});
}
pub fn deinitCooker() void {
//
}
const std = @import("std");
const assets = @import("assets");
const cook = assets.cook;
const CookInfo = assets.cook.CookInfo;
const GenerateError = assets.cook.GenerateError;
const core = @import("core");
const obj = @import("objLoader");
const builtin = @import("builtin");
const mesh = @import("../mesh.zig");
const Mesh = mesh.Mesh;
const Vertex = mesh.MeshVertex;

View File

@ -0,0 +1,60 @@
const TextureConfig = struct {
info: CookInfo = .{ .assetType = "Texture" }, // there must always be a CookInfo field
sourceType: []const u8 = "png",
};
pub fn generateFunction(allocator: std.mem.Allocator, path: []const u8, out: *std.ArrayList(u8)) GenerateError!void {
_ = allocator;
_ = path;
out.clearRetainingCapacity();
std.json.stringify(
TextureConfig{},
.{ .whitespace = .indent_4 },
out.writer(),
) catch return GenerateError.UnableToGenerate;
}
pub fn cookFunction(
allocator: std.mem.Allocator,
dir: std.fs.Dir,
path: []const u8,
params: cook.CookParams,
) cook.CookResult {
_ = params;
const rawFileBytes = cook.loadFileAlloc(allocator, dir, path) catch unreachable;
defer allocator.free(rawFileBytes);
// 1. load the file, and create a bytes buffer
var contents = png.PngContents.initFromBytes(allocator, path, rawFileBytes) catch unreachable;
defer contents.deinit();
// png.PngContents.initFromBytes(allocator: std.mem.Allocator, pathName: []const u8, pngFileContents: []const u8)
// 2. use the PngContents function to cook it.
return .{
.bytes = contents.toBuffer() catch unreachable,
.result = .Success,
};
}
pub fn initCooker(allocator: std.mem.Allocator) !void {
_ = allocator;
const registry = assets.cook.getRegistry();
try registry.install("Texture", generateFunction, cookFunction, &.{
".png",
});
}
pub fn deinitCooker() void {
//
}
const std = @import("std");
const assets = @import("assets");
const cook = assets.cook;
const CookInfo = assets.cook.CookInfo;
const GenerateError = assets.cook.GenerateError;
const core = @import("core");
const png = core.png;

View File

@ -0,0 +1,421 @@
const std = @import("std");
const vk = @import("vulkan");
const vkd_utils = @import("vk_renderer/vkd_utils.zig");
const core = @import("core");
const graphics = @import("graphics.zig");
const assets = @import("assets");
const debug_vert = @import("debug_vert");
const debug_frag = @import("debug_frag");
const tracy = core.tracy;
const gpd = graphics.gpu_pipe_data;
pub const DebugLine = struct {
start: core.Vectorf,
end: core.Vectorf,
pub fn resolve(self: @This(), _: anytype) core.Transform {
var delta = self.start.sub(self.end);
const d = delta.normalize();
const axz = std.math.atan2(-d.z, d.x) + core.radians(180.0);
const ay = -std.math.asin(d.y);
const mat1 = core.zm.matFromRollPitchYaw(0, 0, ay);
const mat2 = core.zm.rotationY(axz);
const len = delta.length();
return core.zm.mul(core.zm.mul(
core.zm.mul(mat1, mat2),
core.zm.scaling(len, len, len),
), core.zm.translationV(self.start.toZm()));
}
};
pub const DebugSphere = struct {
position: core.Vectorf,
radius: f32,
rotation: core.Quat,
pub fn resolve(self: @This(), _: anytype) core.Transform {
return core.zm.mul(core.zm.mul(
core.zm.matFromQuat(self.rotation),
core.zm.scaling(self.radius, self.radius, self.radius),
), core.zm.translationV(self.position.toZm()));
}
};
pub const DebugBox = struct {
position: core.Vectorf,
extents: core.Vectorf,
rotation: core.Quat,
pub fn resolve(self: @This(), _: anytype) core.Transform {
return core.zm.mul(core.zm.mul(
core.zm.matFromQuat(self.rotation),
core.zm.scalingV(self.extents.toZm()),
), core.zm.translationV(self.position.toZm()));
}
};
const DebugPrimitiveType = enum(u8) {
line = 0,
sphere = 1,
box = 2,
};
pub const DebugPrimitive = struct {
primitive: union(DebugPrimitiveType) {
line: DebugLine,
sphere: DebugSphere,
box: DebugBox,
},
color: core.Vectorf = .{ .x = 0.0, .y = 1.0, .z = 0.0 },
duration: f32 = 0.0,
pub fn resolve(self: @This()) core.Transform {
// comptime core.asserts(@sizeOf(DebugPrimitiveGpu) == DebugPrimitiveGpu.TargetSize, "");
switch (self.primitive) {
.line => |inner| {
return inner.resolve(.{});
},
.sphere => |inner| {
return inner.resolve(.{});
},
.box => |inner| {
return inner.resolve(.{});
},
}
unreachable;
// return core.implement_func_for_tagged_union_nonull(self.primitive, "resolve", core.Transform, .{});
}
};
const DebugPrimitiveGpu = struct {
const UnpaddedSize = @sizeOf(core.Transform) + @sizeOf(core.Vectorf);
const TargetSize = 80;
model: core.Transform,
color: core.Vectorf,
pad: [TargetSize - UnpaddedSize]u8 = std.mem.zeroes([TargetSize - UnpaddedSize]u8),
};
const DebugDrawSharedInstance = struct {};
const DebugSharedData = struct {
drawsThisFrame: std.ArrayListUnmanaged(DebugPrimitive) = .{},
lock: std.Thread.Mutex = .{},
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
self.drawsThisFrame.deinit(allocator);
}
};
const objectCount = 2048;
// Debug draw system also an example of how to do plugins in this engine
pub const DebugDrawSubsystem = struct {
// Interfaces and tables
pub const RendererInterfaceVTable = graphics.RendererInterface.from(@This());
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
// Member functions
allocator: std.mem.Allocator,
debugDraws: core.RingQueueU(DebugPrimitive),
meshes: [@as(usize, @intCast(@intFromEnum(DebugPrimitiveType.box) + 1))]core.Name = .{ undefined, undefined, undefined },
gc: *graphics.NeonVkContext = undefined,
pipeData: gpd.GpuPipeData = undefined,
mappedBuffers: []gpd.GpuMappingData(DebugPrimitiveGpu) = undefined,
material: *graphics.Material = undefined,
materialName: core.Name = core.Name.MakeComptime("mat_debugsys"),
deltaTime: f64 = 0,
sharedData: [graphics.NumFrames]DebugSharedData = .{ .{}, .{} },
indirectStaging: graphics.NeonVkBuffer = undefined,
indirectGpu: graphics.NeonVkBuffer = undefined,
const Primitives = [_]assets.AssetImportReference{
assets.MakeImportRef("Mesh", "m_primitive_sphere", "meshes/primitive_sphere.obj"),
assets.MakeImportRef("Mesh", "m_primitive_box", "meshes/primitive_box.obj"),
assets.MakeImportRef("Mesh", "m_primitive_line", "meshes/primitive_line.obj"),
};
pub fn prepareSubsystem(self: *@This(), gc: *graphics.NeonVkContext) !void {
self.gc = gc;
// assign debug meshes
self.meshes[@as(usize, @intCast(@intFromEnum(DebugPrimitiveType.sphere)))] = core.MakeName("m_primitive_sphere");
self.meshes[@as(usize, @intCast(@intFromEnum(DebugPrimitiveType.box)))] = core.MakeName("m_primitive_box");
self.meshes[@as(usize, @intCast(@intFromEnum(DebugPrimitiveType.line)))] = core.MakeName("m_primitive_line");
try self.createPipeData();
try self.createMaterial();
// create indirect command buffers
self.indirectStaging = try gc.vkAllocator.createStagingBuffer(4096 * @sizeOf(vk.DrawIndexedIndirectCommand), "debug draw indirect staging buffer");
self.indirectGpu = try gc.vkAllocator.createIndirectCommandBuffer(4096 * @sizeOf(vk.DrawIndexedIndirectCommand), "debug draw indirect command buffer");
}
pub fn uploadIndirectCommands(self: *@This(), cmd: vk.CommandBuffer, count: u32) void {
vkd_utils.copyStagingSlice(vk.DrawIndexedIndirectCommand, cmd, .{
.src = &self.indirectStaging,
.dst = &self.indirectGpu,
.size = count,
});
}
pub fn createPipeData(self: *@This()) !void {
var dataBuilder = gpd.GpuPipeDataBuilder.init(self.allocator, self.gc);
defer dataBuilder.deinit();
dataBuilder.setObjectCount(objectCount);
try dataBuilder.addBufferBinding(DebugPrimitiveGpu, .storage_buffer, .{ .vertex_bit = true }, .storageBuffer);
self.pipeData = try dataBuilder.build("debug draws");
self.mappedBuffers = try self.pipeData.mapBuffers(self.gc, DebugPrimitiveGpu, 0);
}
pub fn createMaterial(self: *@This()) !void {
var gc: *graphics.NeonVkContext = self.gc;
const vert_spv = debug_vert.spv();
const frag_spv = debug_frag.spv();
var pipelineBuilder = try graphics.NeonVkPipelineBuilder.init(
gc.dev,
gc.vkd,
gc.allocator,
gc.vkAllocator,
vert_spv,
frag_spv,
);
defer pipelineBuilder.deinit();
try pipelineBuilder.add_mesh_description();
try pipelineBuilder.add_layout(self.gc.globalDescriptorLayout);
try pipelineBuilder.add_layout(self.pipeData.descriptorSetLayout);
try pipelineBuilder.add_depth_stencil();
pipelineBuilder.set_polygon_mode(.line);
pipelineBuilder.set_topology(.triangle_list);
try pipelineBuilder.init_triangle_pipeline(gc.actual_extent);
const materialName = self.materialName;
const material = try gc.allocator.create(graphics.Material);
material.* = graphics.Material{
.materialName = materialName,
.pipeline = (try pipelineBuilder.build(gc.renderPass)).?,
.layout = pipelineBuilder.pipelineLayout,
};
try gc.add_material(material);
self.material = material;
}
// Renderer Ineterface Implementation
// pub fn preDraw(self: *@This(), frameId: usize) void {
// var zone = tracy.ZoneN(@src(), "Debug draw renderer");
// defer zone.End();
// const count: usize = self.debugDraws.count();
// var offset: usize = 0;
// while (offset < count) : (offset += 1) {
// const primitive = self.debugDraws.at(offset).?;
// const transform = primitive.resolve();
// const color = primitive.color;
// const object = &self.mappedBuffers[frameId].objects[offset];
// object.*.color = color;
// object.*.model = transform;
// }
// }
pub fn tick(self: *@This(), dt: f64) void {
self.deltaTime = dt;
}
pub fn sendShared(self: *@This(), frameIndex: u32) void {
var zone = tracy.ZoneN(@src(), "debug draw- uploading shared");
defer zone.End();
self.sharedData[frameIndex].lock.lock();
defer self.sharedData[frameIndex].lock.unlock();
var offset: usize = 0;
const count = self.debugDraws.count();
self.sharedData[frameIndex].drawsThisFrame.clearRetainingCapacity();
while (offset < count) {
var primitive: DebugPrimitive = self.debugDraws.pop().?;
self.sharedData[frameIndex].drawsThisFrame.append(self.allocator, primitive) catch unreachable;
primitive.duration -= @as(f32, @floatCast(self.deltaTime));
offset += 1;
if (primitive.duration >= 0) {
// push this primitive so that it goes to the next frame
self.debugDraws.push(primitive) catch continue;
}
}
}
pub fn rtPreDraw(self: *@This(), rt: *graphics.RenderThread, cmd: vk.CommandBuffer, frameIndex: u32) void {
_ = rt;
const shared: *DebugSharedData = &self.sharedData[frameIndex];
var offset: usize = 0;
shared.lock.lock();
defer shared.lock.unlock();
const count: usize = shared.drawsThisFrame.items.len;
if (count == 0)
return;
const mapped = self.gc.vkAllocator.mapBuffer(vk.DrawIndexedIndirectCommand, self.indirectStaging) catch unreachable;
defer self.gc.vkAllocator.unmapMemory(self.indirectStaging);
while (offset < count) : (offset += 1) {
const primitive: DebugPrimitive = shared.drawsThisFrame.items[offset];
var mesh: core.Name = undefined;
switch (primitive.primitive) {
.box => {
mesh = self.meshes[2];
},
.sphere => {
mesh = self.meshes[1];
},
.line => {
mesh = self.meshes[0];
},
}
const indexedMesh = graphics.getIndexedMeshByName(mesh).?;
mapped[offset] = .{
.index_count = indexedMesh.index.size,
.instance_count = 1,
.first_index = indexedMesh.index.start,
.vertex_offset = 0,
.first_instance = @intCast(offset),
};
}
self.uploadIndirectCommands(cmd, @intCast(count));
}
pub fn rtPostDraw(self: *@This(), rt: *graphics.RenderThread, cmd: vk.CommandBuffer, frameIndex: u32) void {
_ = rt;
var zone = tracy.ZoneN(@src(), "Debug draw renderer");
defer zone.End();
const shared: *DebugSharedData = &self.sharedData[frameIndex];
shared.lock.lock();
defer shared.lock.unlock();
var z1 = tracy.ZoneN(@src(), "Debug draw - ssbo upload");
// core.engine_log("count: {d}", .{shared.drawsThisFrame.items.len});
for (shared.drawsThisFrame.items, 0..) |primitive, i| {
const object = &self.mappedBuffers[frameIndex].objects[i];
object.*.color = primitive.color;
object.*.model = primitive.resolve();
}
z1.End();
var vkd = self.gc.vkd;
var z2 = tracy.ZoneN(@src(), "Debug draw - pipeline bind");
vkd.cmdBindPipeline(cmd, .graphics, self.material.pipeline);
var bindOffset: usize = 0;
const count: usize = shared.drawsThisFrame.items.len;
const paddedSceneSize = @as(u32, @intCast(self.gc.pad_uniform_buffer_size(@sizeOf(graphics.NeonVkSceneDataGpu))));
var startOffset: u32 = paddedSceneSize * @as(u32, @intCast(frameIndex));
vkd.cmdBindDescriptorSets(cmd, .graphics, self.material.layout, 0, 1, @ptrCast(&self.gc.frameData[frameIndex].globalDescriptorSet), 1, @ptrCast(&startOffset));
z2.End();
var z3 = tracy.ZoneN(@src(), "Debug draw - render");
var buffers = graphics.getMeshPoolBuffers();
vkd.cmdBindVertexBuffers(cmd, 0, 1, @ptrCast(&buffers.vertex.buffer), @ptrCast(&bindOffset));
vkd.cmdBindIndexBuffer(cmd, buffers.index.buffer, 0, .uint32);
vkd.cmdBindDescriptorSets(cmd, .graphics, self.material.layout, 1, 1, self.pipeData.getDescriptorSet(frameIndex), 0, undefined);
vkd.cmdDrawIndexedIndirect(cmd, self.indirectGpu.buffer, 0, @intCast(count), @sizeOf(vk.DrawIndexedIndirectCommand));
z3.End();
}
// NeonObject Interface Implementation
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
.debugDraws = core.RingQueueU(DebugPrimitive).init(allocator, objectCount) catch unreachable,
};
return self;
}
pub fn shutdown(self: *@This()) void {
for (self.mappedBuffers) |*mapped| {
mapped.unmap(self.gc);
}
self.gc.allocator.free(self.mappedBuffers);
self.gc.vkAllocator.destroyBuffer(&self.indirectGpu);
self.gc.vkAllocator.destroyBuffer(&self.indirectStaging);
self.pipeData.deinit(self.allocator, self.gc);
self.debugDraws.deinit(self.allocator);
for (&self.sharedData) |*shared| {
shared.deinit(self.allocator);
}
core.graphics_logs("shutting down debug draw system");
}
pub fn deinit(self: *@This()) void {
self.shutdown();
self.allocator.destroy(self);
}
};
var gDebugDrawSys: *DebugDrawSubsystem = undefined;
pub fn init_debug_draw_subsystem() !void {
gDebugDrawSys = try core.gEngine.createObject(DebugDrawSubsystem, .{ .can_tick = true });
try gDebugDrawSys.prepareSubsystem(graphics.getContext());
try graphics.registerRendererPlugin(gDebugDrawSys);
try core.installDebugDrawInterface(gDebugDrawSys.allocator, .{
.debugSphereFn = debugSphere,
.debugBoxFn = debugBox,
.debugLineFn = debugLine,
});
}
pub fn shutdown() void {}
const DebugDrawParams = core.DebugDrawParams;
pub fn debugSphere(position: core.Vectorf, radius: f32, params: DebugDrawParams) void {
gDebugDrawSys.debugDraws.push(.{
.primitive = .{ .sphere = .{ .position = position, .radius = radius, .rotation = params.rotation } },
.color = params.color,
.duration = params.duration,
}) catch return;
}
pub fn debugLine(start: core.Vectorf, end: core.Vectorf, params: DebugDrawParams) void {
gDebugDrawSys.debugDraws.push(.{
.primitive = .{ .line = .{
.start = start,
.end = end,
} },
.color = params.color,
.duration = params.duration,
}) catch return;
}
pub fn debugBox(position: core.Vectorf, extents: core.Vectorf, params: DebugDrawParams) void {
gDebugDrawSys.debugDraws.push(.{
.primitive = .{
.box = .{ .position = position, .extents = extents, .rotation = params.rotation },
},
.color = params.color,
.duration = params.duration,
}) catch return;
}

View File

@ -0,0 +1,307 @@
// this folder contains
const std = @import("std");
const root = @import("root");
const bl = root.backlog;
const vk = @import("vulkan");
const core = @import("core");
const graphics = @import("graphics.zig");
const vkinit = graphics.vkinit;
const vma = @import("vma");
const NeonVkContext = graphics.NeonVkContext;
const NeonVkBuffer = graphics.NeonVkBuffer;
const NeonVkAllocator = graphics.NeonVkAllocator;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
// so.. given a single descriptor set:
// 1. create builder
// 2. add buffers for data templates
// 3. finalize and build.
// maybe a better way of doing this:
// NeonGpuObjectBuilder and NeonGpuObject are an abstraction + automation of
// vk.DescriptorSet + vk.Buffer and a way to map them.
// var builder = graphics.NeonGpuObjectBuilder.init(allocator);
// builder.addBuffer(SpriteDataGpu, .objectStorageBuffer);
// builder.addBuffer(CameraDataGpu, .uniform);
// var gpuObject: NeonGpuObject = builder.build();
// TODO: add a way to unmap multiple buffers.. it has been months now. I have no idea what i meant by this.
// ---- Proposed API for implemeting extensions into the game ---
// a GpuPipeData is an API that exists as an API that abstracts both
// vulkan buffer allocation and mapping
pub fn GpuMappingData(comptime ObjectType: type) type {
return struct {
raw: GpuMappingRaw,
objects: []ObjectType, //WARNING! theres a bug do not use this with square operator unless it's a type that's a power of 2
trueObjectSize: usize,
pub fn unmap(self: *@This(), gc: *NeonVkContext) void {
self.raw.unmap(gc);
}
};
}
pub const GpuMappingRaw = struct {
data: []u8,
allocation: vma.Allocation,
pub fn unmap(self: *@This(), gc: *NeonVkContext) void {
gc.vkAllocator.vmaAllocator.unmapMemory(self.allocation);
}
};
pub const GpuPipeDataBinding = struct {
// one slot per frame
buffers: []NeonVkBuffer,
objectCount: usize,
objectSize: usize,
frameCount: usize,
isFrameBuffer: bool = true,
pub fn mapBuffers(self: *@This(), gc: *NeonVkContext, comptime MappingType: type) ![]GpuMappingData(MappingType) {
var frameIndex: usize = 0;
if (self.isFrameBuffer) {
try core.assertf(self.frameCount == self.buffers.len, "mismatched frameBuffer {d} != {d}", .{ self.frameCount, self.buffers.len });
}
// maps buffers for these bindings, one for each frame
var rv = try gc.allocator.alloc(GpuMappingData(MappingType), self.buffers.len);
while (frameIndex < self.buffers.len) : (frameIndex += 1) {
const data = try gc.vkAllocator.vmaAllocator.mapMemory(self.buffers[frameIndex].allocation, MappingType);
var mapping: []MappingType = undefined;
mapping.ptr = @as([*]MappingType, @ptrCast(data));
mapping.len = self.objectCount;
var dataMapping: []u8 = undefined;
dataMapping.ptr = @as([*]u8, @ptrCast(data));
dataMapping.len = self.objectCount;
const gpuMappingData: GpuMappingData(MappingType) = .{
.objects = mapping,
.trueObjectSize = self.objectSize,
.raw = .{ .data = dataMapping, .allocation = self.buffers[frameIndex].allocation },
};
rv[frameIndex] = gpuMappingData;
}
return rv;
}
pub fn deinit(self: *@This(), vkAllocator: *NeonVkAllocator) void {
for (self.buffers) |*buffers| {
buffers.deinit(vkAllocator);
}
}
};
// High level pipe controls for a gpu data pipe
pub const GpuPipeData = struct {
allocator: std.mem.Allocator,
descriptorSetLayout: vk.DescriptorSetLayout,
bindings: []GpuPipeDataBinding,
descriptorSets: []vk.DescriptorSet, // one per frame
descriptorSetLayoutIsAllocated: bool = false,
pub fn getDescriptorSet(self: @This(), frameIndex: usize) [*]const vk.DescriptorSet {
return @as([*]const vk.DescriptorSet, @ptrCast(&self.descriptorSets[frameIndex]));
}
pub fn init(allocator: std.mem.Allocator, bindingCount: usize, frameCount: usize) !@This() {
const self = GpuPipeData{
.descriptorSetLayout = undefined,
.bindings = try allocator.alloc(GpuPipeDataBinding, bindingCount),
.descriptorSets = try allocator.alloc(vk.DescriptorSet, frameCount),
.allocator = allocator,
};
for (self.bindings) |*binding| {
binding.buffers = try allocator.alloc(NeonVkBuffer, frameCount);
binding.frameCount = frameCount;
}
return self;
}
// Maps each buffer per frame
pub fn mapBuffers(self: *@This(), gc: *NeonVkContext, comptime ObjectType: type, binding: usize) ![]GpuMappingData(ObjectType) {
var pipeDataBuffer = self.bindings[binding];
return pipeDataBuffer.mapBuffers(gc, ObjectType);
}
// pub fn unmapAll(self: *@This(), mappings: anytype);
pub fn deinit(self: *@This(), allocator: std.mem.Allocator, gc: *NeonVkContext) void {
if (self.descriptorSetLayoutIsAllocated) {
gc.vkd.destroyDescriptorSetLayout(gc.dev, self.descriptorSetLayout, null);
}
for (self.bindings) |*binding| {
binding.deinit(gc.vkAllocator);
allocator.free(binding.buffers);
}
allocator.free(self.descriptorSets);
allocator.free(self.bindings);
}
};
pub const BindingMode = enum { uniform, storageBuffer };
pub const GpuPipeDataBuilder = struct {
const BindingObjectInfo = struct {
objectCount: usize,
finalObjectSize: usize,
trueObjectSize: usize,
bindingMode: BindingMode,
};
gc: *NeonVkContext,
allocator: std.mem.Allocator,
currentBinding: u32 = 0,
frameCount: usize = graphics.constants.NUM_FRAMES,
objectCount: usize = graphics.constants.MAX_OBJECTS,
bindings: ArrayListUnmanaged(vk.DescriptorSetLayoutBinding) = .{},
bindingObjectInfos: ArrayListUnmanaged(BindingObjectInfo) = .{},
pub fn init(allocator: std.mem.Allocator, gc: *NeonVkContext) @This() {
const self = GpuPipeDataBuilder{
.allocator = allocator,
.gc = gc,
};
return self;
}
pub fn setObjectCount(
self: *@This(),
count: usize,
) void {
self.objectCount = count;
}
pub fn addBufferBinding(
self: *@This(),
comptime BindingType: type,
descriptorType: vk.DescriptorType,
stageFlags: vk.ShaderStageFlags,
bindingMode: BindingMode,
) !void {
var gc = self.gc;
const binding = vkinit.descriptorSetLayoutBinding(descriptorType, stageFlags, self.currentBinding);
// core.graphics_log("builder adding additional binding {any} {any} objectSize = {d}", .{ descriptorType, stageFlags, @sizeOf(BindingType) });
try self.bindings.append(self.allocator, binding);
var objCount: usize = 1;
// todo: there is a bug here because this code is incomplete this only accounts for storage buffers and uniforms
if (descriptorType == .storage_buffer) {
objCount = self.objectCount;
}
var bindingObjectInfo: BindingObjectInfo = .{
.objectCount = objCount,
.finalObjectSize = @sizeOf(BindingType),
.trueObjectSize = @sizeOf(BindingType),
.bindingMode = bindingMode,
};
// uniforms require that the buffer object gets padded to the correct size.
if (descriptorType != .storage_buffer) {
bindingObjectInfo.finalObjectSize = gc.pad_uniform_buffer_size(bindingObjectInfo.finalObjectSize);
// core.engine_log("final object size has been padded: {d}", .{bindingObjectInfo.finalObjectSize});
} else {
var trueSize: usize = 1;
while (trueSize < bindingObjectInfo.finalObjectSize) {
trueSize *= 2;
}
bindingObjectInfo.finalObjectSize = trueSize;
// core.engine_log("final object size has been padded as storage: {d}", .{bindingObjectInfo.finalObjectSize});
}
try self.bindingObjectInfos.append(self.allocator, bindingObjectInfo);
self.currentBinding += 1;
}
pub fn build(self: *@This(), comptime buildName: []const u8) !GpuPipeData {
var rv = try GpuPipeData.init(self.allocator, self.bindings.items.len, self.frameCount);
var gc: *NeonVkContext = self.gc;
var setInfo = vk.DescriptorSetLayoutCreateInfo{ .binding_count = @as(u32, @intCast(self.bindings.items.len)), .flags = .{}, .p_bindings = self.bindings.items.ptr };
rv.descriptorSetLayout = try gc.vkd.createDescriptorSetLayout(gc.dev, &setInfo, null);
rv.descriptorSetLayoutIsAllocated = true;
// core.graphics_log("finalizing build creating descriptor set layout at 0x{x} buildName: {s}", .{ @intFromEnum(rv.descriptorSetLayout), buildName });
for (rv.descriptorSets, 0..) |_, frameId| {
var descriptorAllocInfo = vk.DescriptorSetAllocateInfo{
.descriptor_pool = gc.descriptorPool,
.descriptor_set_count = 1,
.p_set_layouts = @ptrCast(&rv.descriptorSetLayout),
};
try gc.vkd.allocateDescriptorSets(gc.dev, &descriptorAllocInfo, @as([*]vk.DescriptorSet, @ptrCast(&rv.descriptorSets[frameId])));
}
var bindingId: usize = 0;
while (bindingId < self.bindings.items.len) : (bindingId += 1) {
const binding = &rv.bindings[bindingId];
const bindingInfo: BindingObjectInfo = self.bindingObjectInfos.items[bindingId];
// core.graphics_log("allocating {d} frame buffers for binding {d} buffer size = {d} object size = {d}", .{ binding.buffers.len, bindingId, bindingInfo.finalObjectSize * bindingInfo.objectCount, bindingInfo.finalObjectSize });
for (binding.buffers, 0..) |*buffer, frameId| {
var usageFlags: vk.BufferUsageFlags = .{};
var memoryFlags: vma.MemoryUsage = .unknown;
var descriptorType: vk.DescriptorType = .sampler;
switch (bindingInfo.bindingMode) {
.uniform => {
usageFlags.uniform_buffer_bit = true;
memoryFlags = .cpuToGpu;
descriptorType = .uniform_buffer;
},
.storageBuffer => {
usageFlags.storage_buffer_bit = true;
memoryFlags = .cpuToGpu;
descriptorType = .storage_buffer;
},
}
buffer.* = try gc.create_buffer(
bindingInfo.finalObjectSize * bindingInfo.objectCount,
usageFlags,
memoryFlags,
"GPU binding buffer creation " ++ @src().fn_name ++ ": " ++ buildName,
);
var bufferInfo = vk.DescriptorBufferInfo{
.buffer = buffer.buffer,
.offset = 0,
.range = bindingInfo.finalObjectSize * bindingInfo.objectCount,
};
var descriptorWrite = vkinit.writeDescriptorSet(
descriptorType,
rv.descriptorSets[frameId],
&bufferInfo,
@as(u32, @intCast(bindingId)),
);
gc.vkd.updateDescriptorSets(gc.dev, 1, @ptrCast(&descriptorWrite), 0, undefined);
}
}
return rv;
}
pub fn deinit(self: *@This()) void {
self.bindings.deinit(self.allocator);
self.bindingObjectInfos.deinit(self.allocator);
}
};

View File

@ -0,0 +1,196 @@
const core = @import("core");
const assets = @import("assets");
const std = @import("std");
const memory = core.MemoryTracker;
pub const ozz = @import("ozz");
const texture_cooking = @import("cooking/texture_cooking.zig");
const mesh_cooking = @import("cooking/mesh_cooking.zig");
pub const vk_renderer = @import("vk_renderer.zig");
const materials = @import("materials.zig");
pub usingnamespace @import("debug_draws.zig");
pub const gpu_pipe_data = @import("gpu_pipe_data.zig");
pub const BoneHandle = animation_system.BoneHandle;
pub const SkyboxSystem = @import("skybox.zig");
pub const setSkybox = SkyboxSystem.setSkybox;
pub const animation_system = @import("animation/animationSystem.zig");
pub const AnimationSystem = animation_system.AnimationSystem;
pub const Animator = animation_system.Animator;
pub const AnimationTrack = animation_system.AnimationTrack;
pub const Skeleton = animation_system.Skeleton;
pub const animation_resolver = @import("animation/animResolver.zig");
pub const AnimResolverRef = animation_resolver.AnimResolverRef;
pub const AnimResolverInterface = animation_resolver.AnimResolverInterface;
pub const SingleAnimationResolver = animation_resolver.SingleAnimationResolver;
pub const BlenderList = animation_resolver.BlenderList;
pub const AnimSampler = animation_resolver.AnimSampler;
const vk_cubemap = @import("vk_renderer/vk_cubemap.zig");
pub const CubeMapDirs = vk_cubemap.CubeMapDirs;
pub const MakeCubeMapList = vk_cubemap.MakeCubeMapList;
pub const animation_loaders = @import("animation/loaders.zig");
pub const RenderThread = @import("vk_renderer/RenderThread.zig");
pub const vkinit = @import("vk_init.zig");
pub const vk_allocator = @import("vk_allocator.zig");
pub const NeonVkAllocator = vk_allocator.NeonVkAllocator;
pub const NeonVkPipelineBuilder = vk_renderer.NeonVkPipelineBuilder;
pub const NeonVkContext = vk_renderer.NeonVkContext;
pub const constants = @import("vk_constants.zig");
pub const NeonVkImage = vk_renderer.NeonVkImage;
pub const Material = materials.Material;
pub const RendererInterfaceRef = vk_renderer.RendererInterfaceRef;
pub const RendererInterface = vk_renderer.RendererInterface;
pub const texture = @import("texture.zig");
pub const debug_draw = @import("debug_draws.zig");
pub const mesh = @import("mesh.zig");
pub const Mesh = mesh.Mesh;
pub const DynamicMesh = mesh.DynamicMesh;
pub const IndexBuffer = mesh.IndexBuffer;
pub const Texture = texture.Texture;
pub const MeshVertex = mesh.MeshVertex;
pub const mesh_pool = @import("vk_renderer/vk_mesh_pool.zig");
pub const MeshSourceType = mesh_pool.MeshSourceType;
pub const loadIndexedMeshForPooling = mesh_pool.loadIndexedMeshForPooling;
pub const getMeshPoolBuffers = mesh_pool.getMeshPoolBuffers;
pub const getIndexedMeshByName = mesh_pool.getIndexedMeshByName;
// pub const DynamicTexture = @import("dynamic_texture/DynamicTexture.zig");
pub const vk_util = @import("vk_utils.zig");
pub const createAndInstallTextureFromPixels = vk_util.createAndInstallTextureFromPixels;
const vk_api = @import("../vk_api.zig");
pub const vkd = &vk_api.vkd;
pub const vki = &vk_api.vki;
pub const vkb = &vk_api.vkb;
pub const PixelBufferRGBA8 = @import("PixelBufferRGBA8.zig");
pub const vk_assetLoaders = @import("vk_assetLoaders.zig");
pub const PixelPos = vk_renderer.PixelPos;
pub const NeonVkBuffer = vk_renderer.NeonVkBuffer;
pub const NumFrames = constants.NUM_FRAMES;
const engine_logs = core.engine_logs;
const engine_log = core.engine_log;
pub fn getContext() *NeonVkContext {
return vk_renderer.gContext;
}
pub usingnamespace @import("vk_renderer/vk_renderer_types.zig");
pub const render_objects = @import("render_objects.zig");
pub const Camera = render_objects.Camera;
pub const StaticMesh = render_objects.StaticMesh;
pub const IndexedMesh = mesh_pool.IndexedMesh;
pub fn registerRendererPlugin(value: anytype) !void {
const ref = RendererInterfaceRef{
.ptr = value,
.vtable = &@TypeOf(value.*).RendererInterfaceVTable,
};
var gc = getContext();
try gc.rendererPlugins.append(gc.allocator, ref);
}
var gCooking: bool = false;
const primitives = [_]assets.AssetImportReference{
assets.MakeImportRef("Mesh", "m_primitive_sphere", "meshes/primitive_sphere.obj"),
assets.MakeImportRef("Mesh", "m_primitive_box", "meshes/primitive_box.obj"),
assets.MakeImportRef("Mesh", "m_primitive_line", "meshes/primitive_line.obj"),
assets.MakeImportRef("Mesh", "m_skybox", "meshes/skybox.obj"),
};
pub fn start_module(comptime programSpec: anytype, args: anytype, allocator: std.mem.Allocator) !void {
_ = args;
if (!core.isUtility()) {
engine_logs("graphics module starting up...");
const context: *NeonVkContext = core.gEngine.createObject(
NeonVkContext,
.{ .can_tick = true, .isCore = true },
) catch unreachable;
vk_renderer.gContext = context;
const as = try core.createObject(AnimationSystem, .{ .can_tick = false });
try animation_loaders.initLoaders();
try registerRendererPlugin(as);
vk_assetLoaders.init_loaders(allocator) catch unreachable;
try assets.loadList(primitives);
debug_draw.init_debug_draw_subsystem() catch unreachable;
context.skybox = SkyboxSystem.create(context.allocator) catch return core.EngineDataEventError.UnknownStatePanic;
}
if (@hasField(@TypeOf(programSpec), "cooking")) {
gCooking = true;
try texture_cooking.initCooker(allocator);
try mesh_cooking.initCooker(allocator);
}
}
pub fn shutdown_module(allocator: std.mem.Allocator) void {
_ = allocator;
if (gCooking) {
mesh_cooking.deinitCooker();
texture_cooking.deinitCooker();
}
if (!core.isUtility()) {
engine_logs("graphics module shutting down...");
vk_renderer.gContext.shutdown();
}
}
pub var icon: []const u8 = "content/textures/icon.png";
pub fn setStartupSettings(comptime field: []const u8, value: anytype) void {
@field(vk_renderer.gGraphicsStartupSettings, field) = value;
}
pub fn getStartupSettings() *const @TypeOf(vk_renderer.gGraphicsStartupSettings) {
return &vk_renderer.gGraphicsStartupSettings;
}
pub fn loadSpv(allocator: std.mem.Allocator, path: []const u8) ![]const u32 {
core.engine_log("loading path {s}", .{path});
const search_prefixes: []const []const u8 = &.{
"zig-out/shaders",
"shaders",
};
var s_path: [4096]u8 = undefined;
for (search_prefixes) |prefix| {
const s = try std.fmt.bufPrint(&s_path, "{s}/{s}", .{ prefix, path });
var file = std.fs.cwd().openFile(s, .{ .mode = .read_only }) catch continue;
const filesize = (try file.stat()).size;
const buffer: []u8 = try allocator.alignedAlloc(u8, 4, filesize);
try file.reader().readNoEof(buffer);
var rv: []u32 = undefined;
rv.ptr = @as([*]u32, @ptrCast(@alignCast(buffer.ptr)));
rv.len = buffer.len / 4;
return rv;
}
return error.FileNotFound;
}
pub const Module = core.ModuleDescription{
.name = "graphics",
.enabledByDefault = true,
};

View File

@ -0,0 +1,52 @@
const std = @import("std");
const vk = @import("vulkan");
const core = @import("core");
const VkConstants = @import("vk_constants.zig");
const meshes = @import("mesh.zig");
const NeonVkContext = @import("vk_renderer.zig").NeonVkContext;
const vk_pipeline = @import("vk_pipeline.zig");
const NeonVkPipelineBuilder = vk_pipeline.NeonVkPipelineBuilder;
const EulerAngles = core.EulerAngles;
const Mat = core.Mat;
const Vectorf = core.Vectorf;
const Quat = core.Quat;
const zm = core.zm;
const mul = zm.mul;
pub const Material = struct {
materialName: core.Name,
textureSet: vk.DescriptorSet = .null_handle,
pipeline: vk.Pipeline,
layout: vk.PipelineLayout,
pub fn deinit(self: *Material, ctx: *NeonVkContext) void {
ctx.vkd.destroyPipeline(ctx.dev, self.pipeline, null);
ctx.vkAllocator.destroyPipelineLayout(ctx.dev, self.layout);
}
};
pub const MaterialBuilder = struct {
const Self = @This();
allocator: std.mem.Allocator,
ctx: *NeonVkContext,
pipelineBuilder: NeonVkPipelineBuilder,
pub fn init(ctx: *NeonVkContext) MaterialBuilder {
const self = MaterialBuilder{
.allocator = ctx.allocator,
.ctx = ctx,
};
return self;
}
pub fn build(self: *Self) !void {
_ = self;
// try self.ctx.add_material();
}
pub fn deinit(self: *Self) void {
_ = self;
}
};

View File

@ -0,0 +1,303 @@
const std = @import("std");
const core = @import("core");
const vk_renderer = @import("vk_renderer.zig");
const vma = @import("vma");
const vk = @import("vulkan");
const obj_loader = @import("objLoader");
const constants = @import("vk_constants.zig");
const vk_utils = @import("vk_utils.zig");
const vk_dynamic_mesh = @import("vk_dynamic_mesh.zig");
const NeonVkUploader = vk_utils.NeonVkUploader;
const NeonVkBuffer = vk_renderer.NeonVkBuffer;
const ObjMesh = obj_loader.ObjMesh;
const ArrayList = std.ArrayList;
const Vectorf = core.Vectorf;
const Vector2f = core.Vector2f;
const Color = core.colors.Color;
const NeonVkContext = vk_renderer.NeonVkContext;
const debug_struct = core.debug_struct;
pub const DynamicMesh = vk_dynamic_mesh.DynamicMesh;
pub const DynamicMeshManager = vk_dynamic_mesh.DynamicMeshManager;
pub const MeshVertex = extern struct {
position: Vectorf = .{},
normal: Vectorf = .{},
color: Color = .{},
uv: Vector2f = .{},
bones: [4]u8 = .{ 0, 0, 0, 0 },
weights: [4]u8 = .{ 0, 0, 0, 0 },
};
pub const IndexBuffer = struct {
buffer: NeonVkBuffer,
indices: []const u32,
allocator: std.mem.Allocator,
pub fn uploadIndexBuffer(gc: *NeonVkContext, indices: []const u32, allocator: std.mem.Allocator) !@This() {
var self = @This(){
.buffer = undefined,
.indices = indices,
.allocator = allocator,
};
self.indices = try allocator.dupe(u32, indices);
const bufferSize = indices.len * @sizeOf(u32);
const bci = vk.BufferCreateInfo{
.flags = .{},
.size = bufferSize,
.usage = .{ .transfer_src_bit = true },
.sharing_mode = .exclusive,
.queue_family_index_count = 0,
.p_queue_family_indices = undefined,
};
const vmaCreateInfo = vma.AllocationCreateInfo{
.flags = .{},
.usage = .cpuOnly,
};
var stagingBuffer = try gc.vkAllocator.createBuffer(bci, vmaCreateInfo, @src().fn_name ++ " - upload buffer");
defer stagingBuffer.deinit(gc.vkAllocator);
{
const data = try gc.vkAllocator.vmaAllocator.mapMemory(stagingBuffer.allocation, u8);
var dataSlice: []u8 = undefined;
dataSlice.ptr = data;
dataSlice.len = bufferSize;
var iSlice: []const u8 = undefined;
iSlice.ptr = @as([*]const u8, @ptrCast(indices.ptr));
iSlice.len = bufferSize;
@memcpy(dataSlice, iSlice);
gc.vkAllocator.vmaAllocator.unmapMemory(stagingBuffer.allocation);
}
// GPU sided buffer
const gpuBci = vk.BufferCreateInfo{
.flags = .{},
.size = bufferSize,
.usage = .{ .transfer_dst_bit = true, .index_buffer_bit = true },
.sharing_mode = .exclusive,
.queue_family_index_count = 0,
.p_queue_family_indices = undefined,
};
const gpuVmaCreateInfo = vma.AllocationCreateInfo{
.flags = .{},
.usage = .gpuOnly,
};
self.buffer = try gc.vkAllocator.createBuffer(gpuBci, gpuVmaCreateInfo, @src().fn_name ++ " - gpu buffer");
//try gc.start_upload_context(&gc.uploadContext);
try gc.uploader.startUploadContext();
{
var copy = vk.BufferCopy{
.dst_offset = 0,
.src_offset = 0,
.size = bufferSize,
};
const cmd = gc.uploader.commandBuffer;
// core.graphics_log("Starting command copy buffer", .{});
gc.vkd.cmdCopyBuffer(
cmd,
stagingBuffer.buffer,
self.buffer.buffer,
1,
@as([*]const vk.BufferCopy, @ptrCast(&copy)),
);
}
//try gc.finish_upload_context(&gc.uploadContext);
try gc.uploader.finishUploadContext();
return self;
}
pub fn deinit(self: *@This(), gc: *NeonVkContext) void {
self.buffer.deinit(gc.vkAllocator);
self.allocator.free(self.indices);
}
};
pub fn loadObjMeshVertices(vertices: *ArrayList(MeshVertex), mesh: ObjMesh) !void {
try mesh.validate_mesh();
try vertices.ensureTotalCapacity(mesh.v_faces.items.len * 3);
for (mesh.v_faces.items) |face| {
if (face.count == 3) {
var i: u32 = 0;
while (i < 3) : (i += 1) {
const v = vertexFromFaceOffset(mesh, face, i);
try vertices.append(v);
}
} else if (face.count == 4) {
const vx = [_]MeshVertex{
vertexFromFaceOffset(mesh, face, 0),
vertexFromFaceOffset(mesh, face, 1),
vertexFromFaceOffset(mesh, face, 2),
vertexFromFaceOffset(mesh, face, 2),
vertexFromFaceOffset(mesh, face, 3),
vertexFromFaceOffset(mesh, face, 0),
};
try vertices.appendSlice(vx[0..]);
}
}
}
fn vertexFromFaceOffset(mesh: ObjMesh, face: obj_loader.ObjFace, offset: u32) MeshVertex {
const p = mesh.v_positions.items[face.vertex[offset] - 1];
const n = mesh.v_normals.items[face.normal[offset] - 1];
const u = mesh.v_uvs.items[face.texture[offset] - 1];
const v = MeshVertex{
.position = .{ .x = p.x, .y = p.y, .z = p.z },
.normal = .{ .x = n.x, .y = n.y, .z = n.z },
.color = .{ .r = n.x, .g = n.y, .b = n.z, .a = 1.0 },
.uv = .{ .x = u.x, .y = 1 - u.y },
};
return v;
}
// legacy, don't use
pub const Mesh = struct {
vertices: ArrayList(MeshVertex),
buffer: NeonVkBuffer,
allocator: std.mem.Allocator,
pub fn init(context: *NeonVkContext, allocator: std.mem.Allocator) Mesh {
const self = Mesh{
.vertices = ArrayList(MeshVertex).init(allocator),
.buffer = undefined,
.allocator = allocator,
};
_ = context;
return self;
}
pub fn upload(self: *Mesh, ctx: *NeonVkContext) !void {
try ctx.stage_and_push_mesh(self);
}
pub fn loadFromObjFileCooked(self: *Mesh, fileName: []const u8) !void {
const mapping = try core.fs().loadFile(fileName);
defer core.fs().unmap(mapping);
const s = @sizeOf(MeshVertex);
var i: usize = 0;
var vertexOffset: usize = 0;
try self.vertices.resize(1 + mapping.bytes.len / s);
while (i < mapping.bytes.len) : (i += s) {
self.vertices.items[vertexOffset] = @as(*const MeshVertex, @ptrCast(@alignCast(mapping.bytes.ptr + i))).*;
vertexOffset += 1;
}
}
pub fn load_from_obj_file(self: *Mesh, fileName: []const u8) !void {
const mapping = try core.fs().loadFile(fileName);
defer core.fs().unmap(mapping);
var fileObjs = try obj_loader.loadObjBytes(mapping.bytes, self.allocator);
defer fileObjs.deinit();
if (fileObjs.meshes.items.len > 0) {
// default grabbing shape zero
core.graphics_log("loading mesh: {s}", .{fileName});
// fileObjs.meshes.items[0].print_stats();
// try self.load_from_obj_mesh(fileObjs.meshes.items[0]);
try loadObjMeshVertices(&self.vertices, fileObjs.meshes.items[0]);
}
core.graphics_log("mesh loaded with {d} vertices size {d}", .{ self.vertices.items.len, self.vertices.items.len * @sizeOf(MeshVertex) });
}
pub fn deinit(self: *Mesh, ctx: *NeonVkContext) void {
self.buffer.deinit(ctx.vkAllocator);
self.vertices.deinit();
}
};
pub const VertexInputDescription = struct {
bindings: ArrayList(vk.VertexInputBindingDescription),
attributes: ArrayList(vk.VertexInputAttributeDescription),
flags: vk.PipelineVertexInputStateCreateFlags = .{},
pub fn init(allocator: std.mem.Allocator) !VertexInputDescription {
var self = VertexInputDescription{
.bindings = ArrayList(vk.VertexInputBindingDescription).init(allocator),
.attributes = ArrayList(vk.VertexInputAttributeDescription).init(allocator),
};
try self.bindings.append(.{
.binding = 0,
.stride = @sizeOf(MeshVertex),
.input_rate = .vertex,
});
//debug_struct("bindings 0", self.bindings.items[0]);
// position
try self.attributes.append(.{
.binding = 0,
.location = 0,
.format = .r32g32b32_sfloat,
.offset = @offsetOf(MeshVertex, "position"),
});
//debug_struct("attributes 0", self.attributes.items[0]);
// normal
try self.attributes.append(.{
.binding = 0,
.location = 1,
.format = .r32g32b32_sfloat,
.offset = @offsetOf(MeshVertex, "normal"),
});
//debug_struct("attributes 0", self.attributes.items[1]);
// color
try self.attributes.append(.{
.binding = 0,
.location = 2,
.format = .r32g32b32a32_sfloat,
.offset = @offsetOf(MeshVertex, "color"),
});
//debug_struct("attributes 0", self.attributes.items[2]);
try self.attributes.append(.{
.binding = 0,
.location = 3,
.format = .r32g32_sfloat,
.offset = @offsetOf(MeshVertex, "uv"),
});
try self.attributes.append(.{
.binding = 0,
.location = 4,
.format = .a8b8g8r8_uint_pack32,
.offset = @offsetOf(MeshVertex, "bones"),
});
try self.attributes.append(.{
.binding = 0,
.location = 5,
.format = .a8b8g8r8_uint_pack32,
.offset = @offsetOf(MeshVertex, "weights"),
});
return self;
}
pub fn deinit(self: *@This()) void {
self.bindings.deinit();
self.attributes.deinit();
}
};

View File

@ -0,0 +1,316 @@
const std = @import("std");
const vk = @import("vulkan");
const resources = @import("resources");
const core = @import("core");
const VkConstants = @import("vk_constants.zig");
const graphics = @import("graphics.zig");
const meshes = @import("mesh.zig");
const NeonVkContext = @import("vk_renderer.zig").NeonVkContext;
const materials = @import("materials.zig");
const animationSystem = @import("animation/animationSystem.zig");
const Animator = animationSystem.Animator;
const Material = materials.Material;
const EulerAngles = core.EulerAngles;
const Mat = core.Mat;
const Vectorf = core.Vectorf;
const Quat = core.Quat;
const zm = core.zm;
const mul = zm.mul;
const Mesh = meshes.Mesh;
const mesh_pool = @import("vk_renderer/vk_mesh_pool.zig");
const IndexedMesh = mesh_pool.IndexedMesh;
// lol we need to rename this thing again, it should be called RenderMesh
pub const StaticMeshSet = core.SparseSet(StaticMesh);
pub const StaticMesh = struct {
const Self = @This();
mesh: ?IndexedMesh = null,
// texture: ?vk.DescriptorSet = null,
textureId: ?u32 = null,
transform: core.Mat = core.zm.translation(0, 0, 0),
visibility: bool = true,
// new position and rotator based api
position: Vectorf = .{},
rotation: Quat = .{ 0, 0, 0, 1 },
scale: Vectorf = .{ .x = 1, .y = 1, .z = 1 },
textureName: core.Name = core.NameInvalid,
meshName: core.Name = core.NameInvalid,
animated: bool = false, // todo remove
animator: ?*Animator = null,
flags: Flags0 = .{},
pub var BaseContainer: *StaticMeshSet = undefined;
pub const ComponentName = "StaticMesh";
pub const ScriptExports: []const []const u8 = &.{
"applyRelativeRotationX",
"applyRelativeRotationY",
"applyRelativeRotationZ",
"setMesh",
"setTextureByName",
};
pub const Flags0 = packed struct(u32) {
alwaysInFront: bool = false,
useAltFov: bool = false,
_pad: u30 = 0,
};
pub fn setMeshByName(self: *@This(), meshName: core.Name) void {
self.meshName = meshName;
}
// script function
pub fn setMesh(self: *@This(), meshName: []const u8) void {
const name = core.MakeName(meshName);
self.mesh = graphics.getIndexedMeshByName(core.MakeName(meshName));
self.meshName = name;
}
pub fn fromTransform(transform: core.Mat) Self {
var self = Self{
.mesh = null,
.transform = transform,
.position = undefined,
.rotation = undefined,
.scale = undefined,
};
self.updateScalars();
return self;
}
pub fn setTexture(self: *Self, textureName: []const u8) void {
var name = core.MakeName(textureName);
self.textureId = graphics.getContext().textureIds.get(name.handle());
self.textureName = name;
}
pub fn setTextureByName(self: *Self, _name: core.Name) void {
var name = _name;
self.textureId = graphics.getContext().textureIds.get(name.handle());
self.textureName = name;
}
pub fn updateTexture(self: *@This(), gc: *NeonVkContext) void {
self.textureId = gc.textureIds.get(self.textureName.handle());
}
pub fn applyTransform(self: *StaticMesh, transform: core.Mat) void {
self.transform = core.zm.mul(self.transform, transform);
self.updateScalars();
}
pub fn applyRelativeRotationX(self: *StaticMesh, angle: f32) void {
var imat = core.zm.identity();
imat[0][3] = -self.transform[0][3];
imat[1][3] = -self.transform[1][3];
imat[2][3] = -self.transform[2][3];
const rmat = core.zm.identity();
imat[0][3] = self.transform[0][3];
imat[1][3] = self.transform[1][3];
imat[2][3] = self.transform[2][3];
var newTransform = core.zm.mul(imat, self.transform);
newTransform = core.zm.mul(core.zm.rotationX(angle), newTransform);
newTransform = core.zm.mul(rmat, newTransform);
self.transform = newTransform;
}
pub fn applyRelativeRotationZ(self: *StaticMesh, angle: f32) void {
var imat = core.zm.identity();
imat[0][3] = -self.transform[0][3];
imat[1][3] = -self.transform[1][3];
imat[2][3] = -self.transform[2][3];
const rmat = core.zm.identity();
imat[0][3] = self.transform[0][3];
imat[1][3] = self.transform[1][3];
imat[2][3] = self.transform[2][3];
var newTransform = core.zm.mul(imat, self.transform);
newTransform = core.zm.mul(core.zm.rotationZ(angle), newTransform);
newTransform = core.zm.mul(rmat, newTransform);
self.transform = newTransform;
}
pub fn applyRelativeRotationY(self: *StaticMesh, angle: f32) void {
var imat = core.zm.identity();
imat[0][3] = -self.transform[0][3];
imat[1][3] = -self.transform[1][3];
imat[2][3] = -self.transform[2][3];
const rmat = core.zm.identity();
imat[0][3] = self.transform[0][3];
imat[1][3] = self.transform[1][3];
imat[2][3] = self.transform[2][3];
var newTransform = core.zm.mul(imat, self.transform);
newTransform = core.zm.mul(core.zm.rotationY(angle), newTransform);
newTransform = core.zm.mul(rmat, newTransform);
self.transform = newTransform;
}
pub fn updateScalars(self: *StaticMesh) void {
self.position = Vectorf.fromZm(mul(self.transform, Vectorf.new(0.0, 0.0, 0.0).toZm()));
self.rotation = zm.matToQuat(self.transform);
self.scale = core.matToScalef(self.transform);
}
pub fn applyScalars(self: *StaticMesh) void {
var newTransform = core.zm.mul(
core.zm.scalingV(self.scale.toZm()),
core.zm.matFromQuat(self.rotation),
);
newTransform = core.zm.mul(
newTransform,
core.zm.translationV(self.position.toZm()),
);
self.transform = newTransform;
}
pub fn initECS(self: *@This(), handle: core.ObjectHandle) void {
const entity = core.Entity.fromHandle(handle);
if (entity.get(core.Scene)) |scene| {
self.position = scene.getPosition();
self.rotation = scene.getRotation().quat;
self.scale = scene.getScaleV();
} else {
_ = entity.addComponent(core.Scene);
}
}
};
fn makePerspective(fov: f32, aspect: f32, near: f32, far: f32) Mat {
const proj = core.zm.perspectiveFovRh(
core.radians(fov),
aspect,
near,
far,
);
// proj[1][1] *= -1;
return proj;
}
// Camera coordinate system:
//
// from you as a user, staring at the screen:
//
// This is a right handed coordinate system
//
// forward = +Z (index finger)
// left = +X (middle finger)
// up = +Y (thumb)
const ecs = core.ecs;
pub const Camera = struct {
fov: f32 = 70.0,
altFov: f32 = 70.0,
aspect: f32 = 16.0 / 9.0,
near_clipping: f32 = 0.1,
far_clipping: f32 = 10000.0,
position: Vectorf = Vectorf{ .x = 0.0, .y = 0.0, .z = 0.0 },
rotation: Quat, // todo, remove, we only work with euler tracks now for camera.
// applied in that order,
yaw: f32 = 0,
pitch: f32 = 0,
roll: f32 = 0,
transform: core.Transform = zm.identity(),
worldTransform: Mat = zm.identity(),
projection: Mat = makePerspective(
core.radians(70.0), // angle
16.0 / 9.0,
0.1,
200000,
),
projectionAlt: Mat = makePerspective(
core.radians(70.0), // angle
16.0 / 9.0,
0.0001,
1000,
),
final: Mat = zm.identity(),
finalAlt: Mat = zm.identity(),
pub const EcsComponentDefinition = ecs.DefineComponent(@This(), .set); // set, map, multiset, maplist
pub fn init() Camera {
return .{
.rotation = zm.quatFromRollPitchYaw(0.0, 0.0, 0.0),
};
}
pub fn translate(self: *Camera, offset: core.Vectorf) void {
var off: core.Vectorf = offset;
off.y = offset.y;
off.x = offset.x;
off.z = offset.z;
self.*.position = self.position.add(off);
}
pub fn getRotation(self: *Camera) Quat {
return zm.quatFromMat(self.transform);
}
pub fn setRotationEuler(self: *@This(), x: f32, y: f32, z: f32) void {
self.rotation = core.zm.quatFromRollPitchYaw(x, y + core.radians(180.0), z);
}
pub fn updateCamera(self: *Camera) void {
self.projection = zm.perspectiveFovRh(core.radians(self.fov), 16.0 / 9.0, 0.1, 200000);
self.projection[1][1] *= -1;
self.projectionAlt = zm.perspectiveFovRh(core.radians(self.altFov), 16.0 / 9.0, 0.01, 200000);
self.projectionAlt[1][1] *= -1;
// self.projectionAlt = self.projection;
}
pub fn resolve(self: *Camera) void {
{
var base = core.zm.identity();
base = mul(core.zm.rotationY(-self.yaw), base);
base = mul(core.zm.rotationX(self.pitch), base);
base = mul(core.zm.rotationZ(self.roll), base);
const pr2 = core.scene.SceneObjectPosRot{
.position = self.position,
};
self.worldTransform = mul(base, pr2.toTransform());
}
// calculate viewProjections
{
var base = core.zm.rotationY(self.yaw + core.radians(180.0));
base = mul(base, core.zm.rotationX(self.pitch));
base = mul(base, core.zm.rotationZ(self.roll));
self.transform = base;
// self.transform = mul(
// base,
// mul(zm.matFromQuat(self.rotation), zm.rotationY(core.radians(180.0))),
// );
var position = self.position;
// position.x *= -1;
// position.z *= -1;
self.transform = mul(zm.translationV(position.fmul(-1).toZm()), self.transform);
self.final = mul(self.transform, self.projection);
self.finalAlt = mul(self.transform, self.projectionAlt);
}
}
};

View File

@ -0,0 +1,96 @@
allocator: std.mem.Allocator,
cubeMapShared: [graphics.NumFrames]?vk.DescriptorSet = .{ null, null },
cubeMapTextureSet: ?vk.DescriptorSet = null,
cubeMapName: ?core.Name = null,
material: *graphics.Material = undefined,
mesh: ?graphics.IndexedMesh = null,
pub const RendererInterfaceVTable = graphics.RendererInterface.from(@This());
pub var gSkybox: *@This() = undefined;
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
};
gSkybox = self;
try self.initPipeline();
try graphics.registerRendererPlugin(self);
return self;
}
pub fn sendShared(self: *@This(), fi: u32) void {
if (self.cubeMapName == null) {
self.cubeMapShared[fi] = self.cubeMapTextureSet;
return;
} else {
if (self.mesh == null)
self.mesh = graphics.getIndexedMeshByName(core.MakeName("m_skybox"));
if (self.cubeMapTextureSet == null) {
const handle = self.cubeMapName.?.handle();
self.cubeMapTextureSet = graphics.getContext().textureSets.get(handle);
}
}
self.cubeMapShared[fi] = self.cubeMapTextureSet;
}
pub fn initPipeline(self: *@This()) !void {
const gc = graphics.getContext();
var pipelineBuilder = try graphics.NeonVkPipelineBuilder.init(
gc.dev,
gc.vkd,
self.allocator,
gc.vkAllocator,
skybox_vert.spv(),
skybox_frag.spv(),
);
defer pipelineBuilder.deinit();
try pipelineBuilder.add_mesh_description();
try pipelineBuilder.add_layout(gc.globalDescriptorLayout);
try pipelineBuilder.add_layout(gc.singleTextureSetLayout);
try pipelineBuilder.add_depth_stencil(); // todo.. we might not want this for a skybox.
try pipelineBuilder.init_triangle_pipeline(gc.actual_extent);
pipelineBuilder.pdsci.?.depth_write_enable = vk.FALSE;
pipelineBuilder.pdsci.?.depth_test_enable = vk.FALSE;
pipelineBuilder.pdsci.?.depth_compare_op = .never;
const materialName = core.MakeName("Mat_skybox");
self.material = try self.allocator.create(graphics.Material);
self.material.* = graphics.Material{
.materialName = materialName,
.pipeline = (try pipelineBuilder.build(gc.renderPass)).?,
.layout = pipelineBuilder.pipelineLayout,
};
try gc.add_material(self.material);
}
pub fn setSkybox(textureName: []const u8) !void {
const name = core.MakeName(textureName);
gSkybox.cubeMapName = name;
}
pub fn destroy(self: *@This()) void {
self.allocator.destroy(self);
}
const vk_renderer_interface = @import("vk_renderer/vk_renderer_interface.zig");
const RendererInterface = vk_renderer_interface.RendererInterface;
const std = @import("std");
const graphics = @import("graphics.zig");
const core = @import("core");
const vk = @import("vulkan");
const skybox_vert = @import("skybox_vert");
const skybox_frag = @import("skybox_frag");
const vkinit = @import("vk_init.zig");

View File

@ -0,0 +1,39 @@
const std = @import("std");
const core = @import("core");
const vk_renderer = @import("vk_renderer.zig");
const vma = @import("vma");
const vk = @import("vulkan");
const vkinit = @import("vk_init.zig");
const NeonVkContext = vk_renderer.NeonVkContext;
const NeonVkBuffer = vk_renderer.NeonVkBuffer;
const NeonVkImage = vk_renderer.NeonVkImage;
pub const PixelPos = struct {
x: u32,
y: u32,
/// returns y/x of the pixel position
pub fn ratio(self: @This()) f32 {
return @as(f32, @floatFromInt(self.y)) / @as(f32, @floatFromInt(self.x));
}
};
// This is a simple display texture
pub const Texture = struct {
image: NeonVkImage,
imageView: vk.ImageView,
isCube: bool = false,
pub fn deinit(self: *@This(), ctx: *NeonVkContext) void {
ctx.vkd.destroyImageView(ctx.dev, self.imageView, null);
self.image.deinit(ctx.vkAllocator);
}
pub fn getDimensions(self: @This()) PixelPos {
return .{
.x = self.image.pixelWidth,
.y = self.image.pixelHeight,
};
}
};

View File

@ -0,0 +1,382 @@
// simple wrapper around vma with a very slow debug mode that
// shows every single vma event
//
// BECAUSE I CAN'T FIND WHERE I FAILED TO DESTROY SOME MEMORY.
const std = @import("std");
const vk = @import("vulkan");
const vma = @import("vma");
const core = @import("core");
const memory = core.MemoryTracker;
const vk_constants = @import("vk_constants.zig");
const DeviceDispatch = vk_constants.DeviceDispatch;
const BaseDispatch = vk_constants.BaseDispatch;
const InstanceDispatch = vk_constants.InstanceDispatch;
pub const Allocation = vma.Allocation;
pub const Allocator = vma.Allocator;
pub const AllocationCreateInfo = vma.AllocationCreateInfo;
pub const NeonVkBuffer = struct {
buffer: vk.Buffer,
allocation: vma.Allocation,
size: usize,
pub fn deinit(self: *@This(), vkAllocator: *NeonVkAllocator) void {
vkAllocator.destroyBuffer(self);
}
};
pub const NeonVkImage = struct {
image: vk.Image,
allocation: vma.Allocation,
pixelWidth: u32,
pixelHeight: u32,
pub fn deinit(self: *NeonVkImage, allocator: *NeonVkAllocator) void {
allocator.destroyImage(self);
}
/// returns the image ratio of the height over width
pub inline fn getImageRatioFloat(self: @This()) f32 {
return @as(f32, @floatFromInt(self.pixelHeight)) / @as(f32, @floatFromInt(self.pixelWidth));
}
};
pub const AllocationEvent = union(enum) {
allocate: struct {
alloc: usize,
tag: []const u8,
},
destroy: struct {
alloc: usize,
tag: []const u8,
},
pub fn print(self: @This()) void {
switch (self) {
.allocate => |allocate| {
core.graphics_log("allocate @{d} - {s}", .{ allocate.alloc, allocate.tag });
},
.destroy => |destroy| {
core.graphics_log("destroy @{d} - {s}", .{ destroy.alloc, destroy.tag });
},
}
}
};
pub const NeonVkAllocator = struct {
mutex: std.Thread.Mutex = .{},
vmaAllocator: vma.Allocator,
allocator: std.mem.Allocator,
eventsList: std.ArrayList(AllocationEvent),
liveAllocations: std.ArrayList(LiveAlloc),
livePipelines: std.AutoHashMap(u64, []u8),
vkb: vk_constants.BaseDispatch,
vki: vk_constants.InstanceDispatch,
vkd: vk_constants.DeviceDispatch,
const AllocatedObject = union {
image: NeonVkImage,
buffer: NeonVkBuffer,
};
const LiveAlloc = struct {
allocation: usize,
tag: []const u8,
object: AllocatedObject,
};
pub fn createStagingBuffer(
self: *@This(),
bufferSize: u32,
comptime tag: []const u8,
) !NeonVkBuffer {
const bci = vk.BufferCreateInfo{
.flags = .{},
.size = bufferSize,
.usage = .{ .transfer_src_bit = true },
.sharing_mode = .exclusive,
.queue_family_index_count = 0,
.p_queue_family_indices = undefined,
};
const vmaCreateInfo = vma.AllocationCreateInfo{
.flags = .{},
.usage = .cpuOnly,
};
return self.createBuffer(bci, vmaCreateInfo, tag);
}
pub fn createPipelineLayout(self: *@This(), dev: vk.Device, plci: vk.PipelineLayoutCreateInfo, tag: []const u8) !vk.PipelineLayout {
const pipelineLayout = try self.vkd.createPipelineLayout(dev, &plci, null);
try self.livePipelines.put(@intFromEnum(pipelineLayout), try core.dupeString(self.allocator, tag));
return pipelineLayout;
}
pub fn destroyPipelineLayout(self: *@This(), dev: vk.Device, layout: vk.PipelineLayout) void {
self.allocator.free(self.livePipelines.get(@intFromEnum(layout)).?);
_ = self.livePipelines.remove(@intFromEnum(layout));
self.vkd.destroyPipelineLayout(dev, layout, null);
}
pub fn createGpuBuffer(
self: *@This(),
bufferSize: u32,
options: struct {
index_buffer_bit: bool = false,
vertex_buffer_bit: bool = false,
uniform_texel_buffer_bit: bool = false,
storage_texel_buffer_bit: bool = false,
uniform_buffer_bit: bool = false,
storage_buffer_bit: bool = false,
indirect_buffer_bit: bool = false,
},
comptime tag: []const u8,
) !NeonVkBuffer {
const bci = vk.BufferCreateInfo{
.flags = .{},
.size = bufferSize,
.usage = .{
.transfer_dst_bit = true,
.index_buffer_bit = options.index_buffer_bit,
.vertex_buffer_bit = options.vertex_buffer_bit,
.uniform_buffer_bit = options.uniform_buffer_bit,
.storage_buffer_bit = options.storage_buffer_bit,
.indirect_buffer_bit = options.indirect_buffer_bit,
},
.sharing_mode = .exclusive,
.queue_family_index_count = 0,
.p_queue_family_indices = undefined,
};
const vmaCreateInfo = vma.AllocationCreateInfo{
.flags = .{},
.usage = .gpuOnly,
};
return self.createBuffer(bci, vmaCreateInfo, tag);
}
pub fn create(
vmaAllocatorCreateInfo: vma.AllocatorCreateInfo,
allocator: std.mem.Allocator,
vkb: vk_constants.BaseDispatch,
vki: vk_constants.InstanceDispatch,
vkd: vk_constants.DeviceDispatch,
) !*@This() {
const newAllocator = try allocator.create(@This());
newAllocator.* = @This(){
.vmaAllocator = try vma.Allocator.create(vmaAllocatorCreateInfo),
.allocator = allocator,
.eventsList = std.ArrayList(AllocationEvent).init(allocator),
.liveAllocations = std.ArrayList(LiveAlloc).init(allocator),
.livePipelines = std.AutoHashMap(u64, []u8).init(allocator),
.vkb = vkb,
.vki = vki,
.vkd = vkd,
};
return newAllocator;
}
fn pushAllocation(
self: *@This(),
allocation: vma.Allocation,
tag: []const u8,
object: AllocatedObject,
) !void {
try self.liveAllocations.append(.{
.allocation = @intFromEnum(allocation),
.tag = tag,
.object = object,
});
try self.eventsList.append(.{ .allocate = .{
.alloc = @intFromEnum(allocation),
.tag = tag,
} });
}
fn pushDestroy(self: *@This(), allocation: vma.Allocation) void {
// find the corresponding live allocation
var live: LiveAlloc = undefined;
var i: u32 = 0;
var found: bool = false;
while (i < self.liveAllocations.items.len) : (i += 1) {
if (self.liveAllocations.items[i].allocation == @intFromEnum(allocation)) {
found = true;
live = self.liveAllocations.items[i];
break;
}
}
if (found) {
_ = self.liveAllocations.swapRemove(i);
} else {
core.engine_log("We have a big issue here, a destroy was issued for allocation {any}\n But it is not alive", .{allocation});
self.printOutStandingAllocations();
unreachable;
}
self.eventsList.append(.{ .destroy = .{
.alloc = @intFromEnum(allocation),
.tag = live.tag,
} }) catch unreachable;
}
pub fn createIndirectCommandBuffer(
self: *@This(),
bufferSize: u32,
comptime tag: []const u8,
) !NeonVkBuffer {
return try self.createGpuBuffer(bufferSize, .{ .indirect_buffer_bit = true }, tag);
}
pub fn createSsboBuffer(self: *@This(), bufferSize: u32, comptime tag: []const u8) !NeonVkBuffer {
const bci = vk.BufferCreateInfo{
.size = bufferSize,
.usage = .{ .storage_buffer_bit = true },
.flags = .{},
.sharing_mode = .exclusive,
.queue_family_index_count = 0,
.p_queue_family_indices = undefined,
};
const aci = vma.AllocationCreateInfo{
.usage = .cpuToGpu,
};
return try self.createBuffer(bci, aci, tag);
}
pub fn createBuffer(
self: *@This(),
bci: vk.BufferCreateInfo,
aci: AllocationCreateInfo,
comptime tag: []const u8,
) !NeonVkBuffer {
self.mutex.lock();
defer self.mutex.unlock();
const results = try self.vmaAllocator.createBuffer(bci, aci);
const object: AllocatedObject = .{
.buffer = NeonVkBuffer{
.buffer = results.buffer,
.allocation = results.allocation,
.size = bci.size,
},
};
memory.MTAddUntrackedAllocation(bci.size);
try self.pushAllocation(results.allocation, tag, object);
return object.buffer;
}
pub fn destroyBuffer(self: *@This(), buffer: *NeonVkBuffer) void {
self.mutex.lock();
defer self.mutex.unlock();
memory.MTRemoveAllocation(buffer.size);
self.pushDestroy(buffer.allocation);
self.vmaAllocator.destroyBuffer(buffer.buffer, buffer.allocation);
}
pub fn destroyImage(self: *@This(), image: *NeonVkImage) void {
self.mutex.lock();
defer self.mutex.unlock();
self.pushDestroy(image.allocation);
self.vmaAllocator.destroyImage(image.image, image.allocation);
}
pub fn createImage(
self: *@This(),
ici: vk.ImageCreateInfo,
aci: AllocationCreateInfo,
comptime tag: []const u8,
) !NeonVkImage {
self.mutex.lock();
defer self.mutex.unlock();
const result = try self.vmaAllocator.createImage(ici, aci);
const object: AllocatedObject = .{ .image = .{
.image = result.image,
.allocation = result.allocation,
.pixelWidth = ici.extent.width,
.pixelHeight = ici.extent.height,
} };
try self.pushAllocation(result.allocation, tag, object);
return object.image;
}
pub fn mapBuffer(self: *@This(), comptime T: type, buffer: NeonVkBuffer) ![]T {
var slice: []T = undefined;
slice.ptr = try self.mapMemory(buffer, T);
slice.len = buffer.size / @sizeOf(T);
return slice;
}
pub fn mapMemorySlice(self: *@This(), comptime T: type, buffer: NeonVkBuffer, size: usize) ![]T {
var slice: []T = undefined;
slice.ptr = try self.mapMemory(buffer, T);
slice.len = size;
return slice;
}
pub fn mapMemory(self: *@This(), buffer: NeonVkBuffer, comptime T: type) ![*]T {
return try self.vmaAllocator.mapMemory(buffer.allocation, T);
}
pub fn unmapMemory(self: *@This(), buffer: NeonVkBuffer) void {
self.vmaAllocator.unmapMemory(buffer.allocation);
}
pub fn printEventsLog(self: @This()) void {
for (self.eventsList.items) |item| {
item.print();
}
}
pub fn areAllocationsOutstanding(self: *@This()) bool {
return self.liveAllocations.items.len > 0;
}
pub fn printOutStandingAllocations(self: *@This()) void {
core.graphics_log(" == There are {d} allocations outstanding", .{self.liveAllocations.items.len});
for (self.liveAllocations.items) |alloc| {
core.graphics_log("live allocation@{d} tag:\'{s}\' {any}", .{ alloc.allocation, alloc.tag, alloc.object });
}
core.graphics_logs("--- Event log below --- ");
self.printEventsLog();
core.graphics_logs("end of report.");
core.forceFlush();
}
pub fn destroy(self: *@This()) void {
self.eventsList.deinit();
self.liveAllocations.deinit();
{
var iter = self.livePipelines.iterator();
while (iter.next()) |i| {
self.allocator.free(i.value_ptr.*);
}
}
self.livePipelines.deinit();
self.vmaAllocator.destroy();
self.allocator.destroy(self);
}
};

View File

@ -0,0 +1,16 @@
// global api
//
// i hate lugging these variables around.
// device and cmd buffers are fine,
// but the dispatch variables are going to be kept here and easily accessible.
pub var _vkb: constants.BaseDispatch = undefined;
pub var _vki: constants.InstanceDispatch = undefined;
pub var _vkd: constants.DeviceDispatch = undefined;
pub const vkb = &_vkb;
pub const vki = &_vki;
pub const vkd = &_vkd;
const vk = @import("vulkan");
const constants = @import("vk_constants.zig");

View File

@ -0,0 +1,373 @@
const std = @import("std");
const vk = @import("vulkan");
const graphics = @import("graphics.zig");
const core = @import("core");
const assets = @import("assets");
const vk_utils = @import("vk_utils.zig");
const vkinit = @import("vk_init.zig");
const vk_cubemap = @import("vk_renderer/vk_cubemap.zig");
const tracy = core.tracy;
const materials = @import("materials.zig");
const vk_renderer = @import("vk_renderer.zig");
const mesh = @import("mesh.zig");
const texture = @import("texture.zig");
const NeonVkContext = vk_renderer.NeonVkContext;
const Material = materials.Material;
const Mesh = mesh.Mesh;
const Texture = texture.Texture;
pub const TextureLoader = struct {
pub var LoaderInterfaceVTable: assets.AssetLoaderInterface = assets.AssetLoaderInterface.from("Texture", @This());
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
const StagedTextureDescription = struct {
name: core.Name,
stagingResults: vk_utils.LoadAndStageImage,
textureListResults: ?[]vk_utils.LoadAndStageImage = null,
assetRef: assets.AssetRef,
properties: assets.AssetPropertiesBag,
pub fn deinit(self: *@This(), gc: *NeonVkContext) void {
self.stagingResults.deinit(gc.vkAllocator);
if (self.textureListResults) |results| {
for (results) |*result| {
result.deinit(gc.vkAllocator);
}
}
}
};
const RTAssetsReady = struct {
name: core.Name,
texture: *Texture,
textureSet: vk.DescriptorSet,
textureId: u32,
};
gc: *NeonVkContext,
assetsReady: core.RingQueue(StagedTextureDescription),
rtAssetsReady: core.RingQueue(RTAssetsReady),
discarding: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
pub fn loadAsset(self: *@This(), assetRef: assets.AssetRef, props: ?assets.AssetPropertiesBag) assets.AssetLoaderError!void {
if (self.discarding.load(.seq_cst)) {
return;
}
var z = tracy.ZoneN(@src(), "TextureLoader loadAsset");
const Lambda = struct {
loader: *TextureLoader,
assetRef: assets.AssetRef,
gc: *NeonVkContext,
properties: assets.AssetPropertiesBag,
pub fn eFunc(ctx: @This()) !void {
var z1 = tracy.ZoneN(@src(), "Loading file from TextureLoader");
const gc = ctx.gc;
defer {
_ = ctx.gc.outstandingJobsCount.fetchSub(1, .seq_cst);
}
var loadAndStageResults: vk_utils.LoadAndStageImage = undefined;
if (!ctx.properties.textureCube) {
loadAndStageResults = try vk_utils.load_and_stage_image_from_file(gc, ctx.properties.path);
errdefer loadAndStageResults.deinit(gc.vkAllocator);
} else {
loadAndStageResults = try vk_cubemap.stageCubeTexture(ctx.properties.textureList.?);
errdefer loadAndStageResults.deinit(gc.vkAllocator);
}
var assetRefName = ctx.assetRef.name;
tracy.Message(assetRefName.utf8());
tracy.Message(ctx.properties.path);
core.engine_log("loaded: {s} from: {s}", .{ assetRefName.utf8(), ctx.properties.path });
var loadedDescription = StagedTextureDescription{
.name = ctx.assetRef.name,
.stagingResults = loadAndStageResults,
.assetRef = ctx.assetRef,
.properties = ctx.properties,
};
if (ctx.properties.textureList) |textureList| {
const tlResults = try ctx.gc.allocator.alloc(vk_utils.LoadAndStageImage, textureList.len);
errdefer ctx.gc.allocator.free(tlResults);
for (textureList, 0..) |tPath, i| {
const rv = vk_utils.load_and_stage_image_from_file(gc, tPath) catch {
core.engine_log("unable to load file {s}", .{tPath});
return error.FailedToLoad;
};
errdefer rv.deinit();
tlResults[i] = rv;
}
loadedDescription.textureListResults = tlResults;
}
z1.End();
ctx.loader.assetsReady.pushLocked(loadedDescription) catch unreachable;
}
pub fn func(ctx: @This(), _: *core.JobContext) void {
ctx.eFunc() catch unreachable;
}
};
_ = self.gc.outstandingJobsCount.fetchAdd(1, .seq_cst);
core.dispatchJob(Lambda{
.loader = self,
.gc = self.gc,
.assetRef = assetRef,
.properties = props.?,
}) catch return error.UnableToLoad;
z.End();
}
pub fn processRenderThreadEvents(ptr: *anyopaque) void {
const self: *@This() = @ptrCast(@alignCast(ptr));
self.processEventInner() catch {};
}
pub fn createImageFromStagingResult(self: *@This(), name: core.Name, stagingResults: *vk_utils.LoadAndStageImage, properties: assets.AssetPropertiesBag) core.EngineDataEventError!void {
const gc = self.gc;
var stagingBuffer = stagingResults.stagingBuffer;
const image = stagingResults.image;
if (stagingResults.cubeOffsets != null) {
vk_cubemap.submitTextureCube(&gc.uploader, stagingResults) catch return error.UnknownStatePanic;
stagingBuffer.deinit(gc.vkAllocator);
var ivc = vkinit.imageViewCreateInfo(
.r8g8b8a8_srgb,
image.image,
.{ .color_bit = true },
stagingResults.mipLevel,
);
ivc.view_type = .cube;
ivc.subresource_range.layer_count = 6;
const imageView = gc.vkd.createImageView(gc.dev, &ivc, null) catch return error.UnknownStatePanic;
const newTexture = gc.allocator.create(Texture) catch return error.UnknownStatePanic;
newTexture.* = Texture{
.image = image,
.imageView = imageView,
};
const rv = vk_utils.createDescriptorSetForImage(
gc.dev,
gc.descriptorPool,
gc.singleTextureSetLayout,
imageView,
gc.cubeSampler,
false,
) catch return error.UnknownStatePanic;
self.rtAssetsReady.pushLocked(.{
.name = name,
.texture = newTexture,
.textureSet = rv.textureSet,
.textureId = rv.textureId,
}) catch return error.UnknownStatePanic;
} else {
vk_utils.submit_copy_from_staging(gc, stagingBuffer, image, stagingResults.mipLevel) catch return error.UnknownStatePanic;
stagingBuffer.deinit(gc.vkAllocator);
var imageViewCreate = vkinit.imageViewCreateInfo(
.r8g8b8a8_srgb,
image.image,
.{ .color_bit = true },
stagingResults.mipLevel,
);
const imageView = gc.vkd.createImageView(gc.dev, &imageViewCreate, null) catch return error.UnknownStatePanic;
const newTexture = gc.allocator.create(Texture) catch return error.UnknownStatePanic;
newTexture.* = Texture{
.image = image,
.imageView = imageView,
};
const sampler = if (properties.textureUseBlockySampler) gc.blockySampler else gc.linearSampler;
const rv = vk_utils.createDescriptorSetForImage(
gc.dev,
gc.descriptorPool,
gc.singleTextureSetLayout,
imageView,
sampler,
true,
) catch return error.UnknownStatePanic;
self.rtAssetsReady.pushLocked(.{
.name = name,
.texture = newTexture,
.textureSet = rv.textureSet,
.textureId = rv.textureId,
}) catch return error.UnknownStatePanic;
}
}
fn processEventInner(self: *@This()) core.EngineDataEventError!void {
if (self.assetsReady.count() > 0) {
self.assetsReady.lock();
defer self.assetsReady.unlock();
while (self.assetsReady.popFromUnlocked()) |ar| {
var assetReady = ar;
var z1 = tracy.ZoneN(@src(), "Uploading asset loaded by TextureLoader");
tracy.Message("TextureLoader");
tracy.Message(assetReady.assetRef.name.utf8());
tracy.Message(assetReady.properties.path);
core.engine_log("async texture load complete registry: {s}", .{assetReady.name.utf8()});
try self.createImageFromStagingResult(assetReady.name, &assetReady.stagingResults, assetReady.properties);
if (assetReady.textureListResults) |results| {
var buf: [256]u8 = undefined;
for (results, 0..) |res, i| {
var r = res;
var arName = assetReady.name;
const newName = std.fmt.bufPrint(&buf, "{s}[{d}]", .{ arName.utf8(), i }) catch return core.EngineDataEventError.OutOfMemory;
try self.createImageFromStagingResult(core.MakeName(newName), &r, assetReady.properties);
}
self.gc.allocator.free(results);
}
z1.End();
}
}
}
// processing events, some should really be processing events rather than
pub fn processEvents(self: *@This(), frameNumber: u64) core.EngineDataEventError!void {
_ = frameNumber;
if (self.rtAssetsReady.count() > 0) {
self.rtAssetsReady.lock();
defer self.rtAssetsReady.unlock();
while (self.rtAssetsReady.popFromUnlocked()) |a| {
self.gc.install_texture_into_registry(a.name, a.texture, a.textureSet, a.textureId) catch return error.UnknownStatePanic;
}
}
}
pub fn discardAll(self: *@This()) void {
self.discarding.store(true, .seq_cst);
core.graphics_log("discarding {d} outstanding jobs", .{self.assetsReady.count()});
self.assetsReady.lock();
defer self.assetsReady.unlock();
while (self.assetsReady.popFromUnlocked()) |assetReady| {
var copy = assetReady;
StagedTextureDescription.deinit(&copy, self.gc);
}
}
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.gc = vk_renderer.gContext,
//todo: the EngineObjectVTable init function should have a handleable error
.assetsReady = core.RingQueue(StagedTextureDescription).init(allocator, 1024) catch unreachable,
.rtAssetsReady = core.RingQueue(RTAssetsReady).init(allocator, 1024) catch unreachable,
};
try self.gc.renderthread.installListener(self, processRenderThreadEvents);
return self;
}
pub fn destroy(self: *@This(), allocator: std.mem.Allocator) void {
self.assetsReady.deinit();
self.rtAssetsReady.deinit();
allocator.destroy(self);
}
};
pub const MeshLoader = struct {
pub var LoaderInterfaceVTable = assets.AssetLoaderInterface.from("Mesh", @This());
pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());
gc: *NeonVkContext,
pub fn init(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.gc = vk_renderer.gContext,
};
return self;
}
pub fn loadAsset(self: *@This(), assetRef: assets.AssetRef, propertiesBag: ?assets.AssetPropertiesBag) assets.AssetLoaderError!void {
_ = self;
const sourceType = getSourceType(propertiesBag);
core.engine_log("loading mesh asset {s} [{s}]", .{ propertiesBag.?.path, if (sourceType) |s| @tagName(s) else "default" });
graphics.loadIndexedMeshForPooling(assetRef.name, .{
.path = propertiesBag.?.path,
.sourceType = getSourceType(propertiesBag),
.skeletonName = if (propertiesBag.?.skeletonName) |skName| core.MakeName(skName) else null,
}) catch return error.UnableToLoad;
}
fn getSourceType(propertiesBag: ?assets.AssetPropertiesBag) ?graphics.MeshSourceType {
if (propertiesBag) |bag| {
if (bag.meshType) |meshType| {
if (std.mem.eql(u8, meshType, "obj")) {
return graphics.MeshSourceType.obj;
}
if (std.mem.eql(u8, meshType, "gltf")) {
return graphics.MeshSourceType.gltf;
}
}
// try to deduce it by file name, if nothing is set.
const ext = core.getFileExtension(bag.path);
if (std.mem.eql(u8, ext, ".obj")) {
return graphics.MeshSourceType.obj;
}
if (std.mem.eql(u8, ext, ".gltf")) {
return graphics.MeshSourceType.gltf;
}
if (std.mem.eql(u8, ext, ".glb")) {
return graphics.MeshSourceType.gltf;
}
}
return null;
}
pub fn discardAll(self: *@This()) void {
// totally synchronous, nothing to do for a discard
_ = self;
}
pub fn destroy(self: *@This(), allocator: std.mem.Allocator) void {
allocator.destroy(self);
}
};
pub var gTextureLoader: *TextureLoader = undefined;
pub var gMeshLoader: *MeshLoader = undefined;
pub fn init_loaders(allocator: std.mem.Allocator) !void {
gTextureLoader = try core.createObject(TextureLoader, .{
.responds_to_events = true,
});
gMeshLoader = try allocator.create(MeshLoader);
gMeshLoader.* = .{ .gc = vk_renderer.gContext };
try assets.gAssetSys.registerLoader(gTextureLoader);
try assets.gAssetSys.registerLoader(gMeshLoader);
}
// submit an abort message to TextureLoader and MeshLoader
pub fn discardAll() void {
gTextureLoader.discardAll();
gMeshLoader.discardAll();
}

View File

@ -0,0 +1,114 @@
const std = @import("std");
const vk = @import("vulkan");
const core = @import("core");
pub const NumFrames = NUM_FRAMES;
pub const FrameTimeout = 10_000_000_000; // 10 full second frame timeout
pub const MAX_OBJECTS = 100_000; // 100k objects ought to be enough for anyone
pub const NUM_FRAMES: usize = 2;
pub const DEVICE_LAYERS = [_]core.CStr{VK_KHRONOS_VALIDATION_LAYER_STRING};
pub const MAX_SKIN_SLOTS = 100_000;
pub const required_device_layers = [_]core.CStr{"VK_LAYER_KHRONOS_validation"};
pub const VK_KHRONOS_VALIDATION_LAYER_STRING: core.CStr = "VK_LAYER_KHRONOS_validation";
pub const BaseDispatch = vk.BaseWrapper(.{
.createInstance = true,
.getInstanceProcAddr = true,
.enumerateInstanceVersion = true,
.enumerateInstanceLayerProperties = true,
.enumerateInstanceExtensionProperties = true,
});
pub const InstanceDispatch = vk.InstanceWrapper(.{
.getPhysicalDeviceFeatures = true,
.destroyInstance = true,
.createDevice = true,
.destroySurfaceKHR = true,
.enumeratePhysicalDevices = true,
.getPhysicalDeviceProperties = true,
.enumerateDeviceExtensionProperties = true,
.getPhysicalDeviceSurfaceFormatsKHR = true,
.getPhysicalDeviceSurfacePresentModesKHR = true,
.getPhysicalDeviceSurfaceCapabilitiesKHR = true,
.getPhysicalDeviceQueueFamilyProperties = true,
.getPhysicalDeviceSurfaceSupportKHR = true,
.getPhysicalDeviceMemoryProperties = true,
.getPhysicalDeviceFormatProperties = true,
.getDeviceProcAddr = true,
});
pub const DeviceDispatch = vk.DeviceWrapper(.{
.resetCommandBuffer = true,
.destroyDevice = true,
.getDeviceQueue = true,
.createSemaphore = true,
.createFence = true,
.createImageView = true,
.createImage = true,
.destroyImage = true,
.destroyImageView = true,
.destroySemaphore = true,
.destroyFence = true,
.getSwapchainImagesKHR = true,
.createSwapchainKHR = true,
.destroySwapchainKHR = true,
.acquireNextImageKHR = true,
.deviceWaitIdle = true,
.waitForFences = true,
.resetFences = true,
.queueSubmit = true,
.queuePresentKHR = true,
.createCommandPool = true,
.destroyCommandPool = true,
.allocateCommandBuffers = true,
.cmdBlitImage = true,
.freeCommandBuffers = true,
.queueWaitIdle = true,
.createShaderModule = true,
.destroyShaderModule = true,
.createPipelineLayout = true,
.destroyPipelineLayout = true,
.createDescriptorSetLayout = true,
.destroyDescriptorSetLayout = true,
.createDescriptorPool = true,
.allocateDescriptorSets = true,
.freeDescriptorSets = true,
.updateDescriptorSets = true,
.destroyDescriptorPool = true,
.createRenderPass = true,
.destroyRenderPass = true,
.createGraphicsPipelines = true,
.destroyPipeline = true,
.createFramebuffer = true,
.destroyFramebuffer = true,
.beginCommandBuffer = true,
.endCommandBuffer = true,
.allocateMemory = true,
.freeMemory = true,
.createBuffer = true,
.destroyBuffer = true,
.getBufferMemoryRequirements = true,
.mapMemory = true,
.unmapMemory = true,
.bindBufferMemory = true,
.cmdBeginRenderPass = true,
.cmdEndRenderPass = true,
.cmdBindPipeline = true,
.cmdBindIndexBuffer = true,
.cmdDrawIndexed = true,
.cmdDraw = true,
.cmdSetViewport = true,
.cmdSetScissor = true,
.cmdBindVertexBuffers = true,
.cmdCopyBuffer = true,
.cmdPushConstants = true,
.cmdPipelineBarrier = true,
.cmdBindDescriptorSets = true,
.cmdCopyBufferToImage = true,
.createSampler = true,
.destroySampler = true,
.cmdDrawIndexedIndirect = true,
});

Some files were not shown because too many files have changed in this diff Show More