Profile and optimize stationary PRK.Client performance
Compare latest in-game profiler exports with a fresh managed CPU/GC trace from the user's running stationary client. Identify measured dominant costs, implement focused optimizations preserving rendering/gameplay behavior, and validate with regression tests and matched before/after measurements. Keep capture artifacts local; do not restart the user-owned client without coordination. Acceptance: validated baseline; evidence-backed changes with relevant tests/build passing; matched runtime comparison or explicitly recorded pending restart/measurement. Baseline captured from PID 1459890 (Debug .NET 10.0.11), stationary scene. Local artifacts: .dev-data/traces/client-performance/stationary-baseline.nettrace and .speedscope.json, allocations.txt, cpu-summary.txt, validation logs. Latest in-game exports at ~/.local/share/PRK/profiles/profile-20261014-160221-* and profile-20261014-160254-*. In-game culling 7–9 ms, command recording 8–9 ms; roughly 11,296 translucent draws. Managed main-thread wall samples: instance update 49%, culling 35%, command recording 34%, mesh recording 20%, synchronous picking about 8% (inclusive, overlapping; not pure CPU counters). Allocation ticks attribute approximately 516 MB/60 s to attachment SetParentMatrix state/delegate churn and 137 MB/60 s to shader filename extraction. First focused patch on perf/reduce-client-frame-allocation: static transform-update delegates avoid captures; identical transforms preserve state and skip journal notifications; shader support checks use nonallocating filename spans. Red-green allocation tests plus concurrent setter and composition/journal regressions. Debug client build passes with zero warnings/errors; 654 non-install graphics tests pass on host (container native Skia prerequisites were unavailable). No gameplay/FPS improvement claimed yet. After comparison captured from relaunched PID 1481824: stationary-after-allocation.nettrace/.speedscope.json, allocations-after.txt, and comparison.json with reusable compare.py are local under .dev-data/traces/client-performance. Sampled top-30 allocation attribution decreased from 723 MB to 384 MB per 60 seconds (~12.1 to 6.4 MB/s); Gen0 starts 53 to 27, Gen1 4 to 3, no after Gen2. Captured transform delegates and shader filename string allocations no longer appear. Attachment state clones remain ~329 MB/60 seconds because animated joints legitimately change. Runtime caveat: exports include resource loading (50,789 → 65,917 ready entries in original; relaunched export also loads toward 65,917). Raw average elapsed values are not matched FPS evidence. Comparable loaded ready-set slices show median 26.35 → 26.66 ms; last 100 frames 26.08 → 25.93 ms. No material frame-time improvement is established. Last-100 median allocation/frame falls 368,908 → 196,800 bytes. Main-thread wall attribution remains culling ~35%, recording ~31%, mesh recording ~20%, picking ~8% (inclusive overlaps). Optimized Release build with EnableDevClient=true and PrkShipPdbs=true prepared at PRK.Client/PRK.Client/bin/Release/net10.0/PRK.Client.dll; build passes zero warnings/errors and 654 non-install graphics tests pass in Release as well as Debug. Release baseline captured from PID 1491474 at stationary-release-baseline.nettrace/.speedscope.json and allocations-release.txt. Stable export profile-20261014-161934-* has ~65,919 ready entries and 15,807 transparent draws: median frame 12.07 ms, p95 22.22 ms, median culling 3.01 ms, command recording 5.67 ms, translucent 4.12 ms. Picking mean 1.22 ms/frame and p95 9.58 ms; managed sampled wall time ~7.9% in synchronous readback. Release vs Debug difference is build optimization, not the allocation patch's FPS benefit. Second patch: HoverPickerReadback owns one four-byte readback buffer per frame slot, coalesces unrecorded requests, records the pixel copy after the normal frame's render passes, and delivers after that slot's existing frame fence. No extra hover submission/wait. Synchronous click picks are unchanged. Resize invalidates stale recorded/requested hovers; shutdown/device loss and constructor rollback reclaim bounded buffers. Nine affected Vulkan tests (readback/transfer) pass on RX9070/RADV with synchronization validation, including 1–3 frame slots, delayed/once-only callbacks, coalescing, invalid coordinates, resize invalidation, slot reuse rejection and injected construction failure. User explicitly requests live gameplay/performance verification before any review. Review attempt was aborted; no review result or approval is claimed. Verified matched Release after capture from PID 1504697 at stationary-release-after-hover.nettrace/.speedscope.json and allocations-release-after-hover.txt. Latest export profile-20261014-162743-* has same 15,807 transparent draws, 1,746 frustum rejections, 7,653 shadow-only parts and essentially identical ready/submitted/triangle counts. Elapsed mean 13.95 → 12.82 ms (~8.1% decrease); median 12.066 → 12.073 ms (unchanged); p95 22.22 → 20.44 ms. CPU Picking mean 1.219 → 0.00148 ms and p95 9.581 → 0.01229 ms. Managed trace no longer attributes main-thread samples to ServePick/ReadPixelFromImage/ExecuteUploadBatch; before these represented ~7.9% inclusive wall time. GPU pass timings also vary (~8–10% shorter after), so overall elapsed gain is observed, not wholly causally attributed to hover code. Main verified result is removal of synchronous hover stalls, not median FPS improvement. Reusable compare.py and comparison.json/.txt cover all four traces/exports. Broader current-patch validation: 3,150 client tests pass with 52 skipped and 654 graphics tests pass in Release on host; nine affected Vulkan tests previously passed with synchronization validation. Container-wide client attempt failed 71 tests due missing native system SQLite; the identical compiled suite passes on host. No review run/result claimed. User confirmed hover targeting/cursors, clicking and resizing still work with the rebuilt Release client. Shutdown was not explicitly confirmed. This allocation/hover batch has automated validation, matched runtime profiling and user-confirmed interaction behavior; further draw-submission work and review remain separate next steps. Remaining main CPU bottleneck is mesh/draw submission (~5.6 ms command recording, ~4.2 ms translucency); median frame remains ~12.1 ms. Existing install root is /mnt/games/PRK-NEW/client. User-owned process was not stopped/restarted automatically. Pull request: #2662 — https://git.project-rk.com/prk/engines/pulls/2662 Head perf/reduce-client-frame-allocation, base develop; commits ab5c42e82 (allocation reductions) and f42488270 (frame-submitted hover readbacks). Only the six session files are included; remote base comparison is 0 behind/2 ahead and working tree is clean. Additional profiler-disabled Release client build passes with zero warnings/errors. No separate feature review performed. Next scope: optimize draw submission in the user's heavier stationary location, independently of PR #2662. New branch perf/reduce-client-draw-submission starts at f42488270 (stacked on the pending allocation/hover PR; do not mix follow-up changes into its published branch). Fresh baseline from PID 1520220 and export profile-20261014-163658-*; local capture artifacts under .dev-data/traces/client-draw-submission. Release export median elapsed 12.58 ms, recording 5.83 ms, translucent 4.04 ms, culling 3.23 ms, 15,759 transparent draws with 77 meshes/22 materials. Preserve exact draw/transparent ordering, rendering output, resource retirement and portability. Fresh 60-second Release baseline complete: heavy-baseline.nettrace/.speedscope.json and cpu-baseline.txt under .dev-data/traces/client-draw-submission. Sampled main-thread inclusive wall time ~44.4% command recording, ~34.2% Mesh.RecordVulkan, ~30.3% translucency, ~24.5% culling; scopes overlap. Source confirms every static draw previously rebound both geometry buffers and performed checked allocator handle lookups even for an immediately repeated mesh. No native perf collector is installed; sampled managed native-boundary attribution does not resolve driver internals. First draw patch prepared on separate branch: pass-local DrawBindFilter tracks mesh reference alongside pipeline/material state; repeated adjacent mesh draws skip vertex/index rebinding across material/pipeline changes. Mesh changes and each new pass still bind; all draws, instance ranges, order, owner lifetime checks on binds and defaults for other callers are unchanged. Added SceneMeshBinds/TranslucentMeshBinds export counters and unit regressions for alternating/repeated meshes, fresh pass state and stats aggregation. Native vertex-binding call uses the direct pointer overload. No indirect draws or global geometry packing introduced. Release profiler client build passes with zero warnings/errors. 658 non-install graphics tests pass; 12 existing Vulkan upload/lifetime/transfer/sky tests pass on RX9070/RADV with synchronization validation. These existing Vulkan tests do not independently establish live filtered-draw output equivalence. No review triggered. After bind-filter comparison: new PID 1533635, export profile-20261014-164612-* and 60-second heavy-after-bind-filter.nettrace/.speedscope.json. Local compare.py, comparison.json/.txt and allocations-after-bind-filter.txt preserve the evidence. Transparent draw count remains exactly 15,759, instance upload/triangles/submitted parts essentially unchanged, shadow/reflection-only counts match. TranslucentMeshBinds mean 12,441.7 versus the previous per-static-draw binding path (~15,760 with water), ~21% fewer geometry binds (~6,636 avoided native vertex/index bind calls per frame). Median translucent CPU time improves 4.035 → 3.604 ms (~10.7%); command recording 5.830 → 5.476 ms (~6.1%). Frame median 12.582 → 12.428 ms (~1.2%), mean 13.315 → 13.292 ms (essentially unchanged), p95 21.163 → 21.065 ms. Culling is slower in after export (median 3.226 → 3.473 ms), and character/ready counts vary, so no material overall FPS gain claimed. GPU translucent median 1.662 → 1.508 ms; other GPU scopes broadly stable. Managed trace main-thread inclusive recording share changes ~44.4% → 41.6% and monitor slowpath ~6.52% → 5.13%. Mesh.RecordVulkan attribution is not directly comparable after JIT inlining; do not interpret the disappearing separate stack as elimination of all mesh work. User confirmed the bind-filter build still looks visually correct in the heavier scene. This verifies the first submission optimization's live visual check; overall frame-time benefit remains modest. Second submission optimization implemented: DrawStreamBuilder now appends liquid/transparent instance rows in its existing sorted visual/part order, merges only adjacent commands sharing the exact InstanceBatch, and resolves absolute instance offsets after all batches are filled. Warm reused frames refill the saved sorted part-index sequence while preserving newly updated per-instance lighting/shadow/style data. All draws remain in their original logical primitive order; no non-adjacent material/mesh/batch-key reordering or indirect-draw capabilities required. Shader inspection confirms static/grass/body-outline/shadow shaders use gl_InstanceIndex only to select the matching instance row. Vulkan drawing specification defines instance primitive order from lower to higher instance number. Validation for this second patch: 667 non-install graphics tests pass, including coalesced liquid/transparent ordering, material/batch-key boundaries, alternating incompatible draws, reused instance-data updates, camera reordering, visual grouping and zero steady-state allocations; the independent high-level rebuilt-stream model matches optimized draws/instance uploads/bindings over the seeded scenes. 3,150 client tests pass with 52 skipped. Twelve existing native lifetime/transfer/sky regressions pass with synchronization validation; they are not a dedicated image-equivalence test of coalesced production draws. Explicit Release/profiler client build passes with zero warnings/errors and git diff --check is clean. Live coalescing verification complete: user reported no visual issues and exported profile-20261014-170036-* from rebuilt PID 1556628. Fresh 60-second heavy-after-coalescing.nettrace/.speedscope.json and allocations-after-coalescing.txt captured; local compare.py and comparison.json/.txt now explicitly bind all three heavy-scene exports/traces rather than selecting newest files implicitly. Combined submission patch versus original heavier-scene baseline: transparent draws 15,759 → mean 12,442.35 / median 12,443 (~21% fewer) with all 15,759 transparent instances retained, same 77 meshes/22 materials, stable material/pipeline bind counts, 146 reflection-only and 11,953 shadow-only parts, and triangles/instance uploads essentially unchanged (one extra submitted part). Median recording 5.830 → 4.967 ms (~14.8% lower); translucent 4.035 → 3.471 ms (~14.0% lower); frame 12.582 → 11.848 ms (~5.8% lower). Frame mean 13.315 → 12.571 ms (~5.6% lower), p95 21.163 → 19.935 ms (~5.8% lower). Sorted stream building becomes more expensive: median 1.049 → 1.264 ms (+0.215 ms). This overhead is outweighed by observed recording savings. Coalescing-only comparison versus bind filter: recording 5.476 → 4.967 ms, frame 12.428 → 11.848 ms; culling/GPU timings also vary, so total elapsed improvement is observed rather than wholly attributed to one change. All three managed traces remain dominated by command recording/culling. Inclusive sampled shares and separately inlined mesh frames are not directly comparable proof of absolute method speed. Current batch has 667 graphics + 3,150 client tests passing (52 client skips), 12 existing native regressions passing, a clean Release build and user-confirmed visuals. Dedicated native pixel-equivalence coverage and cross-platform gameplay remain limitations. No review triggered. User rejected the submission experiments because the ~0.7 ms gain did not justify instance-layout complexity, and explicitly requested reverting the entire draw-submission batch while retaining PR #2662. All seven tracked experiment files were restored and the two added test files removed; patch/test sources and captures are archived locally in .dev-data/traces/client-draw-submission/discarded. Working tree is clean and exactly matches perf/reduce-client-frame-allocation. The unpublished follow-up branch was renamed perf/cull-authored-grass; no published history or PR branch changed. Restored Release/profiler client builds with zero warnings/errors; the already-running client remains the experimental binary until user relaunch. Current investigation scope: materially reduce authored distant/occluded vegetation submissions, not merely make identical draws cheaper. Verify asset/category attribution before assigning foliage distance policies; preserve buildings, characters, reflections/shadows, correct visibility and camera-motion behavior. Source findings: generated GrassTerrainRenderer already has grass distance/LOD controls and uses an alpha-tested, non-blended material, so it is not the 15,759 transparent draws. Authored MeshStatel placements are ordinary world renderables without per-object grass limits. SceneCuller has a 100 m near-frustum exemption, no per-object maximum draw distance and no geometry-occlusion rejection. PlayfieldChunk preserves authored large/medium/small/tiny groups while streaming, but large and medium share the same large radius and small and tiny share the same small radius. Default ViewDistance (0.8) gives far plane 800 m, small/tiny load radius 760 m, large/medium 920 m. The authored layer/category is not propagated into renderable metadata. This suggests a safe provenance/tagging and draw-attribution path before implementing targeted distance/size/frustum culling; obstruction culling is a separate conservative visibility problem, not an excuse to discard everything blended. Existing ambient-occlusion depth resources are not already draw-occlusion infrastructure. Newland investigation at user-supplied position x=1110.8, y=38.3, z=462.4, playfield/resource 567, zone 577 (outside Newland gates, Earsnest Forest). A local read-only SQLite/RDB census reuses StaticMeshDecoder and authored metadata; source and JSON under .dev-data/foliage-census. Default streaming estimate: 35,751 loaded placements, 37,613 potential blended mesh parts. Nine stock grass mesh variants contribute 34,556 potential blended parts (~91.9%), of which 29,691 originate beyond 200 m and 21,590 beyond 400 m. Identified exact diffuse names grass_small[_dry], grass_medium_high[_dry], extra_tall_grass[_dry], gress_tust[2][_dry]; example root nodes are generic Plane47/Plane23, so node-name substring rules would miss them. Counts are modeled loaded placements, not actual frustum/submission/occlusion counts; hysteresis and texture replacement are not modeled. No mesh decode refusals. The census uses SQLite Mode=ReadOnly and does not modify the installation. Targeted implementation now on perf/cull-authored-grass: exact verified authored diffuse-name allowlist plus Blend tags GroundCover at world-material import. Unknown textures, trees/shrubs/buildings and non-blended materials remain untagged. Material equality/hash includes the tag so material UBO caching stays distinct. CPU SceneCuller rejects tagged grass outside nearest horizontal world-bounds distance from the character-centred grass origin before lighting/stream building, using existing Grass quality ranges (Low 80 m, Medium 128 m, High 192 m). Grass Off now disables generated terrain grass only; authored grass retains the Low 80 m range with a 67.5–80 m fade. Tagged grass no longer takes the 100 m main-frustum exemption; reflection/shadow recovery remains within its distance range. One frame snapshots origin/profile for both culling and engine shader uniforms. Transparency shader reuses the existing material UBO padding slot at byte 12 for GroundCover, preserving the 64-byte C# layout; tagged grass smoothly fades with the existing fade-start/end range and discards fully faded fragments before depth writes. Added GroundCoverCandidates/DistanceRejected/Submitted export counters. No scene occlusion system or grass-specific streaming-radius rewrite is included yet. Validation: 662 non-install graphics tests pass, covering nearest-boundary/height/invalid input, quality limits, early lighting rejection, ordinary scenery preservation, near-frustum behavior, reflection/shadow recovery, material identity and UBO size/offset. 3,163 client tests pass, 52 skipped, including exact asset classification and unchanged authored blend/depth/shadow flags. Updated shader layout contracts pass. All six graphics variants of the modified transparency shader compile with host glslc. Twelve existing native upload/lifetime/transfer/sky tests pass with synchronization validation; they do not independently image-test the new fade shader. Explicit Release/profiler build passes with zero warnings/errors. Live performance verification: relaunched PID 1619065 exported profile-20261014-174238-*; new 60-second managed trace is .dev-data/traces/authored-grass/newland-after-distance.nettrace/.speedscope.json, with allocations.txt and explicit baseline/after comparison.json/.txt generated by local compare.py. Compared with the original heavier Newland export profile-20261014-163658-*: transparent draws 15,759 → 2,358 (~85.0% reduction), submitted parts 29,957 → median 5,550, instance upload 2,875,872 → 532,800 bytes/frame. Tagged grass candidates 26,536, distance rejected 23,270, submitted 1,411. Median culling 3.226 → 1.898 ms, stream build 1.049 → 0.249 ms, recording 5.830 → 2.134 ms, translucent CPU 4.035 → 0.606 ms. Median elapsed 12.582 → 9.650 ms (~23.3% lower), mean 13.315 → 10.006 ms (~24.9% lower), p95 21.163 → 17.017 ms (~19.6% lower). This materially exceeds the discarded submission-only experiment. Scene/reflection draw counts remain close; character/ready counts and GPU timings vary, so total elapsed benefit is observed, not exactly attributable in isolation. Median FrameFence wait grows 0.0033 → 3.494 ms as CPU work drops. Managed inclusive wall attribution shifts from recording ~44.4% / instance update ~36.7% to recording ~22.7% / instance update ~24.9%; sampled method shares overlap and are not pure CPU counters. User reports GPU pinned at 100% at 4K with fully maxed settings, consistent with the new GPU-limited steady-state behavior. Current GPU medians: AO total 3.356 ms (estimate 2.413 ms), scene 2.850 ms, translucent 0.897 ms, shadows 0.914 ms. No obstruction-occlusion culling claimed. User confirmed grass visuals/fading are correct but requested that Off preserve important authored scenery at a short distance. Implemented Off → Low (80 m) for authored grass, while generated Profile(Off) remains disabled. GrassFadeRanges.xy carries generated fading and .zw authored fading, without changing uniform sizes. Both culling and uniforms derive from the same frame's quality snapshot. Per user preference, the player-facing label remains simply Off; help says Controls grass detail and visibility distance. Generated/authored implementation terminology is not exposed in the menu. Nearby authored grass retention remains unchanged. Focused options-presenter validation passes 17 tests with 15 asset-dependent skips after this text-only adjustment. Red-green regression reproduced complete removal of nearby authored grass before the fix; current tests verify near authored grass retained, far grass rejected, generated Off disabled, and Low/Medium/High profiles unchanged. Full 662 graphics/3,163 client tests pass (52 client skips); six transparency variants plus generated grass vertex shader compile; explicit Release/profiler build passes zero warnings/errors. Previous 12 native regression passes precede this Off-only refinement and are not new image validation. Await user live check of revised Off; the measured max-quality performance result remains for the previous live build. No review triggered. Grass pull request: #2663 — https://git.project-rk.com/prk/engines/pulls/2663 Head perf/cull-authored-grass, base develop; single commit ea421b45e. PR #2662 was already squash-merged, so only the new grass commit was replayed onto latest origin/develop (57603a05d), avoiding duplicated allocation/hover commits and discarded experiments. Only the 15 intended grass files are included; working tree clean and branch pushed with upstream. Post-rebase builds pass, but full tests failed when /tmp reached 100% usage (temporary-file and SQLite disk-full errors). A local-temp rerun was interrupted when the user requested opening the PR immediately. The PR accurately records prior full validation, live measurements, the missing fresh Off gameplay check and the incomplete post-rebase full rerun. Status was not changed merely because the PR opened.
Checklist
9 of 10 complete- Capture and correlate in-game frame metrics with managed CPU and allocation baseline
- Compare equivalent stationary runtime before/after captures
- Implement measured hot-path improvements and validate regression tests/build
- Confirm live hover cursors/targets, click picking and resize behavior with the new Release build
- Verify draw-submission improvement and visual behavior in matched live Release captures
- Evaluate draw-submission prototypes and validate regression tests/build (discarded after profiling)
- Identify draw-submission hotspots in the heavier scene from Release trace and export
- Identify authored grass assets and quantify their loaded-placement distance distribution at the Newland test location
- Verify targeted authored-grass distance/fade behavior and material/culling/shader regressions
- Measure live authored-grass draw reduction and frame timings in matched Newland Release captures
- Area
- Project Rubi-Ka
- Project
- engines
- Kind
- Chore
- Task ID
tsk_3vcjdmk66u- Updated