Skip to content

Commit 6753c9c

Browse files
3vcloudclaude
andcommitted
feat(dat): async ReadFile with a callback, delivered by the dat worker
Add GwDatArchive::ReadFileAsync(file_id, callback, stream_id): it queues the request on an internal list and returns immediately. The dat worker, which now wakes on a short poll interval as well as on tasks, scans the list each pass - when a file resolves it fires the callback with the decompressed bytes and drops the entry; if the file hasn't appeared within ~1 minute it fires the callback once with an empty vector and drops it. This lets callers wait for files the client streams in on demand (e.g. the Steam .snapshot archive, which the game persists to disk) without blocking: the existing throttled MaybeRefresh picks the file up on a later poll. Undelivered requests are dropped if the worker is stopped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f18f89b commit 6753c9c

2 files changed

Lines changed: 84 additions & 6 deletions

File tree

GWToolboxdll/Utils/GwDat/GwDatArchive.cpp

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#include <Windows.h>
22

3+
#include <chrono>
34
#include <cstring>
45
#include <cwchar>
6+
#include <utility>
57

68
#include "GwDatArchive.h"
79
#include "xentax.h"
@@ -10,6 +12,8 @@ namespace {
1012
const uint8_t kDatMagic[4] = {0x33, 0x41, 0x4e, 0x1a}; // "3ANa"
1113
constexpr size_t kMaxView = 0x800000; // cap each mapped window at 8 MB
1214
constexpr uint32_t kRefreshThrottleMs = 2000; // min gap between re-parses on a read miss
15+
constexpr uint32_t kAsyncReadTimeoutMs = 60000; // give up on an async read after ~1 minute
16+
constexpr uint32_t kAsyncPollMs = 250; // worker re-scan cadence for pending async reads
1317

1418
DWORD AllocationGranularity()
1519
{
@@ -136,13 +140,19 @@ void GwDatArchive::WorkerLoop()
136140
std::function<void()> task;
137141
{
138142
std::unique_lock<std::mutex> lock(m_task_mutex);
139-
m_task_cv.wait(lock, [this] { return m_worker_stop || !m_tasks.empty(); });
143+
// Wake for queued tasks, or periodically to re-scan pending async reads.
144+
m_task_cv.wait_for(lock, std::chrono::milliseconds(kAsyncPollMs),
145+
[this] { return m_worker_stop || !m_tasks.empty(); });
140146
if (m_worker_stop)
141147
return; // drop any queued tasks on shutdown
142-
task = std::move(m_tasks.front());
143-
m_tasks.pop();
148+
if (!m_tasks.empty()) {
149+
task = std::move(m_tasks.front());
150+
m_tasks.pop();
151+
}
144152
}
145-
task();
153+
if (task)
154+
task();
155+
ProcessPendingReads();
146156
}
147157
}
148158

@@ -167,8 +177,13 @@ void GwDatArchive::StopWorker()
167177
m_task_cv.notify_all();
168178
if (m_worker.joinable())
169179
m_worker.join();
170-
std::scoped_lock<std::mutex> lock(m_task_mutex);
171-
m_worker_running = false;
180+
{
181+
std::scoped_lock<std::mutex> lock(m_task_mutex);
182+
m_worker_running = false;
183+
}
184+
// Drop any undelivered async reads; callers tolerate non-delivery across a worker stop.
185+
std::scoped_lock<std::mutex> lock(m_pending_mutex);
186+
m_pending_reads.clear();
172187
}
173188

