diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md b/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md index c451185..64677f7 100644 --- a/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md +++ b/Standalone/Research/OwnerShapeWorld/speed_round/REPORT.md @@ -60,13 +60,100 @@ The exported-LOD6 captures use the same six fixed cameras as the previous set. E The opt-in LOD8 candidate is in [StandaloneViewer.cpp](../../../Source/StandaloneViewer.cpp). After the final clean build, its W1/W8 hash runs matched all 11 aggregate outputs with zero mismatches; the full-clipmap hashes are density `0x2809003356cf8162`, geometry `0x0123c52b74ad7e7c`, payload `0xcdef10ebb6b24321`, topology `0x1fa3bf7db18a2c8d`. Cache hits were zero and the route audit stayed at 5,642/5,642. Logs: [W1](round3_maxlevel8_final_clean_hash_w1.stdout.log), [W8](round3_maxlevel8_final_clean_hash_w8.stdout.log). -All six fixed captures were rerun with the override. Five match the LOD6 images pixel-for-pixel; passage changes 382/1,024,000 pixels, with maximum channel delta 16 and mean absolute channel delta 0.003917. The change is not visible in side-by-side review. Candidate images: [chamber](round3_maxlevel8_candidate_chamber.png), [tunnel](round3_maxlevel8_candidate_tunnel.png), [passage](round3_maxlevel8_candidate_passage.png), [far view](round3_maxlevel8_candidate_far.png), [floor close-up](round3_maxlevel8_candidate_passage_floor_closeup.png), [tunnel bend](round3_maxlevel8_candidate_tunnel_bend.png). Numeric comparison: [diff log](round3_maxlevel8_capture_diffs.log), using [compare_capture_pair.py](compare_capture_pair.py). The six fixed views and existing shape audits pass; the screenshots do not prove every remote thin stratum across the whole world. +## Round 4 — surface detail band -### Round 3 logs, limits, and failed invocations +### Profile and cost -Latest exported-LOD6 timing logs: [run A](round3_post_classifier_full_w8_a.stdout.log), [run B](round3_post_classifier_full_w8_b.stdout.log). Latest opt-in LOD8 timings: [A](round3_maxlevel8_final_w8_a.stdout.log), [B](round3_maxlevel8_final_w8_b.stdout.log), with [runner A](round3_maxlevel8_final_w8_a.runner.log) and [runner B](round3_maxlevel8_final_w8_b.runner.log). Final opt-in LOD8 hashes: [W1](round3_maxlevel8_final_clean_hash_w1.stdout.log) and [W8](round3_maxlevel8_final_clean_hash_w8.stdout.log). The clean build for these outputs is [round3_maxlevel8_final_build.log](round3_maxlevel8_final_build.log). All workload processes ran BelowNormal with affinity `0xFF00`; builds used `/MP8`. The build succeeded. Its existing setup emitted the French `vswhere.exe` diagnostic and six C4838 narrowing warnings in `VoxelStrateManager.cpp`. +The original W8 profile used the unchanged coherent field and sampled one in 256 distance calls and one in 64 detail calls. It completed the shape audit at BelowNormal priority and affinity `0xFF00`; routes were 5,642/5,642. Logs: [profile output](round4_profile_w8.stdout.log), [runner](round4_profile_w8.runner.log). -The first final-build wrapper could not open its relative log path after `Build.ps1` changed into `Standalone\Build`; it produced no usable build log. I reran the build with an absolute log path and verified exit code 0. An initial C# `System.Drawing.Bitmap` image-diff helper also failed to resolve its assembly; the capture comparison was completed with Pillow and NumPy. The unsupported `--stream-max-lod=8` run completed with exit code 0 but did not exercise the supertile path; the corrected `--stream-max-level=8` run confirmed zero groups and exited 0. The first classifier build failed because its new `std::unordered_set` member lacked the header; the corrected build succeeded, and the prototype was later removed. One classifier benchmark launch was rejected with Win32 267 before a child process started because its working directory was invalid; the corrected launch exited 0. The first capture launched with `--max-clip-level=8` exited 0 but the viewer reload reverted to level 6 and generated 6,498 tiles; I fixed runtime override persistence and reran the six captures at 2,226 tiles. The Step=256 proof build succeeded but produced no sample reduction and was reverted. A final build-wrapper setup line attempted to assign PowerShell's reserved `$Host` variable and emitted assignment errors; `Build.ps1` then set its own BelowNormal/`0xFF00` limits and the build completed successfully. No final benchmark process failed. No viewer belonging to another process was closed, and no commit, push, or stash was made. +| Surface-band profile | Original W8 path | +| --- | ---: | +| Candidate visits / `EffectiveRockDistance` evaluations | 12.021 M / 12.021 M | +| `DetailNoiseOnLine` evaluations / retained proxies | 7.072 M / 7.072 M | +| Detail evaluations per candidate | 0.588 | +| Sign flips | 570,546 | +| Band wall / worker sum | 609.4 ms / 4.848 s | +| Worker time per candidate | 403 ns | +| Sampled distance evaluation | 134.7 ns; 1.619 worker-s estimated | +| Sampled detail evaluation | 478.2 ns; 3.382 worker-s estimated | +| Per-worker candidates / active time | 1.479–1.516 M / 605.9–606.1 ms | + +The sampled work estimates total about 5.00 worker-s, slightly above the measured 4.85 worker-s. They are attribution estimates, not an additive timing decomposition. `DetailNoiseOnLine` is the largest measured cost at about 70% of worker time. It evaluates the three line-cached relief octaves and, on applicable strata, owner cave roughness (FBM/ridged/cellular noise and possible domain warps). The workers finish evenly; candidate-count spread is 2.5%, so thread balance is not the bottleneck. + +### Retained candidate and measured result + +The retained standalone candidate combines a conservative local amplitude envelope with an 8-voxel Z lattice for owner cave roughness. `DetailBand` stays at 6, because a radius-5 candidate mask fails the grid-stair audit. At each candidate, the local fade uses the macro relief, surface depth, cave/floor influence, and layer roughness bound; the plateau is 1.5 voxels and the edge is clamped to the coherent field’s supremum plus its 0.125-voxel margin. Owner roughness cores are evaluated at globally aligned 8-voxel Z anchors and linearly interpolated. The same deterministic owner path is used at all worker counts. This changes the relief field; the captures and hashes below are for the changed field. + +The final W8 sampled profile reports 9.389 M candidates, 9.389 M distance evaluations, 3.527 M detail-noise evaluations, 6.755 M proxies, and 550,169 sign flips. Detail evaluations fell 50.1% from the original profile; 3.228 M candidates skip local detail, or 34.4% of candidate visits. Detail evaluations per candidate fell from 0.588 to 0.376. Per-worker candidates were 1.136–1.210 M and detail evaluations 426–455 K; active times were 510.54–510.74 ms. The output is [round4_owner_coarse8_profile_w8.stdout.log](round4_owner_coarse8_profile_w8.stdout.log), with [runner record](round4_owner_coarse8_profile_w8.runner.log). + +The profile also reports a sampled 875 ns detail call, with nested relief/layer/owner scopes. Those added clocks run inside the sampled call and materially perturb it; the 875 ns value and its nested estimates are not production costs and should not be compared directly with the original 478 ns. The uninstrumented interleaved shape-audit timings give a better speed comparison: + +| Owner roughness lattice | W8 surface-band samples | Median | +| --- | ---: | ---: | +| 4 voxels | 613.166 ms, 559.883 ms | 586.525 ms | +| 8 voxels | 522.047 ms, 588.253 ms | 555.150 ms | + +The step-8 median is 5.3% below step 4, but spread remains substantial. The separate first step-8 audit was 538.295 ms. The paired logs are [step 4 A](round4_coarse_ab_step4_a_w8.stdout.log), [step 8 A](round4_coarse_ab_step8_a_w8.stdout.log), [step 8 B](round4_coarse_ab_step8_b_w8.stdout.log), and [step 4 B](round4_coarse_ab_step4_b_w8.stdout.log). The speed target is not met: the best repeated band measurements remain around 0.50–0.59 s, well above 200 ms. + +### Full-payload headline + +Two cold no-hash W8 runs used full UVs, colors, and material groups, generated all 6,498 clipmap tiles, and recorded zero cache hits. Process wall includes executable startup and world setup; collision cooking and GPU upload are not included. + +| Cold full-payload run | Process wall | Shape stage | Surface band | Full-detail stream | Full clipmap stream | Cache hits | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| A | 1.929 s | 0.800 s | 0.505 s | 0.249 s | 0.995 s | 0 | +| B | 1.976 s | 0.790 s | 0.498 s | 0.250 s | 0.981 s | 0 | +| Mean | **1.953 s** | **0.795 s** | **0.502 s** | **0.250 s** | **0.988 s** | **0** | + +The full clipmap allocated 235.29 MiB (115.57 MiB standalone viewer output); LOD6 contributed 4,770 tiles, 32.717 M density samples, 1,834,820 vertices, and 10,272,252 indices. Logs: [run A](round4_step8_final_payload_w8_a.stdout.log), [run B](round4_step8_final_payload_w8_b.stdout.log), with [runner A](round4_step8_final_payload_w8_a.runner.log) and [runner B](round4_step8_final_payload_w8_b.runner.log). Timing varied during this continuation. Against the previous round’s 0.876 s full-clipmap mean, this pair does not show an end-to-end stream improvement; the surface-band audit pair supports only a modest step-8 versus step-4 reduction. + +### W1/W8 hashes and audits + +The exact step-8 executable produced matching hashes for all nine aggregate outputs at W1 and W8. Both cold hash runs had zero cache hits, emitted full payload, and retained 5,642/5,642 open route samples. Hash logs: [W1](round4_step8_final_hash_w1.stdout.log) and [W8](round4_step8_final_hash_w8.stdout.log); runner logs: [W1](round4_step8_final_hash_w1.runner.log), [W8](round4_step8_final_hash_w8.runner.log). + +| Output | Density hash | Mesh hash | Full-payload hash | Oriented-topology hash | +| --- | --- | --- | --- | --- | +| Full detail | `0x67609e3f9aa3a0b0` | `0xd98eb46b92a59917` | `0x8f148240ca4a4a8f` | `0x954e28f6fd44967f` | +| Whole clipmap | `0x11e7c03ed9bbd1f9` | `0xab871e9191389cd7` | `0xf0d1bd9d26592feb` | `0xfbfd275ff05c3c8a` | +| LOD0 | `0xa40e0e5bcee1fa94` | `0x5376053528f938d0` | `0x6c545a92724cb2ef` | `0x9607f9b6a57e0ee0` | +| LOD1 | `0x94e62d7b0bfa25f9` | `0xd793622d676d8152` | `0xb31b4fd33bf48af5` | `0xe3bb5eb0cd0388ae` | +| LOD2 | `0xc4445b7ece46d37d` | `0x413f1af51a6be5aa` | `0xe374e053b00beb34` | `0x36abcd1505201234` | +| LOD3 | `0x9a7a0ce16ae6bc73` | `0x61c0d5eff17d7b13` | `0x596c8d6a850468bf` | `0x86c10f856855083e` | +| LOD4 | `0xe3d15c9695e1960a` | `0xfc24a13f616fa647` | `0x92cb4114a697493d` | `0x92cf751f3e7fd0aa` | +| LOD5 | `0xe3130577ec7f5b23` | `0x5040b88204df27df` | `0x98c9278497e126fc` | `0x61986faf2cd91ba5` | +| LOD6 | `0xf5342998545c37e7` | `0x7c6436bc51bdba44` | `0xb7f44b30f7fb0797` | `0x202cee15737c1705` | + +The W8 shape audit passed RoomFloorJoin, FloorSlopeJoin, TunnelRadius, FlatSurface, GridStair, ThinSheet, SurfaceBandFamily, FlatPatch, and DetailAudit. GridStair `relative_join_step=0.090` is below the 0.400 limit; there are zero isolated thin sheets, cave detail standard deviation is 0.8737 voxels, largest flat patch is 440 samples, and the detail audit reports supremum 4 voxels, band 6, adjacent correlation 0.9910. See [step-8 shape audit](round4_owner_coarse8_audit_w8.stdout.log). The whole-view shard audit in the W8 hash run found zero thin sheets and zero isolated small components (three boundary-touching small components). + +### Six fixed captures + +These six images were captured from the exact step-8 W8 executable. Every capture process exited 0. The before images are the preceding full-payload references; the field change produces small average pixel deltas but affects many shaded pixels, so these are not pixel-identical claims. The numeric comparison is [round4_step8_capture_pixel_compare.stdout.log](round4_step8_capture_pixel_compare.stdout.log), using [compare_capture_pair.py](compare_capture_pair.py). + +| Fixed view and camera (metres; yaw/pitch) | Before | Step-8 capture | Changed pixels | Mean channel delta | Max channel delta | +| --- | --- | --- | ---: | ---: | ---: | +| Chamber `(-11.75,-123.25,-19.85; 0/0)` | [before](final_halflimit_after_chamber.png) | [after](round4_after_chamber.png) | 903,112 / 1,024,000 | 1.856 | 128 | +| Tunnel `(-11.75,-123.25,-19.85; 90/0)` | [before](final_halflimit_after_tunnel.png) | [after](round4_after_tunnel.png) | 910,352 / 1,024,000 | 1.920 | 129 | +| Passage `(-39.19,6.27,-28.51; 63/0)` | [before](final_halflimit_after_passage.png) | [after](round4_after_passage.png) | 804,901 / 1,024,000 | 5.364 | 135 | +| Far `(-11.61244,-118.2212,-19.8452; 0/0)` | [before](final_halflimit_after_far.png) | [after](round4_after_far.png) | 895,399 / 1,024,000 | 1.992 | 131 | +| Passage floor close-up `(-39.19,6.27,-25.51; 63/-25)` | [before](final_halflimit_after_passage_floor_closeup.png) | [after](round4_after_passage_floor_closeup.png) | 902,403 / 1,024,000 | 1.699 | 95 | +| Tunnel bend `(-198.97,-99.57,-45.58; 33.59/23.17)` | [before](final_halflimit_after_tunnel_bend.png) | [after](round4_after_tunnel_bend.png) | 942,860 / 1,024,000 | 1.777 | 55 | + +Capture runner records: [chamber](round4_step8_capture_chamber_w8.runner.log), [tunnel](round4_step8_capture_tunnel_w8.runner.log), [passage](round4_step8_capture_passage_w8.runner.log), [far](round4_step8_capture_far_w8.runner.log), [floor close-up](round4_step8_capture_floor_closeup_w8.runner.log), and [tunnel bend](round4_step8_capture_tunnel_bend_w8.runner.log). + +### Rejected experiments and limits + +| Experiment | Result | Evidence | +| --- | --- | --- | +| Candidate mask radius 5, with global detail support | GridStair audit failed, exit 7. | [build](round4_band5_build.log), [audit](round4_band5_audit_w8.stdout.log), [runner](round4_band5_audit_w8.runner.log) | +| Radius 5 with local bound and 1.5-voxel plateau | GridStair audit failed, exit 7. | [build](round4_local_band5_build.log), [audit](round4_local_band5_audit_w8.stdout.log), [runner](round4_local_band5_audit_w8.runner.log); the later step-4 variant also failed in [this audit](round4_inner15_band5_audit_w8.stdout.log) with [runner](round4_inner15_band5_audit_w8.runner.log) | +| Exact XY plane-interpolation cache | First implementation changed flips from 570,546 to 582,493. The corrected version restored the flip count, but paired timings had no stable gain; removed. | [first audit](round4_plane_cache_audit_w8.stdout.log), [corrected audit](round4_plane_cache_fixed_audit_w8.stdout.log), [A](round4_ab_cache_a_w8.stdout.log), [B](round4_ab_cache_b_w8.stdout.log) | +| Three-at-once Perlin domain-warp helper | Audit flips matched; paired W8 timings were not faster, so scalar calls were restored. | [audit](round4_warp_x3_audit_w8.stdout.log), [A](round4_warp_ab_x3_a_w8.stdout.log), [B](round4_warp_ab_x3_b_w8.stdout.log) | +| Z-to-layer direct map | Hashes/audits matched, but paired W8 timings were no faster and tended slower; removed. | [audit](round4_layer_lookup_audit_w8.stdout.log), [A](round4_lookup_ab_local_a_w8.stdout.log), [B](round4_lookup_ab_local_b_w8.stdout.log) | +| Owner roughness interpolation at Z step 2 | Audits passed but paired W8 median was slower than local-bound baseline; step 4 then measured better and step 8 is retained. | [profile](round4_owner_coarse2_profile_w8.stdout.log), [A](round4_coarse_ab_local_a_w8.stdout.log), [B](round4_coarse_ab_local_b_w8.stdout.log) | + +The step-4 to original-path A/B was too noisy to support a speedup claim: original band runs were 633 and 1,511 ms; step-4 candidate runs were 1,276 and 906 ms. All four processes exited 0, but they show no stable old-versus-new gain. AVX2 batching of the three value-noise octaves was not implemented; the narrower Perlin helper did not help. Far-mesh overlap was not tested because it can hide time in the total stream but cannot reduce the measured pre-mesh surface-band wall, and the prior overlap mixed fine bins with far work. The `--max-clip-level=8` all-strata shell proof was also not implemented: fixed captures and the LOD0 shard audit do not establish survival of every remote thin stratum, and a trustworthy proof needs an exhaustive far-shell stratum walk. LOD6 remains the default. + +Builds for the final candidate and step-8 trial succeeded with `/MP8`; logs: [step-4 candidate](round4_final_candidate_build.log) and [step-8 candidate](round4_owner_coarse8_build.log). Both retain the existing French `vswhere.exe` setup diagnostic and C4838 narrowing warnings from `VoxelStrateManager.cpp`. The initial continuation profiler wrapper failed with exit 2 after a PowerShell console-title pipe error. Shell starts then returned `0xC0000142`; the machine owner identified memory pressure from a game process using about 13 GB. That prototype was removed. During the resumed work no shell-start failure recurred and no build failed. The only nonzero workload exits in this round were the three radius-5 audit runs listed above (all exit 7 on GridStair). No external viewer was closed. No commit, push, or stash was made. ## Previous-round result (superseded above) diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/compare_capture_pair.py b/Standalone/Research/OwnerShapeWorld/speed_round/compare_capture_pair.py new file mode 100644 index 0000000..1182bff --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/speed_round/compare_capture_pair.py @@ -0,0 +1,32 @@ +from pathlib import Path +import sys + +import numpy as np +from PIL import Image + + +def load(path: str) -> np.ndarray: + return np.asarray(Image.open(Path(path)).convert("RGBA"), dtype=np.int16) + + +paths = sys.argv[1:] +if not paths or len(paths) % 2: + raise SystemExit("pass one or more BEFORE AFTER image path pairs") + +for index in range(0, len(paths), 2): + before_path, after_path = paths[index:index + 2] + before = load(before_path) + after = load(after_path) + if before.shape != after.shape: + raise SystemExit(f"dimension mismatch: {before_path} {before.shape} != {after_path} {after.shape}") + + delta = np.abs(after - before) + changed_pixels = np.any(delta != 0, axis=2) + print( + f"pair={Path(before_path).stem}->{Path(after_path).stem} " + f"size={before.shape[1]}x{before.shape[0]} " + f"changed_pixels={np.count_nonzero(changed_pixels)}/{changed_pixels.size} " + f"changed_channels={np.count_nonzero(delta)}/{delta.size} " + f"max_channel_delta={int(delta.max())} " + f"mean_absolute_channel_delta={float(delta.mean()):.6f}" + ) diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_chamber.png b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_chamber.png new file mode 100644 index 0000000..fd0c4ef Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_chamber.png differ diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_far.png b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_far.png new file mode 100644 index 0000000..7f5d877 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_far.png differ diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_passage.png b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_passage.png new file mode 100644 index 0000000..bd86d6b Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_passage.png differ diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_passage_floor_closeup.png b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_passage_floor_closeup.png new file mode 100644 index 0000000..3266b95 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_passage_floor_closeup.png differ diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_tunnel.png b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_tunnel.png new file mode 100644 index 0000000..cd10c4f Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_tunnel.png differ diff --git a/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_tunnel_bend.png b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_tunnel_bend.png new file mode 100644 index 0000000..7a5591d Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/speed_round/round4_after_tunnel_bend.png differ diff --git a/Standalone/Source/StandaloneShapeWorld.h b/Standalone/Source/StandaloneShapeWorld.h index 5e91c17..49f0070 100644 --- a/Standalone/Source/StandaloneShapeWorld.h +++ b/Standalone/Source/StandaloneShapeWorld.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -48,6 +49,17 @@ public: double WorkerSeconds = 0.0; }; + struct FDetailWorkerProfile + { + uint64 Lines=0,Candidates=0,DetailEvaluations=0,Flips=0; + uint64 LocalBandSkips=0; + uint64 DistanceSamples=0,DistanceSampleNanoseconds=0; + uint64 DetailSamples=0,DetailSampleNanoseconds=0; + uint64 ReliefSampleNanoseconds=0,LayerLookupSampleNanoseconds=0; + uint64 OwnerRoughnessSampleNanoseconds=0,OwnerRoughnessFieldCalls=0; + uint64 OwnerRoughnessActiveCalls=0,OwnerNoiseCacheHits=0,OwnerNoiseCacheMisses=0; + }; + struct FBuildStats { double ShapeSeconds = 0.0; @@ -233,7 +245,7 @@ public: static constexpr int32 QScale = 1 << 28; static constexpr int32 SubPerVoxel = 16; - static constexpr int32 DetailBand = 8; + static constexpr int32 DetailBand = 6; static constexpr float DetailSupremum = 4.0f; static constexpr double JoinBlendRadius = 1.5; static constexpr double RoomUnionBlendRadius = 12.0; @@ -242,6 +254,9 @@ public: static constexpr int32 LineBinSize = 32; static constexpr int32 SpatialBinSize = 256; + static void SetDetailProfileEnabled(bool bEnabled) + {ProfileDetailBand.store(bEnabled,std::memory_order_release);} + bool Build(FStandaloneOwnerWorld& World, const FIntVector& MinVoxels, const FIntVector& MaxVoxels, const FIntVector& ClipMinVoxels, const FIntVector& ClipMaxVoxels, int32 Workers, EDetailVariant Variant, @@ -496,8 +511,11 @@ private: &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir); if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth &&std::abs(RockDistance)<=DetailSupremum+0.125f) - RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence) - *DetailBandFade(RockDistance); + { + const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF, + CaveInfluence,FloorInfluence); + if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)*Fade; + } if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); const bool bRock=RockDistance>=0.0f; float Density=-RockDistance; @@ -601,8 +619,10 @@ public: const float Raw=EffectiveRockDistance(X,Y,Z,&Family,&SurfaceSDF, &bFloorSurface,&CaveInfluence,&FloorInfluence); if(std::abs(Raw)>=0.5f)continue; - const float Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence) - *DetailBandFade(Raw); + const float Fade=DetailBandFadeAt(X,Y,Z,Raw,SurfaceSDF, + CaveInfluence,FloorInfluence); + const float Detail=Fade>0.0f + ?DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)*Fade:0.0f; const size_t FamilyIndex=static_cast(Family); FFamilyStats& F=Families[FamilyIndex];++F.Count;F.Sum+=Detail;F.SumSq+=Detail*Detail; if(std::abs(Detail)<0.05f)++F.NearZero; @@ -674,8 +694,11 @@ public: &&Z>=Min[2]&&Z<=Max[2]; const bool bCriticalAir=IsInsideCriticalAir(X,Y,Z); if(bInside&&DetailVariant!=EDetailVariant::Smooth&&std::abs(D)<=DetailSupremum+0.125f) - {Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence) - *DetailBandFade(D);D+=Detail;} + { + const float Fade=DetailBandFadeAt(X,Y,Z,D,SurfaceSDF,CaveInfluence,FloorInfluence); + if(Fade>0.0f) + {Detail=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)*Fade;D+=Detail;} + } if(bCriticalAir)D=std::min(D,-1.0f/16.0f); if(OutFamily)*OutFamily=Family; if(OutRaw)*OutRaw=D-Detail; @@ -958,6 +981,51 @@ private: constexpr float Width=1.5f; return SmoothUnit((Edge-std::abs(SignedDistance))/Width); } + static constexpr float DetailBandInnerRadius=1.5f; + static float DetailBandFade(float SignedDistance,float Edge) + { + const float Distance=std::abs(SignedDistance); + if(Distance<=DetailBandInnerRadius)return 1.0f; + const float Width=Edge-DetailBandInnerRadius; + return Width>0.0f?SmoothUnit((Edge-Distance)/Width):0.0f; + } + static float LocalDetailBandEdge(float MacroValue,float SurfaceSDF, + float CaveInfluence,float FloorInfluence,const FLayerField* Field) + { + float OwnerBound=0.0f; + if(Field) + { + const FStrateGenerationParams& Params=Field->Params; + const float Depth=Params.SurfaceRoughness*2.0f; + if(Depth>0.0f&&std::abs(SurfaceSDF)0.0f) + { + const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; + // Perlin is bounded by 2; the squared ridged fold by 2.25. This + // uses the largest type bound and remains valid under domain warp. + OwnerBound=3.15f*VOXEL_NOISE_SCALE*Params.SurfaceRoughness*Fade*Fade; + } + } + const float ReliefBound=std::abs(MacroValue) + +(0.80f+0.25f*MacroValue)*(0.85f+0.18f); + const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) + +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); + const float LocalSupremum=std::min(3.9f,(ReliefBound+0.5f*OwnerBound)*Contrast); + return std::clamp(LocalSupremum+0.125f,DetailBandInnerRadius,DetailSupremum+0.125f); + } + + float DetailBandFadeAt(double X,double Y,double Z,float SignedDistance,float SurfaceSDF, + float CaveInfluence,float FloorInfluence) const + { + if(DetailVariant!=EDetailVariant::Coherent + ||std::abs(SignedDistance)<=DetailBandInnerRadius) + return DetailBandFade(SignedDistance); + const float MacroValue=FStandaloneResearchTunnels::ValueNoise( + static_cast(X),static_cast(Y),static_cast(Z),32, + static_cast(Owner->Seed)^0xB5297A4Du); + const FLayerField* Field=FindLayerField(Z); + return DetailBandFade(SignedDistance,LocalDetailBandEdge(MacroValue,SurfaceSDF, + CaveInfluence,FloorInfluence,Field)); + } void BuildLayerBounds() { @@ -2337,13 +2405,19 @@ private: void ApplyDetailBand(FParallelTiming& Timing) { + using Clock=std::chrono::steady_clock; + const bool bProfile=ProfileDetailBand.load(std::memory_order_acquire); + std::array WorkerProfile{}; + std::array WorkerActiveSeconds{}; std::atomic CandidateCount{0},ProxyCount{0},FlipCount{0}; - ParallelLines(static_cast(Size[0])*Size[1],[&](size_t Line) + ParallelLines(static_cast(Size[0])*Size[1],[&](size_t Line,int32 Worker) { const int32 X=Min[0]+static_cast(Line%Size[0]); const int32 Y=Min[1]+static_cast(Line/Size[0]); FDetailNoiseLineContext NoiseLine(static_cast(X),static_cast(Y), static_cast(Owner->Seed)); + FDetailWorkerProfile& Profile=WorkerProfile[static_cast(Worker)]; + if(bProfile)++Profile.Lines; uint64 LocalCandidates=0,LocalProxies=0,LocalFlips=0; const size_t WordBase=Line*WordsPerZ; for(size_t W=0;W=static_cast(Size[2]))continue; const int32 Z=Min[2]+static_cast(Zi); ++LocalCandidates; + const bool bTimeDistance=bProfile&&((Profile.Candidates++&255u)==0); + const auto DistanceStart=bTimeDistance?Clock::now():Clock::time_point{}; ESurfaceFamily SurfaceFamily=ESurfaceFamily::Strata;float SurfaceSDF=100000.0f; bool bFloorSurface=false,bCriticalAir=false;float CaveInfluence=0.0f,FloorInfluence=0.0f; const float SDF=EffectiveRockDistance(X,Y,Z,&SurfaceFamily,&SurfaceSDF, &bFloorSurface,&CaveInfluence,&FloorInfluence,&bCriticalAir); + if(bTimeDistance) + { + Profile.DistanceSampleNanoseconds+=static_cast( + std::chrono::duration_cast(Clock::now()-DistanceStart).count()); + ++Profile.DistanceSamples; + } if(std::abs(SDF)>DetailSupremum+0.125f)continue; - float Displaced=SDF+DetailNoiseOnLine(X,Y,Z,SurfaceSDF,CaveInfluence, - FloorInfluence,NoiseLine) - *DetailBandFade(SDF); + float DetailFade=DetailBandFade(SDF); + const FLayerField* DetailField=nullptr; + bool bDetailFieldKnown=false; + if(DetailVariant==EDetailVariant::Coherent + &&std::abs(SDF)>DetailBandInnerRadius) + { + const float MacroValue=NoiseLine.Macro.Sample(static_cast(Z)); + DetailField=FindLayerField(Z); + bDetailFieldKnown=true; + DetailFade=DetailBandFade(SDF,LocalDetailBandEdge(MacroValue,SurfaceSDF, + CaveInfluence,FloorInfluence,DetailField)); + } + if(DetailFade<=0.0f&&bProfile)++Profile.LocalBandSkips; + const bool bTimeDetail=DetailFade>0.0f&&bProfile + &&((Profile.DetailEvaluations&63u)==0); + const auto DetailStart=bTimeDetail?Clock::now():Clock::time_point{}; + float Displaced=SDF; + if(DetailFade>0.0f) + { + if(bProfile)++Profile.DetailEvaluations; + Displaced+=DetailNoiseOnLine(X,Y,Z,SurfaceSDF,CaveInfluence, + FloorInfluence,NoiseLine,bTimeDetail?&Profile:nullptr, + DetailField,bDetailFieldKnown)*DetailFade; + } + if(bTimeDetail) + { + Profile.DetailSampleNanoseconds+=static_cast( + std::chrono::duration_cast(Clock::now()-DetailStart).count()); + ++Profile.DetailSamples; + } if(bCriticalAir)Displaced=std::min(Displaced,-1.0f/16.0f); const bool bNewRock=Displaced>=0.0f; const bool bOldRock=((RockWords[WordBase+(Zi>>6)]>>(Zi&63u))&1u)!=0; @@ -2371,6 +2480,7 @@ private: { RockWords[WordBase+(Zi>>6)]^=uint64{1}<<(Zi&63u); ++LocalFlips; + if(bProfile)++Profile.Flips; } const size_t Node=(Zi*static_cast(Size[1])+static_cast(Y-Min[1])) *Size[0]+static_cast(X-Min[0]); @@ -2383,10 +2493,63 @@ private: CandidateCount.fetch_add(LocalCandidates,std::memory_order_relaxed); ProxyCount.fetch_add(LocalProxies,std::memory_order_relaxed); FlipCount.fetch_add(LocalFlips,std::memory_order_relaxed); - },&Timing); + },&Timing,&WorkerActiveSeconds); Stats.SurfaceCandidates=CandidateCount.load(std::memory_order_relaxed); Stats.SurfaceProxyNodes=ProxyCount.load(std::memory_order_relaxed); Stats.DetailFlips=FlipCount.load(std::memory_order_relaxed); + if(bProfile) + { + uint64 DistanceCalls=0,DetailCalls=0,DistanceSamples=0,DistanceSampleNs=0; + uint64 DetailSamples=0,DetailSampleNs=0,ReliefSampleNs=0,LocalBandSkips=0; + uint64 LayerLookupSampleNs=0,OwnerSampleNs=0,OwnerFieldCalls=0,OwnerActiveCalls=0; + uint64 OwnerCacheHits=0,OwnerCacheMisses=0; + for(int32 Worker=0;Worker(Worker)]; + DistanceCalls+=P.Candidates;DetailCalls+=P.DetailEvaluations; + DistanceSamples+=P.DistanceSamples;DistanceSampleNs+=P.DistanceSampleNanoseconds; + DetailSamples+=P.DetailSamples;DetailSampleNs+=P.DetailSampleNanoseconds; + LocalBandSkips+=P.LocalBandSkips; + ReliefSampleNs+=P.ReliefSampleNanoseconds; + LayerLookupSampleNs+=P.LayerLookupSampleNanoseconds; + OwnerSampleNs+=P.OwnerRoughnessSampleNanoseconds; + OwnerFieldCalls+=P.OwnerRoughnessFieldCalls; + OwnerActiveCalls+=P.OwnerRoughnessActiveCalls; + OwnerCacheHits+=P.OwnerNoiseCacheHits; + OwnerCacheMisses+=P.OwnerNoiseCacheMisses; + } + const double DistanceAverageNs=DistanceSamples?static_cast(DistanceSampleNs)/DistanceSamples:0.0; + const double DetailAverageNs=DetailSamples?static_cast(DetailSampleNs)/DetailSamples:0.0; + const double CandidateWorkerNs=Stats.SurfaceCandidates + ?Timing.WorkerSeconds*1.0e9/static_cast(Stats.SurfaceCandidates):0.0; + std::printf("[DetailBandProfile] candidates=%llu distance_evaluations=%llu detail_noise_evaluations=%llu proxies=%llu flips=%llu candidate_worker_ns=%.1f distance_sample_ns=%.1f distance_estimated_worker_ms=%.1f detail_sample_ns=%.1f detail_estimated_worker_ms=%.1f worker_sum_ms=%.3f wall_ms=%.3f sampling=(distance_1_of_256,detail_1_of_64)\n", + static_cast(Stats.SurfaceCandidates), + static_cast(DistanceCalls),static_cast(DetailCalls), + static_cast(Stats.SurfaceProxyNodes),static_cast(Stats.DetailFlips), + CandidateWorkerNs,DistanceAverageNs,DistanceAverageNs*static_cast(DistanceCalls)/1.0e6, + DetailAverageNs,DetailAverageNs*static_cast(DetailCalls)/1.0e6, + Timing.WorkerSeconds*1000.0,Timing.WallSeconds*1000.0); + std::printf("[DetailBandSubprofile] relief_sample_ns=%.1f relief_estimated_worker_ms=%.1f layer_lookup_sample_ns=%.1f layer_lookup_estimated_worker_ms=%.1f owner_sample_ns=%.1f owner_estimated_worker_ms=%.1f owner_sampled_field_calls=%llu owner_sampled_noise_calls=%llu owner_sampled_core_cache_hits=%llu owner_sampled_core_cache_misses=%llu local_band_skipped=%llu\n", + DetailSamples?static_cast(ReliefSampleNs)/DetailSamples:0.0, + DetailSamples?static_cast(ReliefSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, + DetailSamples?static_cast(LayerLookupSampleNs)/DetailSamples:0.0, + DetailSamples?static_cast(LayerLookupSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, + DetailSamples?static_cast(OwnerSampleNs)/DetailSamples:0.0, + DetailSamples?static_cast(OwnerSampleNs)/DetailSamples*DetailCalls/1.0e6:0.0, + static_cast(OwnerFieldCalls), + static_cast(OwnerActiveCalls), + static_cast(OwnerCacheHits), + static_cast(OwnerCacheMisses), + static_cast(LocalBandSkips)); + for(int32 Worker=0;Worker(Worker)]; + std::printf("[DetailBandWorker] id=%d active_ms=%.3f lines=%llu candidates=%llu detail_evaluations=%llu flips=%llu\n", + Worker,WorkerActiveSeconds[static_cast(Worker)]*1000.0, + static_cast(P.Lines),static_cast(P.Candidates), + static_cast(P.DetailEvaluations),static_cast(P.Flips)); + } + } } void StoreSurfaceDistance(size_t Node,float Density) @@ -2625,12 +2788,16 @@ private: return SmoothMinDistance(Base,std::max(Cave,Overlay),StrataCaveBlendRadius); } - float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF, - const FStrateGenerationParams& Params, - float CaveInfluence) const + struct FOwnerNoiseLineContext + { + const FLayerField* Field=nullptr; + int32 GridZ0=MIN_int32; + float LowerCore=0.0f,UpperCore=0.0f; + }; + + float OwnerCaveRoughnessCore(double X,double Y,double Z, + const FStrateGenerationParams& Params) const { - const float Depth=Params.SurfaceRoughness*2.0f; - if(!(Depth>0.0f)||std::abs(SurfaceSDF)>=Depth||!(Params.RoughnessFrequency>0.0f))return 0.0f; const uint32 Seed=static_cast(Owner->Seed); const float RF=Params.RoughnessFrequency; const float EffectiveZ=(Params.VerticalScale>0.0f&&Params.VerticalScale!=1.0f) @@ -2676,7 +2843,43 @@ private: Rough=VoxelNoise::FBM(Main.X,Main.Y,Main.Z,3); FineValue=VoxelNoise::FBM(Fine.X,Fine.Y,Fine.Z,2);break; } - float Total=(Rough+FineValue*0.4f)*VOXEL_NOISE_SCALE*Params.SurfaceRoughness; + return (Rough+FineValue*0.4f)*VOXEL_NOISE_SCALE*Params.SurfaceRoughness; + } + + float OwnerCaveRoughness(double X,double Y,double Z,float SurfaceSDF, + const FLayerField& Field,float CaveInfluence, + FOwnerNoiseLineContext& LineContext, + FDetailWorkerProfile* Profile=nullptr) const + { + const FStrateGenerationParams& Params=Field.Params; + const float Depth=Params.SurfaceRoughness*2.0f; + if(!(Depth>0.0f)||std::abs(SurfaceSDF)>=Depth||!(Params.RoughnessFrequency>0.0f))return 0.0f; + if(Profile)++Profile->OwnerRoughnessActiveCalls; + constexpr int32 ZStep=8; + const int32 GridZ0=static_cast(std::floor(Z/static_cast(ZStep)))*ZStep; + const int32 SampleZ0=std::max(GridZ0,Field.Lo); + const int32 SampleZ1=std::min(GridZ0+ZStep,Field.Hi); + float Total=0.0f; + if(SampleZ1>SampleZ0) + { + const bool bCacheHit=LineContext.Field==&Field&&LineContext.GridZ0==GridZ0; + if(Profile) + { + if(bCacheHit)++Profile->OwnerNoiseCacheHits; + else ++Profile->OwnerNoiseCacheMisses; + } + if(!bCacheHit) + { + LineContext.Field=&Field; + LineContext.GridZ0=GridZ0; + LineContext.LowerCore=OwnerCaveRoughnessCore(X,Y,SampleZ0,Params); + LineContext.UpperCore=OwnerCaveRoughnessCore(X,Y,SampleZ1,Params); + } + const float Alpha=static_cast((Z-SampleZ0)/(SampleZ1-SampleZ0)); + Total=FMath::Lerp(LineContext.LowerCore,LineContext.UpperCore,Alpha); + } + else + Total=OwnerCaveRoughnessCore(X,Y,Z,Params); if(SurfaceSDF<0.0f) Total=std::lerp(Total,std::min(Total,0.0f),std::clamp(CaveInfluence,0.0f,1.0f)); const float Fade=1.0f-std::abs(SurfaceSDF)/Depth; @@ -2750,6 +2953,7 @@ private: struct FDetailNoiseLineContext { FValueNoiseLineContext Macro,Mid,Fine; + FOwnerNoiseLineContext OwnerNoise; FDetailNoiseLineContext(float X,float Y,uint32 Seed) :Macro(X,Y,32,Seed^0xB5297A4Du),Mid(X,Y,16,Seed^0x63D83595u), Fine(X,Y,6,Seed^0xA511E9B3u) @@ -2777,8 +2981,9 @@ private: const float Relief=SurfaceRelief(static_cast(X), static_cast(Y),static_cast(Z),static_cast(Owner->Seed)); const FLayerField* Field=FindLayerField(Z); - const float OwnerRelief=Field?OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field->Params, - CaveInfluence):0.0f; + FOwnerNoiseLineContext OwnerNoise; + const float OwnerRelief=Field?OwnerCaveRoughness(X,Y,Z,SurfaceSDF,*Field, + CaveInfluence,OwnerNoise):0.0f; const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); @@ -2786,14 +2991,31 @@ private: float DetailNoiseOnLine(double X,double Y,double Z,float SurfaceSDF, float CaveInfluence,float FloorInfluence, - FDetailNoiseLineContext& NoiseLine) const + FDetailNoiseLineContext& NoiseLine, + FDetailWorkerProfile* Profile=nullptr, + const FLayerField* KnownField=nullptr, + bool bFieldKnown=false) const { if(DetailVariant!=EDetailVariant::Coherent) return DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence); + using Clock=std::chrono::steady_clock; + const auto ReliefStart=Profile?Clock::now():Clock::time_point{}; const float Relief=NoiseLine.SurfaceReliefAt(static_cast(Z)); - const FLayerField* Field=FindLayerField(Z); - const float OwnerRelief=Field?OwnerCaveRoughness(X,Y,Z,SurfaceSDF,Field->Params, - CaveInfluence):0.0f; + if(Profile) + Profile->ReliefSampleNanoseconds+=static_cast( + std::chrono::duration_cast(Clock::now()-ReliefStart).count()); + const auto LookupStart=Profile?Clock::now():Clock::time_point{}; + const FLayerField* Field=bFieldKnown?KnownField:FindLayerField(Z); + if(Profile) + Profile->LayerLookupSampleNanoseconds+=static_cast( + std::chrono::duration_cast(Clock::now()-LookupStart).count()); + const auto OwnerStart=Profile?Clock::now():Clock::time_point{}; + if(Profile&&Field)++Profile->OwnerRoughnessFieldCalls; + const float OwnerRelief=Field?OwnerCaveRoughness(X,Y,Z,SurfaceSDF,*Field, + CaveInfluence,NoiseLine.OwnerNoise,Profile):0.0f; + if(Profile) + Profile->OwnerRoughnessSampleNanoseconds+=static_cast( + std::chrono::duration_cast(Clock::now()-OwnerStart).count()); const float Contrast=1.35f-0.80f*std::clamp(CaveInfluence,0.0f,1.0f) +1.85f*std::clamp(FloorInfluence,0.0f,1.0f); return std::clamp((Relief+OwnerRelief*0.5f)*Contrast,-3.9f,3.9f); @@ -2808,8 +3030,11 @@ private: const bool bInsideFine=X>=Min[0]&&X<=Max[0]&&Y>=Min[1]&&Y<=Max[1]&&Z>=Min[2]&&Z<=Max[2]; if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth &&std::abs(RockDistance)<=DetailSupremum+0.125f) - RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence) - *DetailBandFade(RockDistance); + { + const float Fade=DetailBandFadeAt(X,Y,Z,RockDistance,SurfaceSDF, + CaveInfluence,FloorInfluence); + if(Fade>0.0f)RockDistance+=DetailNoise(X,Y,Z,SurfaceSDF,CaveInfluence,FloorInfluence)*Fade; + } if(bCriticalAir)RockDistance=std::min(RockDistance,-1.0f/16.0f); return RockDistance>=0.0f; } @@ -2866,13 +3091,21 @@ private: return Count?PairAB/std::sqrt(std::max(1.0e-12,PairAA*PairBB)):0.0; } - template void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr) const + template void ParallelLines(size_t Count,Fn&& Work,FParallelTiming* Timing=nullptr, + std::array* WorkerActiveSeconds=nullptr) const { const auto WallStart=std::chrono::steady_clock::now(); if(WorkerCount<=1||Count<512) { const auto WorkerStart=std::chrono::steady_clock::now(); - for(size_t I=0;I)Work(I,0); + else Work(I); + } + if(WorkerActiveSeconds) + (*WorkerActiveSeconds)[0]=std::chrono::duration( + std::chrono::steady_clock::now()-WorkerStart).count(); if(Timing) { const double WorkerSeconds=std::chrono::duration(std::chrono::steady_clock::now()-WorkerStart).count(); @@ -2884,12 +3117,19 @@ private: std::atomic Next{0};std::atomic WorkerNanoseconds{0}; std::vector Threads;Threads.reserve(WorkerCount); for(int32 W=0;W=Count)break;Work(I);} - WorkerNanoseconds.fetch_add(static_cast(std::chrono::duration_cast( - std::chrono::steady_clock::now()-WorkerStart).count()),std::memory_order_relaxed); + for(;;) + { + const size_t I=Next.fetch_add(1,std::memory_order_relaxed);if(I>=Count)break; + if constexpr(std::is_invocable_v)Work(I,W); + else Work(I); + } + const uint64 WorkerNs=static_cast(std::chrono::duration_cast( + std::chrono::steady_clock::now()-WorkerStart).count()); + if(WorkerActiveSeconds)(*WorkerActiveSeconds)[static_cast(W)]=static_cast(WorkerNs)*1.0e-9; + WorkerNanoseconds.fetch_add(WorkerNs,std::memory_order_relaxed); }); for(std::thread& T:Threads)T.join(); if(Timing) @@ -2920,6 +3160,7 @@ private: std::unordered_map CriticalAirBins; std::vector RockWords; std::unique_ptr SurfaceDistanceQ; + inline static std::atomic ProfileDetailBand{false}; std::array,3> CandidateAxisWords; std::vector CandidateWords; FBuildStats Stats; diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index b67aacc..f129f2f 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -2047,6 +2047,7 @@ int main(int Argc,char** Argv) int32 MaxClipLevelOverride=-1; int32 ShapeCompareLevel=-1,ShapeCompareTileLimit=0; bool bStreamBenchmark=false; + bool bProfileDetailBand=false; bool bShardAudit=false; bool bStreamHash=false; bool bSkipUvAndColors=false; @@ -2062,6 +2063,7 @@ int main(int Argc,char** Argv) const std::string_view Arg=Argv[I]; if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10)); else if(Arg=="--profile-attribution")App.bProfileAttribution=true; + else if(Arg=="--profile-detail-band")bProfileDetailBand=true; else if(Arg=="--compare-evaluators")App.bCompareEvaluators=true; else if(Arg=="--parity-only")App.bParityOnly=true; else if(Arg=="--density-hash")App.bDensityHash=true; @@ -2131,6 +2133,7 @@ int main(int Argc,char** Argv) } else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg); } + FStandaloneShapeWorld::SetDetailProfileEnabled(bProfileDetailBand); std::string Error; App.World=std::make_unique(); if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}