// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). // // ea_cpu_budget_test : EA SND's mixer never expels voices for time // on a host (apu/ea_cpu_budget.h; GitHub issue #35: one sound, usually the // engine, much quieter until the next gear change). // 1. EA's rule, as the model in the header computes it: a mix at its usual // cost on a MacBook is far under budget; one block held up 14 ms or more // (the mixer thread not run, as a busy or hot Mac does) puts the // three-block average over it, and EA would expel voices; a budget of // 100% or more is never checked. // 2. The policy: voices are expelled only with NFSMW_EA_CPU_BUDGET=1. // 3. The generated code still matches what the header names (a different // XEX or recompiler config must fail here, not silently in a game): the // mixer thread runs the budget check each block; the check times the // mixer with mftb, keeps the three-block average, and calls the // over-budget step only when the budget is under 100% and the load is // over it; the over-budget step picks a voice and expels it with // (voice, 0) until it fits, returning to 0x826017C8 and 0x826017E4. // Build by hand: c++ -std=c++20 -O2 -I runtime tests/ea_cpu_budget_test.cpp -o /tmp/ea_cpu_budget_test #include #include #include #include #include #include #include #include namespace { int s_failed = 0; void Check(bool ok, const std::string& what) { if (!ok) { s_failed++; fprintf(stderr, "FAIL: %s\n", what.c_str()); } } using namespace apu::ea_budget; constexpr double Ticks(double ms) { return ms * kTicksPerMs; } // Blocks of the mixer's own time (ms each) through EA's average, with // `voicesMs` of voice costs: the most a block was over, in ms of a block. double WorstOverMs(const std::vector& blocksMs, double voicesMs) { Average average; double worst = -1e9; for (double ms : blocksMs) worst = std::max(worst, ToMs(Over(average.Add(Ticks(ms)), Ticks(voicesMs)))); return worst; } void Rule() { // The budget: 90% of a 5.33 ms block, in EA's units (5.13 ms). Check(Budget(90.0) > 15.35e6 && Budget(90.0) < 15.37e6, "the budget is 15.36 M (EA's units)"); Check(ToMs(Budget(90.0)) > 5.12 && ToMs(Budget(90.0)) < 5.14, "the budget is 5.13 ms of a block"); // A MacBook's usual mix: ~0.02-0.1 ms of the mixer's own time and // ~0.5 ms of voices a block (the drag race): far under. Check(WorstOverMs(std::vector(600, 0.1), 0.6) < -4.0, "a usual mix is far under budget"); // One block held up 20 ms (measured: every SND9 voice expelled at // once, the mix 6-7 dB quieter until the next gear change). std::vector held(300, 0.1); held[150] = 20.0; Check(WorstOverMs(held, 0.6) > 1.0, "one block held 20 ms puts EA over budget"); // 14 ms is enough; 13 ms isn't (the three-block average). held[150] = 14.0; Check(WorstOverMs(held, 0.6) > 0.0, "one block held 14 ms puts EA over budget"); held[150] = 13.0; Check(WorstOverMs(held, 0.6) < 0.0, "one block held 13 ms doesn't"); // EA's average: three blocks, the oldest replaced. Average a; a.Add(3); a.Add(6); Check(a.Add(9) == 6.0 && a.Add(0) == 5.0 && a.Add(0) == 3.0 && a.Add(0) == 0.0, "the average is of the last three blocks"); // A budget of 100% or more is never checked. Check(Over(Ticks(50), Ticks(50), 100.0) == 0.0, "a budget of 100% isn't checked"); } void Policy() { static_assert(!Expels(nullptr)); Check(!Expels(nullptr), "no NFSMW_EA_CPU_BUDGET: nothing expelled for time"); Check(!Expels(""), "NFSMW_EA_CPU_BUDGET empty: nothing expelled"); Check(!Expels("0"), "NFSMW_EA_CPU_BUDGET=0: nothing expelled"); Check(!Expels("10"), "NFSMW_EA_CPU_BUDGET=10: nothing expelled"); Check(Expels("1"), "NFSMW_EA_CPU_BUDGET=1: EA's rule"); } // The generated functions: name -> body (the PPC_FUNC_IMPL block). std::map LoadFunctions(const std::filesystem::path& ppc, const std::vector& names) { std::map found; for (const auto& entry : std::filesystem::directory_iterator(ppc)) { const std::string file = entry.path().filename().string(); if (file.rfind("ppc_recomp.", 0) != 0 || entry.path().extension() != ".cpp") continue; std::ifstream in(entry.path(), std::ios::binary); std::stringstream ss; ss << in.rdbuf(); const std::string text = ss.str(); for (const std::string& name : names) { const std::string head = "PPC_FUNC_IMPL(__imp__" + name + ") {"; const size_t at = text.find(head); if (at == std::string::npos) continue; const size_t end = text.find("\n}\n", at); found[name] = text.substr(at, end == std::string::npos ? std::string::npos : end - at); } } return found; } // `body` holds these lines in this order (each a substring of a line, // later ones after earlier ones; other lines may come between). bool InOrder(const std::string& body, const std::vector& lines) { size_t at = 0; for (const std::string& l : lines) { at = body.find(l, at); if (at == std::string::npos) return false; at += l.size(); } return true; } std::string Name(uint32_t address) { char s[32]; snprintf(s, sizeof(s), "sub_%08X", address); return s; } std::string Call(uint32_t address) { char s[32]; snprintf(s, sizeof(s), "// bl 0x%08x", address); return s; } void GeneratedCode(const std::filesystem::path& ppc) { const std::vector names = { Name(kMixerThread), Name(kBudgetCheck), Name(kExpelOverBudget), Name(kPickVoice), Name(kExpelVoice), Name(kTimebase) }; auto f = LoadFunctions(ppc, names); const int before = s_failed; for (const std::string& n : names) Check(f.count(n) == 1, "ppc/ has " + n); if (s_failed != before) return; // The timebase: mftb and nothing else. Check(InOrder(f[Name(kTimebase)], { "// mftb r3", "// blr" }), "sub_825CFEB0 is mftb"); // The mixer thread runs the check on mixer + 68 each block, after the // command buffer and before the voices. Check(InOrder(f[Name(kMixerThread)], { "// bl 0x825cf588", "// addi r3,r31,68", Call(kBudgetCheck), "// bl 0x825d05d0" }), "the mixer thread runs the budget check on mixer + 68 between the commands and the voices"); // The check: the time since the last stamp (mftb), the three-block // average (x 1/3 at +12), then, only while the config's budget (+16) // is under 100, the load against it, and the over-budget step with // the difference. Check(InOrder(f[Name(kBudgetCheck)], { Call(kTimebase), "// lwz r11,4(r31)", "// lwz r10,8(r31)", "// fmuls f8,f9,f13", "// stfs f8,0(r30)", Call(kTimebase), "// lfs f12,16(r11)", "// lfs f0,512(r10)", "// bge cr6,0x82601644", "// lwz r11,100(r11)", "// lfs f3,48(r10)", "// fsubs f1,f0,f12", "// ble cr6,0x82601644", Call(kExpelOverBudget) }), "sub_82601528 averages mftb time and calls the over-budget step when over a budget under 100%"); // The over-budget step: pick, take its cost off, expel (voice, 0), // until it fits; the return addresses the probes and the log rely on. Check(InOrder(f[Name(kExpelOverBudget)], { "// fmr f31,f1", Call(kPickVoice), "ctx.lr = 0x826017C8;", "// li r4,0", "// lfs f13,48(r3)", Call(kExpelVoice), "ctx.lr = 0x826017E4;", "// bgt cr6,0x826017c0" }), "sub_82601788 picks a voice and expels it with (voice, 0) until it fits"); // The pick: the lowest priority (+36) under 100 only. Check(InOrder(f[Name(kPickVoice)], { "// lwz r7,100(r11)", "// lfs f0,36(r9)", "// lfs f0,512(r11)", "// blt cr6,0x82601774" }), "sub_826016B8 picks the lowest-priority voice, only under 100"); // Expelling: state 2, costs cleared, the expel command queued. Check(InOrder(f[Name(kExpelVoice)], { "// li r10,2", "// stb r10,64(r31)", "// stfs f0,48(r31)" }), "sub_825D04B8(voice, 0) marks the voice expelled"); } } int main(int argc, char** argv) { Rule(); Policy(); if (argc > 1) GeneratedCode(argv[1]); else { fprintf(stderr, "FAIL: usage: ea_cpu_budget_test \n"); s_failed++; } if (s_failed) { fprintf(stderr, "%d check(s) failed\n", s_failed); return 1; } printf("ea_cpu_budget_test: all checks passed\n"); return 0; }