Skip to content

Commit bddde4f

Browse files
3vcloudclaude
andcommitted
perf(dat): batch fetch tickles into one request per worker pass
A read miss now queues its file id (still throttled per id) instead of firing the trigger immediately; the worker flushes the accumulated ids to the client as a single FUN_0082da30(count, ids, 0x2) call after each poll. A burst of missing files (e.g. a full icon grid on a fresh streamed install) becomes one game-thread task and one request batch instead of one per file, so no per-file hitch. TriggerFn is now (ids, count); undelivered tickles are dropped on stop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5a22e79 commit bddde4f

3 files changed

Lines changed: 42 additions & 21 deletions

File tree

GWToolboxdll/Modules/GwDatTextureModule.cpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ namespace {
3737
return;
3838
}
3939
Log::Log("[GwDat] game file-request trigger resolved at %p", reinterpret_cast<void*>(RequestFiles_Func));
40-
GwDatArchive::Instance().SetTrigger([](uint32_t file_id) {
41-
GW::GameThread::Enqueue([file_id] { // touches client state; run on the game thread
42-
if (RequestFiles_Func)
43-
RequestFiles_Func(1, &file_id, kTriggerFlags);
40+
GwDatArchive::Instance().SetTrigger([](const uint32_t* ids, size_t count) {
41+
if (!count)
42+
return;
43+
std::vector<uint32_t> batch(ids, ids + count); // copy for the game-thread lambda
44+
GW::GameThread::Enqueue([batch = std::move(batch)] { // touches client state; run on the game thread
45+
if (RequestFiles_Func && !batch.empty())
46+
RequestFiles_Func(static_cast<uint32_t>(batch.size()), batch.data(), kTriggerFlags);
4447
});
4548
});
4649
}

GWToolboxdll/Utils/GwDat/GwDatArchive.cpp

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ void GwDatArchive::WorkerLoop()
154154
if (task)
155155
task();
156156
ProcessPendingReads();
157+
FlushTickles(); // send the reads' fetch requests as one batch
157158
}
158159
}
159160

@@ -182,8 +183,12 @@ void GwDatArchive::StopWorker()
182183
std::scoped_lock<std::mutex> lock(m_task_mutex);
183184
m_worker_running = false;
184185
}
185-
std::scoped_lock<std::mutex> lock(m_pending_mutex);
186-
m_pending_reads.clear(); // undelivered async reads are dropped on stop
186+
{
187+
std::scoped_lock<std::mutex> lock(m_pending_mutex);
188+
m_pending_reads.clear(); // undelivered async reads are dropped on stop
189+
}
190+
std::scoped_lock<std::mutex> tlock(m_trigger_mutex);
191+
m_tickle_batch.clear(); // undelivered fetch requests are dropped on stop
187192
}
188193

189194
void GwDatArchive::EnqueueTask(std::function<void()> task)
@@ -448,23 +453,34 @@ void GwDatArchive::SetTrigger(TriggerFn trigger)
448453
m_trigger = std::move(trigger);
449454
}
450455

