citra: 1 CWE-407 defect, MOADs 0002-0005 CLEAN
MOAD-0001: citra-0001 RasterizerCache page_table surfaces vector O(P*S^2) UnregisterSurface calls std::find(surfaces.begin(), surfaces.end(), surface_id) for each of P pages a surface spans. With S overlapping surfaces per page, total unregister cost is O(P*S) per surface, O(P*S^2) overall. Fix: change std::vector<SurfaceId> to std::unordered_set<SurfaceId> (std::hash<Common::SlotId> already defined). 3.4x measured at S=500, P=64. Hot path: InvalidateRegion called per CPU write to GPU texture memory. MOAD-0002: CLEAN. System singleton is intentional single-emulator architecture; subsystems injected via System& reference, no intertangle coupling found. MOAD-0003: CLEAN. thread_local only used for JNIEnv* JVM attachment in Android JNI (standard pattern, not request-scoped identity). MOAD-0004: CLEAN. No credential values logged verbatim; JWT token size only. MOAD-0005: CLEAN. GetPublicKey static cache is room-server single-threaded; JitEngine shader cache is GPU-thread single-threaded; all others use mutex. Source: azahar-emu/azahar (Citra continuation), depth=1.
This commit is contained in:
parent
7786adc4c0
commit
cdba4c80e9
7 changed files with 222 additions and 0 deletions
47
defects/citra-0001/patch/citra-0001.patch
Normal file
47
defects/citra-0001/patch/citra-0001.patch
Normal file
|
|
@ -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 <functional>
|
||||||
|
+#include <unordered_set>
|
||||||
|
#include <list>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
@@ -215,7 +215,7 @@ private:
|
||||||
|
std::unordered_map<TextureCubeConfig, TextureCube> texture_cube_cache;
|
||||||
|
- tsl::robin_pg_map<u64, std::vector<SurfaceId>, Common::IdentityHash<u64>> page_table;
|
||||||
|
+ tsl::robin_pg_map<u64, std::unordered_set<SurfaceId>, Common::IdentityHash<u64>> page_table;
|
||||||
|
std::unordered_map<FramebufferParams, FramebufferId> 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<T>::ForEachSurfaceInRegion(PAddr addr, std::size_t size, F
|
||||||
|
for (const SurfaceId surface_id : it->second) {
|
||||||
|
|
||||||
|
@@ -1365,8 +1365,8 @@ void RasterizerCache<T>::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<T>::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<SurfaceId>& surfaces = page_it.value();
|
||||||
|
- const auto vector_it = std::find(surfaces.begin(), surfaces.end(), surface_id);
|
||||||
|
- if (vector_it == surfaces.end()) {
|
||||||
|
+ std::unordered_set<SurfaceId>& 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);
|
||||||
|
});
|
||||||
BIN
defects/citra-0001/test/BenchmarkQuick.class
Normal file
BIN
defects/citra-0001/test/BenchmarkQuick.class
Normal file
Binary file not shown.
36
defects/citra-0001/test/BenchmarkQuick.java
Normal file
36
defects/citra-0001/test/BenchmarkQuick.java
Normal file
|
|
@ -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<List<Long>> allPages = new ArrayList<>();
|
||||||
|
for (int i = 0; i < S; i++) {
|
||||||
|
List<Long> 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<Long, List<Integer>> 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<Integer> 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<Long, Set<Integer>> 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<Integer> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
defects/citra-0001/test/CitraTest$PageTableSet.class
Normal file
BIN
defects/citra-0001/test/CitraTest$PageTableSet.class
Normal file
Binary file not shown.
BIN
defects/citra-0001/test/CitraTest$PageTableVector.class
Normal file
BIN
defects/citra-0001/test/CitraTest$PageTableVector.class
Normal file
Binary file not shown.
BIN
defects/citra-0001/test/CitraTest.class
Normal file
BIN
defects/citra-0001/test/CitraTest.class
Normal file
Binary file not shown.
139
defects/citra-0001/test/CitraTest.java
Normal file
139
defects/citra-0001/test/CitraTest.java
Normal file
|
|
@ -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<SurfaceId>
|
||||||
|
* 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<SurfaceId> with std::unordered_set<SurfaceId>
|
||||||
|
* 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<SurfaceId> 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<SurfaceId> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue