diff --git a/android/app/src/main/java/com/freeriders/recompiled/GraphicsTestActivity.java b/android/app/src/main/java/com/freeriders/recompiled/GraphicsTestActivity.java index e45ae94..02ede73 100644 --- a/android/app/src/main/java/com/freeriders/recompiled/GraphicsTestActivity.java +++ b/android/app/src/main/java/com/freeriders/recompiled/GraphicsTestActivity.java @@ -23,6 +23,8 @@ public class GraphicsTestActivity extends Activity { static final String EXTRA = "com.freeriders.recompiled.GRAPHICS_SELFTEST"; static final String CLIP_DISTANCE = "com.freeriders.recompiled.GRAPHICS_SELFTEST_CLIP_DISTANCE"; static final String SETTINGS = "com.freeriders.recompiled.GRAPHICS_SELFTEST_SETTINGS"; + // Scenarios 9 and on prepare the pipelines only (DRAW_SCENARIOS). + static final String PREPARE = "SFR_SELFTEST_DRAWS=0;SFR_PIPELINE_PREWARM_TRACE=1;SFR_PIPELINE_PREWARM_TIMEOUT=60"; // Scenarios 3 and on: how the self test draws (graphics_selftest.cpp). // An old Mali's GPU hung at the first draw with the game's vertex shaders // and twenty vertex attributes (Issue #169); the last scenario declares @@ -34,6 +36,17 @@ public class GraphicsTestActivity extends Activity { "SFR_SELFTEST_VS=trivial;SFR_SELFTEST_ORDER=class", "SFR_SELFTEST_ORDER=class;SFR_RENDER_SCALE=225;SFR_SELFTEST_BATCH=64", "SFR_SELFTEST_ORDER=class;SFR_TEST_VERTEX_ATTRIBUTES=32;SFR_SELFTEST_LIMIT=5", + // Scenarios 9 and on: only the pipeline preparation, each recipe's + // time logged (to logcat too), and a recipe that takes over a minute + // ends the scenario, so the next one runs. An Xclipse 530 never + // finished its first pipeline (Issue #224); these tell which of the + // shader rewrites lets it, one at a time. + PREPARE, + PREPARE + ";SFR_TEST_DISPATCH_LOOP=1", + PREPARE + ";SFR_TEST_NO_DESCRIPTOR_INDEXING=1", + PREPARE + ";SFR_TEST_NO_DEVICE_ADDRESS=1", + PREPARE + ";SFR_TEST_NO_CLIP_DISTANCE=1", + PREPARE + ";SFR_TEST_DISPATCH_LOOP=0", }; private boolean started; diff --git a/docs/vulkan.md b/docs/vulkan.md index e25022c..c424218 100644 --- a/docs/vulkan.md +++ b/docs/vulkan.md @@ -170,6 +170,21 @@ M1(Metal System Trace,比賽中 8 秒):頂點 144.6 → 6.4 ms/幀,GPU `NATIVE_DISPATCH_LOOP removed=0|1`;`SFR_TEST_DISPATCH_LOOP=0` 在 Apple 上保留迴圈,`=1` 在任何 Vulkan 裝置上拿掉(驗證層用)。 +Samsung Xclipse(vendor 0x144d,Exynos 手機上以 AMD RDNA 為基礎的 GPU)也一律拿掉(Issue #224)。 +Galaxy A55(Xclipse 530,驅動 24.0.539/545)編第一條管線(頂點著色器 `eaf61b182ad92401`)時, +驅動的編譯器(logcat `GSC`)花了 209 秒、常駐記憶體漲到 550 MB 以上,最後在組譯器 +`SCGenerateLongBranch` 斷言失敗;玩家看到「PREPARING PIPELINES 0 OF 455」直到 Android 因記憶體 +不足把程式收掉。拿掉迴圈後 455 條 14 秒編完(最慢的一條 253 ms)。描述符索引、device address、 +clip distance 的測試開關都沒用。Firebase Test Lab 的 a55xzh(Galaxy A55)可以重現。 + +準備管線時,一條配方(recipe)超過 5 秒就記 `PIPELINE_PREWARM slow recipe= of= +step=shaders|pipeline vs= ps= seconds= resident_mb=`(之後 10、20、40 秒…再記), +每 10 秒記一次 `PIPELINE_PREWARM progress=`,完成時記最慢的一條;Android 上也寫進 logcat。 +`SFR_PIPELINE_PREWARM_TRACE=1` 記每條的時間,`SFR_PIPELINE_PREWARM_TIMEOUT=` 讓一條超過 s 秒時 +結束程式(自我測試用,好讓下一個情境能跑)。自我測試的情境 9–14 只準備管線並開著這兩個: +9 照裝置、10 拿掉分派迴圈、11 小型材質組、12 不用 device address、13 不用 clip distance、14 保留 +分派迴圈。 + ### macOS:Retina 與縮放顯示器上的 present v0.10.14 以前,macOS 的遊戲視窗不是高 DPI:SDL 的 Metal layer 的 contents scale 是 1,1280x720 diff --git a/src/native_graphics.cpp b/src/native_graphics.cpp index c0d2638..6205a7c 100644 --- a/src/native_graphics.cpp +++ b/src/native_graphics.cpp @@ -495,13 +495,20 @@ void NativeGraphics::initialize() { // Apple's Metal compiler (MoltenVK, on macOS and iOS) spills kilobytes a // thread on XenosRecomp's dispatch loops, most on the skinned vertex // shaders: there the cases run in order behind tests of their own - // instead (spirv_dispatch_loop.h). SFR_TEST_DISPATCH_LOOP=0 keeps the - // loops on Apple, =1 takes them out on any Vulkan device. + // instead (spirv_dispatch_loop.h). Samsung's Xclipse driver (vendor + // 0x144d, AMD RDNA based, Exynos phones) spends minutes and hundreds of + // MB on the first pipeline with such a loop, then asserts in its + // assembler (SCGenerateLongBranch); the game sat at "0 of 455" until + // Android killed it for memory (Issue #224, Galaxy A55, driver 24.0.5xx). + // Without the loops it prepared all 455 in 14 s. SFR_TEST_DISPATCH_LOOP=0 + // keeps the loops on these, =1 takes them out on any Vulkan device. bool without_dispatch_loop = false; if (backend == GraphicsBackend::vulkan) { #if defined(__APPLE__) without_dispatch_loop = true; #endif + constexpr uint32_t samsung_vendor = 0x144d; + if (uint32_t(render_device->getDescription().vendor) == samsung_vendor) without_dispatch_loop = true; const char* forced = std::getenv("SFR_TEST_DISPATCH_LOOP"); if (forced && (std::strcmp(forced, "1") == 0 || std::strcmp(forced, "0") == 0)) without_dispatch_loop = forced[0] == '1'; diff --git a/src/native_renderer.cpp b/src/native_renderer.cpp index 2fd7fed..c02e4d9 100644 --- a/src/native_renderer.cpp +++ b/src/native_renderer.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #include #include @@ -45,9 +47,46 @@ #include #include #endif +#ifdef __ANDROID__ +#include +#endif +#if defined(__linux__) || defined(__ANDROID__) +#include +#endif namespace sfr { namespace { +// A pipeline preparation line for game.log, and on Android for logcat too +// (the graphics self test on Firebase Test Lab runs several scenarios, and +// each start rewrites game.log). +void prewarm_log(const std::string& text) { + std::cerr << text << '\n'; +#ifdef __ANDROID__ + __android_log_print(ANDROID_LOG_INFO, "FreeRiders", "%s", text.c_str()); +#endif +} + +// The process's resident memory in MiB, or -1 where it is not read: a driver +// compiling a shader without end may also take memory without end (Issue #224 +// ended with Android's low-memory reason). +long resident_mib() { +#if defined(__linux__) || defined(__ANDROID__) + long pages = -1, resident = -1; + if (FILE* file = std::fopen("/proc/self/statm", "r")) { + if (std::fscanf(file, "%ld %ld", &pages, &resident) != 2) resident = -1; + std::fclose(file); + } + return resident < 0 ? -1 : long((int64_t(resident) * sysconf(_SC_PAGESIZE)) >> 20); +#else + return -1; +#endif +} + +std::string hex64(uint64_t value) { + char text[17]; + std::snprintf(text, sizeof(text), "%016llx", static_cast(value)); + return text; +} // Same binding model as Marathon Recompiled's D3D12 backend for XenosRecomp // shaders: one bindless texture set bound as spaces 0..2 (2D, 2D array, cube), // samplers in space 3, the survey UAV in space 4 (D3D12 only), and b0..b2 in space 4. @@ -884,10 +923,37 @@ void NativeRenderer::prepare_pipelines() { // the preparation screen's own blit resources. if (!impl_->presentation.preparation_progress(0, total)) throw RuntimeStop("window-closed", 0, "pipeline preparation cancelled"); + // Which recipe the worker is on and since when, so that a driver that + // takes long over one names it in game.log (Issue #224: an Xclipse 530 + // stayed at 0 of 455 with nothing logged). The screen's loop polls these. + // SFR_PIPELINE_PREWARM_TRACE=1 logs every recipe's time; + // SFR_PIPELINE_PREWARM_TIMEOUT= ends the process when one recipe takes + // longer (for the self test, so that its next scenario still runs). + std::atomic current{0}; + std::atomic current_since{0}; // steady-clock ns, 0 between recipes + std::atomic current_vs{0}, current_ps{0}; + std::atomic current_step{0}; // 1 shaders, 2 pipeline + const auto clock_ns = [] { + return int64_t(std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + }; + const char* trace_setting = std::getenv("SFR_PIPELINE_PREWARM_TRACE"); + const bool trace = trace_setting && *trace_setting == '1'; + const char* timeout_setting = std::getenv("SFR_PIPELINE_PREWARM_TIMEOUT"); + const double timeout_seconds = timeout_setting ? std::atof(timeout_setting) : 0.0; + double slowest_ms = 0; + size_t slowest_index = 0; std::jthread worker([&] { try { + size_t index = 0; for (const auto& [key, recipe] : impl_->recipes) { if (cancel.load(std::memory_order_relaxed)) break; + current_vs.store(recipe.vertex.hash, std::memory_order_relaxed); + current_ps.store(recipe.pixel.hash, std::memory_order_relaxed); + current_step.store(1, std::memory_order_relaxed); + current.store(index, std::memory_order_relaxed); + const int64_t recipe_started = clock_ns(); + current_since.store(recipe_started, std::memory_order_release); // Only known packaged stages: never launch a translator from // untrusted manifest contents. Pack corruption remains fatal. const auto* vs = packed_pipeline_shader(ShaderStage::vertex, recipe.vertex.hash, recipe.vertex.size); @@ -925,6 +991,7 @@ void NativeRenderer::prepare_pipelines() { const plume::RenderShader* depth_flip = nullptr; if (flip && !(depth_flip = impl_->presentation.depth_flip_shader(state.topology))) throw std::runtime_error("no depth flip geometry shader for the topology"); + current_step.store(2, std::memory_order_relaxed); auto pipeline = create_draw_pipeline(impl_->graphics.device(), impl_->layout.get(), state, impl_->graphics.backend(), impl_->graphics.depth_format(), depth_flip); @@ -936,17 +1003,65 @@ void NativeRenderer::prepare_pipelines() { std::cerr << "PIPELINE_PREWARM failed=1 recipe=" << completed.load() << '\n'; } } + current_since.store(0, std::memory_order_release); + const double ms = double(clock_ns() - recipe_started) / 1e6; + if (ms > slowest_ms) { + slowest_ms = ms; + slowest_index = index; + } + if (trace) + prewarm_log("PIPELINE_PREWARM recipe=" + std::to_string(index) + " vs=" + hex64(recipe.vertex.hash) + + " ps=" + hex64(recipe.pixel.hash) + " ms=" + std::to_string(ms)); + ++index; completed.fetch_add(1, std::memory_order_release); } } catch (...) { failure = std::current_exception(); } + current_since.store(0, std::memory_order_release); done.store(true, std::memory_order_release); }); try { + // A recipe that has taken 5 s is logged, and again at 10, 20, 40 s...; + // the count every 10 s. + size_t reported_index = SIZE_MAX; + double next_report = 5.0; + auto next_progress = started + std::chrono::seconds(10); while (!done.load(std::memory_order_acquire)) { if (!impl_->presentation.preparation_progress(completed.load(std::memory_order_acquire), total, cancel.load(std::memory_order_relaxed))) cancel.store(true, std::memory_order_relaxed); std::this_thread::sleep_for(std::chrono::milliseconds(33)); + const auto now = std::chrono::steady_clock::now(); + if (now >= next_progress) { + next_progress = now + std::chrono::seconds(10); + prewarm_log("PIPELINE_PREWARM progress=" + std::to_string(completed.load(std::memory_order_acquire)) + + " total=" + std::to_string(total) + " seconds=" + + std::to_string(int(std::chrono::duration(now - started).count())) + + " resident_mb=" + std::to_string(resident_mib())); + } + const int64_t since = current_since.load(std::memory_order_acquire); + if (!since) continue; + const size_t index = current.load(std::memory_order_relaxed); + if (index != reported_index) { + reported_index = index; + next_report = 5.0; + } + const double seconds = double(clock_ns() - since) / 1e9; + const bool timed_out = timeout_seconds > 0 && seconds > timeout_seconds; + if (seconds < next_report && !timed_out) continue; + while (next_report <= seconds) next_report *= 2; + prewarm_log(std::string("PIPELINE_PREWARM slow recipe=") + std::to_string(index) + " of=" + + std::to_string(total) + " step=" + + (current_step.load(std::memory_order_relaxed) == 2 ? "pipeline" : "shaders") + + " vs=" + hex64(current_vs.load(std::memory_order_relaxed)) + + " ps=" + hex64(current_ps.load(std::memory_order_relaxed)) + + " seconds=" + std::to_string(int(seconds)) + " resident_mb=" + std::to_string(resident_mib())); + if (timed_out) { + prewarm_log("PIPELINE_PREWARM timeout=" + std::to_string(int(timeout_seconds)) + " recipe=" + + std::to_string(index)); + std::cerr.flush(); + std::fflush(stderr); + std::_Exit(4); + } } } catch (...) { cancel.store(true, std::memory_order_relaxed); @@ -961,8 +1076,11 @@ void NativeRenderer::prepare_pipelines() { throw RuntimeStop("window-closed", 0, "pipeline preparation cancelled"); impl_->presentation.finish_preparation(); const double ms = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); - std::cerr << "PIPELINE_PREWARM complete=1 total=" << total << " compiled=" << compiled - << " skipped=" << skipped << " depth_flip=" << flip << " milliseconds=" << ms << '\n'; + prewarm_log("PIPELINE_PREWARM complete=1 total=" + std::to_string(total) + " compiled=" + + std::to_string(compiled) + " skipped=" + std::to_string(skipped) + " depth_flip=" + + std::to_string(flip) + " milliseconds=" + std::to_string(ms) + " slowest_ms=" + + std::to_string(slowest_ms) + " slowest_recipe=" + std::to_string(slowest_index) + + " resident_mb=" + std::to_string(resident_mib())); } std::vector NativeRenderer::prepared_draws() { diff --git a/src/test_options.h b/src/test_options.h index 7a0d2ec..297dc62 100644 --- a/src/test_options.h +++ b/src/test_options.h @@ -80,10 +80,12 @@ inline constexpr TestOption table[] = { // ---- graphics {"SFR_TEST_DISPATCH_LOOP", TestGroup::graphics, test_platform::all, {"Rewrite shader dispatch loops", "改寫著色器分派迴圈"}, - {"For testing (Vulkan): runs the shaders' dispatch loops as plain steps, as Macs and iPhones always do. " - "Try On if races are very slow on an older GPU.", - "測試用(Vulkan):把著色器的分派迴圈改為依序執行,如同 Mac 與 iPhone 的做法。若較舊的 GPU 在比賽中非常慢,可試著開啟。"}, - on_off, "#134"}, + {"For testing (Vulkan): runs the shaders' dispatch loops as plain steps, as Macs, iPhones and Samsung " + "Xclipse GPUs always do. Try On if races are very slow on an older GPU, or if preparing pipelines does not " + "advance.", + "測試用(Vulkan):把著色器的分派迴圈改為依序執行,如同 Mac、iPhone 與 Samsung Xclipse GPU 的做法。" + "若較舊的 GPU 在比賽中非常慢,或準備管線一直沒有進度,可試著開啟。"}, + on_off, "#134, #224"}, {"SFR_TEST_NO_DESCRIPTOR_INDEXING", TestGroup::graphics, test_platform::all, {"Small texture sets", "小型材質組"}, {"For testing (Vulkan): each draw binds a small set of textures of its own instead of one large array, "