174189
void GwDatArchive::EnqueueTask(std::function<void()> task)
@@ -396,3 +411,47 @@ bool GwDatArchive::ReadFile(uint32_t file_id, std::vector<uint8_t>& out, uint32_
396411
}
397412
return !out.empty();
398413
}
414+
415+
void GwDatArchive::ReadFileAsync(uint32_t file_id, ReadCallback callback, uint32_t stream_id)
416+
{
417+
if (!callback)
418+
return;
419+
if (!file_id) {
420+
std::vector<uint8_t> empty;
421+
callback(empty); // nothing to look up
422+
return;
423+
}
424+
StartWorker(); // the poll loop must be running to deliver
425+
const uint32_t deadline = GetTickCount() + kAsyncReadTimeoutMs;
426+
std::scoped_lock<std::mutex> lock(m_pending_mutex);
427+
m_pending_reads.push_back({file_id, stream_id, deadline, std::move(callback)});
428+
}
429+
430+
// Worker-thread pass over the pending async reads: deliver any that now resolve, expire any past
431+
// their deadline. Callbacks fire outside the lock so they can't stall other ReadFileAsync callers.
432+
void GwDatArchive::ProcessPendingReads()
433+
{
434+
std::vector<std::pair<ReadCallback, std::vector<uint8_t>>> ready;
435+
{
436+
std::scoped_lock<std::mutex> lock(m_pending_mutex);
437+
if (m_pending_reads.empty())
438+
return;
439+
const uint32_t now = GetTickCount();
440+
for (auto it = m_pending_reads.begin(); it != m_pending_reads.end();) {
441+
std::vector<uint8_t> data;
442+
if (ReadFile(it->file_id, data, it->stream_id)) {
443+
ready.emplace_back(std::move(it->callback), std::move(data));
444+
it = m_pending_reads.erase(it);
445+
}
446+
else if (static_cast<int32_t>(now - it->deadline_ms) >= 0) { // deadline passed (wrap-safe)
447+
ready.emplace_back(std::move(it->callback), std::vector<uint8_t>{});
448+
it = m_pending_reads.erase(it);
449+
}
450+
else {
451+
++it;
452+
}
453+
}
454+
}
455+
for (auto& [callback, data] : ready)
456+
callback(data);
457+
}

GWToolboxdll/Utils/GwDat/GwDatArchive.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ class GwDatArchive {
3737
// Decompressed bytes for a GW file id; stream_id picks a stream (0 = the file's own data). False if absent.
3838
bool ReadFile(uint32_t file_id, std::vector<uint8_t>& out, uint32_t stream_id = 0);
3939

40+
// Async variant of ReadFile: the worker retries the read until the file appears in the dat, then
41+
// invokes `callback` (on the worker thread) with the decompressed bytes; if it hasn't shown up
42+
// within ~1 minute the callback fires once with an empty vector. Lets callers wait for files the
43+
// client streams in on demand (e.g. Steam .snapshot installs) without blocking. Starts the worker
44+
// if needed. Undelivered requests are dropped (callback not called) if the worker is stopped.
45+
using ReadCallback = std::function<void(std::vector<uint8_t>&)>;
46+
void ReadFileAsync(uint32_t file_id, ReadCallback callback, uint32_t stream_id = 0);
47+
4048
// Dedicated worker so reads/decodes run off the caller's thread; EnqueueTask runs inline if not started.
4149
void StartWorker();
4250
void StopWorker();
@@ -46,6 +54,7 @@ class GwDatArchive {
4654
GwDatArchive() = default;
4755

4856
void WorkerLoop();
57+
void ProcessPendingReads(); // deliver/expire queued ReadFileAsync requests (worker thread)
4958

5059
#pragma pack(push, 1)
5160
struct MainHeader {
@@ -95,6 +104,16 @@ class GwDatArchive {
95104
std::queue<std::function<void()>> m_tasks;
96105
bool m_worker_running = false;
97106
bool m_worker_stop = false;
107+
108+
// Async ReadFile requests, retried by the worker until the file appears or the deadline passes.
109+
struct PendingRead {
110+
uint32_t file_id;
111+
uint32_t stream_id;
112+
uint32_t deadline_ms; // GetTickCount() value at which this request gives up
113+
ReadCallback callback;
114+
};
115+
std::mutex m_pending_mutex; // guards m_pending_reads
116+
std::vector<PendingRead> m_pending_reads;
98117
std::shared_mutex m_index_mutex; // guards m_mapping/m_slots/m_fileid_to_slot against MaybeRefresh
99118
uint32_t m_last_refresh_ms = 0; // GetTickCount of the last re-parse, to throttle refreshes
100119
void* m_mapping = nullptr; // file-mapping HANDLE for the dat, replaced by MaybeRefresh when the dat grows

0 commit comments

Comments
 (0)