[Perf] (Magma): wire texture and buffer lookups get direct-mapped fronts and a continued pass stops re-marking its attachments (rd12 monolith 59.6 -> 60.5 fps).

This commit is contained in:
2026-10-07 11:25:02 -04:00
parent eafe1179b3
commit e6281b26de
9 changed files with 102 additions and 11 deletions
@@ -184,8 +184,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} // namespace
VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) {
const auto found = m_wireBuffers.find(WireBufferKey(res));
return found == m_wireBuffers.end() ? nullptr : &found->second;
const Uint64 key = WireBufferKey(res);
auto& lookup = m_wireBufferLookup[key % kWireBufferLookupEntries];
if (lookup.resource != nullptr && lookup.key == key && lookup.eraseCount == m_wireBufferEraseCount)
return lookup.resource;
const auto found = m_wireBuffers.find(key);
if (found == m_wireBuffers.end()) return nullptr;
lookup = {key, m_wireBufferEraseCount, &found->second};
return &found->second;
}
void VkBufferManager::CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc) {
@@ -992,6 +998,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DeferWireRelease(std::move(found->second.buffer),
found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial)
: found->second.lastUseSerial); // P11 B2, as Respecify
++m_wireBufferEraseCount;
m_wireBuffers.erase(found);
PublishWireReclaimGauges();
}
@@ -1163,6 +1170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// DestroyAllDeferredReleases above emptied the parked list; this destroys the stores the
// records still hold, so nothing this arm minted outlives the count.
if (!m_wireBuffers.empty()) ++m_wireStoreDestroyEpoch;
++m_wireBufferEraseCount;
m_wireBuffers.clear();
m_wireStoreCount = 0;
m_wireStoreCountPeak = 0;
@@ -292,6 +292,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
IBufferCopyCommandProvider* m_copyProvider = nullptr;
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
std::unordered_map<Uint64, WireBufferResource> m_wireBuffers;
// P14: a direct-mapped front for m_wireBuffers (every draw resolves each vertex, index and
// uniform buffer). Keyed on WireBufferKey ({slot, gen}, never reused) and believed only at
// the erase count it was taken at: a node never moves until erased, and every erase moves
// the count.
struct WireBufferLookup {
Uint64 key = 0;
Uint64 eraseCount = 0;
WireBufferResource* resource = nullptr;
};
static constexpr SizeT kWireBufferLookupEntries = 256;
Array<WireBufferLookup, kWireBufferLookupEntries> m_wireBufferLookup{};
Uint64 m_wireBufferEraseCount = 0;
// See DeferredWireRelease. ONE FLAT LIST rather than the per-frame-slot buckets above:
// the whole point is that these entries do not wait for a frame slot to come round.
Vector<DeferredWireRelease> m_deferredWireReleases;
@@ -1874,9 +1874,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return nullptr;
}
const Uint64 key = MG_Record::StagedTextureStore::KeyForHandle(handle);
auto [it, inserted] = resources.try_emplace(key);
(void)inserted;
TextureResource& resource = it->second;
auto& lookup = (renderbuffer ? m_wireRenderbufferLookup : m_wireTextureLookup)[key % kWireResourceLookupEntries];
if (lookup.resource == nullptr || lookup.key != key || lookup.eraseEpoch != m_resourceEraseEpoch) {
auto [it, inserted] = resources.try_emplace(key);
(void)inserted;
lookup = {key, m_resourceEraseEpoch, &it->second};
}
TextureResource& resource = *lookup.resource;
if (resource.image != VK_NULL_HANDLE && resource.syncedWireSerial == record.Serial &&
record.PendingUploads.empty() && (!requireStorage || (resource.usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) != 0)) {
return &resource;
@@ -693,6 +693,17 @@ private:
// SyncWireTextureShape defining the RGBA8 storage a YUV shared image is converted into.
Bool m_syncingYuvTextureStorage = false;
Uint64 m_resourceEraseEpoch = 1;
// P14: a direct-mapped front for the two wire resource maps, keyed on the handle key
// ({slot, gen}, never reused) and believed only at the erase epoch it was taken at - an
// unordered_map node never moves until it is erased, and every erase moves the epoch.
struct WireResourceLookup {
Uint64 key = 0;
Uint64 eraseEpoch = 0;
TextureResource* resource = nullptr;
};
static constexpr SizeT kWireResourceLookupEntries = 256;
Array<WireResourceLookup, kWireResourceLookupEntries> m_wireTextureLookup{};
Array<WireResourceLookup, kWireResourceLookupEntries> m_wireRenderbufferLookup{};
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
@@ -825,7 +825,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<WireImage> sameLayoutAttachments;
WireDrawPassKey passKey;
WireRenderPassCompatibilityKey compatibilityKey;
struct GpuWriteMark {
MG_Pipe::MGPipeHandle storage;
Uint32 level, layer, layers;
};
Vector<GpuWriteMark> gpuWriteMarks;
};
// The current wire pass's attachments were marked GPU-written at this StagedTextureStore
// clear generation (SetupWireDraw); false whenever the pass changes.
Bool m_wirePassMarksValid = false;
Uint64 m_wirePassMarksClearGeneration = 0;
WireDrawScratch m_wireDrawScratch;
struct WireRetiredObjects {
Uint64 submitIndex = 0;
@@ -105,6 +105,7 @@
}
m_wireDrawPass.reset();
m_wireDrawPassCompatibilityId = 0;
m_wirePassMarksValid = false;
for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr);
m_wireDrawViews.clear();
m_wireDrawPassKey = {};
@@ -119,6 +120,7 @@
void VulkanRenderer::RetireWireDrawPass() {
m_wireDrawPassCompatibilityId = 0;
m_wirePassMarksValid = false;
if (!m_wireDrawPass) return;
auto& frame = m_frameContext.GetCurrent();
if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass())
@@ -506,6 +508,7 @@
}
auto& sameLayoutAttachments = scratch.sameLayoutAttachments;
sameLayoutAttachments.clear();
scratch.gpuWriteMarks.clear();
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
WireImage image = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/true);
if (!image.image) return VK_ATTACHMENT_UNUSED;
@@ -543,8 +546,9 @@
layers = std::min(layers, image.layers);
samples = image.samples;
passHash = XXH64(&description, sizeof(description), passHash);
// Marked below, once the pass is known: a continued pass repeats the same marks.
if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers);
scratch.gpuWriteMarks.push_back({image.storage, image.level, image.layer, image.layers});
if (image.isDefault) {
if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true);
else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true);
@@ -625,6 +629,19 @@
created.layers = layers;
created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash);
}
// The attachments' GPU-write marks (MarkWireTextureGpuWritten): idempotent while nothing
// clears a GPU-dirty mark, so a draw that continues a pass whose draws already marked the
// same attachments - the pass key compared equal - repeats them only after a clear.
{
auto& store = MG_Record::ServerStagedTexture();
if (!continuing || !m_wirePassMarksValid ||
m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) {
for (const auto& mark : scratch.gpuWriteMarks)
m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers);
m_wirePassMarksValid = true;
m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration();
}
}
auto& pass = *m_wireDrawPass;
// Interned once per pass: a continued pass has the same key, so the same compatibility.
if (m_wireDrawPassCompatibilityId == 0) {
@@ -373,6 +373,7 @@ namespace MobileGL::MG_Record {
m_driverWritten[bucketed] = true;
}
shadow.GpuDirty = false;
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
return CopyRunInto(shadow, key, uploadTarget, level, imageOffset, bytes, byteSize, frontendShadow);
}
@@ -390,6 +391,7 @@ namespace MobileGL::MG_Record {
LevelShadow& shadow = m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)];
RequireDeclaredLevel(shadow, key, uploadTarget, level, extent);
shadow.GpuDirty = false;
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
// P8-E: the caller states these bytes ARE the level, which a followed copy is part of.
shadow.HoldsFollowedCopy = false;
shadow.Bytes.clear();
@@ -454,6 +456,7 @@ namespace MobileGL::MG_Record {
shadow.Covered.clear();
shadow.GpuDirty = false;
shadow.HoldsFollowedCopy = false;
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
}
shadow.Extent = extent;
shadow.InternalFormat = internalFormat;
@@ -471,6 +474,7 @@ namespace MobileGL::MG_Record {
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.erase(bucketed);
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
}
// T5's dirty mark: a GPU-side generation made this level's texels newer than any
@@ -482,14 +486,22 @@ namespace MobileGL::MG_Record {
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows[bucketed].Levels[PackLevel(uploadTarget, level)].GpuDirty = dirty;
if (!dirty) m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
m_any.store(true, std::memory_order_release);
}
// P14: moves whenever a level's GPU-dirty mark may have gone false - a CPU run or adoption
// over it, a redefinition, an explicit clear, or a level/shadow dropped. A caller that
// re-marks the same levels every draw (Magma's attachments) may skip the repeat while it
// has not moved: marking is idempotent until something clears it.
Uint64 GpuDirtyClearGeneration() const { return m_gpuDirtyClears.load(std::memory_order_relaxed); }
void Drop(Uint64 key) {
if (!m_any.load(std::memory_order_acquire)) return;
const ShadowKey bucketed = Bucketed(key);
const std::lock_guard<std::mutex> lock(m_mutex);
m_shadows.erase(bucketed);
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
m_driverWritten.erase(bucketed);
}
@@ -503,10 +515,12 @@ namespace MobileGL::MG_Record {
const std::lock_guard<std::mutex> lock(m_mutex);
if (session == 0) {
m_shadows.clear();
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
m_driverWritten.clear();
return;
}
EraseSession(m_shadows, session);
m_gpuDirtyClears.fetch_add(1, std::memory_order_relaxed);
EraseSession(m_driverWritten, session);
}
@@ -1024,6 +1038,7 @@ namespace MobileGL::MG_Record {
// Read on every IsLevelGpuDirty / LevelExtentOrUndefined, so the monolith cost is one
// acquire load of a never-written flag rather than a mutex and two hash lookups.
std::atomic<Bool> m_any{false};
std::atomic<Uint64> m_gpuDirtyClears{0};
};
// ONE PER PROCESS, and its copying arm is decided ONCE at first use - StagedShadow's
+1 -1
View File
@@ -129,7 +129,7 @@ run_one() {
adb -s "$SER" logcat -c >/dev/null 2>&1
if [ "$(arm_kind "$arm")" = tcp ]; then
python "$(cygpath -w "$REPO_ROOT/tools/trace_replay/tcp_device_server.py")" start --serial "$SER" --package "$pkg" \
--backend "$backend" --listen tcp://127.0.0.1:40613 >"$dir/server-start.log" 2>&1 || { echo server_start_failed >"$dir/STATUS"; log "FAIL tcp server"; return 1; }
--backend "$backend" --listen tcp://127.0.0.1:40613 ${EXTRA_SERVER_ENV:+--env "$EXTRA_SERVER_ENV"} >"$dir/server-start.log" 2>&1 || { echo server_start_failed >"$dir/STATUS"; log "FAIL tcp server"; return 1; }
local waited=0
until adb -s "$SER" logcat -d 2>/dev/null | grep -q "listening on tcp://127.0.0.1:40613"; do
sleep 1; waited=$((waited + 1)); [ $waited -ge 30 ] && { echo server_not_listening >"$dir/STATUS"; log "FAIL tcp listen"; return 1; }
+19 -4
View File
@@ -53,6 +53,21 @@ def at(points, t):
return v0 + (v1 - v0) * (t - t0) / max(1, t1 - t0)
def window_delta(points, start, end):
"""On-CPU ns a counter series gained inside [start, end]: the sum of its positive steps, each
weighted by how much of its interval lies in the window. Robust to a counter that restarts
(a thread id reused by a new thread under the same name reads as a drop, which is skipped)."""
if not points or points[0][0] > start + 250_000_000 or points[-1][0] < end - 250_000_000:
return None
total = 0.0
for (t0, v0), (t1, v1) in zip(points, points[1:]):
if t1 <= start or t0 >= end or v1 <= v0:
continue
overlap = min(t1, end) - max(t0, start)
total += (v1 - v0) * overlap / max(1, t1 - t0)
return total
def reduce_run(run_dir, wl):
bench_path = run_dir / "benchmark.json"
if not bench_path.exists():
@@ -82,14 +97,14 @@ def reduce_run(run_dir, wl):
for (pid, tid, comm), pts in series.items():
if pid < 0:
continue
a, b = at(pts, start), at(pts, end)
if a is None or b is None:
used = window_delta(pts, start, end)
if used is None:
continue
ms = (b - a) / 1e6 / len(steady)
ms = used / 1e6 / len(steady)
if ms <= 0.005:
continue
threads.append({"pid": pid, "tid": tid, "comm": comm, "ms_per_frame": ms,
"util": (b - a) / span})
"util": used / span})
procs[pid] += ms
threads.sort(key=lambda r: -r["ms_per_frame"])
out["threads"] = threads[:8]