diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index 3779dc0d..c9b1068f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index 7c3f1f8d..04b120de 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -292,6 +292,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { IBufferCopyCommandProvider* m_copyProvider = nullptr; Vector> m_deferredBufferReleases; std::unordered_map 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 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 m_deferredWireReleases; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 1c1dc917..cb42f89e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 81c9ea0b..8b42136e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -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 m_wireTextureLookup{}; + Array 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 m_mutableFormatUnsupported; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 86fe84ac..2fb00688 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -825,7 +825,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector sameLayoutAttachments; WireDrawPassKey passKey; WireRenderPassCompatibilityKey compatibilityKey; + struct GpuWriteMark { + MG_Pipe::MGPipeHandle storage; + Uint32 level, layer, layers; + }; + Vector 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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index 8a4c63aa..5c1fc7d4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -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) { diff --git a/MobileGL/MG_Backend/Record/StagedTextureStore.h b/MobileGL/MG_Backend/Record/StagedTextureStore.h index 782fe7b7..d928e87b 100644 --- a/MobileGL/MG_Backend/Record/StagedTextureStore.h +++ b/MobileGL/MG_Backend/Record/StagedTextureStore.h @@ -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 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 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 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 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 m_any{false}; + std::atomic m_gpuDirtyClears{0}; }; // ONE PER PROCESS, and its copying arm is decided ONCE at first use - StagedShadow's diff --git a/tools/device_bench/disagg/matrix_p14.sh b/tools/device_bench/disagg/matrix_p14.sh index 78abae73..900c2641 100644 --- a/tools/device_bench/disagg/matrix_p14.sh +++ b/tools/device_bench/disagg/matrix_p14.sh @@ -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; } diff --git a/tools/device_bench/disagg/p14_report.py b/tools/device_bench/disagg/p14_report.py index 3ac26297..1cfef2dd 100644 --- a/tools/device_bench/disagg/p14_report.py +++ b/tools/device_bench/disagg/p14_report.py @@ -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]