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:
EnderTheCoder
2026-09-28 13:03:37 +08:00
parent 59261868e1
commit 3caf18409b
16 changed files with 2815 additions and 70 deletions
+9
View File
@@ -499,6 +499,15 @@ export namespace ra3::client {
this->shutdown();
return true;
}
/** GPU terrain viewer with a corner minimap overlay. */
[[nodiscard]] auto run_terrain(const display_options &options, const ra3::terrain::gpu_terrain &terrain, ra3::render::camera3d camera,
const image &minimap) -> bool {
if (!this->init(options)) return false;
(void)this->terrain_loop(terrain, camera, minimap);
this->shutdown();
return true;
}
};
/**
+104 -45
View File
@@ -8,14 +8,18 @@ import ra3.terrain;
import ra3.client;
import ra3.ui;
import ra3.vulkan;
import ra3.dx;
/**
* Backend selection for the presentation layer.
*
* The app talks only to `ra3::client::display`; this module picks the concrete
* backend (Vulkan when available, otherwise SDL) so no caller has to know which
* one is in use. The GPU terrain path is Vulkan-only and reports failure so the
* caller can fall back to the software renderer.
* backend so no caller has to know which one is in use. The available backends
* are Vulkan, Direct3D 11 and Direct3D 12 (both GPU terrain), and the SDL
* software blit fallback. The caller may name a preferred backend (the in-game
* menu exposes this); if it does not start, the others are tried in turn. The
* GPU terrain path is Vulkan/D3D-only and reports failure so the caller can fall
* back to the software renderer.
*/
export namespace ra3::display {
using ra3::client::display_options;
@@ -27,72 +31,127 @@ export namespace ra3::display {
using menu_frame = std::function<std::optional<image>(const ui_event &, ra3::core::uint32, ra3::core::uint32, bool &)>;
using camera_frame = std::function<image(const camera3d &, ra3::core::uint32, ra3::core::uint32)>;
/** The backend that actually initialized, or `none`. */
enum class backend { none, vulkan, sdl };
/** A concrete presentation backend, or `none`. */
enum class backend { none, vulkan, d3d11, d3d12, sdl };
/**
* Create and initialize the preferred display (Vulkan, then SDL), or return
* null when neither starts. The caller owns the display and may reuse it for
* a menu, a loading bar and a viewer in sequence.
*/
[[nodiscard]] inline auto create(const display_options &options, bool prefer_vulkan) -> std::unique_ptr<ra3::client::display> {
const auto try_vulkan = [&]() -> std::unique_ptr<ra3::client::display> {
auto d = std::make_unique<ra3::vulkan::vulkan_display>();
if (d->init(options)) return d;
return nullptr;
};
const auto try_sdl = [&]() -> std::unique_ptr<ra3::client::display> {
auto d = std::make_unique<ra3::ui::sdl_display>();
if (d->init(options)) return d;
return nullptr;
};
if (prefer_vulkan) {
if (auto d = try_vulkan()) return d;
return try_sdl();
[[nodiscard]] inline auto backend_name(backend which) -> std::string_view {
switch (which) {
case backend::vulkan: return "vulkan";
case backend::d3d11: return "d3d11";
case backend::d3d12: return "d3d12";
case backend::sdl: return "sdl";
default: return "none";
}
}
[[nodiscard]] inline auto make_backend(backend which) -> std::unique_ptr<ra3::client::display> {
switch (which) {
case backend::vulkan: return std::make_unique<ra3::vulkan::vulkan_display>();
case backend::d3d11: return std::make_unique<ra3::dx::d3d11_display>();
case backend::d3d12: return std::make_unique<ra3::dx::d3d12_display>();
case backend::sdl: return std::make_unique<ra3::ui::sdl_display>();
default: return nullptr;
}
}
/** The order in which backends are attempted for a preferred one. */
[[nodiscard]] inline auto backend_order(backend preferred) -> std::array<backend, 4> {
switch (preferred) {
case backend::d3d11: return {backend::d3d11, backend::d3d12, backend::vulkan, backend::sdl};
case backend::d3d12: return {backend::d3d12, backend::d3d11, backend::vulkan, backend::sdl};
case backend::sdl: return {backend::sdl, backend::d3d11, backend::d3d12, backend::vulkan};
case backend::vulkan:
default: return {backend::vulkan, backend::d3d11, backend::d3d12, backend::sdl};
}
if (auto d = try_sdl()) return d;
return try_vulkan();
}
/**
* Run `action` on the first display that initializes: Vulkan, then SDL.
* Create and initialize the preferred backend, falling back to the others,
* or return null when none starts. The caller owns the display and may reuse
* it for a menu, a loading bar and a viewer in sequence.
*/
[[nodiscard]] inline auto create(const display_options &options, backend preferred) -> std::unique_ptr<ra3::client::display> {
for (const auto which: backend_order(preferred)) {
auto candidate = make_backend(which);
if (candidate && candidate->init(options)) return candidate;
}
return nullptr;
}
/** Create and initialize the preferred display (Vulkan, then SDL). */
[[nodiscard]] inline auto create(const display_options &options, bool prefer_vulkan) -> std::unique_ptr<ra3::client::display> {
return create(options, prefer_vulkan ? backend::vulkan : backend::sdl);
}
/**
* Run `action` on the first backend that initializes.
*
* `action` must call one of the shared `display` loops; it returns false to
* mean "this backend did not start", so the next one is tried.
*/
template<typename Fn>
[[nodiscard]] auto with_display(bool prefer_vulkan, Fn &&action) -> bool {
if (prefer_vulkan) {
if (auto vk = std::make_unique<ra3::vulkan::vulkan_display>(); action(*vk)) return true;
}
if (auto sdl = std::make_unique<ra3::ui::sdl_display>(); action(*sdl)) return true;
if (!prefer_vulkan) {
if (auto vk = std::make_unique<ra3::vulkan::vulkan_display>(); action(*vk)) return true;
[[nodiscard]] auto with_display(backend preferred, Fn &&action) -> bool {
for (const auto which: backend_order(preferred)) {
auto candidate = make_backend(which);
if (candidate && action(*candidate)) return true;
}
return false;
}
/** Interactive menu on the first available backend. */
[[nodiscard]] inline auto run_menu(const display_options &options, const menu_frame &frame) -> bool {
return with_display(true, [&](ra3::client::display &d) { return d.run_menu(options, frame); });
/** Run `action` on the first display that initializes: Vulkan, then SDL. */
template<typename Fn>
[[nodiscard]] auto with_display(bool prefer_vulkan, Fn &&action) -> bool {
return with_display(prefer_vulkan ? backend::vulkan : backend::sdl, std::forward<Fn>(action));
}
/** Pan/zoom image viewer on the first available backend. */
/** Interactive menu on the preferred (or first available) backend. */
[[nodiscard]] inline auto run_menu(const display_options &options, const menu_frame &frame, backend preferred = backend::vulkan) -> bool {
return with_display(preferred, [&](ra3::client::display &d) { return d.run_menu(options, frame); });
}
/** Pan/zoom image viewer on the preferred (or first available) backend. */
[[nodiscard]] inline auto run_image(const display_options &options, const image &scene, view_camera camera, backend preferred) -> bool {
return with_display(preferred, [&](ra3::client::display &d) { return d.run_image(options, scene, camera); });
}
/** Pan/zoom image viewer on the preferred (or first available) backend. */
[[nodiscard]] inline auto run_image(const display_options &options, const image &scene, view_camera camera, bool prefer_vulkan) -> bool {
return with_display(prefer_vulkan, [&](ra3::client::display &d) { return d.run_image(options, scene, camera); });
return run_image(options, scene, camera, prefer_vulkan ? backend::vulkan : backend::sdl);
}
/** Software 3D camera viewer on the first available backend. */
/** Software 3D camera viewer on the preferred (or first available) backend. */
[[nodiscard]] inline auto run_camera(const display_options &options, const camera_frame &provider, camera3d camera, bool prefer_vulkan) -> bool {
return with_display(prefer_vulkan, [&](ra3::client::display &d) { return d.run_camera(options, provider, camera); });
return with_display(prefer_vulkan ? backend::vulkan : backend::sdl,
[&](ra3::client::display &d) { return d.run_camera(options, provider, camera); });
}
/**
* GPU terrain viewer. Vulkan only: returns false when Vulkan is unavailable
* so the caller can render offscreen instead.
* GPU terrain viewer on the preferred (or first available) backend. Returns
* false when no GPU backend that offers terrain starts, so the caller can
* render offscreen instead.
*/
[[nodiscard]] inline auto run_terrain(const display_options &options, const ra3::terrain::gpu_terrain &terrain, camera3d camera,
backend preferred) -> bool {
return with_display(preferred, [&](ra3::client::display &d) {
if (!d.supports_terrain()) return false;
return d.run_terrain(options, terrain, camera);
});
}
/** GPU terrain viewer with a corner minimap overlay. */
[[nodiscard]] inline auto run_terrain(const display_options &options, const ra3::terrain::gpu_terrain &terrain, camera3d camera,
const image &minimap, backend preferred) -> bool {
return with_display(preferred, [&](ra3::client::display &d) {
if (!d.supports_terrain()) return false;
return d.run_terrain(options, terrain, camera, minimap);
});
}
/**
* GPU terrain viewer. Vulkan/D3D only: returns false when the preferred GPU
* backend is unavailable so the caller can render offscreen instead.
*/
[[nodiscard]] inline auto run_terrain(const display_options &options, const ra3::terrain::gpu_terrain &terrain, camera3d camera) -> bool {
auto vk = std::make_unique<ra3::vulkan::vulkan_display>();
return vk->run_terrain(options, terrain, camera);
return run_terrain(options, terrain, camera, backend::vulkan);
}
}
+1392
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
export module ra3.dx;
import std;
export import ra3.core;
import ra3.render;
import ra3.client;
/**
* Fallback Direct3D backends used when the build has no Windows/D3D (for
* example the Linux development build). `init` fails so the caller can select
* another display; the class names match the real `ra3.dx` module so the
* backend factory in `ra3.display` compiles unchanged.
*/
export namespace ra3::dx {
class d3d11_display final : public ra3::client::display {
public:
[[nodiscard]] auto init(const ra3::client::display_options &) -> bool override { return false; }
[[nodiscard]] auto present(const ra3::render::image &, const ra3::render::view_rect &, bool) -> bool override { return false; }
[[nodiscard]] auto poll_event(ra3::render::ui_event &) -> bool override { return false; }
[[nodiscard]] auto window_size() const -> std::pair<int, int> override { return {0, 0}; }
[[nodiscard]] auto key_down(ra3::render::ui_key) const -> bool override { return false; }
auto shutdown() -> void override {}
[[nodiscard]] auto name() const -> std::string_view override { return "d3d11(null)"; }
};
class d3d12_display final : public ra3::client::display {
public:
[[nodiscard]] auto init(const ra3::client::display_options &) -> bool override { return false; }
[[nodiscard]] auto present(const ra3::render::image &, const ra3::render::view_rect &, bool) -> bool override { return false; }
[[nodiscard]] auto poll_event(ra3::render::ui_event &) -> bool override { return false; }
[[nodiscard]] auto window_size() const -> std::pair<int, int> override { return {0, 0}; }
[[nodiscard]] auto key_down(ra3::render::ui_key) const -> bool override { return false; }
auto shutdown() -> void override {}
[[nodiscard]] auto name() const -> std::string_view override { return "d3d12(null)"; }
};
}
+1
View File
@@ -18,4 +18,5 @@ export import ra3.render;
export import ra3.terrain;
export import ra3.ui;
export import ra3.vulkan;
export import ra3.dx;
export import ra3.display;