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
23 changes: 23 additions & 0 deletions docs/lod-quality-controls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# LOD Quality Controls

The render distance preset is the supported user-facing control for distant LOD quality. Presets intentionally expose a small set of stable knobs:

- `lod_radii`: chunk radii for LOD0 through LOD3.
- `horizontal_detail`: target horizontal detail per LOD. This is used as a floor for QEM triangle targets when the experimental QEM mesh path is enabled.
- `vertical_span_budget`: enables rich column/span source data when greater than zero. Defaults keep this disabled to preserve current memory and generation cost.
- `mesh_path`: selects the stable heightfield path by default. `column_spans` and `qem` are available for controlled testing.
- `fog_start_percent`: controls the fade band for each LOD level.
- `memory_budget_mb` and `max_uploads_per_frame`: bound cache pressure and per-frame GPU upload work.

Default presets preserve existing performance expectations by keeping `mesh_path = .heightfield` and `vertical_span_budget = 0`.

## Diagnostics

Set `ZIGCRAFT_LOD_DIAG=1` to log LOD queue, render, and aggregate stats diagnostics. The output includes generation and upload queue depth, cache hits and misses, cache hit rate, mesh counts, vertex counts, visible region filtering, and fallback/culling reasons.

Runtime mesh path overrides are available while alternate mesh paths stabilize:

- `ZIGCRAFT_LOD_MESH_PATH_QEM=1` forces the QEM decimation path.
- `ZIGCRAFT_LOD_MESH_PATH_SPANS=1` forces the column/span mesh path.

Use the stable heightfield defaults for normal gameplay. Use the override flags only when comparing visual quality or diagnosing LOD mesh regressions.
10 changes: 8 additions & 2 deletions modules/engine-core/src/job_system.zig
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ pub const JobQueue = struct {
self.cond.signal();
}

pub fn count(self: *JobQueue) usize {
self.mutex.lock();
defer self.mutex.unlock();
return self.jobs.count();
}

