Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down
15 changes: 15 additions & 0 deletions docs/vulkan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<n> of=<total>
step=shaders|pipeline vs=<hash> ps=<hash> seconds=<s> resident_mb=<MB>`(之後 10、20、40 秒…再記),
每 10 秒記一次 `PIPELINE_PREWARM progress=`,完成時記最慢的一條;Android 上也寫進 logcat。
`SFR_PIPELINE_PREWARM_TRACE=1` 記每條的時間,`SFR_PIPELINE_PREWARM_TIMEOUT=<s>` 讓一條超過 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
Expand Down
11 changes: 9 additions & 2 deletions src/native_graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
122 changes: 120 additions & 2 deletions src/native_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
#include <atomic>
#include <bitset>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
Expand All @@ -45,9 +47,46 @@
#include <wrl/client.h>
#include <dxcapi.h>
#endif
#ifdef __ANDROID__
#include <android/log.h>
#endif
#if defined(__linux__) || defined(__ANDROID__)
#include <unistd.h>
#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<unsigned long long>(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.
Expand Down Expand Up @@ -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=<s> ends the process when one recipe takes
// longer (for the self test, so that its next scenario still runs).
std::atomic<size_t> current{0};
std::atomic<int64_t> current_since{0}; // steady-clock ns, 0 between recipes
std::atomic<uint64_t> current_vs{0}, current_ps{0};
std::atomic<int> current_step{0}; // 1 shaders, 2 pipeline
const auto clock_ns = [] {
return int64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
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);
Expand Down Expand Up @@ -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);
Expand All @@ -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<double>(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);
Expand All @@ -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<double, std::milli>(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<NativeDraw> NativeRenderer::prepared_draws() {
Expand Down
10 changes: 6 additions & 4 deletions src/test_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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, "
Expand Down
Loading