// Tests for gpu::memplan (runtime/gpu/memory_plan.h): where guest memory and // the upload ring go on the devices we know. From the repo root: // clang++ -std=c++20 -O2 -Iruntime tests/memory_plan_test.cpp -o build/memory_plan_test // build/memory_plan_test // The memory types are the devices' own: the Steam Machine's from vulkaninfo // (2026-10-09), the others from their logs' chosen types (flags) and their // drivers' type lists (RADV, NVIDIA's Linux driver, ANV, Turnip, MoltenVK). #include #include #include using namespace gpu::memplan; static int g_failures = 0; #define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0) constexpr uint64_t MB = 1ull << 20, GB = 1ull << 30; // "name:type,..." (import has no type; a place whose heap is too small for // it is marked "!": tried only as a last resort). static std::string Text(const Plan& p, bool ring) { std::string s; for (const Candidate& c : p.order) { if (!s.empty()) s += ","; s += Name(c.place, ring); if (c.place != Place::kImport) s += ":" + std::to_string(c.type); if (!c.fits) s += "!"; } return s; } static void Expect(const char* what, const Plan& p, bool ring, const char* want, const char* whyHas = nullptr) { std::string got = Text(p, ring); printf("%-58s %s (%s)\n", what, got.c_str(), p.why.c_str()); CHECK(got == want, "%s: %s, want %s", what, got.c_str(), want); if (whyHas) CHECK(p.why.find(whyHas) != std::string::npos, "%s: why \"%s\" lacks \"%s\"", what, p.why.c_str(), whyHas); } // --- The devices ------------------------------------------------------- // Steam Machine: RADV NAVI33, discrete, resizable BAR (vulkaninfo). static Memory SteamMachine() { return { { { 0x1, 1 }, { 0x1, 1 }, { 0x6, 0 }, { 0x7, 1 }, { 0x7, 1 }, { 0xE, 0 }, { 0xE, 0 }, { 0xC1, 1 }, { 0xC6, 0 }, { 0xC7, 1 }, { 0xCE, 0 } }, { 16621477888ull, 8573157376ull } }; } // Steam Deck: RADV VANGOGH, integrated. RADV's type list for an APU (the // log: copy in type 2, flags 6; ring in type 3, flags 7); its device-local // heap's budget reads 5.7 GB, the carve-out is 1 GB: either. static Memory SteamDeck(uint64_t vramHeap) { return { { { 0x1, 1 }, { 0x1, 1 }, { 0x6, 0 }, { 0x7, 1 }, { 0x7, 1 }, { 0xE, 0 }, { 0xE, 0 } }, { 7 * GB, vramHeap } }; } // Steam Frame: Turnip (Adreno 750), unified memory, no host-memory import // (the log: copy and ring in type 0, flags 7). static Memory SteamFrame() { return { { { 0x7, 0 }, { 0xF, 0 }, { 0xB, 0 } }, { 11 * GB } }; } // Apple silicon (MoltenVK): private, shared (the log: ring in type 1, flags // F), memoryless. static Memory Apple() { return { { { 0x1, 0 }, { 0xF, 0 }, { 0x11, 0 } }, { 10922 * MB } }; } // NVIDIA's Linux driver without resizable BAR (GTX 1080 Ti; an RTX 4070 // with it off): VRAM, system memory, and the 256 MB BAR window. static Memory NvidiaNoRebar() { return { { { 0x0, 1 }, { 0x1, 0 }, { 0x6, 1 }, { 0xE, 1 }, { 0x7, 2 } }, { 11 * GB, 15 * GB, 256 * MB } }; } // The same with resizable BAR: the window is all of VRAM. static Memory NvidiaRebar() { return { { { 0x0, 1 }, { 0x1, 0 }, { 0x6, 1 }, { 0xE, 1 }, { 0x7, 2 } }, { 12 * GB, 15 * GB, 12 * GB } }; } // RADV on a discrete GPU without resizable BAR: VRAM split into the part the // CPU can't map and the 256 MB it can. static Memory RadvNoRebar() { return { { { 0x1, 1 }, { 0x1, 1 }, { 0x6, 0 }, { 0x7, 2 }, { 0x7, 2 }, { 0xE, 0 }, { 0xE, 0 } }, { 15 * GB, 7936 * MB, 256 * MB } }; } // ANV on Arrow Lake (issue #2's Zenbook): one system-memory heap, every type // device-local (the log: ring in type 0, flags 7). static Memory AnvArrowLake() { return { { { 0x7, 0 }, { 0xF, 0 } }, { 15 * GB } }; } static GuestInput Input(Gpu gpu, bool importable, uint32_t driver = 0, const char* shared = nullptr, const char* shadow = nullptr) { GuestInput in; in.gpu = gpu; in.importable = importable; in.importRefusal = ImportRefusal(driver); in.overrides = ParseOverrides(shared, shadow, nullptr); return in; } // The copy's type when the copy is used, for PlanRing (kNoType: imported). static uint32_t CopyType(const Plan& guest) { return guest.order.front().place == Place::kImport ? kNoType : guest.order.front().type; } // --- Tests ------------------------------------------------------------- // The devices that play today keep exactly what v0.1.0 chose. static void Unchanged() { printf("unchanged:\n"); { Memory m = SteamMachine(); Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true)); Expect("Steam Machine, guest memory", g, false, "copy-vram:3,import,copy-system:2,copy-cached:5", "resizable BAR"); Expect("Steam Machine, ring", PlanRing(m, ~0u, CopyType(g)), true, "ring-vram:3,ring-cached:5,ring-system:2"); } for (uint64_t vram : { 1 * GB, 5692 * MB }) { Memory m = SteamDeck(vram); Plan g = PlanGuestMemory(m, Input(Gpu::kIntegrated, true)); // RADV refuses the import (the guest's memory is a shared mapping), // so the copy lands in type 2, as the Deck's log says. Expect("Steam Deck, guest memory", g, false, "import,copy-system:2,copy-cached:5,copy-vram:3"); Expect("Steam Deck, ring (copy in type 2)", PlanRing(m, ~0u, 2), true, "ring-vram:3,ring-cached:5,ring-system:2"); } { Memory m = SteamFrame(); Plan g = PlanGuestMemory(m, Input(Gpu::kIntegrated, false)); Expect("Steam Frame, guest memory", g, false, "copy-vram:0", "can't import"); Expect("Steam Frame, ring", PlanRing(m, ~0u, CopyType(g)), true, "ring-vram:0,ring-cached:1"); } { Memory m = Apple(); Plan g = PlanGuestMemory(m, Input(Gpu::kIntegrated, true, 14)); // VK_DRIVER_ID_MOLTENVK Expect("Apple, guest memory", g, false, "import,copy-vram:1"); Expect("Apple, ring", PlanRing(m, ~0u, CopyType(g)), true, "ring-vram:1,ring-cached:1"); } // A software rasterizer (llvmpipe: one cached host-visible type). { Memory m{ { { 0xF, 0 } }, { 16 * GB } }; Expect("llvmpipe, guest memory", PlanGuestMemory(m, Input(Gpu::kOther, true)), false, "import,copy-vram:0"); } } // Issue #2, A: a discrete GPU without resizable BAR. static void NoRebar() { printf("no resizable BAR:\n"); { Memory m = NvidiaNoRebar(); Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true, 4)); // VK_DRIVER_ID_NVIDIA_PROPRIETARY Expect("NVIDIA 256 MB BAR, guest memory", g, false, "import,copy-system:2,copy-cached:3,copy-vram:4!", "no resizable BAR"); // What issue #2's commenter found to work: import, the ring cached. Expect("NVIDIA 256 MB BAR, ring (imported)", PlanRing(m, ~0u, kNoType), true, "ring-cached:3,ring-system:2,ring-vram:4!", "too small"); Expect("NVIDIA 256 MB BAR, ring (copy in type 2)", PlanRing(m, ~0u, 2), true, "ring-cached:3,ring-system:2,ring-vram:4!"); } { Memory m = NvidiaRebar(); Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true, 4)); Expect("NVIDIA resizable BAR, guest memory", g, false, "copy-vram:4,import,copy-system:2,copy-cached:3"); Expect("NVIDIA resizable BAR, ring", PlanRing(m, ~0u, CopyType(g)), true, "ring-vram:4,ring-cached:3,ring-system:2"); } { Memory m = RadvNoRebar(); Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true)); Expect("RADV 256 MB BAR, guest memory", g, false, "import,copy-system:2,copy-cached:5,copy-vram:3!"); Expect("RADV 256 MB BAR, ring (copy in type 2)", PlanRing(m, ~0u, 2), true, "ring-cached:5,ring-system:2,ring-vram:3!"); } // A 512 MB window: the copy doesn't fit, the ring does. { Memory m = NvidiaNoRebar(); m.heaps[2] = 512 * MB; Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true, 4)); Expect("NVIDIA 512 MB BAR, guest memory", g, false, "import,copy-system:2,copy-cached:3,copy-vram:4!"); Expect("NVIDIA 512 MB BAR, ring (imported)", PlanRing(m, ~0u, kNoType), true, "ring-vram:4,ring-cached:3,ring-system:2"); } // Both in one heap: the copy fits, the ring beside it doesn't. { Memory m = SteamMachine(); m.heaps[1] = 1300 * MB; // half: 650 MB, the copy 512, both 704 Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true)); Expect("1300 MB BAR, guest memory", g, false, "copy-vram:3,import,copy-system:2,copy-cached:5"); Expect("1300 MB BAR, ring beside the copy", PlanRing(m, ~0u, CopyType(g)), true, "ring-cached:5,ring-system:2,ring-vram:3!", "beside the copy"); Expect("1300 MB BAR, ring (copy went elsewhere)", PlanRing(m, ~0u, 2), true, "ring-vram:3,ring-cached:5,ring-system:2"); } // A driver without host-memory import: the copy in system memory. { Memory m = NvidiaNoRebar(); Expect("NVIDIA 256 MB BAR, no import", PlanGuestMemory(m, Input(Gpu::kDiscrete, false, 4)), false, "copy-system:2,copy-cached:3,copy-vram:4!", "can't import"); } // No CPU-mapped VRAM at all. { Memory m{ { { 0x1, 0 }, { 0x6, 1 }, { 0xE, 1 } }, { 8 * GB, 16 * GB } }; Expect("no CPU-mapped VRAM, guest memory", PlanGuestMemory(m, Input(Gpu::kDiscrete, true)), false, "import,copy-system:1,copy-cached:2", "without CPU-mapped VRAM"); Expect("no CPU-mapped VRAM, ring", PlanRing(m, ~0u, kNoType), true, "ring-cached:2,ring-system:1"); } } // Issue #2, B: Intel's Linux driver loses the device with imported memory. static void Intel() { printf("Intel:\n"); CHECK(ImportRefusal(6) != nullptr, "ANV (VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA) imports"); for (uint32_t driver : { 0u, 1u, 3u, 4u, 5u, 13u, 14u }) // unknown, AMD Windows, RADV, NVIDIA, Intel Windows, llvmpipe, MoltenVK CHECK(ImportRefusal(driver) == nullptr, "driver %u refused", driver); Memory m = AnvArrowLake(); Plan g = PlanGuestMemory(m, Input(Gpu::kIntegrated, true, 6)); Expect("ANV, guest memory", g, false, "copy-vram:0", "Intel's Linux driver"); Expect("ANV, ring", PlanRing(m, ~0u, CopyType(g)), true, "ring-vram:0,ring-cached:1"); // NFSMW_SHARED_MEMORY=import still imports (to check a newer kernel). Expect("ANV, NFSMW_SHARED_MEMORY=import", PlanGuestMemory(m, Input(Gpu::kIntegrated, true, 6, "import")), false, "import,copy-vram:0", "NFSMW_SHARED_MEMORY=import"); // A discrete Arc card keeps its copy in VRAM with resizable BAR, and // doesn't import without it. Memory arc{ { { 0x1, 0 }, { 0x7, 0 }, { 0x6, 1 }, { 0xE, 1 } }, { 8 * GB, 16 * GB } }; Expect("Arc, resizable BAR", PlanGuestMemory(arc, Input(Gpu::kDiscrete, true, 6)), false, "copy-vram:1,copy-system:2,copy-cached:3"); arc.heaps[0] = 256 * MB; Expect("Arc, 256 MB BAR", PlanGuestMemory(arc, Input(Gpu::kDiscrete, true, 6)), false, "copy-system:2,copy-cached:3,copy-vram:1!", "Intel's Linux driver"); } // The environment's overrides still lead, and the default order follows. static void EnvOverrides() { printf("overrides:\n"); Memory sm = SteamMachine(); Expect("SM, NFSMW_SHARED_MEMORY=import", PlanGuestMemory(sm, Input(Gpu::kDiscrete, true, 0, "import")), false, "import,copy-vram:3,copy-system:2,copy-cached:5"); Expect("SM, NFSMW_SHARED_MEMORY=shadow", PlanGuestMemory(sm, Input(Gpu::kDiscrete, true, 0, "shadow")), false, "copy-vram:3,copy-system:2,copy-cached:5"); Expect("SM, NFSMW_SHADOW_MEMORY=gtt", PlanGuestMemory(sm, Input(Gpu::kDiscrete, true, 0, nullptr, "gtt")), false, "copy-system:2,import,copy-vram:3,copy-cached:5"); Expect("Deck, NFSMW_SHADOW_MEMORY=vram", PlanGuestMemory(SteamDeck(GB), Input(Gpu::kIntegrated, true, 0, nullptr, "vram")), false, "import,copy-vram:3,copy-system:2,copy-cached:5"); Expect("Deck, shadow + vram", PlanGuestMemory(SteamDeck(GB), Input(Gpu::kIntegrated, true, 0, "shadow", "vram")), false, "copy-vram:3,copy-system:2,copy-cached:5"); // Issue #2's commenter on an NVIDIA card without resizable BAR. Expect("NVIDIA, NFSMW_SHARED_MEMORY=import", PlanGuestMemory(NvidiaNoRebar(), Input(Gpu::kDiscrete, true, 4, "import")), false, "import,copy-system:2,copy-cached:3,copy-vram:4!"); // NFSMW_SHADOW_MEMORY=vram where it can't fit: tried first anyway. Expect("NVIDIA, NFSMW_SHADOW_MEMORY=vram", PlanGuestMemory(NvidiaNoRebar(), Input(Gpu::kDiscrete, true, 4, nullptr, "vram")), false, "copy-vram:4!,import,copy-system:2,copy-cached:3"); Overrides o = ParseOverrides(nullptr, nullptr, "cached"); CHECK(o.ring == Place::kCached, "NFSMW_RING_MEMORY=cached"); Expect("SM ring, NFSMW_RING_MEMORY=cached", PlanRing(sm, ~0u, 3, o.ring), true, "ring-cached:5,ring-vram:3,ring-system:2"); // MoltenVK has no cached type outside device memory: as before, the // ring goes to its shared type. Expect("Apple ring, NFSMW_RING_MEMORY=cached", PlanRing(Apple(), ~0u, kNoType, Place::kCached), true, "ring-cached:1,ring-vram:1"); Expect("NVIDIA ring, NFSMW_RING_MEMORY=vram", PlanRing(NvidiaNoRebar(), ~0u, kNoType, ParseOverrides(nullptr, nullptr, "vram").ring), true, "ring-vram:4!,ring-cached:3,ring-system:2"); Expect("SM ring, NFSMW_RING_MEMORY=system", PlanRing(sm, ~0u, 3, ParseOverrides(nullptr, nullptr, "system").ring), true, "ring-system:2,ring-vram:3,ring-cached:5"); CHECK(ParseOverrides("nonsense", "nonsense", "nonsense").shared == Overrides::Shared::kAuto, "unknown values are the default"); } // The test hook (NFSMW_TEST_BAR_HEAP_MB): the Steam Machine plans as a GPU // without resizable BAR, exactly as RADV's split heaps would. static void TestHook() { printf("NFSMW_TEST_BAR_HEAP_MB:\n"); Memory m = SteamMachine(); ShrinkMappedVram(m, 256 * MB); CHECK(m.heaps[1] == 256 * MB && m.heaps[0] == 16621477888ull, "heaps %llu, %llu", (unsigned long long)m.heaps[0], (unsigned long long)m.heaps[1]); Plan g = PlanGuestMemory(m, Input(Gpu::kDiscrete, true)); Expect("SM at 256 MB, guest memory", g, false, "import,copy-system:2,copy-cached:5,copy-vram:3!", "no resizable BAR"); Expect("SM at 256 MB, ring (copy in type 2)", PlanRing(m, ~0u, 2), true, "ring-cached:5,ring-system:2,ring-vram:3!"); // The Deck's choice, planned on the Steam Machine (NFSMW_TEST_GPU_KIND=integrated). Expect("SM as integrated", PlanGuestMemory(SteamMachine(), Input(Gpu::kIntegrated, true)), false, "import,copy-system:2,copy-cached:5,copy-vram:3"); } // Memory types a buffer can't use, and the special ones, are never planned. static void Types() { printf("types:\n"); Memory sm = SteamMachine(); CHECK(FindType(sm, ~0u, Place::kVram, false) == 3, "first CPU-mapped VRAM type"); CHECK(FindType(sm, ~(1u << 3), Place::kVram, false) == 4, "type 3 not allowed"); CHECK(FindType(sm, (1u << 9), Place::kVram, false) == kNoType, "AMD's device-coherent type"); CHECK(FindType(sm, ~0u, Place::kSystem, false) == 2, "system"); CHECK(FindType(sm, ~0u, Place::kCached, false) == 5, "cached"); CHECK(FindType(Apple(), ~0u, Place::kCached, false) == kNoType, "no cached system memory on Apple"); CHECK(FindType(Apple(), ~0u, Place::kCached, true) == 1, "the ring's cached type on Apple"); CHECK(Fits(sm, 3, kGuestBytes, kRingBytes), "the Steam Machine's VRAM holds both"); CHECK(!Fits(NvidiaNoRebar(), 4, kRingBytes), "a 256 MB window holds 192 MB at most at half"); CHECK(Size(256 * MB) == "256 MB" && Size(8573157376ull) == "7.98 GB" && Size(16621477888ull) == "15 GB", "sizes %s %s %s", Size(256 * MB).c_str(), Size(8573157376ull).c_str(), Size(16621477888ull).c_str()); } int main() { Unchanged(); NoRebar(); Intel(); EnvOverrides(); TestHook(); Types(); if (g_failures) { printf("%d FAILED\n", g_failures); return 1; } printf("all passed\n"); return 0; }