Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions modules/world-lod/src/lod_chunk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ pub const ILODConfig = struct {
getQEMTarget: *const fn (ptr: *anyopaque, lod: LODLevel) u32,
getQEMMinInputTriangles: *const fn (ptr: *anyopaque) u32,
getFogStartPercent: *const fn (ptr: *anyopaque, lod: LODLevel) f32,
getFallbackMissingChildThreshold: *const fn (ptr: *anyopaque) f32,
};

pub fn getRadii(self: ILODConfig) [LODLevel.count]i32 {
Expand Down Expand Up @@ -264,6 +265,10 @@ pub const ILODConfig = struct {
pub fn getFogStartPercent(self: ILODConfig, lod: LODLevel) f32 {
return self.vtable.getFogStartPercent(self.ptr, lod);
}

pub fn getFallbackMissingChildThreshold(self: ILODConfig) f32 {
return self.vtable.getFallbackMissingChildThreshold(self.ptr);
}
};

pub fn activeLODCount(config: ILODConfig) usize {
Expand Down Expand Up @@ -297,6 +302,10 @@ pub const LODConfig = struct {

active_lod_count: u32 = 4,

/// Maximum fraction of direct finer child regions that may be missing before
/// a coarser parent must remain visible as fallback terrain.
fallback_missing_child_threshold: f32 = 0.2,

pub fn getQEMTarget(self: *const LODConfig, lod: LODLevel) u32 {
return self.qem_triangle_targets[@intFromEnum(lod)];
}
Expand Down Expand Up @@ -355,6 +364,7 @@ pub const LODConfig = struct {
.getQEMTarget = getQEMTargetWrapper,
.getQEMMinInputTriangles = getQEMMinInputTrianglesWrapper,
.getFogStartPercent = getFogStartPercentWrapper,
.getFallbackMissingChildThreshold = getFallbackMissingChildThresholdWrapper,
};

fn getRadiiWrapper(ptr: *anyopaque) [LODLevel.count]i32 {
Expand Down Expand Up @@ -408,6 +418,10 @@ pub const LODConfig = struct {
const self: *LODConfig = @ptrCast(@alignCast(ptr));
return self.fog_start_percent[@intFromEnum(lod)];
}
fn getFallbackMissingChildThresholdWrapper(ptr: *anyopaque) f32 {
const self: *LODConfig = @ptrCast(@alignCast(ptr));
return std.math.clamp(self.fallback_missing_child_threshold, 0.0, 1.0);
}
};

// Tests
Expand Down Expand Up @@ -503,3 +517,15 @@ test "ILODConfig.calculateMaskRadius" {
config.radii[0] = 32;
try std.testing.expectEqual(@as(f32, 480.0), interface.calculateMaskRadius());
}

test "ILODConfig exposes fallback missing child threshold" {
var config = LODConfig{ .fallback_missing_child_threshold = 0.2 };
var interface = config.interface();
try std.testing.expectEqual(@as(f32, 0.2), interface.getFallbackMissingChildThreshold());

config.fallback_missing_child_threshold = -1.0;
try std.testing.expectEqual(@as(f32, 0.0), interface.getFallbackMissingChildThreshold());

config.fallback_missing_child_threshold = 2.0;
try std.testing.expectEqual(@as(f32, 1.0), interface.getFallbackMissingChildThreshold());
}
198 changes: 192 additions & 6 deletions modules/world-lod/src/lod_renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ pub fn LODRenderer(comptime RHI: type) type {
diag.bad_state += 1;
continue;
}
if (self.isCoveredByFinerLOD(entry.key_ptr.*, all_meshes, all_regions)) {
if (self.isCoveredByFinerLOD(entry.key_ptr.*, all_meshes, all_regions, config)) {
diag.covered_finer_lod += 1;
continue;
}
Expand Down Expand Up @@ -338,6 +338,7 @@ pub fn LODRenderer(comptime RHI: type) type {
key: LODRegionKey,
all_meshes: *const [LODLevel.count]MeshMap,
all_regions: *const [LODLevel.count]RegionMap,
config: ILODConfig,
) bool {
if (key.lod == .lod1) return false;

Expand All @@ -350,20 +351,38 @@ pub fn LODRenderer(comptime RHI: type) type {
const finer_min_rz = @divFloor(bounds.min_z, finer_scale);
const finer_max_rz = @divFloor(bounds.max_z, finer_scale);

var total_children: u32 = 0;
var missing_children: u32 = 0;

var rz = finer_min_rz;
while (rz <= finer_max_rz) : (rz += 1) {
var rx = finer_min_rx;
while (rx <= finer_max_rx) : (rx += 1) {
total_children += 1;
const finer_key = LODRegionKey{ .rx = rx, .rz = rz, .lod = finer_lod };
const finer_chunk = all_regions[finer_index].get(finer_key) orelse return false;
if (finer_chunk.state != .renderable) return false;
const finer_chunk = all_regions[finer_index].get(finer_key) orelse {
missing_children += 1;
continue;
};
if (finer_chunk.state != .renderable) {
missing_children += 1;
continue;
}

const finer_mesh = all_meshes[finer_index].get(finer_key) orelse return false;
if (!finer_mesh.ready or finer_mesh.vertex_count == 0) return false;
const finer_mesh = all_meshes[finer_index].get(finer_key) orelse {
missing_children += 1;
continue;
};
if (!finer_mesh.ready or finer_mesh.vertex_count == 0) {
missing_children += 1;
}
}
}

return true;
if (total_children == 0) return false;

const missing_fraction = @as(f32, @floatFromInt(missing_children)) / @as(f32, @floatFromInt(total_children));
return missing_fraction <= config.getFallbackMissingChildThreshold();
}

const CoverageResult = struct {
Expand Down Expand Up @@ -1014,6 +1033,173 @@ test "LODRenderer skips coarse LOD when finer coverage is ready" {
try std.testing.expectEqual(@as(u32, 4), mock_state.draw_calls);
}

test "LODRenderer keeps coarse LOD when a finer child is missing" {
const allocator = std.testing.allocator;

const MockRHIState = struct {
draw_calls: u32 = 0,
handle_sum: u32 = 0,
};

const MockRHI = struct {
state: *MockRHIState,

pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 {
return 1;
}
pub fn destroyBuffer(_: @This(), _: u32) void {}
pub fn getFrameIndex(_: @This()) usize {
return 0;
}
pub fn setModelMatrix(_: @This(), _: Mat4, _: Vec3, _: f32) void {}
pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {}
pub fn setSelectionMode(_: @This(), _: bool) void {}
pub fn draw(self: @This(), handle: u32, _: u32, _: anytype) void {
self.state.draw_calls += 1;
self.state.handle_sum += handle;
}
};

var mock_state = MockRHIState{};
const mock_rhi = MockRHI{ .state = &mock_state };

const Renderer = LODRenderer(MockRHI);
const renderer = try Renderer.init(allocator, mock_rhi);
defer renderer.deinit();

var meshes: [LODLevel.count]MeshMap = undefined;
var regions: [LODLevel.count]RegionMap = undefined;
for (0..LODLevel.count) |i| {
meshes[i] = MeshMap.init(allocator);
regions[i] = RegionMap.init(allocator);
}
defer {
for (0..LODLevel.count) |i| {
meshes[i].deinit();
regions[i].deinit();
}
}

var coarse_mesh = LODMesh.init(allocator, .lod2);
coarse_mesh.buffer_handle = 100;
coarse_mesh.vertex_count = 12;
coarse_mesh.ready = true;
var coarse_chunk = LODChunk.init(2, 0, .lod2);
coarse_chunk.state = .renderable;
const coarse_key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod2 };
try meshes[2].put(coarse_key, &coarse_mesh);
try regions[2].put(coarse_key, &coarse_chunk);

const finer_keys = [_]LODRegionKey{
.{ .rx = 4, .rz = 0, .lod = .lod1 },
.{ .rx = 5, .rz = 0, .lod = .lod1 },
.{ .rx = 4, .rz = 1, .lod = .lod1 },
};
var finer_chunks: [finer_keys.len]LODChunk = undefined;
var finer_meshes: [finer_keys.len]LODMesh = undefined;
for (finer_keys, 0..) |finer_key, idx| {
finer_chunks[idx] = LODChunk.init(finer_key.rx, finer_key.rz, .lod1);
finer_chunks[idx].state = .renderable;
finer_meshes[idx] = LODMesh.init(allocator, .lod1);
finer_meshes[idx].buffer_handle = @as(u32, @intCast(idx + 1));
finer_meshes[idx].vertex_count = 24;
finer_meshes[idx].ready = true;
try meshes[1].put(finer_key, &finer_meshes[idx]);
try regions[1].put(finer_key, &finer_chunks[idx]);
}

var mock_config = LODConfig{ .radii = .{ 16, 32, 64, 100 } };

renderer.render(&meshes, &regions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null);

try std.testing.expectEqual(@as(u32, 4), mock_state.draw_calls);
try std.testing.expectEqual(@as(u32, 106), mock_state.handle_sum);
}

test "LODRenderer resolves finer coverage across negative region boundaries" {
const allocator = std.testing.allocator;

const MockRHIState = struct {
draw_calls: u32 = 0,
handle_sum: u32 = 0,
};

const MockRHI = struct {
state: *MockRHIState,

pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 {
return 1;
}
pub fn destroyBuffer(_: @This(), _: u32) void {}
pub fn getFrameIndex(_: @This()) usize {
return 0;
}
pub fn setModelMatrix(_: @This(), _: Mat4, _: Vec3, _: f32) void {}
pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {}
pub fn setSelectionMode(_: @This(), _: bool) void {}
pub fn draw(self: @This(), handle: u32, _: u32, _: anytype) void {
self.state.draw_calls += 1;
self.state.handle_sum += handle;
}
};

var mock_state = MockRHIState{};
const mock_rhi = MockRHI{ .state = &mock_state };

const Renderer = LODRenderer(MockRHI);
const renderer = try Renderer.init(allocator, mock_rhi);
defer renderer.deinit();

var meshes: [LODLevel.count]MeshMap = undefined;
var regions: [LODLevel.count]RegionMap = undefined;
for (0..LODLevel.count) |i| {
meshes[i] = MeshMap.init(allocator);
regions[i] = RegionMap.init(allocator);
}
defer {
for (0..LODLevel.count) |i| {
meshes[i].deinit();
regions[i].deinit();
}
}

var coarse_mesh = LODMesh.init(allocator, .lod2);
coarse_mesh.buffer_handle = 100;
coarse_mesh.vertex_count = 12;
coarse_mesh.ready = true;
var coarse_chunk = LODChunk.init(-1, -1, .lod2);
coarse_chunk.state = .renderable;
const coarse_key = LODRegionKey{ .rx = -1, .rz = -1, .lod = .lod2 };
try meshes[2].put(coarse_key, &coarse_mesh);
try regions[2].put(coarse_key, &coarse_chunk);

const finer_keys = [_]LODRegionKey{
.{ .rx = -2, .rz = -2, .lod = .lod1 },
.{ .rx = -1, .rz = -2, .lod = .lod1 },
.{ .rx = -2, .rz = -1, .lod = .lod1 },
.{ .rx = -1, .rz = -1, .lod = .lod1 },
};
var finer_chunks: [finer_keys.len]LODChunk = undefined;
var finer_meshes: [finer_keys.len]LODMesh = undefined;
for (finer_keys, 0..) |finer_key, idx| {
finer_chunks[idx] = LODChunk.init(finer_key.rx, finer_key.rz, .lod1);
finer_chunks[idx].state = .renderable;
finer_meshes[idx] = LODMesh.init(allocator, .lod1);
finer_meshes[idx].buffer_handle = @as(u32, @intCast(idx + 1));
finer_meshes[idx].vertex_count = 24;
finer_meshes[idx].ready = true;
try meshes[1].put(finer_key, &finer_meshes[idx]);
try regions[1].put(finer_key, &finer_chunks[idx]);
}

var mock_config = LODConfig{ .radii = .{ 16, 32, 64, 100 } };

renderer.render(&meshes, &regions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null);

try std.testing.expectEqual(@as(u32, 4), mock_state.draw_calls);
try std.testing.expectEqual(@as(u32, 10), mock_state.handle_sum);
}

test "LODRenderer createGPUBridge and toInterface round-trip" {
const allocator = std.testing.allocator;

Expand Down
Loading