✦ SPIELWELTENMoto Racer - Rampage ↗

Campaign resource lifetime review

Memory evidence updated: 15 September 2026, 14:37 Vienna / 12:37 UTC. Two complete native 3×7 load/clear studies found a reproducible texture leak. The second identified its exact owner. The targeted cleanup in the 14:18 Windows intermediate build has now passed the same actual 21-load rendered repetition. This is a resource-lifetime pass for that measured content/build, not final release or adapter-VRAM acceptance.

Native findings and exact ownership

Both studies used one actual Intel UHD / Direct3D11 player at 1920×1080, three cycles of the seven chapter-opening stages, eight stationary riders, and the real RampageApp.StartStage / Home path. Measurements followed three frame boundaries and managed GC. They did not clear material/asset caches or run Resources.UnloadUnusedAssets.

Native study Result Evidence
13:26 run Completed all 21 loads; failed the texture plateau criterion Corresponding cycle-2/3 snapshots add 7 Texture objects and 3,134,796 runtime-size bytes. Mesh, Material, VolumeProfile and VolumeComponent count deltas are zero. Generated route/rider owners return to zero.
13:48 inventory run Completed all 21 loads; failed again Per-texture inventory identifies exactly one new ProbeRealTimeTexture_Garage reflection capture per load/Home cycle, each a 128×128 RenderTexture with reported runtime size 447,828 bytes.
14:31 post-fix run Passed, 21/21 loads in 50.798 seconds plateauObserved=true, zero errors, rendered Intel UHD / D3D11, no unused-assets sweep. Invocation independently records exit 0, successful event checks, unchanged production/QA files and matching build SHA.

The identified garage texture count is 1 at initial Home, then 8, 15 and 22 after the last Home of cycles 1, 2 and 3. Its total reported runtime sizes are respectively 447,828; 3,582,624; 6,717,420; and 9,852,216 bytes. That accounts exactly for the repeated 3,134,796-byte increase. The reports confirm unchanged production profiles, restored isolated QA files and no harness exception; completed execution does not make their failed plateau a pass.

GarageBuilder.Build creates a realtime/on-demand HDAdditionalReflectionData for every new garage. LoadRace destroys that garage, while Home creates another. In the pinned HDRP package, HDProbe.OnDestroy releases its realtime colour/depth RTHandles only inside #if UNITY_EDITOR. The player's HDAdditionalReflectionData.OnDestroy only removes its registry entry. The native texture inventory corroborates this source ownership gap.

The targeted fix is GarageReflectionResources.cs, attached to the dynamically created probe by SceneLighting.cs. It retains that probe reference and releases only its colour/depth RTHandles on destruction. The managed getter remains usable if the other component has already received OnDestroy; duplicate colour/depth ownership is guarded. RTHandle.Release removes the handle from its owner and destroys its owned RenderTexture. No package source, shared imported texture, reflection atlas, global cache or graphics setting is modified.

Post-fix evidence: the new texture inventory contains exactly one active garage probe texture at initial Home and each cycle-end Home. Its IDs change from -3334 to -217492, -430406 and -643320; each replacement remains 447,828 bytes instead of accumulating predecessors. At all three cycle-end Home snapshots the counts are identical: Mesh 560, Material 216, Texture 317, VolumeProfile 2, VolumeComponent 496. Texture runtime-size total is exactly 793,929,064 bytes each time. Corresponding warm-cycle checkpoints all pass; generated owner cleanup, production-profile preservation and QA-file restoration pass too. The bound assembly SHA-256 is 40D67A70E16EFEC97F1E44069E2459BCFBC4FD2E86BCC3B75A9B2A47CC2F35CB, build GUID ceb9f2a664944331ba78d21bfe328d31. No cache clear or unused-assets sweep was used. This confirms the targeted owner correction in the tested player; subsequent source changes still need appropriate regression.

Primary installed sources: Library/PackageCache/com.unity.render-pipelines.high-definition@700710090fa9/Runtime/Lighting/Reflection/{HDProbe,HDAdditionalReflectionData}.cs and Library/PackageCache/com.unity.render-pipelines.core@0bb36005e9ba/Runtime/Textures/RTHandle.cs.

Other ownership findings

Runtime lighting profiles: SceneLighting.Apply previously destroyed the old VolumeProfile without destroying its separately allocated VolumeComponents. Cleanup now explicitly destroys components belonging to that runtime-created profile, clears its list and destroys the profile; imported/shared profiles are excluded. The two native cycles above show no warm-cycle growth in either object class. This is measured evidence for that ownership correction, not proof that every HDRP resource is released.

Generated geometry: WorldMesh.Emit registers generated render/collision meshes with GeneratedWorldResources; its HashSet prevents duplicate ownership. Per-stage terrain material clones are also owned. Imported source meshes remain shared. Real load/clear measurements show no warm-cycle Mesh/Material count growth and no remaining track, bike or generated-owner roots after cleanup.

