mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Feat] (DirectGLES/Managers, MG_Remote/Server): R-11 - copy staged bytes into a server-owned StagedShadowStore with exact coverage; Fatal{StageSnapshotTooNarrow} before the flush drain instead of a widened INVALIDATE_RANGE
This commit is contained in:
@@ -16,6 +16,12 @@
|
||||
#include <MG_Pipe/MGPipeHostSpan.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// R-11's server-owned staging copy. Header-only and package v1's; see its own header block for
|
||||
// why GLESBufferResource does not simply gain a member.
|
||||
#include <MG_Remote/Server/StagedShadow.h>
|
||||
#endif
|
||||
|
||||
#include "Utils.h"
|
||||
#include "DirectGLES.h"
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
@@ -1008,6 +1014,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return StorageMatchesSize(resource, bufferObject.GetSize());
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// -------------------------------------------------------------------------------
|
||||
// R-11 - THE SERVER'S OWN COPY OF THE STAGED BYTES. Package v1.
|
||||
//
|
||||
// The rule, the evidence and the reason the storage is a side table rather than a
|
||||
// member of GLESBufferResource are all in MG_Remote/Server/StagedShadow.h. This is
|
||||
// only the instance and the four call sites.
|
||||
//
|
||||
// ONE PER PROCESS, and its copying arm is decided ONCE at first use: the two arms
|
||||
// hold the authoritative bytes in DIFFERENT places, so an answer that changed
|
||||
// mid-run would strand every resource already staged - the same reason
|
||||
// ResolveResourceSubsystemArm latches (Managers.h). Leaked at exit like every other
|
||||
// MG_Remote singleton (ID-8): ~BufferObject reaches the destroy path from exit
|
||||
// handlers, after this TU's globals would already be gone.
|
||||
// -------------------------------------------------------------------------------
|
||||
MG_Remote::Server::StagedShadowStore& ServerStaged() {
|
||||
static MG_Remote::Server::StagedShadowStore& store =
|
||||
*new MG_Remote::Server::StagedShadowStore(
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith);
|
||||
return store;
|
||||
}
|
||||
|
||||
// THE COVERAGE RULE FOR THE THREE-TIER FLUSH DRAIN, ASSERTED BEFORE THE CALL AND NOT
|
||||
// INSIDE IT. FlushPendingRangesFrom is a G5-pinned body (p3a_untouched_regions.sh's
|
||||
// PINNED_FUNCTIONS, ID-41): the split arm CALLS it, it does not re-spell it, and it
|
||||
// does not add a line to it either. Its tier-1 arm - the range-invalidating map -
|
||||
// copies with its own Memcpy and never reaches UploadRangeFrom, so the one tier that
|
||||
// DECLARES THE OLD BYTES DEAD is the one tier no later check can see; the only place
|
||||
// left to say "every queued range is inside the staged coverage" is the instant before
|
||||
// the drain takes the queue. The clamp is the drain's own (limit = the smaller of the
|
||||
// frontend size and the backend store; bytes past either end have nowhere to land and
|
||||
// are not this rule's subject), so the two agree about which bytes are meant.
|
||||
//
|
||||
// Fires only for a base that IS this resource's server shadow: RequireCoverage
|
||||
// answers nothing for the legacy arm's MappedData(), which is valid for the whole
|
||||
// store. `pendingMutex` is taken here and released before the drain takes it again.
|
||||
void RequireStagedCoverageForPendingRanges(GLESBufferResource& resource, const Uint8* hostBase,
|
||||
SizeT frontendSize, const char* site) {
|
||||
if (hostBase == nullptr) return;
|
||||
const SizeT limit = std::min(frontendSize, resource.storageSize);
|
||||
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
||||
for (const auto& range : resource.pendingRanges) {
|
||||
const SizeT end = std::min(range.end, limit);
|
||||
const SizeT start = std::min(range.start, end);
|
||||
if (start == end) continue;
|
||||
ServerStaged().RequireCoverage(&resource, hostBase, start, end, site);
|
||||
}
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
|
||||
// The four hostBytes sites read the same in both builds. The non-split expansion is the
|
||||
// ORIGINAL EXPRESSION, character for character - `raw - offset` - so a push or verify build
|
||||
// compiles exactly what it compiled before R-11 and the split arm is the only new behaviour.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \
|
||||
ServerStaged().Adopt(&(res), (width), (bytes), (offset), (size))
|
||||
#define MGL_SERVER_STAGED_DROP(res) ServerStaged().Drop(&(res))
|
||||
#define MGL_SERVER_STAGED_DROP_ALL() ServerStaged().DropAll()
|
||||
#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) \
|
||||
ServerStaged().RequireCoverage(&(res), (base), (start), (end), (site))
|
||||
#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) \
|
||||
RequireStagedCoverageForPendingRanges((res), (base), (frontendSize), (site))
|
||||
#else
|
||||
#define MGL_SERVER_STAGED_ADOPT(res, width, bytes, offset, size) \
|
||||
(static_cast<const Uint8*>(bytes) - (offset))
|
||||
#define MGL_SERVER_STAGED_DROP(res) ((void)0)
|
||||
#define MGL_SERVER_STAGED_DROP_ALL() ((void)0)
|
||||
#define MGL_SERVER_STAGED_REQUIRE(res, base, start, end, site) ((void)0)
|
||||
#define MGL_SERVER_STAGED_REQUIRE_PENDING(res, base, frontendSize, site) ((void)0)
|
||||
#endif
|
||||
|
||||
// The host bytes a range upload/flush reads. On the legacy arm the frontend object's
|
||||
// shadow; on the handle arm the base the last content-carrying call handed over.
|
||||
void UploadRangeFrom(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, SizeT end) {
|
||||
@@ -1015,6 +1092,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
if (start >= end || hostBase == nullptr) return;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MGL_SERVER_STAGED_REQUIRE(resource, hostBase, start, end, "upload_range");
|
||||
#endif
|
||||
BindBufferId(TempBufferTarget, resource.id);
|
||||
g_GLESFuncs.glBufferSubData(TempBufferTarget, (GLintptr)start, (GLsizeiptr)(end - start),
|
||||
hostBase + start);
|
||||
@@ -1946,9 +2026,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// every reader below treats a null base as "no bytes to move", which is the
|
||||
// honest answer, and the ensure path re-reads the live base from the
|
||||
// frontend object it still holds.
|
||||
resource->hostBytes = (desc.HasDefinedContent != 0 && initialBytes != nullptr)
|
||||
? static_cast<const Uint8*>(initialBytes)
|
||||
: nullptr;
|
||||
if (desc.HasDefinedContent != 0 && initialBytes != nullptr) {
|
||||
// R-11: in monolith this is the client's shadow base, unchanged; under
|
||||
// split it is copied into server-owned storage first. A respecify's
|
||||
// companion pointer is the base at offset 0 and covers the whole store.
|
||||
// Under split it is ALWAYS null (contract table 1 row 19 -
|
||||
// initialBytes does not cross, and the content arrives as
|
||||
// resource_subdata records right behind this record), so in practice
|
||||
// this arm is the monolith one and the else arm is the split one.
|
||||
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, static_cast<SizeT>(desc.Width),
|
||||
initialBytes, 0,
|
||||
static_cast<SizeT>(desc.Width));
|
||||
} else {
|
||||
MGL_SERVER_STAGED_DROP(*resource);
|
||||
resource->hostBytes = nullptr;
|
||||
}
|
||||
}
|
||||
if (!resource) return; // lazy: the ensure path full-uploads on creation
|
||||
if (resource->immutableStorage) {
|
||||
@@ -2018,7 +2110,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// and the ensure path's republication (the 3c55e027 fix) is what runs at a draw.
|
||||
if (bytes != nullptr) {
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->hostBytes = static_cast<const Uint8*>(bytes) - offset;
|
||||
// R-11: MONOLITH keeps the client's base; SPLIT copies [offset, offset+size)
|
||||
// into server-owned storage and points hostBytes at that. Inside the SAME
|
||||
// lock as the queued range, because the base and the range are one fact
|
||||
// ("these bytes, at this base") and the drain takes this mutex to lift them.
|
||||
resource->hostBytes =
|
||||
MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes, offset, size);
|
||||
}
|
||||
if (resource->pendingRespecify) return; // full re-upload pending anyway
|
||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||
@@ -2067,10 +2164,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (!resource) return;
|
||||
const SizeT start = static_cast<SizeT>(record.Offset);
|
||||
const SizeT end = start + static_cast<SizeT>(record.Size);
|
||||
// M-2, the second store site - same lock, same reason as Ops_H_SubData's.
|
||||
// M-2, the second store site - same lock, same reason as Ops_H_SubData's, and
|
||||
// the same R-11 copy. UNDER SPLIT `bytes` IS ALWAYS NULL HERE by ruling (C-6 /
|
||||
// contract table 1 row 20: resource_flush_range carries no bytes at all - the
|
||||
// ladder it drives rewrites its range from the authoritative shadow, which rule
|
||||
// C makes server-owned, so resource_subdata is already the only way bytes reach
|
||||
// it and a second carrier would be a forgeable way to say the same thing). So
|
||||
// this arm keeps whatever the preceding subdata records staged, which is
|
||||
// exactly what the ladder must read.
|
||||
if (bytes != nullptr) {
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->hostBytes = static_cast<const Uint8*>(bytes) - start;
|
||||
resource->hostBytes = MGL_SERVER_STAGED_ADOPT(*resource, ResourceWidthOf(res), bytes,
|
||||
start, end - start);
|
||||
}
|
||||
if (resource->pendingRespecify) return;
|
||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||
@@ -2100,6 +2205,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
resource->hostBytes != nullptr) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The kill-switch arm's own Memcpy, and the one that passes
|
||||
// GL_MAP_INVALIDATE_RANGE_BIT - i.e. the one that tells the driver the old
|
||||
// bytes are dead. Bytes outside the staged coverage are exactly the ones
|
||||
// that claim is false for.
|
||||
MGL_SERVER_STAGED_REQUIRE(*resource, resource->hostBytes, start, end,
|
||||
"flush_range_invalidate_map");
|
||||
#endif
|
||||
BindBufferId(TempBufferTarget, resource->id);
|
||||
void* mappedData = g_GLESFuncs.glMapBufferRange(
|
||||
@@ -2145,7 +2258,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (size == 0) return;
|
||||
|
||||
// Queued app writes must land in the backend store before it is read back, or
|
||||
// the writeback below would revert them in the shadow.
|
||||
// the writeback below would revert them in the shadow. Under split every queued
|
||||
// range must lie inside the server shadow's staged coverage first (R-11 / the
|
||||
// M-6 ruling): the drain's tier-1 map would otherwise declare live GPU bytes
|
||||
// dead over a range nothing staged.
|
||||
MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, resource->hostBytes, ResourceWidthOf(res),
|
||||
"readback_flush_pending");
|
||||
FlushPendingRangesFrom(*resource, resource->hostBytes, ResourceWidthOf(res));
|
||||
|
||||
BindBufferId(TempBufferTarget, resource->id);
|
||||
@@ -2187,6 +2305,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
void Ops_H_Destroy(MG_Pipe::MGPipeHandle res) {
|
||||
// R-11's server copy dies with the resource, and BEFORE the twin leaves the
|
||||
// table - the side map is keyed by the twin's address, so this is the last
|
||||
// moment that address can be looked up.
|
||||
if (GLESBufferResource* dying = FindBufferResourceForHandle(res); dying != nullptr) {
|
||||
MGL_SERVER_STAGED_DROP(*dying);
|
||||
}
|
||||
// The twin comes OUT of the table first, so the three outcomes below are
|
||||
// reached with the entry already retired. The SLOT is the client's to free,
|
||||
// after this returns (D-L).
|
||||
@@ -2272,6 +2396,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// PipeResource::AdoptPersistentMap then does m_shadow->clear() +
|
||||
// shrink_to_fit(), so the shadow base any earlier content-carrying call
|
||||
// recorded is a FREED allocation from here on. The live bytes are persistentPtr.
|
||||
// R-11's server copy goes with it: the bytes are the coherent map's now, and a
|
||||
// stale server shadow would answer a later drain with pre-map content.
|
||||
MGL_SERVER_STAGED_DROP(*resource);
|
||||
resource->hostBytes = nullptr;
|
||||
resource->storageSize = static_cast<SizeT>(size);
|
||||
resource->storageInitialized = true;
|
||||
@@ -2631,6 +2758,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// handles (no GL) and let the next draw / texture upload recreate them.
|
||||
ResetRingForNewContext(g_uboRing);
|
||||
ResetRingForNewContext(g_unpackRing);
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// R-11's server copies die with the context for the same reason the rings' ids do:
|
||||
// the resource twins they are keyed by are about to be rebuilt against a new
|
||||
// generation, and a shadow that outlived its twin would be looked up by a RECYCLED
|
||||
// address on the next allocation - which is the quietest possible wrong answer.
|
||||
MGL_SERVER_STAGED_DROP_ALL();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ProcessDeferredBufferReleases() {
|
||||
@@ -2920,6 +3054,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (resource->pendingRespecify || !resource->storageInitialized || resource->storageSize != size) {
|
||||
RespecifyStorageWith(*resource, size, usage, initialData, serial);
|
||||
} else if (!resource->pendingRanges.empty()) {
|
||||
// Same rule as Ops_H_Readback's, at the draw-time drain: with no frontend object
|
||||
// `hostBase` is the server shadow and every queued range must be staged.
|
||||
MGL_SERVER_STAGED_REQUIRE_PENDING(*resource, hostBase, size, "ensure_flush_pending");
|
||||
FlushPendingRangesFrom(*resource, hostBase, size);
|
||||
resource->syncedChangeSerial = serial;
|
||||
} else if (resource->syncedChangeSerial != serial) {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Server/StagedShadow.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// R-11 - THE SERVER'S OWN COPY OF THE STAGED BYTES. Owner: package v1.
|
||||
//
|
||||
// GLESBufferResource::hostBytes (Managers.h:839) is the tree's ONE violation of rule C ("an
|
||||
// applier entry point may not hold a pointer past its return"): Ops_H_SubData (Managers.cpp:
|
||||
// 1980-1983) and Ops_H_FlushRange (:2035) record the client's shadow base and six later drains
|
||||
// read it (:2000, :2062, :2080, :2111, :2741, :2843). In monolith that is correct - the bytes
|
||||
// belong to a frontend object that outlives the call. Under split the pointer names SEG_STAGE,
|
||||
// which is valid only until retiredSeq passes the record that named it, and w1's
|
||||
// MOBILEGL_IPC_AUDIT=1 fills retired staging bytes with 0xDD precisely so an implementation
|
||||
// that kept the pointer is DISTINGUISHABLE from one that copied. So this copies.
|
||||
//
|
||||
// THE SNAPSHOT EXTENT IS EXACTLY WHAT THE RECORD DECLARED, NEVER WIDENED (the integrator's
|
||||
// ruling on b1's open M-6 half). Widening looked free once before and was not: a page-aligned
|
||||
// INVALIDATE_RANGE clobbered GPU-written data - an SSBO counter beside the app's SubData - with
|
||||
// stale shadow bytes (Managers.cpp:1126-1129). Tier 1's INVALIDATE_RANGE is an ASSERTION that
|
||||
// the old bytes are dead, so declaring bytes covered that nothing staged is not a missing
|
||||
// optimisation, it is a silent data loss. The coverage set below is therefore exact, and a
|
||||
// drain that reaches outside it is Fatal rather than a re-read of whatever happens to be there.
|
||||
//
|
||||
// WHY IT IS A HEADER AND NOT A BLOCK INSIDE Managers.cpp. Two reasons, and the second is the
|
||||
// one that matters. Managers.h is package b1's, so v1's R-11 edit may not add a member to
|
||||
// GLESBufferResource and the storage has to live beside it rather than in it. And a block
|
||||
// inside Managers.cpp could only ever be exercised by a test that also has a GL context, a
|
||||
// resource twin and a live session - which is exactly how a rule ends up with no check that
|
||||
// can fail for its own reason (R-16). Here, CopiesIntoServerStorage is a parameter rather than
|
||||
// a read of MG_Config::Transport, so a unit case builds one store of each kind and asserts the
|
||||
// DIFFERENCE between them.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Math/VectorTypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
// Keyed by the resource twin's ADDRESS, which is stable: the twins are heap-allocated and
|
||||
// held by SharedPtr in the backend slot table, and every event that ends a base's life -
|
||||
// an orphaning respecify, a successful map_persistent, destroy, context death - already has
|
||||
// a call site in Managers.cpp to drop it from.
|
||||
class StagedShadowStore {
|
||||
public:
|
||||
// `copies` is "this process is really split". False reproduces the monolith expression
|
||||
// character for character (`raw - offset`), which is what keeps every push and verify
|
||||
// lane byte-identical to what it was before R-11.
|
||||
explicit StagedShadowStore(Bool copies) : m_copies(copies) {}
|
||||
|
||||
Bool CopiesIntoServerStorage() const { return m_copies; }
|
||||
|
||||
// Copies [offset, offset+size) of the record's staged bytes into server-owned storage
|
||||
// and returns the SERVER base (offset 0 of the resource). Under monolith it returns the
|
||||
// client base unchanged and allocates nothing.
|
||||
const Uint8* Adopt(const void* key, SizeT width, const void* bytes, SizeT offset, SizeT size) {
|
||||
const auto* raw = static_cast<const Uint8*>(bytes);
|
||||
if (!m_copies) return raw - offset;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
Shadow& shadow = m_shadows[key];
|
||||
const SizeT needed = std::max<SizeT>(width, offset + size);
|
||||
if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0);
|
||||
if (size != 0 && raw != nullptr) {
|
||||
std::memcpy(shadow.Bytes.data() + offset, raw, size);
|
||||
CoverageAdd(shadow.Covered, offset, offset + size);
|
||||
}
|
||||
m_any.store(true, std::memory_order_release);
|
||||
// Growing REALLOCATES, so every caller assigns the returned base to hostBytes on
|
||||
// the same call. No other resource's base moves: each Shadow owns its own vector,
|
||||
// and a rehash of the map MOVES that vector, which preserves its data pointer.
|
||||
return shadow.Bytes.data();
|
||||
}
|
||||
|
||||
void Drop(const void* key) {
|
||||
if (!m_any.load(std::memory_order_acquire)) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_shadows.erase(key);
|
||||
}
|
||||
|
||||
void DropAll() {
|
||||
if (!m_any.load(std::memory_order_acquire)) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_shadows.clear();
|
||||
}
|
||||
|
||||
// Fatal when a drain reaches bytes no record staged. It fires ONLY for a base that is
|
||||
// this key's server shadow: the legacy arm passes the frontend object's own
|
||||
// MappedData(), which is valid for the whole store and is not this rule's subject.
|
||||
void RequireCoverage(const void* key, const Uint8* hostBase, SizeT start, SizeT end,
|
||||
const char* site) const {
|
||||
if (!m_any.load(std::memory_order_acquire) || hostBase == nullptr) return;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto it = m_shadows.find(key);
|
||||
if (it == m_shadows.end() || it->second.Bytes.data() != hostBase) return;
|
||||
if (CoverageHas(it->second.Covered, start, end)) return;
|
||||
MGLOG_F("MGPipe: Fatal{StageSnapshotTooNarrow, \"%s\"} - the server's ladder wants "
|
||||
"[%zu, %zu) of a buffer whose staged coverage does not include it. Under "
|
||||
"split the authoritative shadow is SERVER-OWNED (rule C) and "
|
||||
"resource_subdata is the only way bytes reach it, so bytes outside a staged "
|
||||
"range have never existed on this side. Re-reading them would move zeroes "
|
||||
"into the store, and an INVALIDATE_RANGE over them would declare live "
|
||||
"GPU-written bytes dead (Managers.cpp:1126-1129). This is a missing record, "
|
||||
"not a missing widening",
|
||||
site, start, end);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// Diagnostics the unit cases read, so that a check can assert WHAT HAPPENED rather than
|
||||
// that nothing blew up.
|
||||
Bool IsCovered(const void* key, SizeT start, SizeT end) const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto it = m_shadows.find(key);
|
||||
if (it == m_shadows.end()) return false;
|
||||
return CoverageHas(it->second.Covered, start, end);
|
||||
}
|
||||
SizeT CoveredRunCount(const void* key) const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto it = m_shadows.find(key);
|
||||
return it == m_shadows.end() ? 0 : it->second.Covered.size();
|
||||
}
|
||||
SizeT TrackedResources() const {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_shadows.size();
|
||||
}
|
||||
|
||||
// Adjacent ranges merge - there is no gap between them, so the union really is one run.
|
||||
// Ranges with a gap do NOT merge, and that is the whole mechanism: it is what makes a
|
||||
// missing record detectable instead of papered over.
|
||||
static void CoverageAdd(Vector<Range1D>& covered, SizeT start, SizeT end) {
|
||||
if (start >= end) return;
|
||||
Vector<Range1D> merged;
|
||||
merged.reserve(covered.size() + 1);
|
||||
SizeT s = start;
|
||||
SizeT e = end;
|
||||
for (const auto& range : covered) {
|
||||
if (range.end < s || range.start > e) {
|
||||
merged.push_back(range);
|
||||
continue;
|
||||
}
|
||||
s = std::min(s, range.start);
|
||||
e = std::max(e, range.end);
|
||||
}
|
||||
merged.push_back({s, e});
|
||||
std::sort(merged.begin(), merged.end(),
|
||||
[](const Range1D& a, const Range1D& b) { return a.start < b.start; });
|
||||
covered = std::move(merged);
|
||||
}
|
||||
|
||||
static Bool CoverageHas(const Vector<Range1D>& covered, SizeT start, SizeT end) {
|
||||
if (start >= end) return true;
|
||||
for (const auto& range : covered) {
|
||||
if (range.start <= start && end <= range.end) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Shadow {
|
||||
Vector<Uint8> Bytes;
|
||||
// Sorted, disjoint, EXACT.
|
||||
Vector<Range1D> Covered;
|
||||
};
|
||||
|
||||
const Bool m_copies;
|
||||
mutable std::mutex m_mutex;
|
||||
ska::flat_hash_map<const void*, Shadow> m_shadows;
|
||||
// Read on every UploadRangeFrom, so the monolith cost is one acquire load of a
|
||||
// never-written flag rather than a mutex and a hash lookup.
|
||||
std::atomic<Bool> m_any{false};
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Server
|
||||
Reference in New Issue
Block a user