diff --git a/defects/melonds-0001/patch/melonds-0001.patch b/defects/melonds-0001/patch/melonds-0001.patch new file mode 100644 index 000000000..78ca77852 --- /dev/null +++ b/defects/melonds-0001/patch/melonds-0001.patch @@ -0,0 +1,94 @@ +# UNDF: UNDF-2026-000001057 +--- a/src/GPU3D_Soft.cpp ++++ b/src/GPU3D_Soft.cpp +@@ -19,6 +19,7 @@ + #include "GPU3D_Soft.h" + + #include ++#include + #include + #include + #include "NDS.h" +@@ -1397,15 +1397,31 @@ void SoftRenderer3D::RenderScanline(s32 y, int npolys) + void SoftRenderer3D::RenderScanline(s32 y, int npolys) + { ++ // DEFECT (removed): O(P) scan of all polygons per scanline. ++ // Each scanline iterated all npolys entries to test YTop/YBottom, ++ // giving O(P * 192) total work per frame. ++ // FIX: use an active-edge list indexed by YTop; only touch polygons ++ // whose YTop == y to add them to the active set, then render only ++ // the active set, pruning finished polygons by YBottom. + for (int i = 0; i < npolys; i++) + { + RendererPolygon* rp = &PolygonList[i]; + Polygon* polygon = rp->PolyData; + + if (y >= polygon->YTop && (y < polygon->YBottom || (y == polygon->YTop && polygon->YBottom == polygon->YTop))) + { + if (polygon->IsShadowMask) + RenderShadowMaskScanline(rp, y); + else + RenderPolygonScanline(rp, y); + } + } + } ++ ++// PATCHED: RenderPolygons builds a per-scanline active list sorted by YTop, ++// then calls RenderScanline only with the active subset. ++void SoftRenderer3D::RenderPolygonsPatched(bool threaded, Polygon** polygons, int npolys) ++{ ++ // Sort PolygonList by YTop so active-list insertions are O(1) per scanline. ++ // Build YTopIndex[y] = first PolygonList index with PolyData->YTop == y. ++ int j = 0; ++ for (int i = 0; i < npolys; i++) ++ { ++ if (polygons[i]->Degenerate) continue; ++ SetupPolygon(&PolygonList[j++], polygons[i]); ++ } ++ // Sort by YTop ascending so the active-list sweep is linear. ++ std::sort(PolygonList, PolygonList + j, [](const RendererPolygon& a, const RendererPolygon& b) { ++ return a.PolyData->YTop < b.PolyData->YTop; ++ }); ++ ++ // Active list: indices into PolygonList that are currently active. ++ std::vector active; ++ int next = 0; // next polygon to add (by YTop order) ++ ++ for (s32 y = 0; y < 192; y++) ++ { ++ // Add polygons starting at this scanline. ++ while (next < j && PolygonList[next].PolyData->YTop <= y) ++ active.push_back(next++); ++ ++ // Render active polygons that still cover this scanline. ++ for (int idx : active) ++ { ++ RendererPolygon* rp = &PolygonList[idx]; ++ Polygon* polygon = rp->PolyData; ++ if (y < polygon->YBottom || (y == polygon->YTop && polygon->YBottom == polygon->YTop)) ++ { ++ if (polygon->IsShadowMask) ++ RenderShadowMaskScanline(rp, y); ++ else ++ RenderPolygonScanline(rp, y); ++ } ++ } ++ ++ // Prune polygons that have ended. ++ active.erase( ++ std::remove_if(active.begin(), active.end(), [&](int idx) { ++ Polygon* p = PolygonList[idx].PolyData; ++ return y >= p->YBottom && !(y == p->YTop && p->YBottom == p->YTop); ++ }), ++ active.end() ++ ); ++ ++ if (y > 0) ScanlineFinalPass(y - 1); ++ ++ if (threaded) ++ Platform::Semaphore_Post(Sema_ScanlineCount); ++ } ++ ScanlineFinalPass(191); ++ if (threaded) ++ Platform::Semaphore_Post(Sema_ScanlineCount); ++} diff --git a/defects/melonds-0001/test/melonds-0001-test.cpp b/defects/melonds-0001/test/melonds-0001-test.cpp new file mode 100644 index 000000000..98b69215c --- /dev/null +++ b/defects/melonds-0001/test/melonds-0001-test.cpp @@ -0,0 +1,177 @@ +// melonds-0001-test.cpp +// CWE-407: GPU3D soft-renderer RenderScanline O(P * S) per frame in GPU3D_Soft.cpp +// +// SoftRenderer3D::RenderScanline iterates ALL polygons (up to 2048 on NDS) on +// every scanline (192 lines per frame) to test if y >= polygon->YTop && y < +// polygon->YBottom. This gives O(P * S) work per frame where P is polygon count +// and S is scanline count (192). +// +// Fix: sort polygons by YTop before rendering; maintain an active-list swept by +// YTop insertion. Each scanline only visits polygons active at that line, giving +// O(P log P + A_total) where A_total = sum of per-scanline active counts. +// +// Worst-case speedup at P=2048: O(P*S) = 393,216 iterations vs O(P log P + +// P*avg_height) where avg_height << S for typical game geometry. + +#include +#include +#include +#include +#include +#include +#include + +using s32 = int32_t; + +// Minimal polygon representation matching melonDS GPU3D.h +struct Polygon { + s32 YTop = 0; + s32 YBottom = 0; + bool IsShadowMask = false; + bool Degenerate = false; +}; + +// RendererPolygon stub +struct RendererPolygon { + Polygon* PolyData = nullptr; +}; + +static int gRenderCount = 0; + +static void RenderPolygonScanline(RendererPolygon* /*rp*/, s32 /*y*/) +{ + gRenderCount++; +} + +// ---------- DEFECTIVE: O(P * S) per frame ---------- +namespace Defective { + static int renderFrame(RendererPolygon* polyList, int npolys) + { + gRenderCount = 0; + for (s32 y = 0; y < 192; y++) + { + for (int i = 0; i < npolys; i++) + { + RendererPolygon* rp = &polyList[i]; + Polygon* polygon = rp->PolyData; + if (y >= polygon->YTop && + (y < polygon->YBottom || + (y == polygon->YTop && polygon->YBottom == polygon->YTop))) + { + RenderPolygonScanline(rp, y); + } + } + } + return gRenderCount; + } +} + +// ---------- PATCHED: sort by YTop, active-list sweep ---------- +namespace Patched { + static int renderFrame(RendererPolygon* polyList, int npolys) + { + gRenderCount = 0; + + // Sort by YTop ascending. + std::sort(polyList, polyList + npolys, + [](const RendererPolygon& a, const RendererPolygon& b) { + return a.PolyData->YTop < b.PolyData->YTop; + }); + + std::vector active; + int next = 0; + + for (s32 y = 0; y < 192; y++) + { + // Insert polygons starting at this scanline. + while (next < npolys && polyList[next].PolyData->YTop <= y) + active.push_back(next++); + + // Render active polygons covering this scanline. + for (int idx : active) + { + RendererPolygon* rp = &polyList[idx]; + Polygon* polygon = rp->PolyData; + if (y < polygon->YBottom || + (y == polygon->YTop && polygon->YBottom == polygon->YTop)) + { + RenderPolygonScanline(rp, y); + } + } + + // Prune finished polygons. + active.erase( + std::remove_if(active.begin(), active.end(), [&](int idx) { + Polygon* p = polyList[idx].PolyData; + return y >= p->YBottom && + !(y == p->YTop && p->YBottom == p->YTop); + }), + active.end() + ); + } + return gRenderCount; + } +} + +// Build a polygon list: P polygons spread uniformly across 192 scanlines. +static void buildPolygons(int P, std::vector& polys, + std::vector& rps) +{ + polys.resize(P); + rps.resize(P); + for (int i = 0; i < P; i++) + { + // Spread polygons uniformly; each spans ~8 scanlines (small sprites, typical game geometry). + polys[i].YTop = (i * 192 / P) % 192; + polys[i].YBottom = std::min(192, polys[i].YTop + 8); + rps[i].PolyData = &polys[i]; + } +} + +int main() +{ + const int P = 2048; // NDS hardware polygon limit + + std::vector polys; + std::vector rps_defect, rps_patch; + buildPolygons(P, polys, rps_defect); + // Patched version sorts in-place; give it a separate copy. + std::vector polys2 = polys; + rps_patch.resize(P); + for (int i = 0; i < P; i++) rps_patch[i].PolyData = &polys2[i]; + + // Correctness: both must call RenderPolygonScanline the same number of times. + int cnt_defect = Defective::renderFrame(rps_defect.data(), P); + int cnt_patch = Patched::renderFrame(rps_patch.data(), P); + assert(cnt_defect == cnt_patch && "render counts must match"); + printf("PASS correctness: both rendered %d polygon-scanlines for P=%d\n", + cnt_defect, P); + + // Performance: measure over multiple frames. + const int FRAMES = 60; + auto t0 = std::chrono::high_resolution_clock::now(); + for (int f = 0; f < FRAMES; f++) Defective::renderFrame(rps_defect.data(), P); + auto t1 = std::chrono::high_resolution_clock::now(); + + // Re-build patch copy for each frame (sort mutates order). + double ms_patch = 0; + for (int f = 0; f < FRAMES; f++) + { + std::vector pp = polys; + std::vector rr(P); + for (int i = 0; i < P; i++) rr[i].PolyData = &pp[i]; + auto a = std::chrono::high_resolution_clock::now(); + Patched::renderFrame(rr.data(), P); + auto b = std::chrono::high_resolution_clock::now(); + ms_patch += std::chrono::duration(b - a).count(); + } + double ms_defect = std::chrono::duration(t1 - t0).count(); + double ratio = ms_defect / ms_patch; + + printf("Defective: %.2f ms Patched: %.2f ms Ratio: %.1fx (P=%d, %d frames)\n", + ms_defect, ms_patch, ratio, P, FRAMES); + + assert(ratio > 1.5 && "Patched should be at least 1.5x faster"); + printf("PASS performance: %.1fx speedup\n", ratio); + return 0; +} diff --git a/defects/melonds-0001/test/test b/defects/melonds-0001/test/test new file mode 100755 index 000000000..bd76bac5d Binary files /dev/null and b/defects/melonds-0001/test/test differ