mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user