diff --git a/defects/citra-0001/patch/citra-0001.patch b/defects/citra-0001/patch/citra-0001.patch new file mode 100644 index 000000000..f51be468f --- /dev/null +++ b/defects/citra-0001/patch/citra-0001.patch @@ -0,0 +1,47 @@ +--- a/src/video_core/rasterizer_cache/rasterizer_cache_base.h ++++ b/src/video_core/rasterizer_cache/rasterizer_cache_base.h +@@ -5,6 +5,7 @@ + #pragma once + + #include ++#include + #include + #include + #include +@@ -215,7 +215,7 @@ private: + std::unordered_map texture_cube_cache; +- tsl::robin_pg_map, Common::IdentityHash> page_table; ++ tsl::robin_pg_map, Common::IdentityHash> page_table; + std::unordered_map framebuffers; + +--- a/src/video_core/rasterizer_cache/rasterizer_cache.h ++++ b/src/video_core/rasterizer_cache/rasterizer_cache.h +@@ -820,7 +820,7 @@ void RasterizerCache::ForEachSurfaceInRegion(PAddr addr, std::size_t size, F + for (const SurfaceId surface_id : it->second) { + +@@ -1365,8 +1365,8 @@ void RasterizerCache::RegisterSurface(SurfaceId surface_id) { + UpdatePagesCachedCount(surface.addr, surface.size, 1); + ForEachPage(surface.addr, surface.size, +- [this, surface_id](u64 page) { page_table[page].push_back(surface_id); }); ++ [this, surface_id](u64 page) { page_table[page].insert(surface_id); }); + } + +@@ -1379,12 +1379,10 @@ void RasterizerCache::UnregisterSurface(SurfaceId surface_id) { + ForEachPage(surface.addr, surface.size, [this, surface_id](u64 page) { + const auto page_it = page_table.find(page); + if (page_it == page_table.end()) { + ASSERT_MSG(false, "Unregistering unregistered page=0x{:x}", page << CITRA_PAGEBITS); + return; + } +- std::vector& surfaces = page_it.value(); +- const auto vector_it = std::find(surfaces.begin(), surfaces.end(), surface_id); +- if (vector_it == surfaces.end()) { ++ std::unordered_set& surfaces = page_it.value(); ++ if (surfaces.find(surface_id) == surfaces.end()) { + ASSERT_MSG(false, "Unregistering unregistered surface in page=0x{:x}", + page << CITRA_PAGEBITS); + return; + } +- surfaces.erase(vector_it); ++ surfaces.erase(surface_id); + }); diff --git a/defects/citra-0001/test/BenchmarkQuick.class b/defects/citra-0001/test/BenchmarkQuick.class new file mode 100644 index 000000000..3d3cc1270 Binary files /dev/null and b/defects/citra-0001/test/BenchmarkQuick.class differ diff --git a/defects/citra-0001/test/BenchmarkQuick.java b/defects/citra-0001/test/BenchmarkQuick.java new file mode 100644 index 000000000..2c56533b6 --- /dev/null +++ b/defects/citra-0001/test/BenchmarkQuick.java @@ -0,0 +1,36 @@ +import java.util.*; +public class BenchmarkQuick { + public static void main(String[] args) { + int[] sizes = {20, 50, 100, 200, 500}; + int P = 64; + System.out.println("S\tVector(ms)\tHashSet(ms)\tRatio"); + for (int S : sizes) { + long baseAddr = 0x10000000L; + List> allPages = new ArrayList<>(); + for (int i = 0; i < S; i++) { + List pages = new ArrayList<>(); + for (int p = 0; p < P; p++) pages.add(baseAddr + p); + allPages.add(pages); + } + // Defective + long d = 0; + for (int r = 0; r < 15; r++) { + Map> vec = new HashMap<>(); + for (int i = 0; i < S; i++) for (long pg : allPages.get(i)) vec.computeIfAbsent(pg, k->new ArrayList<>()).add(i); + long t = System.nanoTime(); + for (int i = 0; i < S; i++) for (long pg : allPages.get(i)) { List s = vec.get(pg); s.remove(Integer.valueOf(i)); } + if (r >= 5) d += System.nanoTime() - t; + } + // Fixed + long f = 0; + for (int r = 0; r < 15; r++) { + Map> set = new HashMap<>(); + for (int i = 0; i < S; i++) for (long pg : allPages.get(i)) set.computeIfAbsent(pg, k->new HashSet<>()).add(i); + long t = System.nanoTime(); + for (int i = 0; i < S; i++) for (long pg : allPages.get(i)) { Set s = set.get(pg); s.remove(i); } + if (r >= 5) f += System.nanoTime() - t; + } + System.out.printf("%d\t%.3f\t\t%.3f\t\t%.1f%n", S, d/10.0/1e6, f/10.0/1e6, (double)d/f); + } + } +} diff --git a/defects/citra-0001/test/CitraTest$PageTableSet.class b/defects/citra-0001/test/CitraTest$PageTableSet.class new file mode 100644 index 000000000..3feb033c6 Binary files /dev/null and b/defects/citra-0001/test/CitraTest$PageTableSet.class differ diff --git a/defects/citra-0001/test/CitraTest$PageTableVector.class b/defects/citra-0001/test/CitraTest$PageTableVector.class new file mode 100644 index 000000000..db0bff8d3 Binary files /dev/null and b/defects/citra-0001/test/CitraTest$PageTableVector.class differ diff --git a/defects/citra-0001/test/CitraTest.class b/defects/citra-0001/test/CitraTest.class new file mode 100644 index 000000000..c3df237ed Binary files /dev/null and b/defects/citra-0001/test/CitraTest.class differ diff --git a/defects/citra-0001/test/CitraTest.java b/defects/citra-0001/test/CitraTest.java new file mode 100644 index 000000000..f38646352 --- /dev/null +++ b/defects/citra-0001/test/CitraTest.java @@ -0,0 +1,139 @@ +import java.util.*; + +/** + * CitraTest -- MOAD-0001 (CWE-407) regression test for citra-0001. + * + * Defect: RasterizerCache page_table stored surfaces as std::vector + * per page. UnregisterSurface called std::find (O(S)) for each of P pages the + * surface spans, giving O(P * S) per eviction where S = number of overlapping + * surfaces on a shared page. Total over all S surfaces: O(P * S^2). + * + * Fix: Replace std::vector with std::unordered_set + * so that find and erase are O(1), reducing total to O(P * S). + * + * This test models the same access pattern in Java using primitive int arrays + * (no boxing overhead) to approximate the C++ unboxed SurfaceId (uint32_t) + * performance characteristics. + */ +public class CitraTest { + + // ---- Defective: int[] per page, linear scan to find and remove ---- + // Simulates std::vector with std::find + erase O(S) per page + + static long benchVector(int S, int P) { + // pageVec[p][0] = current count, pageVec[p][1..S] = surface ids + int[][] pageVec = new int[P][S + 1]; + for (int[] pv : pageVec) pv[0] = 0; + + // Register all S surfaces on all P pages (simulate RegisterSurface) + for (int i = 0; i < S; i++) { + for (int p = 0; p < P; p++) { + pageVec[p][++pageVec[p][0]] = i; + } + } + + long t0 = System.nanoTime(); + // Unregister all surfaces: O(S) find per page per surface = O(P * S^2) total + for (int i = 0; i < S; i++) { + for (int p = 0; p < P; p++) { + int count = pageVec[p][0]; + for (int k = 1; k <= count; k++) { + if (pageVec[p][k] == i) { + // swap-with-last erase (order-independent, same as vector erase) + pageVec[p][k] = pageVec[p][count]; + pageVec[p][0]--; + break; + } + } + } + } + return System.nanoTime() - t0; + } + + // ---- Fixed: BitSet per page, O(1) set/clear ---- + // Simulates std::unordered_set with O(1) insert and erase + + static long benchSet(int S, int P) { + BitSet[] pageSet = new BitSet[P]; + for (int p = 0; p < P; p++) pageSet[p] = new BitSet(S); + + // Register all S surfaces on all P pages + for (int i = 0; i < S; i++) { + for (int p = 0; p < P; p++) { + pageSet[p].set(i); + } + } + + long t0 = System.nanoTime(); + // Unregister all surfaces: O(1) per page per surface = O(P * S) total + for (int i = 0; i < S; i++) { + for (int p = 0; p < P; p++) { + pageSet[p].clear(i); + } + } + return System.nanoTime() - t0; + } + + public static void main(String[] args) { + // S = overlapping surfaces on same page (texture cache at busy scene transition) + // P = pages per surface (256x256 RGBA texture on 4KB pages = 64 pages) + final int S = 500; + final int P = 64; + final int WARMUP = 5; + final int RUNS = 10; + + System.out.println("citra-0001: RasterizerCache page_table surfaces O(P*S^2) -> O(P*S)"); + System.out.printf("S=%d overlapping surfaces, P=%d pages per surface%n", S, P); + System.out.println(); + + long defectTotal = 0; + for (int r = 0; r < WARMUP + RUNS; r++) { + long elapsed = benchVector(S, P); + if (r >= WARMUP) defectTotal += elapsed; + } + double defectMs = defectTotal / 1e6 / RUNS; + + long fixedTotal = 0; + for (int r = 0; r < WARMUP + RUNS; r++) { + long elapsed = benchSet(S, P); + if (r >= WARMUP) fixedTotal += elapsed; + } + double fixedMs = fixedTotal / 1e6 / RUNS; + + double ratio = defectMs / fixedMs; + System.out.printf("Defective (vector linear-scan unregister): %.3f ms%n", defectMs); + System.out.printf("Fixed (bitset O(1) unregister): %.3f ms%n", fixedMs); + System.out.printf("Speedup: %.1fx%n", ratio); + System.out.println(); + + // Correctness: after all unregisters, page entries should be empty + int[][] verifyVec = new int[P][3]; + for (int[] pv : verifyVec) pv[0] = 0; + verifyVec[0][++verifyVec[0][0]] = 42; + // Linear remove of 42 + int found = -1; + for (int k = 1; k <= verifyVec[0][0]; k++) { + if (verifyVec[0][k] == 42) { found = k; break; } + } + if (found < 0) throw new AssertionError("correctness: 42 not found"); + verifyVec[0][found] = verifyVec[0][verifyVec[0][0]--]; + if (verifyVec[0][0] != 0) throw new AssertionError("correctness: count should be 0 after remove"); + + BitSet verifySet = new BitSet(100); + verifySet.set(42); + verifySet.clear(42); + if (verifySet.get(42)) throw new AssertionError("correctness: bitset should be empty"); + // Double-clear is idempotent (no error) - same as unordered_set erase returns count + verifySet.clear(42); + + System.out.println("All correctness assertions PASS"); + + if (ratio < 2.0) { + System.err.printf("FAIL: speedup %.1fx less than expected minimum 2x at S=%d%n", + ratio, S); + System.exit(1); + } else { + System.out.printf("PASS: %.1fx speedup >= 2x minimum%n", ratio); + } + } +}