451-
// Ask the client to fetch a just-missed file so a later read may find it; throttled per id so a
452-
// per-frame retry loop doesn't spam. Fires the trigger outside the lock.
456+
// Queue a just-missed file for the client to fetch so a later read may find it; throttled per id so a
457+
// per-frame retry loop doesn't spam. The batch is sent by FlushTickles (worker thread).
453458
void GwDatArchive::TickleFetch(uint32_t file_id)
454459
{
460+
std::scoped_lock<std::mutex> lock(m_trigger_mutex);
461+
if (!m_trigger)
462+
return;
463+
const uint32_t now = GetTickCount();
464+
const auto it = m_tickled_at.find(file_id);
465+
if (it != m_tickled_at.end() && now - it->second < kTickleThrottleMs)
466+
return; // asked recently (elapsed is unsigned, wrap-safe for a session)
467+
m_tickled_at[file_id] = now;
468+
m_tickle_batch.push_back(file_id);
469+
}
470+
471+
// Hand every queued fetch id to the client in one request instead of one call per file.
472+
void GwDatArchive::FlushTickles()
473+
{
474+
std::vector<uint32_t> batch;
455475
TriggerFn trigger;
456476
{
457477
std::scoped_lock<std::mutex> lock(m_trigger_mutex);
458-
if (!m_trigger)
478+
if (m_tickle_batch.empty() || !m_trigger)
459479
return;
460-
const uint32_t now = GetTickCount();
461-
const auto it = m_tickled_at.find(file_id);
462-
if (it != m_tickled_at.end() && now - it->second < kTickleThrottleMs)
463-
return; // asked recently (elapsed is unsigned, wrap-safe for a session)
464-
m_tickled_at[file_id] = now;
480+
batch.swap(m_tickle_batch);
465481
trigger = m_trigger;
466482
}
467-
trigger(file_id);
483+
trigger(batch.data(), batch.size());
468484
}
469485

470486
// Worker pass: deliver resolved reads and time out stale ones. A miss tickles the client via ReadFile.

GWToolboxdll/Utils/GwDat/GwDatArchive.h

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ class GwDatArchive {
4343
using ReadCallback = std::function<void(std::vector<uint8_t>&)>;
4444
void ReadFileAsync(uint32_t file_id, ReadCallback callback, uint32_t stream_id = 0);
4545

46-
// Optional hook to ask the client to fetch a file id. ReadFile calls it (throttled per id) whenever
47-
// a read misses, so a streamed-only file gets pulled in for a later read. Wired by the GWCA layer;
48-
// the reader works with or without it.
49-
using TriggerFn = std::function<void(uint32_t file_id)>;
46+
// Optional hook to ask the client to fetch file ids. Read misses accumulate (throttled per id) and
47+
// are handed to this in batches by the worker, so a burst of missing files is one request, not one
48+
// per file. Wired by the GWCA layer; the reader works with or without it.
49+
using TriggerFn = std::function<void(const uint32_t* file_ids, size_t count)>;
5050
void SetTrigger(TriggerFn trigger);
5151

5252
// Dedicated worker so reads/decodes run off the caller's thread; EnqueueTask runs inline if not started.
@@ -59,7 +59,8 @@ class GwDatArchive {
5959

6060
void WorkerLoop();
6161
void ProcessPendingReads(); // deliver/expire queued ReadFileAsync requests (worker thread)
62-
void TickleFetch(uint32_t file_id); // ask the client to fetch a missed file (throttled per id)
62+
void TickleFetch(uint32_t file_id); // queue a missed file for the client to fetch (throttled per id)
63+
void FlushTickles(); // hand the queued fetch ids to the trigger as one batch (worker thread)
6364

6465
#pragma pack(push, 1)
6566
struct MainHeader {
@@ -119,9 +120,10 @@ class GwDatArchive {
119120
std::mutex m_pending_mutex; // guards m_pending_reads
120121
std::vector<PendingRead> m_pending_reads;
121122

122-
std::mutex m_trigger_mutex; // guards m_trigger + m_tickled_at
123+
std::mutex m_trigger_mutex; // guards m_trigger + m_tickled_at + m_tickle_batch
123124
TriggerFn m_trigger;
124125
std::unordered_map<uint32_t, uint32_t> m_tickled_at; // file_id -> GetTickCount() of last fetch tickle
126+
std::vector<uint32_t> m_tickle_batch; // file ids queued for the next FlushTickles
125127
std::shared_mutex m_index_mutex; // guards m_mapping/m_slots/m_fileid_to_slot against MaybeRefresh
126128
uint32_t m_last_refresh_ms = 0; // GetTickCount of the last re-parse, to throttle refreshes
127129
long long m_indexed_size = 0; // dat size at the last parse; skip re-parsing if unchanged (guarded by m_load_mutex, like m_last_refresh_ms)

0 commit comments

Comments
 (0)