pub fn pop(self: *JobQueue) ?Job {
self.mutex.lock();
defer self.mutex.unlock();
Expand All @@ -198,8 +204,8 @@ pub const JobQueue = struct {

/// Internal: rebuild queue with updated distances (called under lock)
fn doReprioritize(self: *JobQueue) void {
const count = self.jobs.count();
if (count == 0) return;
const job_count = self.jobs.count();
if (job_count == 0) return;

var temp = std.ArrayListUnmanaged(Job).empty;
defer temp.deinit(self.allocator);
Expand Down
27 changes: 27 additions & 0 deletions modules/engine-rhi/src/render_settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,20 @@ pub const RenderDistancePreset = enum(u32) {
}
};

pub const LODMeshPath = enum(u8) {
/// Stable heightfield mesh path used by current defaults.
heightfield,
/// Rich column/span mesh path for vertical detail where source data provides spans.
column_spans,
/// Quadric error metric decimation path for experimentation while it stabilizes.
qem,
};

pub const RenderDistancePresetConfig = struct {
lod_radii: [LODLevel.count]i32,
horizontal_detail: [LODLevel.count]u32,
vertical_span_budget: u8,
mesh_path: LODMeshPath,
fog_start_percent: [LODLevel.count]f32,
active_lod_count: u32,
qem_targets: [LODLevel.count]u32,
Expand All @@ -38,6 +50,9 @@ pub const RenderDistancePresetConfig = struct {
pub const RENDER_DISTANCE_PRESETS = [_]RenderDistancePresetConfig{
.{
.lod_radii = .{ 6, 6, 6, 160 },
.horizontal_detail = .{ 16, 32, 48, 48 },
.vertical_span_budget = 0,
.mesh_path = .heightfield,
.fog_start_percent = .{ 0.5, 0.5, 0.4, 0.3 },
.active_lod_count = 4,
.qem_targets = .{ 0, 1200, 300, 48 },
Expand All @@ -49,6 +64,9 @@ pub const RENDER_DISTANCE_PRESETS = [_]RenderDistancePresetConfig{
},
.{
.lod_radii = .{ 10, 10, 10, 160 },
.horizontal_detail = .{ 16, 32, 48, 48 },
.vertical_span_budget = 0,
.mesh_path = .heightfield,
.fog_start_percent = .{ 0.5, 0.5, 0.4, 0.4 },
.active_lod_count = 4,
.qem_targets = .{ 0, 2000, 800, 200 },
Expand All @@ -60,6 +78,9 @@ pub const RENDER_DISTANCE_PRESETS = [_]RenderDistancePresetConfig{
},
.{
.lod_radii = .{ 12, 12, 12, 160 },
.horizontal_detail = .{ 16, 32, 48, 48 },
.vertical_span_budget = 0,
.mesh_path = .heightfield,
.fog_start_percent = .{ 0.5, 0.5, 0.4, 0.3 },
.active_lod_count = 4,
.qem_targets = .{ 0, 2000, 800, 200 },
Expand All @@ -71,6 +92,9 @@ pub const RENDER_DISTANCE_PRESETS = [_]RenderDistancePresetConfig{
},
.{
.lod_radii = .{ 14, 14, 14, 160 },
.horizontal_detail = .{ 16, 32, 48, 48 },
.vertical_span_budget = 0,
.mesh_path = .heightfield,
.fog_start_percent = .{ 0.5, 0.5, 0.4, 0.3 },
.active_lod_count = 4,
.qem_targets = .{ 0, 2000, 800, 200 },
Expand All @@ -82,6 +106,9 @@ pub const RENDER_DISTANCE_PRESETS = [_]RenderDistancePresetConfig{
},
.{
.lod_radii = .{ 16, 16, 16, 160 },
.horizontal_detail = .{ 16, 32, 48, 48 },
.vertical_span_budget = 0,
.mesh_path = .heightfield,
.fog_start_percent = .{ 0.5, 0.5, 0.4, 0.3 },
.active_lod_count = 4,
.qem_targets = .{ 0, 2000, 800, 200 },
Expand Down
1 change: 1 addition & 0 deletions modules/engine-rhi/src/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub const Texture = texture.Texture;
pub const Config = texture.Config;
pub const RenderDistancePreset = render_settings.RenderDistancePreset;
pub const RenderDistancePresetConfig = render_settings.RenderDistancePresetConfig;
pub const LODMeshPath = render_settings.LODMeshPath;
pub const RENDER_DISTANCE_PRESETS = render_settings.RENDER_DISTANCE_PRESETS;
pub const RenderSettingsAdapter = render_settings.RenderSettingsAdapter;
pub const getPresetConfig = render_settings.getPresetConfig;
Expand Down
3 changes: 3 additions & 0 deletions modules/game-core/src/session.zig
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ pub const GameSession = struct {
LODConfig{
.radii = preset_radii,
.fog_start_percent = preset_cfg.fog_start_percent,
.horizontal_detail = preset_cfg.horizontal_detail,
.vertical_span_budget = preset_cfg.vertical_span_budget,
.mesh_path = preset_cfg.mesh_path,
.qem_triangle_targets = preset_cfg.qem_targets,
.memory_budget_mb = preset_cfg.memory_budget_mb,
.max_uploads_per_frame = preset_cfg.max_uploads_per_frame,
Expand Down
50 changes: 50 additions & 0 deletions modules/world-lod/src/lod_chunk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Chunk = world_core.Chunk;
const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X;
const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z;
const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y;
pub const LODMeshPath = @import("engine-rhi").LODMeshPath;

/// LOD level enum - higher values = more simplified
pub const LODLevel = @import("lod_types.zig").LODLevel;
Expand Down Expand Up @@ -219,6 +220,9 @@ pub const ILODConfig = struct {
calculateMaskRadius: *const fn (ptr: *anyopaque) f32,
getQEMTarget: *const fn (ptr: *anyopaque, lod: LODLevel) u32,
getQEMMinInputTriangles: *const fn (ptr: *anyopaque) u32,
getHorizontalDetail: *const fn (ptr: *anyopaque, lod: LODLevel) u32,
getVerticalSpanBudget: *const fn (ptr: *anyopaque) u8,
getMeshPath: *const fn (ptr: *anyopaque) LODMeshPath,
getFogStartPercent: *const fn (ptr: *anyopaque, lod: LODLevel) f32,
getFallbackMissingChildThreshold: *const fn (ptr: *anyopaque) f32,
};
Expand Down Expand Up @@ -262,6 +266,18 @@ pub const ILODConfig = struct {
return self.vtable.getQEMMinInputTriangles(self.ptr);
}

pub fn getHorizontalDetail(self: ILODConfig, lod: LODLevel) u32 {
return self.vtable.getHorizontalDetail(self.ptr, lod);
}

pub fn getVerticalSpanBudget(self: ILODConfig) u8 {
return self.vtable.getVerticalSpanBudget(self.ptr);
}

pub fn getMeshPath(self: ILODConfig) LODMeshPath {
return self.vtable.getMeshPath(self.ptr);
}

pub fn getFogStartPercent(self: ILODConfig, lod: LODLevel) f32 {
return self.vtable.getFogStartPercent(self.ptr, lod);
}
Expand Down Expand Up @@ -292,6 +308,12 @@ pub const LODConfig = struct {
/// Values closer to 0.0 start fog near the player; 1.0 disables fog for that level.
fog_start_percent: [LODLevel.count]f32 = .{ 0.55, 0.48, 0.38, 0.28 },

horizontal_detail: [LODLevel.count]u32 = .{ 16, 32, 48, 48 },

vertical_span_budget: u8 = 0,

mesh_path: LODMeshPath = .heightfield,

qem_triangle_targets: [LODLevel.count]u32 = .{ 0, 2000, 800, 200 },

qem_min_input_triangles: u32 = 50,
Expand Down Expand Up @@ -363,6 +385,9 @@ pub const LODConfig = struct {
.calculateMaskRadius = calculateMaskRadiusWrapper,
.getQEMTarget = getQEMTargetWrapper,
.getQEMMinInputTriangles = getQEMMinInputTrianglesWrapper,
.getHorizontalDetail = getHorizontalDetailWrapper,
.getVerticalSpanBudget = getVerticalSpanBudgetWrapper,
.getMeshPath = getMeshPathWrapper,
.getFogStartPercent = getFogStartPercentWrapper,
.getFallbackMissingChildThreshold = getFallbackMissingChildThresholdWrapper,
};
Expand Down Expand Up @@ -414,6 +439,18 @@ pub const LODConfig = struct {
const self: *LODConfig = @ptrCast(@alignCast(ptr));
return self.qem_min_input_triangles;
}
fn getHorizontalDetailWrapper(ptr: *anyopaque, lod: LODLevel) u32 {
const self: *LODConfig = @ptrCast(@alignCast(ptr));
return self.horizontal_detail[@intFromEnum(lod)];
}
fn getVerticalSpanBudgetWrapper(ptr: *anyopaque) u8 {
const self: *LODConfig = @ptrCast(@alignCast(ptr));
return @min(self.vertical_span_budget, @as(u8, @intCast(world_core.MAX_LOD_VERTICAL_SPANS)));
}
fn getMeshPathWrapper(ptr: *anyopaque) LODMeshPath {
const self: *LODConfig = @ptrCast(@alignCast(ptr));
return self.mesh_path;
}
fn getFogStartPercentWrapper(ptr: *anyopaque, lod: LODLevel) f32 {
const self: *LODConfig = @ptrCast(@alignCast(ptr));
return self.fog_start_percent[@intFromEnum(lod)];
Expand Down Expand Up @@ -529,3 +566,16 @@ test "ILODConfig exposes fallback missing child threshold" {
config.fallback_missing_child_threshold = 2.0;
try std.testing.expectEqual(@as(f32, 1.0), interface.getFallbackMissingChildThreshold());
}

test "ILODConfig exposes LOD quality tuning controls" {
var config = LODConfig{
.horizontal_detail = .{ 16, 24, 32, 40 },
.vertical_span_budget = 99,
.mesh_path = .qem,
};
const interface = config.interface();

try std.testing.expectEqual(@as(u32, 32), interface.getHorizontalDetail(.lod2));
try std.testing.expectEqual(@as(u8, world_core.MAX_LOD_VERTICAL_SPANS), interface.getVerticalSpanBudget());
try std.testing.expectEqual(LODMeshPath.qem, interface.getMeshPath());
}
Loading
Loading