Async asset loading
This commit is contained in:
325
src/assets.zig
325
src/assets.zig
@@ -1,104 +1,285 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const sdl = @import("sdl");
|
|
||||||
const err = @import("error.zig");
|
const err = @import("error.zig");
|
||||||
const c = @import("c.zig");
|
|
||||||
const comp = @import("components.zig");
|
|
||||||
const Game = @import("game.zig");
|
const Game = @import("game.zig");
|
||||||
const Graphics = @import("graphics.zig");
|
const FileLoader = @import("assets/file.zig");
|
||||||
|
const TextureLoader = @import("assets/texture.zig");
|
||||||
const Assets = @This();
|
const Assets = @This();
|
||||||
|
|
||||||
const Storage = comp.Storage(Asset, .{});
|
pub const Texture = AssetContainer(TextureLoader);
|
||||||
|
|
||||||
var storage: Storage = undefined;
|
const WORKERS_MAX = 4;
|
||||||
|
var next_worker_update: usize = 0;
|
||||||
|
var workers: [WORKERS_MAX]WorkerState = undefined;
|
||||||
|
const WorkerState = struct {
|
||||||
|
running: bool = false,
|
||||||
|
thread: ?std.Thread = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssetMap = std.HashMapUnmanaged(AssetId, *AssetCell, AssetContext, 80);
|
||||||
|
var asset_map_mutex: std.Thread.Mutex = .{};
|
||||||
|
var asset_map: AssetMap = undefined;
|
||||||
|
|
||||||
|
const RequestBoard = std.ArrayListUnmanaged(*AssetCell);
|
||||||
|
var request_board_mutex: std.Thread.Mutex = .{};
|
||||||
|
var request_board: RequestBoard = undefined;
|
||||||
|
var request_board_counter: usize = 0;
|
||||||
|
|
||||||
|
const FreeBoard = std.ArrayListUnmanaged(*AssetCell);
|
||||||
|
var free_board_mutex: std.Thread.Mutex = .{};
|
||||||
|
var free_board: FreeBoard = undefined;
|
||||||
|
|
||||||
|
const AssetId = struct {
|
||||||
|
type: AssetType,
|
||||||
|
path: []const u8,
|
||||||
|
};
|
||||||
|
const AssetContext = struct {
|
||||||
|
pub fn hash(self: @This(), key: AssetId) u64 {
|
||||||
|
_ = self;
|
||||||
|
var hasher = std.hash.Wyhash.init(@intFromEnum(key.type));
|
||||||
|
hasher.update(key.path);
|
||||||
|
return hasher.final();
|
||||||
|
}
|
||||||
|
pub fn eql(self: @This(), a: AssetId, b: AssetId) bool {
|
||||||
|
_ = self;
|
||||||
|
return a.type == b.type and std.mem.eql(u8, a.path, b.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const LoadError = error{
|
||||||
|
DependencyError,
|
||||||
|
} || std.mem.Allocator.Error || std.fs.File.OpenError || std.fs.File.ReadError;
|
||||||
|
|
||||||
pub const AssetType = enum {
|
pub const AssetType = enum {
|
||||||
|
file,
|
||||||
texture,
|
texture,
|
||||||
};
|
|
||||||
pub const Texture = struct {
|
pub fn getType(comptime self: @This()) type {
|
||||||
handle: Storage.Key,
|
return switch (self) {
|
||||||
|
.file => FileLoader,
|
||||||
|
.texture => TextureLoader,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Asset = struct {
|
const AssetState = union(enum) {
|
||||||
|
not_loaded,
|
||||||
|
loaded,
|
||||||
|
fail: LoadError,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const AssetCell = struct {
|
||||||
|
mutex: std.Thread.Mutex,
|
||||||
|
type: AssetType,
|
||||||
|
data: *void,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
data: union(AssetType) {
|
loader: *const fn (*AssetCell, std.mem.Allocator) LoadError!void,
|
||||||
texture: AssetTexture,
|
unloader: *const fn (*AssetCell, std.mem.Allocator) void,
|
||||||
|
state: AssetState,
|
||||||
|
counter: usize,
|
||||||
|
|
||||||
|
fn load(self: *AssetCell, alloc: std.mem.Allocator) void {
|
||||||
|
self.loader(self, alloc) catch |e| {
|
||||||
|
self.state = .{ .fail = e };
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.state = .loaded;
|
||||||
|
}
|
||||||
|
fn unload(self: *AssetCell, alloc: std.mem.Allocator) void {
|
||||||
|
self.unloader(self, alloc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn AssetContainer(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
data_pointer: ?*T = null,
|
||||||
|
asset_pointer: *AssetCell,
|
||||||
|
last_state: AssetState = .not_loaded,
|
||||||
|
|
||||||
|
pub fn get(self: *@This()) ?*T {
|
||||||
|
switch (self.last_state) {
|
||||||
|
.loaded => {
|
||||||
|
@branchHint(.likely);
|
||||||
|
return self.data_pointer;
|
||||||
},
|
},
|
||||||
};
|
.fail => {
|
||||||
pub const AssetTexture = struct {
|
return null;
|
||||||
texture: *sdl.GPUTexture,
|
},
|
||||||
sampler: *sdl.GPUSampler,
|
.not_loaded => {
|
||||||
};
|
if (self.asset_pointer.mutex.tryLock()) {
|
||||||
|
defer self.asset_pointer.mutex.unlock();
|
||||||
|
self.last_state = self.asset_pointer.state;
|
||||||
|
}
|
||||||
|
if (self.last_state == .loaded) {
|
||||||
|
self.data_pointer = @alignCast(@ptrCast(self.asset_pointer.data));
|
||||||
|
return self.data_pointer;
|
||||||
|
} else return null;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// To be used by worker threads to request other assets
|
||||||
|
pub fn getSync(self: *@This()) !*T {
|
||||||
|
sw: switch (self.last_state) {
|
||||||
|
.loaded => {
|
||||||
|
return self.data_pointer.?;
|
||||||
|
},
|
||||||
|
.fail => |e| {
|
||||||
|
return e;
|
||||||
|
},
|
||||||
|
.not_loaded => {
|
||||||
|
// TODO: Do something else while the asset is locked?
|
||||||
|
self.asset_pointer.mutex.lock();
|
||||||
|
defer self.asset_pointer.mutex.unlock();
|
||||||
|
self.asset_pointer.load(Game.alloc);
|
||||||
|
self.last_state = self.asset_pointer.state;
|
||||||
|
if (self.last_state == .loaded) {
|
||||||
|
self.data_pointer = @alignCast(@ptrCast(self.asset_pointer.data));
|
||||||
|
}
|
||||||
|
continue :sw self.last_state;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init() void {
|
pub fn init() void {
|
||||||
Assets.storage = Storage.init();
|
Assets.next_worker_update = 0;
|
||||||
|
Assets.workers = .{WorkerState{}} ** WORKERS_MAX;
|
||||||
|
Assets.asset_map_mutex = .{};
|
||||||
|
Assets.asset_map = AssetMap.empty;
|
||||||
|
Assets.request_board_mutex = .{};
|
||||||
|
Assets.request_board = RequestBoard.empty;
|
||||||
|
Assets.request_board_counter = 0;
|
||||||
|
Assets.free_board_mutex = .{};
|
||||||
|
Assets.free_board = FreeBoard.empty;
|
||||||
}
|
}
|
||||||
pub fn deinit() void {
|
pub fn deinit() void {
|
||||||
var iter = Assets.storage.iter();
|
for (&Assets.workers) |*worker| {
|
||||||
|
if (worker.thread == null) continue;
|
||||||
|
worker.thread.?.join();
|
||||||
|
}
|
||||||
|
var iter = Assets.asset_map.valueIterator();
|
||||||
while (iter.next()) |asset| {
|
while (iter.next()) |asset| {
|
||||||
Assets.freeAsset(asset);
|
std.debug.assert(asset.*.counter == 0);
|
||||||
|
if (asset.*.state == .loaded)
|
||||||
|
asset.*.unload(Game.alloc);
|
||||||
|
Game.alloc.destroy(asset.*);
|
||||||
}
|
}
|
||||||
Assets.storage.deinit();
|
Assets.asset_map.clearAndFree(Game.alloc);
|
||||||
|
Assets.request_board.clearAndFree(Game.alloc);
|
||||||
|
Assets.free_board.clearAndFree(Game.alloc);
|
||||||
}
|
}
|
||||||
pub fn load(comptime asset_type: AssetType, path: []const u8) typeFromAssetType(asset_type) {
|
pub fn update() void {
|
||||||
switch (asset_type) {
|
const worker = &Assets.workers[Assets.next_worker_update];
|
||||||
.texture => {
|
if (!@atomicLoad(bool, &worker.running, .acquire) and worker.thread != null) {
|
||||||
const data = loadFile(Game.alloc, path) catch |e| err.file(e, path);
|
worker.thread.?.join();
|
||||||
var x: i32 = undefined;
|
worker.thread = null;
|
||||||
var y: i32 = undefined;
|
|
||||||
var z: i32 = undefined;
|
|
||||||
const image = c.stbi_load_from_memory(@ptrCast(data), @intCast(data.len), &x, &y, &z, 4);
|
|
||||||
Game.alloc.free(data);
|
|
||||||
if (image == null) err.stbi();
|
|
||||||
const image_slice = image[0..@intCast(x * y * z)];
|
|
||||||
const texture, const sampler = Graphics.loadTexture(@intCast(x), @intCast(y), image_slice);
|
|
||||||
c.stbi_image_free(image);
|
|
||||||
return .{ .handle = Assets.storage.add(.{
|
|
||||||
.path = path,
|
|
||||||
.data = .{ .texture = .{
|
|
||||||
.texture = texture,
|
|
||||||
.sampler = sampler,
|
|
||||||
} },
|
|
||||||
}) };
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
if (worker.thread == null and @atomicLoad(usize, &Assets.request_board_counter, .monotonic) > 4 * Assets.next_worker_update) {
|
||||||
pub fn free(asset: anytype) void {
|
worker.running = true;
|
||||||
if (Assets.storage.free(asset.handle)) |stored| {
|
worker.thread = std.Thread.spawn(.{}, loaderLoop, .{Assets.next_worker_update}) catch err.oom();
|
||||||
freeAsset(stored);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
pub fn freeAsset(asset: *Asset) void {
|
Assets.next_worker_update += 1;
|
||||||
switch (asset.data) {
|
if (Assets.next_worker_update >= WORKERS_MAX) {
|
||||||
.texture => {
|
Assets.next_worker_update = 0;
|
||||||
Graphics.unloadTexture(asset.data.texture.texture, asset.data.texture.sampler);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
pub fn get(asset: anytype) ?assetTypeFromType(@TypeOf(asset)) {
|
Assets.free_board_mutex.lock();
|
||||||
if (Assets.storage.get(asset.handle)) |stored| {
|
defer Assets.free_board_mutex.unlock();
|
||||||
switch (@TypeOf(asset)) {
|
if (Assets.free_board.items.len == 0) return;
|
||||||
Texture => {
|
|
||||||
return stored.data.texture;
|
// TODO: Delegate freeing to worker threads?
|
||||||
},
|
Assets.asset_map_mutex.lock();
|
||||||
else => @compileError("Cannot get asset of type " ++ @typeName(@TypeOf(asset))),
|
defer Assets.asset_map_mutex.unlock();
|
||||||
|
while (Assets.free_board.pop()) |request| {
|
||||||
|
if (@atomicLoad(usize, &request.counter, .monotonic) == 0) {
|
||||||
|
if (!Assets.asset_map.remove(.{ .type = request.type, .path = request.path })) continue;
|
||||||
|
request.unload(Game.alloc);
|
||||||
|
Game.alloc.destroy(request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unreachable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn loadFile(alloc: std.mem.Allocator, path: []const u8) ![]u8 {
|
pub fn load(comptime asset_type: AssetType, path: []const u8) AssetContainer(asset_type.getType()) {
|
||||||
const file = try std.fs.cwd().openFile(path, .{});
|
const asset = mapAsset(asset_type, path);
|
||||||
defer file.close();
|
{
|
||||||
return file.readToEndAlloc(alloc, std.math.maxInt(i32));
|
Assets.request_board_mutex.lock();
|
||||||
|
Assets.request_board.append(Game.alloc, asset) catch err.oom();
|
||||||
|
_ = @atomicRmw(usize, &Assets.request_board_counter, .Add, 1, .monotonic);
|
||||||
|
Assets.request_board_mutex.unlock();
|
||||||
|
}
|
||||||
|
return .{ .asset_pointer = asset };
|
||||||
}
|
}
|
||||||
fn typeFromAssetType(comptime asset_type: AssetType) type {
|
|
||||||
return switch (asset_type) {
|
pub fn free(asset: anytype) void {
|
||||||
.texture => Texture,
|
const prev = @atomicRmw(usize, &asset.asset_pointer.counter, .Sub, 1, .monotonic);
|
||||||
|
if (prev == 1) {
|
||||||
|
Assets.free_board_mutex.lock();
|
||||||
|
Assets.free_board.append(Game.alloc, asset.asset_pointer) catch err.oom();
|
||||||
|
Assets.free_board_mutex.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loaderLoop(worker_id: usize) void {
|
||||||
|
var processed: usize = 0;
|
||||||
|
defer @atomicStore(bool, &Assets.workers[worker_id].running, false, .release);
|
||||||
|
while (true) {
|
||||||
|
const asset = blk: {
|
||||||
|
Assets.request_board_mutex.lock();
|
||||||
|
defer Assets.request_board_mutex.unlock();
|
||||||
|
|
||||||
|
const request = Assets.request_board.pop() orelse return;
|
||||||
|
_ = @atomicRmw(usize, &Assets.request_board_counter, .Sub, 1, .monotonic);
|
||||||
|
break :blk request;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defer processed += 1;
|
||||||
|
asset.mutex.lock();
|
||||||
|
if (asset.state == .not_loaded)
|
||||||
|
asset.load(Game.alloc);
|
||||||
|
asset.mutex.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn assetTypeFromType(comptime T: type) type {
|
|
||||||
return switch (T) {
|
fn mapAsset(comptime asset_type: AssetType, path: []const u8) *AssetCell {
|
||||||
Texture => AssetTexture,
|
Assets.asset_map_mutex.lock();
|
||||||
else => unreachable,
|
defer Assets.asset_map_mutex.unlock();
|
||||||
|
|
||||||
|
const res = Assets.asset_map.getOrPut(Game.alloc, .{ .type = asset_type, .path = path }) catch err.oom();
|
||||||
|
if (!res.found_existing) {
|
||||||
|
res.value_ptr.* = Game.alloc.create(AssetCell) catch err.oom();
|
||||||
|
res.value_ptr.*.* = .{
|
||||||
|
.mutex = .{},
|
||||||
|
.type = asset_type,
|
||||||
|
.data = undefined,
|
||||||
|
.path = path,
|
||||||
|
.loader = Assets.makeLoader(asset_type.getType(), asset_type.getType().load),
|
||||||
|
.unloader = Assets.makeUnloader(asset_type.getType(), asset_type.getType().unload),
|
||||||
|
.state = .not_loaded,
|
||||||
|
.counter = 1,
|
||||||
};
|
};
|
||||||
|
} else _ = @atomicRmw(usize, &res.value_ptr.*.counter, .Add, 1, .monotonic);
|
||||||
|
return res.value_ptr.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makeLoader(comptime T: type, comptime func: *const fn ([]const u8, std.mem.Allocator) LoadError!T) *const fn (*AssetCell, std.mem.Allocator) LoadError!void {
|
||||||
|
const Container = struct {
|
||||||
|
pub fn loader(cell: *AssetCell, alloc: std.mem.Allocator) LoadError!void {
|
||||||
|
const mem = try alloc.create(T);
|
||||||
|
errdefer alloc.destroy(mem);
|
||||||
|
mem.* = try func(cell.path, alloc);
|
||||||
|
cell.data = @ptrCast(mem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Container.loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makeUnloader(comptime T: type, comptime func: *const fn (T, std.mem.Allocator) void) *const fn (*AssetCell, std.mem.Allocator) void {
|
||||||
|
const Container = struct {
|
||||||
|
pub fn unloader(cell: *AssetCell, alloc: std.mem.Allocator) void {
|
||||||
|
func(@as(*T, @alignCast(@ptrCast(cell.data))).*, alloc);
|
||||||
|
alloc.destroy(@as(*T, @alignCast(@ptrCast(cell.data))));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Container.unloader;
|
||||||
}
|
}
|
||||||
|
14
src/assets/file.zig
Normal file
14
src/assets/file.zig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Assets = @import("../assets.zig");
|
||||||
|
|
||||||
|
bytes: []u8,
|
||||||
|
|
||||||
|
pub fn load(path: []const u8, alloc: std.mem.Allocator) Assets.LoadError!@This() {
|
||||||
|
const file = try std.fs.cwd().openFile(path, .{});
|
||||||
|
defer file.close();
|
||||||
|
return .{ .bytes = try file.readToEndAlloc(alloc, std.math.maxInt(i32)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unload(self: @This(), alloc: std.mem.Allocator) void {
|
||||||
|
alloc.free(self.bytes);
|
||||||
|
}
|
34
src/assets/texture.zig
Normal file
34
src/assets/texture.zig
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const sdl = @import("sdl");
|
||||||
|
const err = @import("../error.zig");
|
||||||
|
const c = @import("../c.zig");
|
||||||
|
const Assets = @import("../assets.zig");
|
||||||
|
const Graphics = @import("../graphics.zig");
|
||||||
|
|
||||||
|
texture: *sdl.GPUTexture,
|
||||||
|
sampler: *sdl.GPUSampler,
|
||||||
|
|
||||||
|
pub fn load(path: []const u8, alloc: std.mem.Allocator) Assets.LoadError!@This() {
|
||||||
|
_ = alloc;
|
||||||
|
var file = Assets.load(.file, path);
|
||||||
|
defer Assets.free(file);
|
||||||
|
const data = (try file.getSync()).bytes;
|
||||||
|
|
||||||
|
var x: i32 = undefined;
|
||||||
|
var y: i32 = undefined;
|
||||||
|
var z: i32 = undefined;
|
||||||
|
const image = c.stbi_load_from_memory(@ptrCast(data), @intCast(data.len), &x, &y, &z, 4);
|
||||||
|
if (image == null) err.stbi();
|
||||||
|
const image_slice = image[0..@intCast(x * y * z)];
|
||||||
|
const texture, const sampler = Graphics.loadTexture(@intCast(x), @intCast(y), image_slice);
|
||||||
|
c.stbi_image_free(image);
|
||||||
|
return .{
|
||||||
|
.texture = texture,
|
||||||
|
.sampler = sampler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unload(self: @This(), alloc: std.mem.Allocator) void {
|
||||||
|
_ = alloc;
|
||||||
|
Graphics.unloadTexture(self.texture, self.sampler);
|
||||||
|
}
|
@@ -62,6 +62,7 @@ pub fn run() void {
|
|||||||
World.draw();
|
World.draw();
|
||||||
Game.endDraw();
|
Game.endDraw();
|
||||||
}
|
}
|
||||||
|
Assets.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -420,9 +420,9 @@ pub fn clearDepth() void {
|
|||||||
sdl.PushGPUVertexUniformData(Graphics.command_buffer, 0, &Graphics.camera.matrix, 16 * 4);
|
sdl.PushGPUVertexUniformData(Graphics.command_buffer, 0, &Graphics.camera.matrix, 16 * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drawMesh(mesh: Mesh, texture: Assets.Texture, transform: Transform) void {
|
pub fn drawMesh(mesh: Mesh, texture: *Assets.Texture, transform: Transform) void {
|
||||||
if (Graphics.render_pass == null) return;
|
if (Graphics.render_pass == null) return;
|
||||||
const asset_texture = Assets.get(texture) orelse return;
|
const asset_texture = texture.get() orelse return;
|
||||||
|
|
||||||
sdl.PushGPUVertexUniformData(Graphics.command_buffer, 1, &transform.matrix(), 16 * 4);
|
sdl.PushGPUVertexUniformData(Graphics.command_buffer, 1, &transform.matrix(), 16 * 4);
|
||||||
sdl.BindGPUFragmentSamplers(Graphics.render_pass, 0, &sdl.GPUTextureSamplerBinding{
|
sdl.BindGPUFragmentSamplers(Graphics.render_pass, 0, &sdl.GPUTextureSamplerBinding{
|
||||||
|
@@ -91,6 +91,7 @@ pub fn initDebug() void {
|
|||||||
};
|
};
|
||||||
World.object_map.put(Game.alloc, @intCast(i), i) catch err.oom();
|
World.object_map.put(Game.alloc, @intCast(i), i) catch err.oom();
|
||||||
}
|
}
|
||||||
|
Assets.free(Assets.load(.texture, "data/yakuza.png"));
|
||||||
|
|
||||||
World.plane_mesh = Graphics.loadMesh(@ptrCast(&PLANE_MESH_DATA));
|
World.plane_mesh = Graphics.loadMesh(@ptrCast(&PLANE_MESH_DATA));
|
||||||
World.cube_mesh = Graphics.loadMesh(@ptrCast(&CUBE_MESH_DATA));
|
World.cube_mesh = Graphics.loadMesh(@ptrCast(&CUBE_MESH_DATA));
|
||||||
@@ -323,16 +324,16 @@ pub fn updateObject(object: *Object, delta: f32) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw() void {
|
pub fn draw() void {
|
||||||
Graphics.drawMesh(World.table_mesh, World.texture, .{});
|
Graphics.drawMesh(World.table_mesh, &World.texture, .{});
|
||||||
|
|
||||||
for (World.objects.items) |*object| {
|
for (World.objects.items) |*object| {
|
||||||
if (object.parent != .dock)
|
if (object.parent != .dock)
|
||||||
Graphics.drawMesh(object.mesh, object.texture, object.drawingTransform());
|
Graphics.drawMesh(object.mesh, &object.texture, object.drawingTransform());
|
||||||
}
|
}
|
||||||
|
|
||||||
Graphics.drawMesh(
|
Graphics.drawMesh(
|
||||||
World.plane_mesh,
|
World.plane_mesh,
|
||||||
World.hand_texture,
|
&World.hand_texture,
|
||||||
Graphics.Transform.combineTransforms(
|
Graphics.Transform.combineTransforms(
|
||||||
.{
|
.{
|
||||||
.position = .{ World.hand_scale * 0.5, -World.hand_scale * 0.5, 0 },
|
.position = .{ World.hand_scale * 0.5, -World.hand_scale * 0.5, 0 },
|
||||||
@@ -345,7 +346,7 @@ pub fn draw() void {
|
|||||||
Graphics.clearDepth();
|
Graphics.clearDepth();
|
||||||
for (World.objects.items) |*object| {
|
for (World.objects.items) |*object| {
|
||||||
if (object.parent == .dock)
|
if (object.parent == .dock)
|
||||||
Graphics.drawMesh(object.mesh, object.texture, object.drawingTransform());
|
Graphics.drawMesh(object.mesh, &object.texture, object.drawingTransform());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user