Skip to content

Commit e66b3c3

Browse files
committed
fix render under ingame ui
1 parent 42f9905 commit e66b3c3

6 files changed

Lines changed: 107 additions & 28 deletions

File tree

GWToolboxdll/Modules/TestHarness.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,10 @@ void TestHarness::Initialize()
530530
{
531531
ToolboxModule::Initialize();
532532
#ifdef HARNESS_ENABLED
533+
// A command written before this instance existed is stale (e.g. the reload script's `shutdown`
534+
// left unconsumed when no toolbox was loaded); executing it would kill the fresh instance.
535+
std::error_code ec;
536+
std::filesystem::remove(cmd_path(), ec);
533537
std::string existing;
534538
if (!Resources::ReadFile(config_path(), existing)) {
535539
Resources::WriteFile(config_path(),

GWToolboxdll/Utils/GameWorldCompositor.cpp

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
290322
bool GameWorldCompositor::IsActive()
291323
{
292324
return compositor_hooked && !compositor_failed;

GWToolboxdll/Utils/GameWorldCompositor.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ namespace GameWorldCompositor {
5454
[[nodiscard]] IDirect3DPixelShader9* PixelShader();
5555
[[nodiscard]] IDirect3DVertexDeclaration9* VertexDeclaration();
5656

57+
#ifdef _DEBUG
58+
// Log every FrCacheRenderAll invocation's buffer layout for the next few calls (harness diagnostics).
59+
void RequestBufferDump();
60+
#endif
61+
5762
// Remove the hook and release shared GPU resources (shutdown).
5863
void Terminate();
5964
} // namespace GameWorldCompositor

GWToolboxdll/Windows/InfoWindow.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,12 @@ namespace {
132132
}
133133
bool FileIdField(const char* label, const wchar_t* enc_str)
134134
{
135-
return FileIdField(label, ArenaNetFileParser::FileHashToFileId(enc_str));
135+
// Show the raw enc chars, not a re-encoding of the decoded id — when decode fails (0), the raw
136+
// string is the only clue to what the game actually passed.
137+
EncInfoField(label, enc_str);
138+
const std::string label2 = std::format("{} (File ID)", label);
139+
InfoField(label2.c_str(), "0x%X", ArenaNetFileParser::FileHashToFileId(enc_str));
140+
return false;
136141
}
137142

138143
void GetIdsFromFileId(const uint32_t param_1, short* param_2)
@@ -1034,7 +1039,13 @@ namespace {
10341039
switch (message_id) {
10351040
case GW::UI::UIMessage::kLoadMapContext: {
10361041
const auto packet = static_cast<GW::UI::UIPacket::kLoadMapContext*>(wParam);
1037-
wcscpy(mapfile, packet->file_name);
1042+
if (packet->file_name) {
1043+
wcsncpy(mapfile, packet->file_name, _countof(mapfile) - 1);
1044+
mapfile[_countof(mapfile) - 1] = 0;
1045+
}
1046+
else {
1047+
mapfile[0] = 0;
1048+
}
10381049
} break;
10391050
}
10401051
}

GWToolboxdll/Windows/Pathfinding/PathfindingWindow.cpp

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,18 @@ namespace {
448448
}
449449
}
450450

451+
// A ctx-fallback entry (bounds mismatch below) is keyed by map_id + live node count, not fid — probe for it
452+
// or the resident-only path never sees it and the prewarm loop re-reads the DAT forever.
453+
if (map_id == GW::Map::GetMapID()) {
454+
if (const auto mc = GW::GetMapContext(); mc && mc->path && mc->path->staticData) {
455+
const auto ctx_hash = static_cast<uint64_t>(map_id) | (static_cast<uint64_t>(mc->path->pathNodes.size()) << 32);
456+
if (const auto it = mile_paths_by_coords.find(ctx_hash); it != mile_paths_by_coords.end()) {
457+
TouchLru(ctx_hash);
458+
return it->second;
459+
}
460+
}
461+
}
462+
451463
// Not resident: the DAT read + parse below blocks the caller. Refuse on the game thread.
452464
if (!allow_load) return nullptr;
453465

@@ -478,7 +490,9 @@ namespace {
478490
auto& map_data = *chosen;
479491

480492
// file_id was not resident above, so this full key (file_id + pathNodeSize) is new too.
481-
auto hash = static_cast<uint64_t>(fid);
493+
// Ctx-fallback data must not be cached under the (stale) fid: it would shadow the real file's mesh
494+
// for every other map sharing that fid. Key it by map_id like LoadMapFromContext does.
495+
auto hash = chosen == &ctx_data ? static_cast<uint64_t>(map_id) : static_cast<uint64_t>(fid);
482496
hash |= static_cast<uint64_t>(map_data.pathNodeSize) << 32;
483497

484498
// Cache map info for bounds drawing
@@ -2396,8 +2410,20 @@ namespace {
23962410

23972411
std::set<uint32_t> file_id_mismatch_warned; // maps already warned about
23982412

2413+
// Maps whose outpost shares the explorable's MapID but loads a different map file (the constant table holds the
2414+
// explorable file; the outpost's kLoadMapContext file_name doesn't decode, so the runtime override can't catch it).
2415+
constexpr std::pair<GW::Constants::MapID, uint32_t> outpost_file_id_overrides[] = {
2416+
{GW::Constants::MapID::Domain_of_Anguish, 0x3452b}, // outpost = shared torment gate room; explorable = 0x3584f
2417+
};
2418+
23992419
uint32_t GetMapFileId(GW::Constants::MapID map_id)
24002420
{
2421+
if (map_id == GW::Map::GetMapID() && GW::Map::GetInstanceType() == GW::Constants::InstanceType::Outpost) {
2422+
for (const auto& [mid, outpost_fid] : outpost_file_id_overrides) {
2423+
if (mid == map_id) return outpost_fid;
2424+
}
2425+
}
2426+
24012427
// Check runtime lookup table (populated from constant_maps_info + StoC packets)
24022428
auto it = map_id_to_file_hash.find(map_id);
24032429
if (it != map_id_to_file_hash.end()) return it->second;

GWToolboxdll/Windows/Pathfinding/maps_constant_data.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ std::unordered_map<int, std::vector<MapConstantInfo>> constant_maps_info = {
148148
{GW::Constants::MapID::Kamadan_Jewel_of_Istan_explorable, 0},
149149
{GW::Constants::MapID::The_Tribunal, 0},
150150
{GW::Constants::MapID::Cliffs_of_Dohjok, 0},
151-
{GW::Constants::MapID::The_Ebony_Citadel_of_Mallyx_mission, 0},
152151
{GW::Constants::MapID::A_Land_of_Heroes, 0},
153152
{GW::Constants::MapID::Crystal_Overlook, 0},
154153
{GW::Constants::MapID::Gate_of_Anguish_elite_mission, 0},
@@ -891,7 +890,6 @@ std::unordered_map<int, std::vector<MapConstantInfo>> constant_maps_info = {
891890
{{GW::Constants::MapID::Gate_of_Torment_outpost, 0x3452b},
892891
{GW::Constants::MapID::Gate_of_Fear_outpost, 0x3452b},
893892
{GW::Constants::MapID::Gate_of_Secrets_outpost, 0x3452b},
894-
{GW::Constants::MapID::Domain_of_Anguish, 0x3452b},
895893
{GW::Constants::MapID::Gate_of_Pain, 0x3452b},
896894
{GW::Constants::MapID::Gate_of_Madness, 0x3452b},
897895
{GW::Constants::MapID::Gate_of_the_Nightfallen_Lands_outpost, 0x3452b}}},
@@ -912,6 +910,9 @@ std::unordered_map<int, std::vector<MapConstantInfo>> constant_maps_info = {
912910
{{GW::Constants::MapID::Domain_of_Pain, 0x34bcc}}},
913911
{0x3583e,
914912
{{GW::Constants::MapID::Domain_of_Secrets, 0x3583e}}},
913+
{0x3584f,
914+
{{GW::Constants::MapID::Domain_of_Anguish, 0x3584f},
915+
{GW::Constants::MapID::The_Ebony_Citadel_of_Mallyx_mission, 0x3584f}}},
915916
{0x33c20,
916917
{{GW::Constants::MapID::Jennurs_Horde, 0x33c20}}},
917918
{0x3443c,

0 commit comments

Comments
 (0)