Repository navigation
feat(acceptance): the #1259 integrated AAA acceptance scene, measured and attributed - #1597
Merged
drsnuggles8 merged 48 commits intoOct 11, 2026
Merged
Conversation
HeadRigged.gltf's only material, HeadSkin, had a base colour texture and nothing else, so the animated head of #1259 could never have a thin ear. - tools/skin-thickness bakes from a .gltf too (--mesh HeadRigged.gltf, metres, v flipped to the FBX convention). The rig keeps every UV of the scan (both meshes rasterise the same 961 403 texels; per-vertex UVs match Head.fbx to 3e-8), but the bust is 1.34x the size DigitalHuman.olo gives Head.fbx, so it gets its own measurement: Textures/HeadRigged_Thickness.png. - prepare_head_rig.py writes it as a KHR_materials_volume thicknessTexture with thicknessFactor 0.02 (thickness / 20 mm), refuses to name a missing map, sets baseColorFactor 1 (the exporter wrote its 0.8 socket default, darkening the scan's albedo by a fifth) and roughness 0.42 (the static Cranium's). Re-run in Blender 5.2: HeadRigged.bin is byte-identical, so the cooked groom binding stays valid. The asset is a CC BY 3.0 derivative; the README records the new file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ImportedMaterialCodec persisted KHR_materials_volume's thicknessFactor but not its thickness TEXTURE. Both carriers lost it: a static glTF's warm .omesh load and every asset-pack MeshSource came back with the factor alone, so a skin material measured thin at the ear rendered uniformly thick from the second load on (the cache-stored silent-drop class), and the pack cook never asked for the texture's colour space. - Codec v4: a Thickness TextureRef per record, in the shared slot list, so Describe/Realize/CanPersistEveryTexture all see it; linear intent in the pack cook's colour-space pass; the cook warns about it like any other slot. - OMeshFormat v15 and AssetPackFile v8 move with the codec, so an older cache is re-imported and an older pack rejected instead of decode-failing. - ImportedMaterialPackTest's pack and .omesh round trips and the wire-format test now carry a thickness map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Past the detailed distance a casting layer's card plants are grouped by square area cells (a whole number of 16 m registry cells) instead of 256-plant slices per registry cell. The cell is the smallest one whose count over the layer's view disc, pi * (R / S + sqrt 2)^2, stays within UpdatesPerFrame / 8: 32 m for Grass (R 150), 48 m for TallGrass (R 170). A cell denser than VerticesPerFrame / 16 plants splits into equal parts. Mesh groups, their card fallback and detailed cards keep their size; the 0.25 m error bound and the 1,024-build budget are unchanged. IntegratedRenderer, Vulkan, Release editor: casting builds demanded drop from 1,024 (capped) to 103 at the meadow edge and 243 inside, and groups left out from 414 / 935 to 15 / 31. The TLAS is still withheld there: the per-frame VERTEX budget now binds (1.02-1.04 M of 1,048,576), because at the editor's 0.1 s step every card proxy refreshes every frame and the meadow holds ~400k casting plants. Not sufficient on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every foliage cull view slot held a compacted stream sized to the whole layer, twice over for a layer with an authored mesh, in each of the five slots: 320 MB of the traversal scene's 371 MB foliage GPU floor. A slot is now sized when it is first created from the spatial groups its view's frustum and draw distance admit (FoliageGPUCuller::EstimateSurvivors, an upper bound on the GPU's survivors), with half again of headroom (StreamCapacityFor). The mesh region is sized from the groups within MeshRegionReach of the eye and sits right after the list instead of a second whole-layer copy. A view that outgrows its slot overflows the existing bounded, counted way (FoliageCullOutput / FoliageCullDropped) and now also records what it wanted in a demand pair at the end of the State header, which the CPU never seeds. FoliageCullDemandReadback copies the pairs out through a fenced ring after the frame's culls (ShadowRenderPass end, or the next main cull) and polls it without waiting; a slot whose demand exceeds its capacity grows at its next cull and says so in the log. Memory (#1342 rows, FoliageCostBaselineTest at load): traversal pinned 370.8 -> 146.8 MB, cull streams 319.7 -> 95.7 MB Meadow pinned 299.5 -> 94.0 MB, cull streams 273.5 -> 67.9 MB Woodland pinned 121.6 -> 53.9 MB, cull streams 91.9 -> 24.2 MB FloraTraversalEvidenceTest PNGs are bit-identical to the before binary's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A MaterialOverridesComponent could never apply to a one-mesh rigged model, for two reasons, so the rigged scan head (#1259) could not be made skin: - The mesh an entity draws had no imported material table: only the combined source of a multi-mesh model carried one. A patch found nothing to patch, and an entity without a MaterialComponent drew the engine default. AnimatedModel::GetEntityMeshSource now attaches the model's material to its one mesh as entry 0. - Loading re-wires the model before the generated blocks add the MaterialOverridesComponent, so ModelImporter gave the entity an entity-wide copy of its imported material, which overrides every submesh: the Scene then reported every patch inactive. The deserializer drops a MaterialComponent the import ADDED when the entity patches its materials (an authored one stays), and ModelImporter does not add one to an entity that already has patches. An entity with no patches still gets its imported material as a MaterialComponent, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SkinRiggedHeadEvidenceTest (L8, GL) renders the rigged scan head of #1259's acceptance scene: HeadRigged.gltf playing LookAround, carrying GroomAnimals' cooked hair coat, loaded through SceneSerializer from kAcceptanceHumanEntity -- the exact .olo block the acceptance scene should use, its handles pinned to the committed scenes'. - The import carries HeadSkin's measured map; the patch makes it Skin with ReferenceHead.oloskin and no MaterialComponent shadows it. - Backlit A/B on every path: the right ear changes when the map is removed (uniform 20 mm), the forehead and closed lids do not move at all; the deferred transmission AOV is lit at the ear only with the map. - Close-up framings (front, three-quarter, profile, backlit) with the coat under TAA on every path, and seven frames through LookAround. - The rest pose against the static Cranium placed by the rig's own normalisation: silhouette IoU 0.98 / 0.97, ear transmission comparable. - The game build stages the thickness map beside the glTF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan's README sized the bust at roughly 1.4 units. Loaded, its bounds are 0.842 units tall (0.43 m / the 0.51058 scale SkinRiggedHeadEvidenceTest measures), which is also what prepare_head_rig.py normalises from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The apps get /DEBUG in Release through olo_set_link_options, but the test executable is not configured through olo_configure_app, so its Release link wrote no PDB. The GL capture host is the timing host for Release benchmarks (#1259), and a crash in it (an access violation in a groom asset lookup) gave an LLDB stack of bare addresses. The objects already carry /Z7 debug info; this only asks the linker to write it out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`TotalFramesRendered` in an editor-host result.json was the warm-up frame count. That was everything the host rendered when #974 wrote it; measured frames came later and were never added, so a 180-frame-per-camera run reported 192 frames instead of 732. The test-binary host has always counted every stepped frame; the editor host now reports the stepped-frame counter it already keeps for the pinned clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A benchmark capture is an edit-mode tick (Scene::OnUpdateEditor) on a pinned clock, and edit mode never runs the gameplay scheduler's "AnimalPaths" system. So in every capture the herd played its walk clip on the spot: the #1338 integrated-renderer runs measured and pictured a population that never moved, although the scene authors 40 animals on closed-form paths. ApplyEntityMotion, which both capture hosts call once per rendered frame, now advances the paths by the manifest's fixed step first (through a new public Scene::AdvanceAnimalPaths), and the editor host calls it on every step rather than only when the manifest has EntityMotion entries. The path stays closed form in elapsed time, so both hosts place the herd identically at a frame. Pinned by AnimalPathFollowsTerrain.ACaptureStepWalksTheHerdOneFixedStepPerFrame (added with the ground-following change that follows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AnimalPathComponent traces a closed-form figure at a fixed height, so a herd crossing the acceptance scene's meadow-to-woodland relief floated over the dips and sank into the rises. The new FollowTerrain flag sets the animal's height each tick to the terrain surface under it, sampled from the loaded height field in the terrain entity's own space (the nav-mesh bake's mapping), so a moved or Y-turned terrain answers correctly. The sampler is a free function, SampleTerrainGroundHeight (Scene/TerrainGround), shared with the acceptance scene's camera-clearance contract. Off every terrain, or where no height field exists yet (a headless host never builds one), the animal keeps its height and the engine warns once rather than dropping it to zero silently. Touch-points: the flag takes the struct's former padding byte (still 44 bytes, no padding, BitwiseEqualLayout holds); generated scene YAML/binary and field registries; save-game serializer with kSaveGameFormatVersion 44; the inspector's "Follow terrain" checkbox. Lua and C# do not expose this component. Tests: AnimalPathFollowsTerrainTest (Functional): the walker stands on a sloped, translated terrain at every step of its figure; without the flag, or off the terrain, the height stays; and a capture step walks the herd one fixed step per frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cord presentation Three capture gaps the #1259 acceptance gate needs closed: * RendererSettings.VirtualShadowMaps pins ShadowMap::Settings.VSM.Enabled, which no scene serializes. The gate is measured with VSM on, so without it a manifest could not say which shadow technique it timed. Decoded strictly: a typo is refused, not read as "off". result.json records it as requested and as selected. * A camera's Sequence {Every, Count, Attachments} is motion footage: after the warm-up the camera keeps moving for Count steps of Every frames and reads the named attachments back after each step, written to <camera>/sequence/ as <Name>_<step> with the step's frame, clock and pose in result.json. A sequence reads back mid-run, so the parser refuses it beside a Measurement block. Both hosts implement it; the end-of-camera capture follows the sequence and its recorded pose accounts for it (CameraFrameCount). * provenance.presentation / provenance.vsync: "offscreen" for the test host; "window" with the window's VSync for the editor host, whose frame time is a completed-frame interval that includes present and snaps to the refresh under VSync. The sequence types are plain members with TIsTriviallyRelocatable traits, so the capture records stay TArray-relocatable. Tests: ReferenceFixtureManifest.ParsesVirtualShadowMapsAndRefusesNonBooleans, ParsesACameraSequenceAndRefusesUnusableOnes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/perf/acceptance-benchmark.py runs the #1259 measurement protocol over a plan of cells (a manifest on the GL test host or the Vulkan editor host): * Release only, with a declared compiler; a cell whose counted runs come from different binaries is refused, and a binary, shader or asset change aborts the campaign. * Interleaved fresh-process runs, the start cell rotated each round, at least three counted runs per cell. * Other GPU load checked before every run and sampled every 30 s during it (foliage-cost-baseline's probe); a contended run is kept but not counted, with the competing process named, and retried. * Provenance per binary: binary/shader/asset hashes, commit, dirty diff and untracked files (controlled-benchmark's provenance(), which now accepts precomputed tree hashes). * Per camera: actual internal/display sizes, resolved upscaler, VSM selected, and p50/p95/p99/max plus deadline misses for frame, CPU, fence, present, recording and total GPU time, and per-pass GPU distributions from measurement-passes.jsonl (valid, distinct frame ids, sub-passes separate). `requirePasses` refuses a run in which a named pass never produced a valid sample, the evidence that a technique actually ran. * Ranges across runs, never resampled frames; PASS only if every counted run's p95 is within the gate (default 16.667 ms). Exit 0/1/2. Tests: test_acceptance_benchmark.py (29 cases, no GPU), with six mutations each caught (dedupe, sub-pass separation, verdict, contention, process ancestry, rotation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the editor loads
Opening Scenes/GroomAnimals.olo refused both horse coats ("the target mesh is
not the surface this binding was built against"), on master too: the coats
stood at their bind pose while the horses walked.
Root cause: stale cooked data, not an import or cache bug. The bindings were
cooked (d14c66b) against the HorseSkin primitive alone, 4316 vertices and
22170 indices. A day later 404acda (#1533) made an animated entity draw every
mesh of its model, so the surface a horse coat binds to is body + eyes, 8284
vertices and 27930 indices, and nobody re-exported. A cold import and a warm
.omesh load of Horse.gltf give the identical surface (same index and rest
hashes), so the cache is not involved; the Human binding still matches.
- Re-cooked through the existing exporter
(GroomAnimalsAcceptanceEvidenceTest.ExportsTheLiveScene). The grooms and the
Human binding came out byte-identical, so only the two horse bindings change;
the scene and registry it also rewrites were left as committed (same handles,
and the scene's entity UUIDs are referenced elsewhere).
- GroomAnimalsCookedBindingTest loads each committed coat against a cold and a
warm import of its body and asks CheckCompatibility for the Scene's verdict.
It fails on master (both horses, TargetTopologyMismatch) and passes here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d contracts (#1259) Scenes/Benchmark/Acceptance.olo is generated by scripts/perf/generate-acceptance-scene.py from committed scenes: the meadow-to-woodland flora traversal, the moving fox population laid along it, the bound short- and long-coat horses walking in the meadow, and the rigged human head with its hair in a studio beside the terrain, plus a moving shadowed light. Every walking animal follows the ground, every coat is parented to the body its binding targets, and the 16 subjects nearest the cameras push the grass (the interaction field's slot count). The flora content is adjusted for legibility (gentler relief, no snow, meadow grass heights, more pines); no layer is removed and no density lowered. The docs/analysis note says so. Manifests (acceptance-*.yaml): the gate cells per path with VSM on and a 16.667 ms deadline, hybrid RT on Vulkan, CSM controls, an upscaler cell, motion-footage sequences, and Deferred lighting-tap AOV runs. Five cameras: head close-up, head into the sun, coat close-up, a dolly through the herd toward the wood, and a rapid turn. Cameras are placed from a committed ground grid exported from the real height field. AcceptanceSceneEvidenceTest (integration, GL): every coat bound and parented to its body, every walker on the terrain and on the ground after walking, the interaction field not oversubscribed; every manifest camera clears the terrain along its whole path; every gate camera sees a non-flat frame on every raster path. It caught two generator bugs while being written (a FollowTerrain key written under GroomLodComponent, which the loader silently ignores, and a herd path running off the terrain). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A groom's apparent size (GroomLod, the ray-tracing proxy tier, the animal scheduler) was measured from the groom asset's rest bounds through the groom entity's transform. For a BOUND coat those bounds sit where the coat was when the binding was cooked; the deformation maps the body into the groom's space with this frame's inverse(groomWorld) * bodyWorld, so the two agree only at the cook placement. In the acceptance scene the rigged head stands at [-34, 1.24, 196], turned -2.2143 rad, with HumanCoat parented to it; the rest bounds then read the hair as 3.7 m from where it is drawn. Scene::BoundGroomPlacementOffset averages the binding's bind-time root origins (groom space) and the same roots on the body's surface (barycentrics on the bound triangles), cached on the binding handle and surface generation, and shifts the rest bounds by where this frame's mapping puts that centroid. The binding itself was already surface-relative: at the acceptance placement it binds, deforms every root and holds none at rest. SkinRiggedHeadCloseUp.TheCoatBindsAndFollowsAtTheAcceptanceScenePlacement loads that configuration and compares it with the cook placement from a camera placed identically relative to the head: coverage per 16 px cell differed by 0.110 before this change (the coat thinned in the close-up) and 0.012 after; both placements draw all 44235 strands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion (#1580) Foliage went to the VSM as a deforming footprint every frame, so every page under it in all sixteen clip levels was cleared and every level re-culled and redrew every layer. Policy (FoliageShadowInvalidation.h, FoliageRenderer::CollectVsmInvalidations): a layer's footprint is invalidated each frame only in levels whose texel is finer than its declared wind bound (FoliageWindMaximumDisplacement); around each live interaction influence (its cylinder box, plus last frame's) in levels finer than wind + FoliageInteractionMaximumDisplacement; when the main eye moves, in the levels a receiver within twice the view distance selects plus the fallback level; coarser levels keep their pages and are refreshed one level every 4 frames in rotation. Silhouette changes without motion go through ShadowFamilyCache with FoliageRenderer::GetShadowRevision. The invalidation queue carries a clip-level mask in the min's w lane (0 = every level, what every other caster still submits). The clip-level cull now also rejects every spatial group over no dirty page (FoliageGroupCullVsm.comp, the vsmFootprintHasDirtyPage test VSM_CullCasters uses), so a level that keeps its pages draws no foliage. Also: the #1581 demand capture moves to the frame epilogue (after GPUReadbackStats::EndFrame, registered as out-of-band epilogue work), the VSM lanes sit after the demand pair in the cull State header, and FoliageCostBaselineTest records the memory floor after its cells and takes OLO_FOLIAGE_COST_VSM_POLICY=everyframe for a same-binary A/B. Measured: FoliageVsmCacheVisualEvidenceTest shows the shipped cache equal to a full redraw at the same clock (0 px of shadow-factor difference, three angles) with a never-invalidate control at 75-149 k px. On the committed scenes the policy itself saves nothing: with 0.1-0.84 m wind bounds only levels 10-15 may cache and no resident page is there (1533 pages redrawn per frame against 1522 with per-frame invalidation), and the GL headless VSM cells are unchanged. Live on Vulkan the VSM frame at the traversal Step4 framing went from 39.0 to 15.1 ms (shadow pass 35.5 -> 11.5 ms), which is the dirty-page group cull; see docs/agent-rules/foliage-vsm-page-caching.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1580) into the #1259 acceptance branch Matched interleaved GL A/B (same shaders per binary, 2 rounds, RTX 4090, Release): Meadow Deferred VSM 63.0 -> 55.9 ms, Woodland Deferred VSM 97.9 -> 89.1 ms, Traversal Step4 Forward CSM 15.1 -> 14.6 ms; no overflowed frame in any arm. Foliage GPU floor: traversal 370.8 -> 146.8 MB, Meadow 299.5 -> 94.0 MB, Woodland 121.6 -> 53.9 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bound coat is its own entity, parented to the body it rides, so a herd authored that way carries its AnimalBudgetComponent on the coat while the skeleton sits one binding away. The gather priced no bones for it and ShouldPoseAnimalThisFrame never found a schedule for the body: the deformation axis silently did nothing for the whole herd. The gather now takes the bone count from the binding's target (when the target has no budget of its own), and the body is posed by its coat's schedule through m_AnimalBodySchedules (finer step wins for two coats). BoundCoatPopulationFixture holds it, with a bindings-off control arm in which every body poses every frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…budget (#1259) The coat-volume budget was eight resident volumes of any size, first come. In AnimalPopulation.olo forty fox coats reached the slots before the hero, and a coat refused its volume loses its dual scattering with it: the hero and most of the herd rendered dark brown and grey where they are orange (GL and Vulkan, editor and headless capture alike). Past the eighth volume a newcomer is now admitted while the resident set stays within 256 MiB (estimate: the whole ring at RGBA32F plus the body texture). A herd's distant coats are 16^3-32^3 volumes of a few hundred KiB. FoxCoatEvidenceTest.AHerdKeepsEveryCoatVolume (16 bound foxes, all drawn coats shadowed, BudgetExhausted 0) lands with the fox coats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce (#1259) FoxCoatEvidenceTest grows a summer (short) and a winter (long) red-fox coat on the Khronos Fox's own bind-pose surface, cooks them and a card level with the real cookers, and binds them with the real binder. The coat is authored in metres (the Fox's centimetres times 0.0125) and its entity carries a local scale of 80 under the fox, so SurfaceToGroom is the same at every fox scale. - FoxShortCoat: 265,724 strands, 5.4 points each, 8,947 cards - FoxLongCoat: 232,394 strands, 6.6 points each, 8,890 cards - FoxCoatColor.png: the Fox texture remapped to a fox's colours, islands dilated, region edges softened, low-frequency value noise Cases: the roots sit on the independently skinned skin through the Walk (p99 0.000 mm, max 0.010 mm; bind-pose control p99 200-340 mm); skin showing at four hero framings per coat 2.0-4.5% (bound 5%, head 6%); cards keep coverage 0.91-0.99 and energy 0.81-1.04 past the hand-over; the bindings accept a cold and a warm load of the Fox; the colour map reads back the same through the editor import; a 16-fox herd keeps every coat volume. Regenerate: OLO_FOX_COATS_EXPORT=1 OloEngine-Tests --gtest_filter=FoxCoatEvidenceTest.ExportsTheFoxCoats (cwd OloEditor/), which also registers the files in AssetRegistry.oar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every AnimalPopulation fox is now a Fox body (model, clip, path) and a coat entity parented to it and bound to it: FoxLongCoat on the hero, the featured foxes and every other herd animal, FoxShortCoat on the rest. Keys are copied from Dog.olo / GroomAnimals.olo. The generator reads the coats' handles from AssetRegistry.oar by path, so a re-export needs only a re-run. Authoring choices, each measured in the live editor: - MaxRenderStrands is each coat's exact strand count. A round 1,000,000 and its ladder halvings built the same full coat under three cache keys. - VariationSeed 0 on every fox. The seed is in the build digest, so a seed per animal gave each fox its own ~350 MiB rest stream (~15 GiB requested). - Herd coats do not cast into the scene shadow maps (hero and featured do): each casting coat holds caster entries per shadow step. - The hero faces the camera three-quarter on (yaw 0.6). - The AnimalBudgetComponent sits on the coat; the scheduler poses its body. IntegratedRenderer.olo and its manifests are regenerated from it; the stale "unbound blob" notes are gone, and animal-population.diagnostic.yaml lists the fox coats, bindings and colour map with their hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ance branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o frame (#1259) The pair capture looked from x = 0, between the two foxes, so the long fox showed the flank that faces away from the sun. In this sky-less fixture that flank is near-black for either coat, and the PNG read as a broken long coat. Measured with mirrored framings: the long coat is 0.059 mean linear luma from the shaded side and 0.101 from the lit side; the short coat 0.039 shaded and 0.132 lit. No engine state was involved: the pair was as dark when captured first, before any other view, toggle or history reset. The pair is now framed from the sun's side, and each coat's mean luma there must stay within 0.65-1.5x of its own lit side view (0.92 short, 0.85 long), so a coat that skins the body but renders dark -- the old framing at 0.54, a coat refused its density volume at ~0.1 -- fails rather than passing on skin coverage alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scene::PublishGroomStrandRequests deformed, simulated and submitted every coat every frame. In the #1259 acceptance scene the 41-fox herd behind the camera cost ~80 ms of CPU (binding, guide simulation, upload) and ~65 ms of GPU root frames for coats that drew nothing. A coat is now published only when its rest bounds, posed and padded (a quarter of the extent plus 0.25 m, for a walking body and a swaying coat), reach the culling camera's frustum, or, for a coat that casts scene shadows, when its shadow can: its box swept along a shadowing sun for the light's shadow distance, or inside a shadowing point/spot light whose range reaches the view. The frustum is the one this frame's BeginScene derived, so each camera that renders the scene culls against its own view. A coat that comes back resumes through the deformer's DeformationSkipped history reset and the simulation's re-seed, and stays "live" while culled, so its guide-influence table, streaming key and LOD hysteresis are not evicted and rebuilt. Measured (GL, Release, acceptance-deferred, 300 frames per camera, one run): the into-the-sun head camera 274 -> 20.5 ms p50, the rapid turn 276 -> 80 ms. Cameras that keep the herd inside the frustum, even at 230 m, are unchanged; that is the per-coat deformation cost, not the cull. Tests: GroomViewCullTest (cullinglod): in view submitted, behind not, behind-but-casting-into-view submitted, behind-and-casting-away not; with frustum culling off every coat is submitted (the control). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerated from AnimalPopulation now that every fox wears a bound coat. The coat (and its AnimalBudgetComponent) is a child of the fox, so the generator relocates the root animated bodies and lets the coats follow. 44 bound coats (41 foxes, 2 horses, the head's hair), 43 ground-following walkers, 16 grass pushers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into the #1259 acceptance branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coat view cull posed the groom's rest bounds through the coat entity's transform. For a bound coat those bounds sit where the binding was cooked, so a coat moved with its body (the acceptance scene's horses, or the head's hair 3.7 m from its cook placement) was tested metres from where it is drawn. The cull now reuses the placed rest bounds the LOD size already computes through Scene::BoundGroomPlacementOffset, so the two cannot disagree about where a coat is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kness The acceptance scene's rigged head patches its imported HeadSkin to the reference skin (ReferenceHead.oloskin, 20 mm thickness factor) through a MaterialOverridesComponent; the measured thickness map rides in HeadRigged.gltf. No MaterialComponent, which would switch the patch off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The acceptance scene's resident set exceeds the test harness's 6 GB guard, so its GL captures need --olo-rss-ceiling-mb=0; the plan's testHostArgs carries that (and any other test-host flag) to every GL run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ches a run writes A GL capture writes git-ignored, generated files under the asset root (the mesh cache, the scene's .scenebin sidecar), so hashing every file on disk made the campaign's asset fingerprint change after its first run and abort it as a mid-campaign build change. The asset hash now covers tracked files plus untracked ones git does not ignore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hybrid manifest turns the renderer's ray-traced shadows on, but the tier traces only the lights that request it (RayTracedShadows on the light, #1056), and none of the source scenes' lights do. The hybrid cell traced nothing: olo_rt_scene_stats on the live Vulkan editor showed no traced light, and the cell measured the same as Deferred. The generator now sets RayTracedShadows on the sun, the head's key and side lights and the moving lantern; a raster cell ignores it. AcceptanceSceneTest.EverySubjectIsBoundGroundedAndOnTheTerrain checks it for every shadowed light, and fails on the previous scene naming all four. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor host's benchmark capture reopens the scene, and a second load of the acceptance scene's 44 bound coats in one editor process stalls the main thread past the MCP wait, so no Vulkan cell completed through acceptance-benchmark.py. acceptance-live.py opens the scene once and, per render path (and hybrid ray-traced shadows) with VSM on, poses each manifest camera and keeps the last N completed frames' frame, CPU, GPU, fence and present split and a pass-timing snapshot. An attribution pass on the same editor adds averaged CPU scopes, groom budget stats, per-pass CPU, the raw series and, for hybrid, the RT scene stats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A coat is composited after the deferred lighting pass by GroomStrand.glsl, which never read the lighting tap (#1526), so every tap showed the coat's full lit colour: the five taps counted every coat five times, and the #1259 acceptance AOVs showed the head's hair and the horses' manes at full brightness in each one. The shader now records the split the other lit shaders record: the R lobe (cuticle reflection) is the specular term and what entered the fibre (TT, TRT, the residual, the coat's multiple scattering) the diffuse one; the lights are direct and the environment indirect, taken as the result minus the lights' share. The composite is linear, so the four sum to the lit colour. An unlit coat and the tangent diagnostic are the remainder, and the primary sun's scene visibility feeds the shadow tap. A tap-off frame is the same expression as before. LightingTapEvidenceTest's scene gains a lit tuft between the spheres. With the previous shader the partition fails on every cell (0.3-4.4% of lit pixels out, worst relative error 4: the coat counted five times); with this one, 0 pixels out on all eight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renderer3D::WantsRayTracingVegetation (and WantsRayTracingGrooms, which shares it) ran the producers on a Deferred frame whenever a light asked for ray-traced shadows, but RayTracedShadowPass only runs when the shadow technique is RayTraced. A scene whose lights request the tier therefore built vegetation and groom proxies on every raster-shadowed Deferred frame for no consumer. The predicate is now RayTracing::HybridProducersWanted: a light's request counts only while the tier is armed (shadows on, technique RayTraced), the condition RenderPipeline enables the pass on; reflections are as before. VegetationPolicy.HybridProducersRunOnlyForAnArmedConsumer pins it. Live Vulkan editor, #1259 acceptance scene, Deferred with raster shadows (its lights request the tier for the hybrid cell): Vegetation::Queue 24-39 ms -> 0, frame p50 72 -> 30 ms (head-backlit), 137 -> 96 (coats-closeup), 190 -> 136 (population-dolly), 161 -> 99 (rapid-turn). The hybrid cell still builds them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd raw data The gate (16.667 ms p95 with VSM, 1920x1080, RTX 4090) is missed on every cell: GL Forward, Forward+ and Deferred under the controlled protocol (three counted runs each), and Vulkan Forward, Forward+, Deferred and hybrid live in the editor. The frame is CPU-bound on both backends, and each miss is attributed to its scope or pass with an open issue: bound-coat deformation and the strand pass's CPU side (#1588), GroomPass on GL (#1589), ShadowPass outside its brackets with a CSM control (#1590), the load-time memory transient that defeats the editor-host capture (#1591), CPU hitches on the live editor (#1594), the vegetation build budget that withholds the hybrid TLAS (#1578), lamps without groom shadows under VSM (#1592) and the head's hair (#1593). Evidence: the fifteen per-path stills, GL motion footage and AOV sheets, the lighting-tap sheet, Vulkan stills of the head, the GL protocol summary, the raw live Vulkan runs, and a validation walk under the Khronos layer (no error or warning, with an info-level control). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two conflicts. AssetRegistry.oar is a three-way union of the binary registry (338 base entries, +5 here: the fox coats and their bindings, +36 on master: the dog breeds), every handle kept, no path on two handles. kSaveGameFormatVersion: both sides changed the payload under 44 (AnimalPathComponent::m_FollowTerrain here, GroomComponent's authored segment budget on master), so the combined layout is 45. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…byte budget Past the eighth resident coat volume a newcomer is admitted while the set stays within kMaxResidentCoatVolumeBytes (5972968). When it did not fit, the pass reclaimed ONE least-recently-used volume and granted the slot whatever its size, so a close-up coat could take a 300 KiB herd volume's slot and push the resident set hundreds of megabytes past the budget. GroomCoatShadow::AdmitCoatVolume now reclaims until the newcomer fits by count or bytes and refuses it (BudgetExhausted) when nothing more can be reclaimed. ReclaimLeastRecentlyUsedCoatVolume returns the bytes it freed, and the resident bytes are summed once per newcomer, only once the count is reached, instead of walking the cache on every check. GroomCoatShadowSelection.ALargeNewcomerReclaimsUntilItFitsOrIsRefused pins all three cases; FoxCoatEvidenceTest.AHerdKeepsEveryCoatVolume stays green. Found by the #1259 self-review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… read once a tick UpdateAnimalPaths sampled the terrain at the animal's LOCAL translation and wrote the world height into local Y, so an animal under an offset or turned group floated or sank by the group's transform. It now takes the path point through the parent's world matrix, sets it on the ground there and brings it back into the parent's space. The terrain sampler (TerrainGroundSampler) gathers the terrains and their inverse world matrices once per UpdateAnimalPaths call instead of inverting one per animal per terrain; SampleTerrainGroundHeight is a one-shot wrapper over it. The "AnimalPaths" system declares the read it makes, Reads(kWorldTransforms), beside its ReadsWrites(kLocalTransforms). AnimalPathFollowsTerrain.AParentedAnimalStandsOnTheGroundUnderItsWorldPosition pins the parented case. Found by the #1259 self-review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApplyEntityMotion advanced the scene's animal paths on every captured frame (c9cc753), but a running scene's gameplay schedule already runs "AnimalPaths", so a capture taken in Play walked the herd at twice the speed and off the test host's trajectory. It now steps them only while the scene is not running. On the editor host, a step with no active scene is again a failure only for a manifest that names an EntityMotion target. The editor host also read the window's VSync on the MCP tool thread after the restore epilogue; it is now read on the main thread while the capture's settings are applied, and that is the value result.json records. AnimalPathFollowsTerrain.ACaptureStepLeavesARunningSceneToItsOwnTick pins the first. Found by the #1259 self-review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eckboxes "Follow terrain" was inserted between "Orient to path" and its tooltip, so hovering it showed the yaw explanation and "Orient to path" showed nothing. Each checkbox now carries its own. Found by the #1259 self-review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s dropped Two gaps in the view-sized cull slots (#1581) as the VSM caches pages (#1580): - A shadow slot is shared by every shadow view and learns of an overflow only through the fenced demand readback, so the frames before it grows (up to the ring's depth) draw short -- and the VSM's coarse levels keep such pages. When a shadow slot grows, the next CollectVsmInvalidations now redraws every clip level once. - A demand capture in flight indexes the layers as they were. Applied after a regeneration rebuilt the layer list, it grew whichever slot then sat at its index, and slots only grow. FoliageCullDemandReadback::DiscardInFlight retires those captures without returning them; a slot that still overflows writes its demand again. The FoliageGPUCuller.h note that "the frame that overflowed is the only one" to draw short is corrected. FoliageGPUCullEvidenceTest and FoliageVsmCacheVisualEvidenceTest stay green; neither race has a CPU seam to pin it with. Found by the #1259 self-review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot by its getter AnimatedModel::GetEntityMeshSource() is const, yet for a one-mesh model (fb98963) it rewrote the shared m_Meshes[0] in place on every call: every submesh's material index set to 0 and an imported-material table attached, a side effect any other holder of that MeshSource saw depending on call order. AttachOneMeshMaterialTable does it once, at the end of LoadModel's cache and import paths, and the getter only returns the mesh. m_Materials holds one entry per mesh, so entry 0 remains the right material. SkinRiggedHead* and the GroomAnimals binding tests stay green. Found by the #1259 self-review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
glTF names its buffers and images by URI, so "editable buffer.bin" is
written "editable%20buffer.bin". Assimp 6.0.4 opens the URI as a literal
path and fails ("GLTF: could not open referenced file"), so a valid glTF
whose external files have a space -- or any escaped byte -- in their names
did not import. AnimatedModelCacheSurface.AnExternalBufferEditInvalidatesBothCaches,
added with the external-buffer cache invalidation on master (482f8cf),
builds exactly that file and failed on every machine with a GPU (it skips
on CI, which has none).
Both importer setups (ConfigureAnimatedImporter, ConfigureStaticImporter)
now install PercentDecodingAssimpIO: the literal path first, so a file whose
name really contains '%' still opens, then the percent-decoded one, the same
decoding MeshCache fingerprints the buffers with. DefaultIOSystem is final,
so it is wrapped. The test now passes, and 787 model, import, mesh-cache and asset tests
pass with it. DogShowcaseEvidenceTest fails before any import, on the
committed Dog.rig.json having no "assembly" block (filed).
Found by the #1259 merge with master.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Important Review skippedToo many files! This PR contains 145 files, which is 45 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configuration
⛔ Files ignored due to path filters (130)
📒 Files selected for processing (145)
You can disable this status message by setting the
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
AssetContentValidity.EverySupportedAssetOnDiskIsInTheRegistry failed in CI: Scenes/Benchmark/Acceptance.olo is on disk but not in AssetRegistry.oar. The registry this branch carried was the three-way union from the master merge, and the scene's entry had only ever been added by a local editor run, never committed. Rebased with AssetContentValidity.DISABLED_RebaseAssetRegistry: one entry added, none removed or changed (380 total); all 28 AssetContentValidity cases pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 of 9 tasks
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #1259. Closes #1581. Refs #1578, #1580.
What this is
The #1259 acceptance gate: one generated scene holding every subject of the #1225 programme at once, captured on every backend and path under one protocol, with the result published. The gate (16.7 ms p95 with virtual shadow maps, 1920x1080, RTX 4090) is not met on any cell. Every miss is attributed to its pass or CPU scope and has an open issue. Nothing was disabled or thinned to get closer. Full write-up:
docs/analysis/integrated-acceptance-1259.md.The scene.
Scenes/Benchmark/Acceptance.olois generated byscripts/perf/generate-acceptance-scene.py, never hand-edited. It contains:19 manifests cover the gate cells, CSM controls, upscale cells, motion footage and lighting taps.
Engine work the scene needed:
AnimalPathComponent::m_FollowTerrain, save-game format 45);Decided sibling items merged in:
Measurement tooling:
scripts/perf/acceptance-benchmark.py, the interleaved fresh-process Release protocol for the GL test host;scripts/perf/acceptance-live.py, the live editor cells with CPU-scope, groom, RT and per-pass-CPU attribution.The editor-host capture cannot load this scene within its main-thread wait (#1591), so the Vulkan cells are live measurements.
Results (RTX 4090, driver 617.14, i7-14700KF, Release, native 1080p, VSM on)
Where the frame goes:
Groom::Deformcosts 59–128 ms of main-thread CPU whenever the herd is in view, with 16–43 coats on the card tier.Evidence is in
docs/testing/evidence/integrated-acceptance-1259/:Verification
acceptance-benchmark.py, 3 interleaved fresh-process runs, 300 frames per camera;AcceptanceSceneTest.*(every subject bound and grounded, cameras clear the terrain, every gate camera sees its subject on every raster path;Acceptance_GL_*PNGs inspected)acceptance-live.pyon a Release--rhi=vulkaneditor, 3 passes; attribution pass; post-fix Deferred checkinfologsCURRENT-VALIDATION-ENABLEDGroomStrand.glslthrough glslc (vulkan1.2, opengl4.5); GL logsLightingTapEvidenceTest.*with a lit tuft added*Import*:*Model*:*MeshCache*:*Asset*:*Vegetation*:*Fox*:*Dog*Found and fixed
olo_rt_scene_stats.AcceptanceSceneTestnames all four lights on the old scene.LightingTapEvidenceTest's new tuft fails every cell on the old shader. A shader-swap A/B shows 40 groom evidence PNGs pixel-identical tap-off.VegetationPolicy.HybridProducersRunOnlyForAnArmedConsumer; live Deferred frame p50 72 → 30 ms at head-backlit.GroomCoatShadowSelection.ALargeNewcomerReclaimsUntilItFitsOrIsRefused.AnimalPathFollowsTerrain.AParentedAnimalStandsOnTheGroundUnderItsWorldPosition.AnimalPathFollowsTerrain.ACaptureStepLeavesARunningSceneToItsOwnTick.AnimatedModelCacheSurface.AnExternalBufferEditInvalidatesBothCachesfailed on every GPU machine. It passes now.AssetRegistry.oartaken as a three-way union of the binary registry (+5 here, +36 there). Save-game format 45 covers both payload changes.Filed, not fixed
03cd22966with each worktree's Release binary.Dog.rig.jsonhas no assembly block. Too big: a Blender re-export.Review guide
Where I'd look hardest
OloEditor/assets/shaders/GroomStrand.glsl, the tap split inmain(). Direct versus environment is taken as the result minus a snapshot taken after the light loop. A future edit that adds light after that snapshot would land in the indirect taps.OloEngine/src/OloEngine/Scene/Scene.cpp,UpdateAnimalPathsand the newReads(kWorldTransforms)on "AnimalPaths". The read is declared so the derived order holds, and the parented round trip writes the full local translation, not only Y.OloEngine/src/OloEngine/Renderer/PercentDecodingAssimpIO.h, now in every static and animated import. It decodes only when the literal path does not exist.What I verified, and how
Least confident about
97b214c82) are reasoned and regression-tested, not reproduced.🤖 Generated with Claude Code