@@ -42,6 +42,9 @@ namespace {
4242 bool compositor_failed = false ;
4343 bool compositor_hooked = false ;
4444 bool drawn_this_frame = false ; // guard: draw only in the first world pass per frame
45+ #ifdef _DEBUG
46+ int dump_calls_remaining = 0 ; // harness `frcache` diagnostics: log buffer layout for the next N calls
47+ #endif
4548
4649 // Registered overlay draws, invoked in registration order between world and HUD.
4750 std::vector<std::pair<int , GameWorldCompositor::DrawCallback>> callbacks;
@@ -113,6 +116,20 @@ namespace {
113116 GW::Hook::EnterHook ();
114117 IDirect3DDevice9* device = GW::Render::GetDevice ();
115118
119+ #ifdef _DEBUG
120+ if (dump_calls_remaining > 0 && render_buffer) {
121+ dump_calls_remaining--;
122+ auto & buf = *render_buffer;
123+ Log::Log (" [frcache] call size=%u drawn_this_frame=%d" , buf.size (), drawn_this_frame ? 1 : 0 );
124+ for (uint32_t i = 0 ; i < buf.size (); i++) {
125+ const auto & e = buf[i];
126+ const GW ::UI ::Frame* f = (e.type != FRCACHE_GPU_RENDER && frame_array && e.index < frame_array->size ()) ? (*frame_array)[e.index ] : nullptr ;
127+ Log::Log (" [frcache] i=%u type=%u index=%u param=0x%x frame_id=%d visible=%d" ,
128+ i, (uint32_t )e.type , e.index , e.param , f ? (int )f->frame_id : -1 , f && f->IsVisible () ? 1 : 0 );
129+ }
130+ }
131+ #endif
132+
116133 // Nothing to do (no overlays, unusable buffer/device) -> run the original untouched.
117134 if (callbacks.empty () || !render_buffer || !device) {
118135 FrCacheRenderAll_Ret (param_1, param_2);
@@ -122,37 +139,45 @@ namespace {
122139
123140 auto & buffer = *render_buffer;
124141
125- // The main 3D scene is the FIRST GPU_RENDER block; everything after is HUD - including the 2D HUD and
126- // agent/UI 3D renders, which all correctly draw over us. Split right after the first GPU block. (Skipping
127- // further consecutive GPU blocks would swallow GPU-rendered HUD entries into the world side, so our
128- // overlays would then draw on top of them.) UI-only/sub-render passes have no GPU block, so boundary
129- // stays == size and is skipped below.
130- uint32_t boundary = buffer.size ();
142+ // The main 3D scene starts with the FIRST GPU_RENDER entry; the HUD follows, possibly interleaved
143+ // with further GPU entries (e.g. Domain of Anguish's frame keeps GPU slices to the very end). GPU
144+ // entries are (index, count) slices of ONE contiguous GR command stream - FlushCommandQueue at the
145+ // boundary materialises the whole queued world regardless of where the slices sit, so splitting
146+ // right after the first GPU entry is safe. A GPU entry whose index does NOT continue the stream
147+ // (index != previous index + count) is a SECOND world-render dispatch (e.g. a 3D bust in the
148+ // hero/character panel): running that in a split-off second call re-runs the per-object cloth/hair
149+ // physics off a fixed 1/30s accumulator not designed to advance twice in one real frame, desyncing
150+ // constraint indices from the double-buffered positions (garbage spring-solver index / GrBound.cpp
151+ // assert in wild dumps). UI-only/sub-render passes have no GPU entry, so boundary stays 0 and is
152+ // skipped below.
153+ uint32_t boundary = 0 ;
154+ bool stream_restart = false ;
155+ uint32_t stream_expect = 0 ;
131156 for (uint32_t i = 0 ; i < buffer.size (); i++) {
132- if (buffer[i].type == FRCACHE_GPU_RENDER ) {
133- boundary = i + 1 ;
157+ const auto & e = buffer[i];
158+ if (e.type != FRCACHE_GPU_RENDER ) continue ;
159+ if (boundary == 0 ) boundary = i + 1 ;
160+ else if (e.index != stream_expect) {
161+ stream_restart = true ;
134162 break ;
135163 }
164+ stream_expect = e.index + e.param ;
136165 }
137166
138- // A second GPU_RENDER block after the boundary (e.g. a 3D bust in the hero/character panel)
139- // means GW is about to run its full world-render dispatch again this frame. That dispatch
140- // drives per-object animation/physics ticks off a fixed 1/30s accumulator that isn't safe to
141- // advance twice within one real frame -- doing so desyncs a cloth/hair object's constraint
142- // indices from its double-buffered positions (crash: garbage index in the spring solver, or
143- // a degenerate bounding volume asserting in GrBound.cpp). Bail out to a single untouched call
144- // in that case; our overlay draws on top of that rare secondary 3D content instead of under it.
145- bool has_second_gpu_render = false ;
146- for (uint32_t i = boundary; i < buffer.size (); i++) {
147- if (buffer[i].type == FRCACHE_GPU_RENDER ) {
148- has_second_gpu_render = true ;
149- break ;
150- }
167+ // Only the main world-then-HUD pass draws; others pass through, and the guard draws exactly once per frame.
168+ if (boundary == 0 || boundary >= buffer.size () || drawn_this_frame) {
169+ FrCacheRenderAll_Ret (param_1, param_2);
170+ GW::Hook::LeaveHook ();
171+ return ;
151172 }
152173
153- // Only the main world-then-HUD pass draws; others pass through, and the guard draws exactly once per frame.
154- if (boundary == 0 || boundary >= buffer.size () || drawn_this_frame || has_second_gpu_render) {
174+ // Second dispatch present: keep GW's calls untouched (single call, single physics tick) and draw the
175+ // overlays on top of the HUD for just this frame.
176+ if (stream_restart) {
155177 FrCacheRenderAll_Ret (param_1, param_2);
178+ GW::Render::FlushCommandQueue ();
179+ RunCallbacks (device);
180+ drawn_this_frame = true ;
156181 GW::Hook::LeaveHook ();
157182 return ;
158183 }
@@ -287,6 +312,13 @@ void GameWorldCompositor::UnregisterDraw(const int token)
287312 }
288313}
289314
315+ #ifdef _DEBUG
316+ void GameWorldCompositor::RequestBufferDump ()
317+ {
318+ dump_calls_remaining = 50 ;
319+ }
320+ #endif
321+
290322bool GameWorldCompositor::IsActive ()
291323{
292324 return compositor_hooked && !compositor_failed;
0 commit comments