mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-18 07:01:21 +02:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
187 lines
11 KiB
C++
187 lines
11 KiB
C++
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
|
//
|
|
// Write watches on guest physical memory (as Xenia's shared-memory watches):
|
|
// the renderer write-protects the pages behind a texture after loading it;
|
|
// the first CPU store to such a page faults, the handler bumps that page's
|
|
// write sequence and makes it writable again, and the texture reloads on its
|
|
// next use. GPU writes (resolves) bump the sequence explicitly (MarkWritten).
|
|
// No hashing of texture memory is needed. Sequences are kept per guest page
|
|
// (4 KB): a CPU write bumps every guest page of its host page (16 KB on
|
|
// Apple), a resolve just the guest pages it wrote. With
|
|
// NFSMW_WATCH_SUBPAGES=1 on such hosts a CPU write marks just the guest page
|
|
// it faulted on: the host page's other guest pages are copied at the fault
|
|
// and compared before anything relies on them again (write_watch.cpp).
|
|
//
|
|
// The same protection guards memory the GPU still has to access (Xenia
|
|
// publishes fences when the command processor reaches them; so do we, see
|
|
// renderer::QueueWrite). Recording a draw marks the pages it reads (vertex
|
|
// data, texture sources) or writes (resolves) with the submission that
|
|
// uses them; a CPU access to such a page before that submission completes
|
|
// waits for it. This restores the hardware's ordering exactly where the
|
|
// guest reuses memory early, instead of holding every fence until the GPU
|
|
// catches up (which throttled the game to one frame in flight).
|
|
//
|
|
// Physical memory is mapped three times (0xA/0xC/0xE windows); a watched
|
|
// page is protected in every mirror. Host code that writes guest memory
|
|
// through a syscall (pread) must call BeforeHostWrite first, since the
|
|
// kernel returns EFAULT for protected pages instead of faulting.
|
|
#pragma once
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <atomic>
|
|
|
|
namespace writewatch
|
|
{
|
|
void Initialize();
|
|
|
|
// Current global sequence: store it when (re)loading a resource, then
|
|
// compare with WriteSequence() to know whether it was written since.
|
|
uint64_t Current();
|
|
|
|
// Protect the pages of [physical, physical + size) against CPU writes.
|
|
void Watch(uint32_t physical, uint32_t size);
|
|
|
|
// Changes after every page write is recorded (a fault, MarkWritten, a
|
|
// host write): read it before WriteSequence; unchanged since then means
|
|
// no page anywhere was written. With NFSMW_WATCH_SUBPAGES, stores into
|
|
// an open snapshot's other guest pages move neither this nor a sequence
|
|
// until something compares them: unchanged then means nothing was
|
|
// written in the ranges last checked through WriteSequence or
|
|
// WrittenSince (they compare first, see Settle), and opening a snapshot
|
|
// moves it, so that ranges checked before are checked again.
|
|
uint64_t WriteEpoch();
|
|
|
|
// Highest write sequence of the pages of the range.
|
|
uint64_t WriteSequence(uint32_t physical, uint32_t size);
|
|
// NFSMW_WATCH_SUBPAGES: compare the snapshots open on the range's host
|
|
// pages now (the sequence readers do this themselves), e.g. before
|
|
// Current() is taken for a load that Watch then protects. Else nothing:
|
|
// inline, a load and a branch while no snapshot is open anywhere (always
|
|
// when off), the walk out of line (SettleOpen).
|
|
extern std::atomic<uint32_t> g_snapsOpen;
|
|
void SettleOpen(uint32_t physical, uint32_t size);
|
|
inline void Settle(uint32_t physical, uint32_t size)
|
|
{
|
|
if (g_snapsOpen.load(std::memory_order_acquire) != 0 && size != 0)
|
|
SettleOpen(physical, size);
|
|
}
|
|
|
|
// NFSMW_LOG_UPLOADS (the renderer's texture-load log): what last moved
|
|
// each guest page's write sequence on, kept only while that is set.
|
|
// `tag`: the faulting or written address, or the resolve's destination;
|
|
// `pc`: a fault's host pc, as an offset in the executable (0: unknown).
|
|
// NFSMW_WATCH_SUBPAGES: kCompared, a CPU store into a guest page its
|
|
// host page's snapshot found (`tag`: that page); kUnverified, a guest
|
|
// page marked because its snapshot closed without a compare (`tag`: the
|
|
// fault that opened it).
|
|
enum class Writer : uint8_t { kNone, kCpuFault, kResolve, kHostWrite, kGpuStore, kRange, kCompared, kUnverified };
|
|
struct LastWrite { Writer kind = Writer::kNone; uint32_t frame = 0, tag = 0, pc = 0; };
|
|
void SetWriterFrame(uint64_t frame); // stamped on the records from now on
|
|
LastWrite LastWriter(uint32_t physical); // of the guest page holding `physical`
|
|
// Guest pages of the range written after `sequence`: how many; the
|
|
// addresses of the first `max` go to `pages`.
|
|
uint32_t WrittenPages(uint32_t physical, uint32_t size, uint64_t sequence, uint32_t* pages, uint32_t max);
|
|
|
|
// GPU or host writes that don't fault (resolves). Returns the pages' new
|
|
// sequence when they share one (range sequences, the default), else 0.
|
|
uint64_t MarkWritten(uint32_t physical, uint32_t size, Writer kind = Writer::kRange, uint32_t tag = 0);
|
|
// WriteSequence(physical, size) > sequence, skipping 256 KB blocks
|
|
// written before `sequence` without looking at their pages.
|
|
bool WrittenSince(uint32_t physical, uint32_t size, uint64_t sequence);
|
|
// Returns once every write being recorded on another thread (a fault, a
|
|
// host write) has stored its sequences: one that took its sequence
|
|
// before this call is seen by WrittenSince after it (they are recorded
|
|
// under one lock). For a reader that compares with a sequence it just
|
|
// made (MarkWritten), not with an older Current().
|
|
void Quiesce();
|
|
// No host page of the range is unreadable now (PROT_NONE: a GPU write
|
|
// to it, pending, or done and not accessed since): reading it can't
|
|
// fault. Only the command processor makes pages unreadable, so this
|
|
// holds there until it does (for debug checks that read guest memory).
|
|
bool CpuReadable(uint32_t physical, uint32_t size);
|
|
|
|
// A syscall is about to write host memory at [host, host + size): if it
|
|
// is guest physical memory, unprotect and mark it.
|
|
void BeforeHostWrite(const void* host, size_t size);
|
|
|
|
// From the SIGSEGV/SIGBUS handler: true if this was a watched page (now
|
|
// writable again, so the faulting store can simply be retried). Waits
|
|
// first while the GPU still uses the page (not on GPU threads); with
|
|
// NFSMW_WATCH_SUBPAGES, a store also waits for a texture's untile still
|
|
// reading another guest page of its host page (see GpuRead; at most
|
|
// 100 ms, then that guest page is marked instead).
|
|
bool HandleFault(const void* host, bool write = true);
|
|
|
|
// GPU use guard. `submission` is the renderer's number for the command
|
|
// buffer being recorded; SetCompleted publishes finished ones (in order).
|
|
// `tag` names the use for NFSMW_LOG_GUARD (a short static string).
|
|
// `untile`: a texture's load, whose image the texture cache keeps.
|
|
void GpuRead(uint32_t physical, uint32_t size, uint64_t submission, const char* tag = "read", bool untile = false);
|
|
void GpuWrite(uint32_t physical, uint32_t size, uint64_t submission, const char* tag = "write"); // reads fault too
|
|
void SetCompleted(uint64_t submission);
|
|
// A GPU write to the range not completed yet (its CPU copy is stale).
|
|
bool GpuWritePending(uint32_t physical, uint32_t size);
|
|
// Protect again host pages a fault opened while another of their guest
|
|
// pages still had a GPU hazard (command processor, at submissions). Also
|
|
// closes NFSMW_WATCH_SUBPAGES snapshots nothing compared for a while.
|
|
void Rearm();
|
|
// Set by a waiter whose submission is still being recorded; the command
|
|
// processor submits when it sees it.
|
|
extern std::atomic<bool> g_submitRequested;
|
|
// Threads that complete GPU work (command processor, completion thread)
|
|
// never wait on it: they store through GpuThreadStore instead.
|
|
extern thread_local bool t_gpuThread;
|
|
inline thread_local const void* t_faultPc = nullptr; // host pc of the fault, for NFSMW_LOG_GUARD
|
|
void GpuThreadStore(void* host, const void* data, size_t size);
|
|
// As GpuThreadStore, for several ordered stores into [host, host + size)
|
|
// under one opening of its pages (an occlusion query's report):
|
|
// `write(context)` runs with them writable and the write watch's lock
|
|
// held, so it only stores.
|
|
void GpuThreadWrite(void* host, size_t size, void (*write)(const void* context), const void* context);
|
|
template<typename Write>
|
|
void GpuThreadWrite(void* host, size_t size, const Write& write)
|
|
{
|
|
GpuThreadWrite(host, size, [](const void* w) { (*static_cast<const Write*>(w))(); }, &write);
|
|
}
|
|
|
|
// Shadow mode: the renderer keeps its own GPU copy of physical memory
|
|
// (`shadow`, 512 MB, CPU-mapped) instead of importing guest memory.
|
|
void EnableShadow(uint8_t* shadow);
|
|
// Any page of the range written by the CPU after `sequence` (Current())?
|
|
// Coarse and cheap: true may still mean nothing to copy.
|
|
bool RangeWrittenSince(uint32_t physical, uint32_t size, uint64_t sequence);
|
|
// Copy the range's pages the CPU changed since their last copy into the
|
|
// shadow (and track further writes). Returns 0, or a submission that
|
|
// still reads a page's old copy: wait for it, then call again. `since`:
|
|
// Current() before the range's last complete sync, 0 if none.
|
|
uint64_t SyncShadow(uint32_t physical, uint32_t size, uint64_t since = 0);
|
|
// A GPU write (resolve) into the shadow: the guest's copy of those pages
|
|
// is stale until a CPU access reads them back (after the write completes).
|
|
void MarkGpuOwned(uint32_t physical, uint32_t size);
|
|
bool AnyGpuOwned(uint32_t physical, uint32_t size);
|
|
|
|
// Totals so far, for the [perf] line and hitch reports (protects: mprotect
|
|
// calls, and their time on all threads; hostWriteBytes: file reads into
|
|
// guest memory; firstUploadBytes: shadow pages copied for the first time;
|
|
// uploadNs: time copying to the shadow). NFSMW_WATCH_SUBPAGES (`subpages`:
|
|
// in use): snapshots a fault opened, closed by a compare that found no
|
|
// pending page changed or some changed, or closed without one (aged out,
|
|
// a host or GPU-thread write, or the oldest when a fault found every slot
|
|
// taken: `snapFull` of them); write faults that marked the whole host
|
|
// page instead, for the GPU (a GPU write to it pending, or just read back
|
|
// in shadow mode; or a GPU read pending on each of its other guest pages);
|
|
// guest pages a snapshot left out and marked at its fault, for a GPU read
|
|
// pending on them (an untile only behind a later read there, or after a
|
|
// wait for it gave up); write faults that waited for an untile on another
|
|
// guest page first (in `stalls` too), and those waits that gave up.
|
|
struct GuardStats
|
|
{
|
|
uint64_t stalls = 0, stallUs = 0, faults = 0, opened = 0, uploadBytes = 0, readbackBytes = 0, protects = 0, protectNs = 0,
|
|
hostWriteBytes = 0, firstUploadBytes = 0, uploadNs = 0;
|
|
bool subpages = false;
|
|
uint64_t snapOpened = 0, snapClean = 0, snapChanged = 0, snapEvicted = 0, snapFull = 0, snapWhole = 0, snapReading = 0,
|
|
snapWaits = 0, snapGaveUp = 0;
|
|
};
|
|
GuardStats GetGuardStats();
|
|
}
|