v0.4.0: Direct3D 11/12 backends, in-game renderer selection, per-run logging
- ra3.dx: D3D11 and D3D12 presentation backends (runtime HLSL via d3dcompiler); 2D image blit and the GPU heightfield terrain raymarch, with the corner minimap/FPS overlays. Non-Windows builds link a null fallback. - ra3.display: preferred backend plus ordered fallback (Vulkan/D3D11/D3D12/SDL); menu gains a Renderer option and the CLI gains --dx11/--dx12/--sdl, which the render command now honours too. - vendor libenderlog (MIT): every run writes openra3.log next to the exe and archives the previous run's log; records at warn and above carry a call stack (native fallback, since libc++ has no <stacktrace>). - Windows crash reporter writes openra3_crash.log (faulting module + backtrace); D3D/DXGI diagnostics are routed through the logger.
This commit is contained in:
+160
-12
@@ -1,7 +1,104 @@
|
||||
#if defined(_WIN32)
|
||||
#define NOMINMAX
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
import std;
|
||||
import ra3;
|
||||
import ender.log;
|
||||
|
||||
namespace {
|
||||
#if defined(_WIN32)
|
||||
/** Path of the crash report written next to the executable. */
|
||||
[[nodiscard]] auto crash_log_path() -> const std::filesystem::path & {
|
||||
static const auto path = [] {
|
||||
std::wstring buffer(32768U, L'\0');
|
||||
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
|
||||
buffer.resize(length);
|
||||
return std::filesystem::path{buffer}.parent_path() / L"openra3_crash.log";
|
||||
}();
|
||||
return path;
|
||||
}
|
||||
|
||||
auto crash_write(std::string_view text) -> void {
|
||||
const HANDLE file = CreateFileW(crash_log_path().wstring().c_str(), FILE_APPEND_DATA, FILE_SHARE_READ, nullptr, OPEN_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) return;
|
||||
DWORD written = 0;
|
||||
WriteFile(file, text.data(), static_cast<DWORD>(text.size()), &written, nullptr);
|
||||
CloseHandle(file);
|
||||
}
|
||||
|
||||
/** `address` rendered as `module.dll+0xRVA`, or `0x...` when unmapped. */
|
||||
auto crash_describe(std::uintptr_t address, char *out, std::size_t size) -> void {
|
||||
HMODULE module = nullptr;
|
||||
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(address), &module)) {
|
||||
wchar_t wide[260] = L"?";
|
||||
GetModuleFileNameW(module, wide, 260U);
|
||||
char narrow[260] = "?";
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide, -1, narrow, sizeof(narrow), nullptr, nullptr);
|
||||
const char *base = std::strrchr(narrow, '\\');
|
||||
const auto rva = address - reinterpret_cast<std::uintptr_t>(module);
|
||||
std::snprintf(out, size, "%s+0x%llX", base != nullptr ? base + 1 : narrow, static_cast<unsigned long long>(rva));
|
||||
return;
|
||||
}
|
||||
std::snprintf(out, size, "0x%llX", static_cast<unsigned long long>(address));
|
||||
}
|
||||
|
||||
LONG WINAPI openra3_crash_handler(EXCEPTION_POINTERS *info) {
|
||||
char line[512];
|
||||
const auto *record = info->ExceptionRecord;
|
||||
std::snprintf(line, sizeof(line), "\n=== OpenRA3 crash ===\nmodule=%s\nexception=0x%08lX address=0x%p\n",
|
||||
"openra3", static_cast<unsigned long>(record->ExceptionCode), record->ExceptionAddress);
|
||||
crash_write(line);
|
||||
void *frames[48] = {};
|
||||
const USHORT count = CaptureStackBackTrace(0U, 48U, frames, nullptr);
|
||||
for (USHORT i = 0; i < count; ++i) {
|
||||
char described[320] = {};
|
||||
crash_describe(reinterpret_cast<std::uintptr_t>(frames[i]), described, sizeof(described));
|
||||
std::snprintf(line, sizeof(line), " #%02u %s\n", static_cast<unsigned>(i), described);
|
||||
crash_write(line);
|
||||
}
|
||||
crash_write("=== end ===\n");
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
auto install_crash_handler() -> void {
|
||||
SetUnhandledExceptionFilter(&openra3_crash_handler);
|
||||
std::set_terminate([] {
|
||||
crash_write("\n=== OpenRA3 std::terminate ===\n");
|
||||
if (const auto exception = std::current_exception()) {
|
||||
try {
|
||||
std::rethrow_exception(exception);
|
||||
} catch (const std::exception &error) {
|
||||
char line[512];
|
||||
std::snprintf(line, sizeof(line), "what(): %s\n", error.what());
|
||||
crash_write(line);
|
||||
} catch (...) {
|
||||
crash_write("what(): (non-std exception)\n");
|
||||
}
|
||||
}
|
||||
std::abort();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Configure the process logger and start a fresh per-run log file.
|
||||
*
|
||||
* `ender::log::file_sink` archives the previous `openra3.log` to a
|
||||
* timestamped file on open, so every run gets its own log and the previous
|
||||
* run's log is preserved.
|
||||
*/
|
||||
auto setup_logging(const std::filesystem::path &exe_dir) -> void {
|
||||
namespace log = ender::log;
|
||||
log::configure({.minimum = log::level::info, .stacktrace_from = log::level::warn});
|
||||
log::add_file_sink(exe_dir / "openra3.log", {.max_file_size = 4U * 1024U * 1024U, .max_archives = 10U});
|
||||
log::info("OpenRA3 started");
|
||||
}
|
||||
|
||||
auto print_usage() -> void {
|
||||
std::puts("OpenRA3 - Red Alert 3 reconstruction");
|
||||
std::puts("usage:");
|
||||
@@ -15,6 +112,7 @@ namespace {
|
||||
std::puts(" [--3d] [--cam-pitch DEG] [--cam-yaw DEG] [--cam-x U] [--cam-y U]");
|
||||
std::puts(" [--cam-height U] [--fov DEG] [--width W] [--height H]");
|
||||
std::puts(" [--fullscreen] [--fps N] (0 = vsync)");
|
||||
std::puts(" [--vulkan] [--dx11] [--dx12] [--sdl] (preferred display backend)");
|
||||
std::puts("assets: read from <exe_dir>/assets (extracted at build time by the openra3_assets target)");
|
||||
}
|
||||
|
||||
@@ -29,6 +127,14 @@ namespace {
|
||||
return std::find(args.begin(), args.end(), name) != args.end();
|
||||
}
|
||||
|
||||
/** Preferred display backend from the command line (default: Vulkan). */
|
||||
[[nodiscard]] auto display_backend_from_args(const std::vector<std::string> &args) -> ra3::display::backend {
|
||||
if (has_flag(args, "--dx11") || has_flag(args, "--d3d11")) return ra3::display::backend::d3d11;
|
||||
if (has_flag(args, "--dx12") || has_flag(args, "--d3d12")) return ra3::display::backend::d3d12;
|
||||
if (has_flag(args, "--sdl")) return ra3::display::backend::sdl;
|
||||
return ra3::display::backend::vulkan;
|
||||
}
|
||||
|
||||
/** Directory the executable lives in. */
|
||||
auto executable_dir(const char *argv0) -> std::filesystem::path {
|
||||
std::error_code ec;
|
||||
@@ -328,7 +434,14 @@ namespace {
|
||||
options.height = static_cast<int>(height);
|
||||
options.fullscreen = has_flag(args, "--fullscreen");
|
||||
options.fps_limit = option_value(args, "--fps") ? std::stoi(*option_value(args, "--fps")) : 0;
|
||||
gpu_shown = ra3::display::run_terrain(options, gpu, camera);
|
||||
ra3::render::image mini;
|
||||
if (has_flag(args, "--minimap")) {
|
||||
terrain::render_options mini_opts;
|
||||
mini_opts.scale = 1U;
|
||||
mini_opts.pitch = 1.0F;
|
||||
mini = render::downscale(terrain::render(terrain, textures, mini_opts), 220U);
|
||||
}
|
||||
gpu_shown = ra3::display::run_terrain(options, gpu, camera, mini, display_backend_from_args(args));
|
||||
if (gpu_shown) return 0;
|
||||
std::printf("GPU terrain unavailable; showing the top-down map instead\n");
|
||||
}
|
||||
@@ -409,7 +522,7 @@ namespace {
|
||||
viewer.height = static_cast<int>(composed.height());
|
||||
viewer.fullscreen = has_flag(args, "--fullscreen");
|
||||
viewer.fps_limit = option_value(args, "--fps") ? std::stoi(*option_value(args, "--fps")) : 0;
|
||||
shown = ra3::display::run_image(viewer, composed, camera, has_flag(args, "--vulkan"));
|
||||
shown = ra3::display::run_image(viewer, composed, camera, display_backend_from_args(args));
|
||||
}
|
||||
if (!shown) {
|
||||
const auto path = out.value_or("openra3_view.bmp");
|
||||
@@ -448,6 +561,7 @@ namespace {
|
||||
bool thumbnail = false;
|
||||
bool fullscreen = false;
|
||||
int fps_index = 0; ///< 0 = vsync, 1..3 = 30/60/120, 4 = uncapped.
|
||||
int renderer = 0; ///< Preferred display backend (see renderer_backends).
|
||||
std::string out;
|
||||
};
|
||||
|
||||
@@ -455,6 +569,19 @@ namespace {
|
||||
inline constexpr std::array<int, 5> fps_caps{0, 30, 60, 120, -1};
|
||||
inline constexpr std::array<std::string_view, 5> fps_names{"vsync", "30", "60", "120", "uncapped"};
|
||||
|
||||
/**
|
||||
* Preferred display backends offered by the menu. Each entry is tried first,
|
||||
* then the others in turn, so an unavailable backend degrades gracefully.
|
||||
* Direct3D is Windows-only; on other hosts those entries fall through.
|
||||
*/
|
||||
inline constexpr std::array<ra3::display::backend, 4> renderer_backends{ra3::display::backend::vulkan, ra3::display::backend::d3d11,
|
||||
ra3::display::backend::d3d12, ra3::display::backend::sdl};
|
||||
inline constexpr std::array<std::string_view, 4> renderer_names{"Vulkan", "Direct3D 11", "Direct3D 12", "SDL (software)"};
|
||||
|
||||
[[nodiscard]] inline auto renderer_backend(int index) -> ra3::display::backend {
|
||||
return renderer_backends[static_cast<std::size_t>(std::clamp(index, 0, static_cast<int>(renderer_backends.size()) - 1))];
|
||||
}
|
||||
|
||||
inline constexpr int menu_width = 1280;
|
||||
inline constexpr int menu_height = 720;
|
||||
|
||||
@@ -479,7 +606,7 @@ namespace {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto menu_field_count() -> int { return 19; }
|
||||
[[nodiscard]] auto menu_field_count() -> int { return 20; }
|
||||
|
||||
[[nodiscard]] auto menu_field_label(int field) -> std::string_view {
|
||||
switch (field) {
|
||||
@@ -501,6 +628,7 @@ namespace {
|
||||
case 15: return "Fullscreen";
|
||||
case 16: return "Frame rate";
|
||||
case 17: return "Play";
|
||||
case 18: return "Renderer";
|
||||
default: return "Quit";
|
||||
}
|
||||
}
|
||||
@@ -533,6 +661,7 @@ namespace {
|
||||
case 15: return s.fullscreen ? "yes" : "no";
|
||||
case 16: return std::string{fps_names[static_cast<std::size_t>(std::clamp(s.fps_index, 0, 4))]} + " fps";
|
||||
case 17: return "start";
|
||||
case 18: return std::string{renderer_names[static_cast<std::size_t>(std::clamp(s.renderer, 0, 3))]};
|
||||
default: return "exit";
|
||||
}
|
||||
}
|
||||
@@ -555,6 +684,7 @@ namespace {
|
||||
case 13: s.thumbnail = !s.thumbnail; break;
|
||||
case 15: s.fullscreen = !s.fullscreen; break;
|
||||
case 16: s.fps_index = (s.fps_index + delta + 5) % 5; break;
|
||||
case 18: s.renderer = (s.renderer + delta + 4) % 4; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -589,7 +719,7 @@ namespace {
|
||||
L.rows = std::max(1, (L.list_h - L.S(46)) / L.row_h);
|
||||
L.set_x = L.S(650);
|
||||
L.set_w = static_cast<int>(width) - L.set_x - L.S(24);
|
||||
L.field_row = std::max(1, L.S(27));
|
||||
L.field_row = std::max(1, L.S(26));
|
||||
return L;
|
||||
}
|
||||
|
||||
@@ -724,9 +854,9 @@ namespace {
|
||||
st.field = field;
|
||||
if (field == 17) {
|
||||
st.action = menu_action::play;
|
||||
} else if (field == 18) {
|
||||
} else if (field == 19) {
|
||||
st.action = menu_action::quit;
|
||||
} else if (field == 0 || field == 13 || field == 15 || field == 16) {
|
||||
} else if (field == 0 || field == 13 || field == 15 || field == 16 || field == 18) {
|
||||
menu_adjust(field, st.settings, 1);
|
||||
st.dirty = true;
|
||||
} else if (field == 14) {
|
||||
@@ -783,9 +913,9 @@ namespace {
|
||||
case ui_key::confirm:
|
||||
if (st.pane == 0 || st.field == 17) {
|
||||
st.action = menu_action::play;
|
||||
} else if (st.field == 18) {
|
||||
} else if (st.field == 19) {
|
||||
st.action = menu_action::quit;
|
||||
} else if (st.field == 0 || st.field == 13 || st.field == 15 || st.field == 16) {
|
||||
} else if (st.field == 0 || st.field == 13 || st.field == 15 || st.field == 16 || st.field == 18) {
|
||||
menu_adjust(st.field, st.settings, 1);
|
||||
st.dirty = true;
|
||||
}
|
||||
@@ -951,6 +1081,10 @@ namespace {
|
||||
if (has_flag(args, "--thumbnail")) st.settings.thumbnail = true;
|
||||
if (has_flag(args, "--fullscreen")) st.settings.fullscreen = true;
|
||||
if (has_flag(args, "--3d")) st.settings.mode = 0;
|
||||
if (has_flag(args, "--vulkan")) st.settings.renderer = 0;
|
||||
if (has_flag(args, "--dx11") || has_flag(args, "--d3d11")) st.settings.renderer = 1;
|
||||
if (has_flag(args, "--dx12") || has_flag(args, "--d3d12")) st.settings.renderer = 2;
|
||||
if (has_flag(args, "--sdl")) st.settings.renderer = 3;
|
||||
if (const auto value = option_value(args, "--width")) st.settings.width = std::stoi(*value);
|
||||
if (const auto value = option_value(args, "--height")) st.settings.height = std::stoi(*value);
|
||||
if (const auto value = option_value(args, "--cam-pitch")) st.settings.cam_pitch = std::stof(*value);
|
||||
@@ -978,7 +1112,9 @@ namespace {
|
||||
options.height = menu_height;
|
||||
options.fullscreen = st.settings.fullscreen;
|
||||
options.fps_limit = fps_caps[static_cast<std::size_t>(std::clamp(st.settings.fps_index, 0, 4))];
|
||||
auto display = ra3::display::create(options, true);
|
||||
int applied_renderer = st.settings.renderer;
|
||||
auto display = ra3::display::create(options, renderer_backend(applied_renderer));
|
||||
if (display) ender::log::info(std::format("menu display backend: {}", display->name()));
|
||||
if (!display) {
|
||||
if (!windowed) return console_menu(assets, maps, names, st.settings);
|
||||
return 0;
|
||||
@@ -1006,9 +1142,16 @@ namespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Apply fullscreen / frame-rate changes immediately (re-init the window).
|
||||
// Apply renderer / fullscreen / frame-rate changes (recreate the window).
|
||||
const int want_fps = fps_caps[static_cast<std::size_t>(std::clamp(st.settings.fps_index, 0, 4))];
|
||||
if (st.settings.fullscreen != options.fullscreen || want_fps != options.fps_limit) {
|
||||
if (st.settings.renderer != applied_renderer) {
|
||||
display->shutdown();
|
||||
applied_renderer = st.settings.renderer;
|
||||
options.fullscreen = st.settings.fullscreen;
|
||||
options.fps_limit = want_fps;
|
||||
display = ra3::display::create(options, renderer_backend(applied_renderer));
|
||||
if (!display) return console_menu(assets, maps, names, st.settings);
|
||||
} else if (st.settings.fullscreen != options.fullscreen || want_fps != options.fps_limit) {
|
||||
display->shutdown();
|
||||
options.fullscreen = st.settings.fullscreen;
|
||||
options.fps_limit = want_fps;
|
||||
@@ -1071,8 +1214,13 @@ namespace {
|
||||
}
|
||||
|
||||
auto main(int argc, char **argv) -> int {
|
||||
#if defined(_WIN32)
|
||||
install_crash_handler();
|
||||
#endif
|
||||
const std::vector<std::string> args{argv + 1, argv + argc};
|
||||
const auto assets = executable_dir(argc > 0 ? argv[0] : ".") / "assets";
|
||||
const auto exe_dir = executable_dir(argc > 0 ? argv[0] : ".");
|
||||
const auto assets = exe_dir / "assets";
|
||||
setup_logging(exe_dir);
|
||||
|
||||
if (args.empty()) return command_menu({}, assets);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user