java-topology/defects/citra-0001/test/CitraTest.java
russell@unturf.com cdba4c80e9 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.
2026-03-31 19:59:10 -04:00

139 lines
5.4 KiB
Java

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);
}
}
}