Shared chapter caches: imported tree/rock sources and shared materials are deliberately retained in ChapterAssets.Sources, AlpineFirAssets.LodParts, MaterialLibrary.Cache, AlpineTerrainMaterial.template, ChapterTerrain.Surfaces and the regional asset modules. The fixed-content repeated-cycle object counts plateau apart from the proven garage leak. This does not implement an active-chapter-only resource policy, prove a GPU budget, or cover all 21 distinct stage assets. Broader retention changes require measured demand and explicit ownership; a blanket cache clear is not this fix.

Source locations: RampageApp.cs, WorldGeometry.cs, ChapterTerrain.cs, ChapterAssets.cs, AlpineFirAssets.cs, MaterialLibrary.cs.

Loading cost: historical source estimate

The earlier source estimate is bound to its recorded source hashes. It is not recomputed for the new orthophoto, regional models, asphalt, sky or bank profile. Reproduce explicitly with python Tools/Verification/audit-campaign-memory.py when a new estimate is needed.

Historical route estimate Terrain tiles Generated vertices, including skirts Estimated vertex/index payload
Alps01 504 2,404,608 118.45 MiB
Alps02 504 2,369,024 116.35 MiB
Alps03 504 2,914,816 143.83 MiB

That estimate assumes float position/normal/UV/tangent streams and production index formats. It excludes CPU copies, managed construction lists, PhysX cooking, native objects, roads, textures, vegetation, riders, HDRP targets and driver allocations. It is not VRAM usage. The immutable DEM is approximately 0.603 MiB of managed short samples. Constructing the terrain and cooking nearby colliders synchronously inside Track.Build remains a load-time cost; these counts alone do not measure its duration. Intra-frame load peaks and the visible-valley/corridor policy remain separate work.

Implemented harness and acceptance

CampaignMemoryQA.cs runs the first stage of each chapter for exactly three cycles. It seeds unlocks only in the isolated profile and reports those fixture IDs; it is not completed story progression. Each eight-rider load pauses before GO, settles, records resources, and returns Home through actual app cleanup. It supplies no driving commands or position, velocity, force or progress corrections. Quality and livery stay fixed.

Root-coordinated native invocation:

-rampageTest memory -rampageMemoryQA -rampageMemoryReport "<output-directory>"
-rampageMemorySeconds 1800 -rampageMemoryStageSeconds 180

Outputs include snapshots, a partial report and a final report, plus the added per-texture name/instance-ID inventory. Counts/runtime-size estimates cover Mesh, Material, Texture, VolumeProfile and VolumeComponent; Unity allocated/reserved/managed and process memory are separate metrics. All normal checkpoints and profile restoration must be present. No positive warm-cycle object count growth is accepted; settled Unity allocation growth above max(32 MiB, 2%) also fails. Reserved-heap high-water marks alone are not interpreted as leaks.

The harness and external runner have finite bounds; synchronous loading can block its coroutine, so the external process timeout remains necessary. -batchmode -nographics can run an explicitly headless loading study but cannot establish rendering or GPU-budget acceptance. Even in rendered runs, Profiler.GetAllocatedMemoryForGraphicsDriver is a Unity counter, not adapter-local committed VRAM or its budget. The preliminary 6 GiB graphics planning target still needs actual adapter measurements and load peaks.

Optional -rampageMemoryUnloadUnused performs a separately labelled final diagnostic sweep, excluded from ordinary-cycle plateau criteria. Static chapter caches are not cleared. It must not conceal a production-path leak or be reported as its repair.

External runner and evidence-preservation repair

Tools/memory-test.ps1 starts one new player after checking that Unity/Rampage is not already open. It uses a fresh QA directory, a monotonic overall timeout, executable/PID/start-time ownership, native-log validation, System GPU/display event checks and before/after hashes for production and QA main/backup/temp files. It closes only its own matching process on failure. Missing/partial reports, contradictory success flags or unavailable required event evidence do not pass.

.\Tools\memory-test.ps1 -GraphicsApi Direct3D11 -DeviceIndex 1
.\Tools\memory-test.ps1 -Headless -Seconds 1800 -StageSeconds 180

The adapter index is machine-specific; the example does not automatically select Intel.

The 13:23 startup attempt failed before a completed memory study. Its final failure JSON reached invocation.json.tmp, but PowerShell converted the null File.Replace backup-path argument to an empty string. The root preserved the preparing manifest and copied the untouched failure JSON into invocation.json after the owned process had exited. runner-failure-note.txt documents that recovery. The failed attempt supplies no native memory pass and remains in the evidence set.

The runner now passes [NullString]::Value to preserve the required null backup path. The offline runner verification reports 88 passed / 0 failed, including the added replacement/manifest guards. These tests exercise actual extracted runner guards with synthetic JSON, temporary files and mocked events; they start no Unity/player, terminate no process and measure no native memory. The two subsequent native runs demonstrate usable reporting and retain their actual leak failures.

Still open