diff --git a/defects/scummvm-0001/patch/scummvm-0001.patch b/defects/scummvm-0001/patch/scummvm-0001.patch new file mode 100644 index 000000000..a465d2ef4 --- /dev/null +++ b/defects/scummvm-0001/patch/scummvm-0001.patch @@ -0,0 +1,50 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/engines/crab/PathfindingGrid.cpp ++++ b/engines/crab/PathfindingGrid.cpp +@@ -27,6 +27,8 @@ + + #include "crab/PathfindingGrid.h" + #include "crab/TMX/TMXMap.h" ++#include + + namespace Crab { + +@@ -234,21 +236,22 @@ PathfindingGraphNode *PathfindingGrid::getNearestOpenNode(Vector2f nodePos, Vect + if (startNode->getMovementCost() > 0) // If the clicked node is open, we're done! + return startNode; + + PathfindingGraphNode *returnNode = nullptr; + + float shortestDistance = 0.0f; + + Common::List checkNodes; + checkNodes.push_back(startNode); + +- Common::Array allUsedNodes; +- allUsedNodes.push_back(startNode); ++ // Use an unordered_set for O(1) visited-node lookup instead of O(N) linear ++ // scan over allUsedNodes on every neighbor check. Without this fix the BFS ++ // is O(N^2) in the number of grid nodes visited. ++ std::unordered_set visitedNodes; ++ visitedNodes.insert(startNode); + + // Iterate through the nodes, check if they are open then check their distance from the compare point. + while (!checkNodes.empty()) { + if (checkNodes.front()->getMovementCost() > 0) { + float distance = (comparePos - checkNodes.front()->getPosition()).magSqr(); + + if (shortestDistance == 0.0f || distance) { // If this is the new shortest distance, this becomes the new return. + shortestDistance = distance; + + returnNode = checkNodes.front(); + } + } else { + for (uint i = 0; i < checkNodes.front()->_neighborNodes.size(); ++i) { + // If the neighbor hasn't been checked yet, add it to the list to check. +- if (Common::find(allUsedNodes.begin(), allUsedNodes.end(), checkNodes.front()->_neighborNodes[i]) == allUsedNodes.end()) { +- allUsedNodes.push_back(checkNodes.front()->_neighborNodes[i]); ++ if (visitedNodes.find(checkNodes.front()->_neighborNodes[i]) == visitedNodes.end()) { ++ visitedNodes.insert(checkNodes.front()->_neighborNodes[i]); + checkNodes.push_back(checkNodes.front()->_neighborNodes[i]); + } + } diff --git a/defects/scummvm-0001/test/scummvm-0001-test b/defects/scummvm-0001/test/scummvm-0001-test new file mode 100755 index 000000000..3a458b212 Binary files /dev/null and b/defects/scummvm-0001/test/scummvm-0001-test differ diff --git a/defects/scummvm-0001/test/scummvm-0001-test.cpp b/defects/scummvm-0001/test/scummvm-0001-test.cpp new file mode 100644 index 000000000..5acb23902 --- /dev/null +++ b/defects/scummvm-0001/test/scummvm-0001-test.cpp @@ -0,0 +1,227 @@ +// scummvm-0001-test.cpp +// Unit test: CRAB engine PathfindingGrid::getNearestOpenNode O(N^2) BFS (CWE-407) +// +// DEFECT: In engines/crab/PathfindingGrid.cpp, getNearestOpenNode() performs a +// BFS over the pathfinding grid to find the nearest walkable node. The visited +// set is a Common::Array named allUsedNodes. On each +// neighbor check the BFS calls: +// Common::find(allUsedNodes.begin(), allUsedNodes.end(), neighbor) +// which is O(|visited|) per check. Over a BFS visiting N nodes with an average +// degree D, total work is O(N * D * N) = O(N^2 * D). On a 64x64 tile map this +// function can visit thousands of nodes per click. +// +// FIX: Replace allUsedNodes (Common::Array) with std::unordered_set. +// Membership test drops from O(N) to O(1). Total BFS cost becomes O(N * D). +// +// BUILD: g++ -std=c++17 -O2 -o scummvm-0001-test scummvm-0001-test.cpp && ./scummvm-0001-test + +#include +#include +#include +#include +#include +#include +#include +#include + +// Minimal simulated node +struct Node { + int x, y; + bool blocked; + std::vector neighbors; + Node(int x, int y, bool blocked) : x(x), y(y), blocked(blocked) {} +}; + +// Build a grid of W*H nodes; border nodes are blocked, inner nodes are open +static std::vector> build_grid(int W, int H) { + std::vector> grid(W, std::vector(H)); + for (int i = 0; i < W; i++) + for (int j = 0; j < H; j++) + grid[i][j] = new Node(i, j, (i == 0 || j == 0 || i == W-1 || j == H-1)); + + // Connect 4-neighbors + for (int i = 0; i < W; i++) { + for (int j = 0; j < H; j++) { + if (i > 0) grid[i][j]->neighbors.push_back(grid[i-1][j]); + if (i < W-1) grid[i][j]->neighbors.push_back(grid[i+1][j]); + if (j > 0) grid[i][j]->neighbors.push_back(grid[i][j-1]); + if (j < H-1) grid[i][j]->neighbors.push_back(grid[i][j+1]); + } + } + return grid; +} + +static void free_grid(std::vector> &grid) { + for (auto &col : grid) + for (auto *n : col) + delete n; +} + +// DEFECT: BFS with std::vector visited set = O(N^2) +static Node *get_nearest_open_defect(Node *start) { + if (!start->blocked) return start; + + Node *returnNode = nullptr; + float shortestDistance = 0.0f; + + std::list checkNodes; + checkNodes.push_back(start); + + std::vector allUsedNodes; // O(N) find on every neighbor check + allUsedNodes.push_back(start); + + while (!checkNodes.empty()) { + Node *front = checkNodes.front(); + + if (!front->blocked) { + float dx = (float)(front->x - start->x); + float dy = (float)(front->y - start->y); + float dist = dx*dx + dy*dy; + if (shortestDistance == 0.0f || dist < shortestDistance) { + shortestDistance = dist; + returnNode = front; + } + } else { + for (Node *nb : front->neighbors) { + // Original O(N) membership check + if (std::find(allUsedNodes.begin(), allUsedNodes.end(), nb) == allUsedNodes.end()) { + allUsedNodes.push_back(nb); + checkNodes.push_back(nb); + } + } + } + + if (returnNode != nullptr) return returnNode; + checkNodes.pop_front(); + } + return nullptr; +} + +// FIX: BFS with unordered_set visited = O(N) +static Node *get_nearest_open_fixed(Node *start) { + if (!start->blocked) return start; + + Node *returnNode = nullptr; + float shortestDistance = 0.0f; + + std::list checkNodes; + checkNodes.push_back(start); + + std::unordered_set visitedNodes; // O(1) find + visitedNodes.insert(start); + + while (!checkNodes.empty()) { + Node *front = checkNodes.front(); + + if (!front->blocked) { + float dx = (float)(front->x - start->x); + float dy = (float)(front->y - start->y); + float dist = dx*dx + dy*dy; + if (shortestDistance == 0.0f || dist < shortestDistance) { + shortestDistance = dist; + returnNode = front; + } + } else { + for (Node *nb : front->neighbors) { + if (visitedNodes.find(nb) == visitedNodes.end()) { + visitedNodes.insert(nb); + checkNodes.push_back(nb); + } + } + } + + if (returnNode != nullptr) return returnNode; + checkNodes.pop_front(); + } + return nullptr; +} + +static void test_correctness() { + // 10x10 grid, start at (0,0) which is blocked + auto grid = build_grid(10, 10); + Node *start = grid[0][0]; + + Node *r_defect = get_nearest_open_defect(start); + Node *r_fixed = get_nearest_open_fixed(start); + + assert(r_defect != nullptr); + assert(r_fixed != nullptr); + // Both should find same nearest open node (same distance) + float dx_d = (float)(r_defect->x - start->x); + float dy_d = (float)(r_defect->y - start->y); + float dx_f = (float)(r_fixed->x - start->x); + float dy_f = (float)(r_fixed->y - start->y); + float dist_d = dx_d*dx_d + dy_d*dy_d; + float dist_f = dx_f*dx_f + dy_f*dy_f; + assert(dist_d == dist_f); + + free_grid(grid); + printf("PASS correctness: both implementations find nearest open at same distance\n"); +} + +static long long bench(int W, int H, int repeats, bool fixed) { + auto grid = build_grid(W, H); + // Block everything; no open node is found so BFS visits all N*W nodes + for (int i = 0; i < W; i++) + for (int j = 0; j < H; j++) + grid[i][j]->blocked = true; + Node *start = grid[0][0]; + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int r = 0; r < repeats; r++) { + Node *result = fixed ? get_nearest_open_fixed(start) : get_nearest_open_defect(start); + (void)result; + } + auto t1 = std::chrono::high_resolution_clock::now(); + + free_grid(grid); + return std::chrono::duration_cast(t1 - t0).count(); +} + +int main() { + printf("scummvm-0001: CRAB PathfindingGrid::getNearestOpenNode O(N^2) BFS (CWE-407)\n\n"); + + test_correctness(); + + // Benchmark: fully-blocked grid forces BFS to visit ALL N nodes before returning + // null. This maximizes the visited-set membership checks, exposing the O(N^2) scan. + const int W = 80, H = 80; // 6400 nodes + auto grid = build_grid(W, H); + for (int i = 0; i < W; i++) + for (int j = 0; j < H; j++) + grid[i][j]->blocked = true; + + Node *start = grid[0][0]; + const int REPS = 20; + + printf("\nBenchmark: %dx%d fully-blocked grid, %d reps from (0,0)\n", W, H, REPS); + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int r = 0; r < REPS; r++) { + auto *result = get_nearest_open_defect(start); + (void)result; + } + auto t1 = std::chrono::high_resolution_clock::now(); + long long us_defect = std::chrono::duration_cast(t1 - t0).count(); + + auto t2 = std::chrono::high_resolution_clock::now(); + for (int r = 0; r < REPS; r++) { + auto *result = get_nearest_open_fixed(start); + (void)result; + } + auto t3 = std::chrono::high_resolution_clock::now(); + long long us_fixed = std::chrono::duration_cast(t3 - t2).count(); + + free_grid(grid); + + printf(" Defect (O(N^2) BFS): %lld us\n", us_defect); + printf(" Fixed (O(N) BFS): %lld us\n", us_fixed); + + if (us_fixed > 0 && us_defect > 0) { + double ratio = (double)us_defect / (double)us_fixed; + printf(" Speedup ratio: %.1fx\n", ratio); + } + + printf("\nPASS\n"); + return 0; +} diff --git a/defects/scummvm-0002/patch/scummvm-0002.patch b/defects/scummvm-0002/patch/scummvm-0002.patch new file mode 100644 index 000000000..6abb5cca6 --- /dev/null +++ b/defects/scummvm-0002/patch/scummvm-0002.patch @@ -0,0 +1,73 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/engines/tsage/core.h ++++ b/engines/tsage/core.h +@@ -835,6 +835,8 @@ class WalkRegions { + private: + void loadOriginal(); + void loadRevised(); ++ ++#include ++ + public: + int _resNum; + RouteEnds _routeEnds; +@@ -843,7 +845,9 @@ public: + Common::Array _idxList; + Common::Array _idxList2; +- Common::List _disabledRegions; ++ // Changed from Common::List to std::unordered_set to make ++ // contains() and disableRegion() O(1) instead of O(D). The recursive ++ // calculateRestOfRoute() was calling contains(_disabledRegions, ...) on ++ // every connected region per step, producing O(D * R * depth) total work. ++ std::unordered_set _disabledRegions; + public: + WalkRegions() { _resNum = -1; } +--- a/engines/tsage/core.cpp ++++ b/engines/tsage/core.cpp +@@ -4302,18 +4302,18 @@ void WalkRegions::synchronize(Serializer &s) { + // Synchronize the list of disabled regions as a list of values terminated with a '-1' + int regionId = 0; + if (s.isLoading()) { + _disabledRegions.clear(); + + s.syncAsSint16LE(regionId); + while (regionId != -1) { +- _disabledRegions.push_back(regionId); ++ _disabledRegions.insert(regionId); + s.syncAsSint16LE(regionId); + } + } else { +- Common::List::iterator i; +- for (i = _disabledRegions.begin(); i != _disabledRegions.end(); ++i) { +- regionId = *i; ++ for (const int id : _disabledRegions) { ++ regionId = id; + s.syncAsSint16LE(regionId); + } + + regionId = -1; + s.syncAsSint16LE(regionId); + } + } + +@@ -4325,8 +4325,8 @@ void WalkRegions::synchronize(Serializer &s) { + void WalkRegions::disableRegion(int regionId) { +- if (!contains(_disabledRegions, regionId)) +- _disabledRegions.push_back(regionId); ++ _disabledRegions.insert(regionId); // O(1); unordered_set deduplicates automatically + } + + void WalkRegions::enableRegion(int regionId) { +- _disabledRegions.remove(regionId); ++ _disabledRegions.erase(regionId); // O(1) + } +--- a/engines/tsage/core.cpp (calculateRestOfRoute change) ++++ b/engines/tsage/core.cpp +@@ -920,7 +920,7 @@ int PlayerMover::calculateRestOfRoute(int *routeList, int srcRegion, int destReg + // Check every connected region until we find a route to the destination (or we have no more to check). + int bestDistance = 31990; + while (((currDest = g_globals->_walkRegions._idxList[srcWalkRegion._idxListIndex + foundIndex]) != 0) && (!foundRoute)) { + // Only check the region if it isn't in the list of explicitly disabled regions +- if (!contains(g_globals->_walkRegions._disabledRegions, (int)currDest)) { ++ if (g_globals->_walkRegions._disabledRegions.find((int)currDest) == g_globals->_walkRegions._disabledRegions.end()) { + int newDistance = calculateRestOfRoute(tempList, currDest, destRegion, foundRoute); diff --git a/defects/scummvm-0002/test/scummvm-0002-test b/defects/scummvm-0002/test/scummvm-0002-test new file mode 100755 index 000000000..815f1cbd5 Binary files /dev/null and b/defects/scummvm-0002/test/scummvm-0002-test differ diff --git a/defects/scummvm-0002/test/scummvm-0002-test.cpp b/defects/scummvm-0002/test/scummvm-0002-test.cpp new file mode 100644 index 000000000..8c9ba3528 --- /dev/null +++ b/defects/scummvm-0002/test/scummvm-0002-test.cpp @@ -0,0 +1,162 @@ +// scummvm-0002-test.cpp +// Unit test: TsAGE WalkRegions _disabledRegions O(D) linear scan in recursive pathfinding (CWE-407) +// +// DEFECT: In engines/tsage/core.cpp, calculateRestOfRoute() is a recursive +// function that finds optimal walk paths across scene regions. In the inner +// while loop it calls: +// contains(_disabledRegions, (int)currDest) +// where contains() performs a linear O(D) scan over a Common::List. +// With D disabled regions, R connected regions per walk region, and recursive +// depth up to the number of regions, the total cost per pathfinding call is +// O(D * R * depth). Also WalkRegions::indexOf() scans O(R*I) where I is +// the size of the ignored-index list passed in. +// +// FIX: Change _disabledRegions from Common::List to +// std::unordered_set. insert(), erase(), and find() all become O(1). +// The save/load serialization is updated to use insert() and range iteration. +// +// BUILD: g++ -std=c++17 -O2 -o scummvm-0002-test scummvm-0002-test.cpp && ./scummvm-0002-test + +#include +#include +#include +#include +#include +#include +#include + +// Simulated Common::List contains (original defect path) +static bool list_contains(const std::list &l, int v) { + return std::find(l.begin(), l.end(), v) != l.end(); +} + +// Route-finding context: simulate calculateRestOfRoute with disabled region check +// Graph: nodes 1..N in a chain, each connected to next 3 nodes +static const int REGION_LIST_SIZE = 40; + +struct DefectRouter { + int N; // total regions + std::vector> adj; // adjacency + std::list disabledRegions; // O(D) linear scan + + DefectRouter(int n, const std::vector &disabled) : N(n), adj(n + 1) { + // Build adjacency: each region connects to up to 3 forward neighbors + for (int i = 1; i <= N; i++) { + for (int k = 1; k <= 3 && i + k <= N; k++) { + adj[i].push_back(i + k); + } + } + for (int d : disabled) disabledRegions.push_back(d); + } + + // Returns path length or 32000 if no route + int findRoute(int src, int dest, int depth = 0) { + if (depth > REGION_LIST_SIZE) return 32000; + if (src == dest) return 0; + + int best = 32000; + for (int next : adj[src]) { + // DEFECT: O(D) linear scan on every recursive call + if (!list_contains(disabledRegions, next)) { + int d = findRoute(next, dest, depth + 1); + if (d < best) best = 1 + d; + } + } + return best; + } +}; + +struct FixedRouter { + int N; + std::vector> adj; + std::unordered_set disabledRegions; // O(1) lookup + + FixedRouter(int n, const std::vector &disabled) : N(n), adj(n + 1) { + for (int i = 1; i <= N; i++) { + for (int k = 1; k <= 3 && i + k <= N; k++) { + adj[i].push_back(i + k); + } + } + for (int d : disabled) disabledRegions.insert(d); + } + + int findRoute(int src, int dest, int depth = 0) { + if (depth > REGION_LIST_SIZE) return 32000; + if (src == dest) return 0; + + int best = 32000; + for (int next : adj[src]) { + // FIX: O(1) hash lookup + if (disabledRegions.find(next) == disabledRegions.end()) { + int d = findRoute(next, dest, depth + 1); + if (d < best) best = 1 + d; + } + } + return best; + } +}; + +static void test_correctness() { + const int N = 20; + // Disable every 3rd region to simulate partially blocked maps + std::vector disabled; + for (int i = 3; i <= N; i += 3) disabled.push_back(i); + + DefectRouter dr(N, disabled); + FixedRouter fr(N, disabled); + + for (int src = 1; src <= N; src++) { + for (int dst = src; dst <= N; dst++) { + int rd = dr.findRoute(src, dst); + int rf = fr.findRoute(src, dst); + assert(rd == rf); + } + } + printf("PASS correctness: defect and fixed agree on all region pairs\n"); +} + +int main() { + printf("scummvm-0002: TsAGE _disabledRegions O(D) linear scan in pathfinding (CWE-407)\n\n"); + + test_correctness(); + + // Benchmark: many disabled regions, find route across map, many calls + const int N = 30; + std::vector disabled; + // Disable every other region -- maximizes disabled list size (D = N/2) + for (int i = 2; i <= N; i += 2) disabled.push_back(i); + + const int CALLS = 5000; + + printf("\nBenchmark: N=%d regions, D=%zu disabled, %d route-find calls\n", + N, disabled.size(), CALLS); + + DefectRouter dr(N, disabled); + FixedRouter fr(N, disabled); + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int c = 0; c < CALLS; c++) { + // Route from region 1 to region N -- traverses full map + (void)dr.findRoute(1, N); + } + auto t1 = std::chrono::high_resolution_clock::now(); + long long us_defect = std::chrono::duration_cast(t1 - t0).count(); + + auto t2 = std::chrono::high_resolution_clock::now(); + for (int c = 0; c < CALLS; c++) { + (void)fr.findRoute(1, N); + } + auto t3 = std::chrono::high_resolution_clock::now(); + long long us_fixed = std::chrono::duration_cast(t3 - t2).count(); + + printf(" Defect (O(D*R^depth) per call): %lld us\n", us_defect); + printf(" Fixed (O(R^depth) per call): %lld us\n", us_fixed); + + if (us_fixed > 0 && us_defect > 0) { + double ratio = (double)us_defect / (double)us_fixed; + printf(" Speedup ratio: %.1fx\n", ratio); + } + + printf("\nPASS\n"); + return 0; +} diff --git a/defects/vita3k-0001/patch/vita3k-0001.patch b/defects/vita3k-0001/patch/vita3k-0001.patch index 91ecb0a08..50bc135c3 100644 --- a/defects/vita3k-0001/patch/vita3k-0001.patch +++ b/defects/vita3k-0001/patch/vita3k-0001.patch @@ -1,7 +1,7 @@ # UNDF: UNDF-2026-XXXXXXXXX --- a/vita3k/ngs/src/route.cpp +++ b/vita3k/ngs/src/route.cpp -@@ -17,6 +17,8 @@ +@@ -17,21 +17,24 @@ #include @@ -10,7 +10,16 @@ #include namespace ngs { -@@ -28,7 +30,13 @@ bool deliver_data(const MemState &mem, const std::vector &voice_queue, +-bool deliver_data(const MemState &mem, const std::vector &voice_queue, Voice *source, const uint8_t output_port, ++bool deliver_data(const MemState &mem, const std::unordered_set &voice_queue_set, Voice *source, const uint8_t output_port, + const VoiceProduct &data_to_deliver) { + if (!data_to_deliver.data) { + return false; + } + + for (auto &patch_ptr : source->patches[output_port]) { + Patch *patch = patch_ptr.get(mem); + if (!patch || patch->output_sub_index == -1) continue; @@ -21,35 +30,36 @@ const std::lock_guard guard(*patch->dest->voice_mutex); --- a/vita3k/ngs/include/ngs/system.h +++ b/vita3k/ngs/include/ngs/system.h -@@ -15,6 +15,8 @@ - // with this program; if not, write to the Free Software Foundation, Inc., - // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +@@ -18,6 +18,8 @@ + #pragma once +#include + - #pragma once - #include -@@ -218,4 +220,4 @@ struct Rack; + #include + #include +@@ -218,7 +220,7 @@ struct Rack; -bool deliver_data(const MemState &mem, const std::vector &voice_queue, Voice *source, const uint8_t output_port, -+bool deliver_data(const MemState &mem, const std::vector &voice_queue, const std::unordered_set &voice_queue_set, Voice *source, const uint8_t output_port, ++bool deliver_data(const MemState &mem, const std::unordered_set &voice_queue_set, Voice *source, const uint8_t output_port, const VoiceProduct &data_to_deliver); --- a/vita3k/ngs/src/scheduler.cpp +++ b/vita3k/ngs/src/scheduler.cpp -@@ -120,10 +120,12 @@ void VoiceScheduler::update(KernelState &kern, const MemState &mem, const SceUI +@@ -120,10 +120,13 @@ void VoiceScheduler::update(KernelState &kern, const MemState &mem, const SceUI // make a copy of the queue, this way we have no issue if it is modified in a callback std::vector queue_copy = queue; ++ // Build O(1) set so deliver_data membership test is O(1) per patch, ++ // not O(V) per patch. Total cost per frame: O(V) build + O(V*P*out) lookups ++ // instead of O(V^2 * P * out) with the old linear scan. + const std::unordered_set queue_set(queue_copy.begin(), queue_copy.end()); // Do a first routine to clear inputs from previous update session for (ngs::Voice *voice : queue_copy) { voice->inputs.reset_inputs(); } - -@@ -160,7 +162,7 @@ void VoiceScheduler::update(KernelState &kern, const MemState &mem, const SceUI +@@ -162,6 +166,6 @@ void VoiceScheduler::update(KernelState &kern, const MemState &mem, const SceUI for (size_t i = 0; i < voice->rack->vdef->output_count; i++) { if (voice->products[i].data) - deliver_data(mem, queue_copy, voice, static_cast(i), voice->products[i]); -+ deliver_data(mem, queue_copy, queue_set, voice, static_cast(i), voice->products[i]); ++ deliver_data(mem, queue_set, voice, static_cast(i), voice->products[i]); } diff --git a/defects/vita3k-0001/test/vita3k-0001-test b/defects/vita3k-0001/test/vita3k-0001-test new file mode 100755 index 000000000..9cbeb61ec Binary files /dev/null and b/defects/vita3k-0001/test/vita3k-0001-test differ diff --git a/defects/vita3k-0001/test/vita3k-0001-test.cpp b/defects/vita3k-0001/test/vita3k-0001-test.cpp new file mode 100644 index 000000000..02ca42502 --- /dev/null +++ b/defects/vita3k-0001/test/vita3k-0001-test.cpp @@ -0,0 +1,170 @@ +// vita3k-0001-test.cpp +// Unit test: NGS audio scheduler deliver_data voice_queue O(V^2*P) per frame (CWE-407) +// +// DEFECT: In vita3k/ngs/src/route.cpp, deliver_data() is called for every +// voice on every audio frame. For each output port's patch, it calls: +// vector_utils::contains(voice_queue, patch->dest) +// which is a linear O(V) scan over all voices. With V voices, P patches per +// port, and O output ports, the total cost per frame is O(V * O * P * V) = +// O(V^2 * O * P). NGS supports up to 256 voices per rack in a PS Vita game. +// At 48kHz audio, this runs continuously. +// +// FIX: Build a std::unordered_set from queue_copy once per frame in +// VoiceScheduler::update(), then pass it to deliver_data() for O(1) lookup. +// Total cost per frame drops to O(V) set build + O(V * O * P) lookups. +// +// BUILD: g++ -std=c++17 -O2 -o vita3k-0001-test vita3k-0001-test.cpp && ./vita3k-0001-test + +#include +#include +#include +#include +#include +#include + +// Simulated Voice structure (minimal for testing) +struct Voice { + int id; + explicit Voice(int i) : id(i) {} +}; + +// Simulated patch: points to a destination voice +struct Patch { + Voice *dest; + explicit Patch(Voice *d) : dest(d) {} +}; + +// DEFECT: O(V) linear scan per patch membership check +static bool deliver_data_defect(const std::vector &voice_queue, + const std::vector &patches) +{ + int received = 0; + for (const Patch &patch : patches) { + // Original: vector_utils::contains = O(V) linear scan + bool in_queue = (std::find(voice_queue.begin(), voice_queue.end(), patch.dest) != voice_queue.end()); + if (in_queue) { + ++received; + } + } + return received > 0; +} + +// FIX: O(1) hash-set lookup per patch +static bool deliver_data_fixed(const std::unordered_set &voice_queue_set, + const std::vector &patches) +{ + int received = 0; + for (const Patch &patch : patches) { + bool in_queue = (voice_queue_set.find(patch.dest) != voice_queue_set.end()); + if (in_queue) { + ++received; + } + } + return received > 0; +} + +static long long bench_defect(int num_voices, int patches_per_voice, int frames) { + std::vector voices; + voices.reserve(num_voices); + for (int i = 0; i < num_voices; i++) voices.emplace_back(i); + + std::vector voice_queue; + for (auto &v : voices) voice_queue.push_back(&v); + + // Each voice has patches connecting to voices at higher indices (ring-like) + std::vector> all_patches(num_voices); + for (int i = 0; i < num_voices; i++) { + for (int p = 0; p < patches_per_voice; p++) { + int dest_idx = (i + p + 1) % num_voices; + all_patches[i].emplace_back(&voices[dest_idx]); + } + } + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int f = 0; f < frames; f++) { + for (int i = 0; i < num_voices; i++) { + deliver_data_defect(voice_queue, all_patches[i]); + } + } + auto t1 = std::chrono::high_resolution_clock::now(); + return std::chrono::duration_cast(t1 - t0).count(); +} + +static long long bench_fixed(int num_voices, int patches_per_voice, int frames) { + std::vector voices; + voices.reserve(num_voices); + for (int i = 0; i < num_voices; i++) voices.emplace_back(i); + + std::vector voice_queue; + for (auto &v : voices) voice_queue.push_back(&v); + // Build set once per simulated frame (as fixed scheduler does) + std::unordered_set voice_queue_set(voice_queue.begin(), voice_queue.end()); + + std::vector> all_patches(num_voices); + for (int i = 0; i < num_voices; i++) { + for (int p = 0; p < patches_per_voice; p++) { + int dest_idx = (i + p + 1) % num_voices; + all_patches[i].emplace_back(&voices[dest_idx]); + } + } + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int f = 0; f < frames; f++) { + for (int i = 0; i < num_voices; i++) { + deliver_data_fixed(voice_queue_set, all_patches[i]); + } + } + auto t1 = std::chrono::high_resolution_clock::now(); + return std::chrono::duration_cast(t1 - t0).count(); +} + +// Correctness: both implementations must agree on membership results +static void test_correctness() { + const int N = 32; + std::vector voices; + for (int i = 0; i < N; i++) voices.emplace_back(i); + + std::vector queue; + // Put only even-indexed voices in the queue + for (int i = 0; i < N; i += 2) queue.push_back(&voices[i]); + + std::unordered_set queue_set(queue.begin(), queue.end()); + + for (int i = 0; i < N; i++) { + std::vector patches = { Patch(&voices[i]) }; + bool defect_result = deliver_data_defect(queue, patches); + bool fixed_result = deliver_data_fixed(queue_set, patches); + assert(defect_result == fixed_result); + } + printf("PASS correctness: defect and fixed agree on all %d voices\n", N); +} + +int main() { + printf("vita3k-0001: NGS deliver_data voice_queue O(V^2) linear scan (CWE-407)\n\n"); + + test_correctness(); + + const int V = 256; // PS Vita NGS max voices per rack + const int P = 4; // typical patches per output port + const int FRAMES = 200; + + printf("\nBenchmark: V=%d voices, P=%d patches, %d frames\n", V, P, FRAMES); + + long long t_defect = bench_defect(V, P, FRAMES); + long long t_fixed = bench_fixed(V, P, FRAMES); + + printf(" Defect (O(V^2*P) per frame): %lld us\n", t_defect); + printf(" Fixed (O(V*P) per frame): %lld us\n", t_fixed); + + if (t_fixed > 0 && t_defect > 0) { + double ratio = (double)t_defect / (double)t_fixed; + printf(" Speedup ratio: %.1fx\n", ratio); + // Expect meaningful improvement at V=256; at minimum 3x faster + if (ratio < 1.5) { + printf("WARNING: ratio %.1f lower than expected -- check benchmark timing\n", ratio); + } + } + + printf("\nPASS\n"); + return 0; +}