From 3bfb95f4232b7f02cdecba74b5294963a33d325e Mon Sep 17 00:00:00 2001 From: ercmine Date: Sun, 5 Apr 2026 19:45:55 -0500 Subject: [PATCH] Harden renderer lifecycle, resize, and frame flow --- CMakeLists.txt | 9 +- docs/rendering.md | 125 +++++--- engine/render/README.md | 12 + engine/render/bgfx/renderer_bgfx.cpp | 334 +++++++++++++++++++--- engine/render/renderer.hpp | 10 +- engine/render/renderer_types.hpp | 53 ++++ engine/shell/main.cpp | 15 +- tests/render/renderer_lifecycle_tests.cpp | 38 +++ 8 files changed, 520 insertions(+), 76 deletions(-) create mode 100644 tests/render/renderer_lifecycle_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c6f0b3..6796ed9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,6 +181,11 @@ if(RENDER_BUILD_TESTS) render_apply_project_options(render_headless_validation) render_apply_warnings(render_headless_validation) + add_executable(render_renderer_lifecycle_tests tests/render/renderer_lifecycle_tests.cpp) + target_link_libraries(render_renderer_lifecycle_tests PRIVATE render::renderer) + render_apply_project_options(render_renderer_lifecycle_tests) + render_apply_warnings(render_renderer_lifecycle_tests) + add_test(NAME unit.platform.types COMMAND render_platform_types_tests) set_tests_properties(unit.platform.types PROPERTIES LABELS "unit;platform") add_test(NAME unit.core.runtime COMMAND render_core_runtime_tests) @@ -189,12 +194,14 @@ if(RENDER_BUILD_TESTS) set_tests_properties(unit.serialization.roundtrip PROPERTIES LABELS "unit;serialization") add_test(NAME unit.filesystem.service COMMAND render_filesystem_tests) set_tests_properties(unit.filesystem.service PROPERTIES LABELS "unit;filesystem") + add_test(NAME unit.renderer.lifecycle COMMAND render_renderer_lifecycle_tests) + set_tests_properties(unit.renderer.lifecycle PROPERTIES LABELS "unit;renderer") add_test(NAME headless.smoke.startup COMMAND render_headless_validation) set_tests_properties(headless.smoke.startup PROPERTIES LABELS "headless;smoke") add_custom_target(render_test_unit COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure --label-regex unit - DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests + DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" COMMENT "Running render unit tests" ) diff --git a/docs/rendering.md b/docs/rendering.md index a83d3f5..4b1b132 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -1,52 +1,109 @@ -# Renderer Integration (Statement 4) +# Renderer Runtime Lifecycle (Statement 9) ## Scope -Statement 4 introduces a renderer foundation powered by bgfx/bx/bimg but exposed through engine-owned interfaces. +Statement 9 hardens renderer runtime behavior around startup, backend selection, frame flow, resize handling, frame pacing, and recovery-oriented state management. -## Where code lives +## Lifecycle model -- Public interfaces and engine-owned types: `engine/render/*.hpp` -- bgfx backend implementation: `engine/render/bgfx/renderer_bgfx.cpp` -- Shader sources: `shaders/src/*.sc` -- Shader build hook: `cmake/ShaderCompilation.cmake`, `cmake/compile_shaders.cmake` +`render::rendering::Renderer` now owns an explicit lifecycle state machine: -## Allowed direct third-party usage +- `Uninitialized` +- `Initializing` +- `Ready` +- `Resizing` +- `Recovering` +- `ShuttingDown` +- `Failed` -- `engine/render/bgfx/*`: may include and call bgfx/bx/bimg APIs -- `engine/platform/sdl/*`: may include and call SDL APIs +The runtime shell and future systems can query: -All other engine/gameplay modules should stay on engine-owned types. +- `state()` +- `is_ready()` +- `can_render()` +- `status()` (`RendererStatus` snapshot including frame stats) -## SDL window handoff +The renderer logs every lifecycle transition. -Renderer initialization receives `PlatformRuntime` and reads the SDL window pointer through `native_window_handle()`. -The bgfx backend extracts native platform handles from SDL window properties and fills `bgfx::Init::platformData`. +## Startup and backend selection -## Current renderer API coverage +Startup is configuration-driven via `RendererConfig`: -- lifecycle (`initialize`, `shutdown`) -- backend selection (`RendererBackend`) -- frame flow (`begin_frame`, `end_frame`, `resize`) -- view setup (`set_view`, `set_view_transform`) -- resource creation (`create_vertex_buffer`, `create_index_buffer`, `create_program`, `create_solid_color_texture`) -- draw submission (`submit`) -- diagnostics (`capabilities`, debug text) +- `backend` (engine-owned enum, auto or explicit) +- `width`, `height` +- `debug` +- `vsync` +- `reset_flags` (engine-owned config mask) +- `min_frame_time_ms` (lightweight frame pacing floor) +- `allow_automatic_recovery` -## Current validation target +Validation is explicit through `validate_renderer_config(...)`. + +Backend behavior: + +1. Engine backend enum maps to bgfx renderer type internally. +2. Requested backend and actual backend are both logged. +3. Explicit requests that are not selected log a warning and continue with the chosen backend. + +## Frame lifecycle contract + +Per-frame flow is now explicit and misuse-resistant: + +1. `begin_frame()` + - rejects invalid states + - detects begin/begin misuse + - applies deferred resize + - returns `false` when rendering should be skipped (minimized/invalid state) +2. app records view setup + submissions +3. `end_frame()` + - detects end without begin + - presents (`bgfx::frame()`) + - records frame stats and optional pacing sleep + +This creates a stable contract for future view/pass expansion without redesigning loop ownership. + +## Resize policy + +Resize is explicit and resilient: + +- shell forwards resize events with `request_resize(width, height)` +- resize requests are deduplicated and applied at frame boundary +- zero/minimized dimensions are treated as non-renderable and do not hard-fail the renderer +- rendering automatically resumes when a valid size arrives + +## Frame pacing policy (current stage) + +Current pacing is intentionally lightweight: + +- primary pacing remains present/vsync (`vsync` + reset flags) +- optional minimum frame duration (`min_frame_time_ms`) prevents runaway present loops when desired +- no simulation scheduler or fixed-step governor is introduced at this stage + +## Recovery/device-loss philosophy + +A sane engine-owned recovery path is established: + +- failed/recovery states are explicit +- `try_recover()` attempts an in-place reset path (`bgfx::reset`) using current backbuffer +- automatic recovery attempts are gated by `allow_automatic_recovery` +- recovery attempts and outcomes are logged + +This is a practical base for later full resource re-creation pipelines. + +## Runtime shell integration `render_shell` now: -1. starts SDL runtime/window -2. initializes renderer with config -3. clears the main view every frame -4. submits a simple indexed triangle when shader binaries are available -5. handles resize and clean shutdown +- forwards platform resize events through `request_resize` +- gates rendering work on `begin_frame()` success +- avoids undefined frame flow when minimized or otherwise non-renderable +- keeps shutdown ordered (`destroy resources` -> `renderer.shutdown()` -> `runtime.shutdown()`) + +## Current non-goals / deferred work -## Follow-up work (later statements) +Still deferred to later statements: -- richer resource lifetime/allocator systems -- robust shader pipeline tooling and packaging -- material and scene rendering systems -- texture/image asset loading pipeline -- render graph/pass scheduling +- full multi-device-loss backend edge-case matrix +- explicit persistent resource registry for automated rebuild +- render graph and multi-pass scheduling +- scene/material-driven rendering orchestration diff --git a/engine/render/README.md b/engine/render/README.md index d5fd7eb..ecae2eb 100644 --- a/engine/render/README.md +++ b/engine/render/README.md @@ -8,3 +8,15 @@ Engine-owned rendering interfaces and resource types. - bgfx-specific backend implementation: `engine/render/bgfx/*` Higher-level engine and gameplay code should include only the public renderer headers. + +## Lifecycle contract (Statement 9) + +The public renderer API now exposes an explicit lifecycle and frame contract: + +- Startup/shutdown: `initialize`, `shutdown` +- Query: `state`, `status`, `is_ready`, `can_render` +- Frame flow: `begin_frame`, `end_frame` +- Resize flow: `request_resize`, `resize` +- Recovery hook: `try_recover` + +This keeps app/runtime code independent of bgfx lifecycle details. diff --git a/engine/render/bgfx/renderer_bgfx.cpp b/engine/render/bgfx/renderer_bgfx.cpp index fea718d..1332ff2 100644 --- a/engine/render/bgfx/renderer_bgfx.cpp +++ b/engine/render/bgfx/renderer_bgfx.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include #include @@ -17,6 +19,8 @@ namespace render::rendering { namespace { +constexpr std::uint16_t kMainView = 0; + bgfx::RendererType::Enum to_bgfx_renderer_type(const RendererBackend backend) { switch (backend) { case RendererBackend::Noop: return bgfx::RendererType::Noop; @@ -116,21 +120,138 @@ RendererBackend to_backend(bgfx::RendererType::Enum type) { } } +constexpr std::uint32_t minimum_dimension(std::uint32_t value) { + return std::max(value, 1U); +} + } // namespace +const char* to_string(const RendererBackend backend) noexcept { + switch (backend) { + case RendererBackend::Auto: return "auto"; + case RendererBackend::Noop: return "noop"; + case RendererBackend::Direct3D11: return "d3d11"; + case RendererBackend::Direct3D12: return "d3d12"; + case RendererBackend::Metal: return "metal"; + case RendererBackend::Vulkan: return "vulkan"; + case RendererBackend::OpenGL: return "opengl"; + default: return "unknown"; + } +} + +const char* to_string(const RendererLifecycleState state) noexcept { + switch (state) { + case RendererLifecycleState::Uninitialized: return "uninitialized"; + case RendererLifecycleState::Initializing: return "initializing"; + case RendererLifecycleState::Ready: return "ready"; + case RendererLifecycleState::Resizing: return "resizing"; + case RendererLifecycleState::Recovering: return "recovering"; + case RendererLifecycleState::ShuttingDown: return "shutting-down"; + case RendererLifecycleState::Failed: return "failed"; + default: return "unknown"; + } +} + +RendererConfigValidation validate_renderer_config(const RendererConfig& config) { + if (config.width == 0U || config.height == 0U) { + return {.valid = false, .reason = "initial dimensions must be greater than zero"}; + } + if (config.min_frame_time_ms > 1000U) { + return {.valid = false, .reason = "min_frame_time_ms must be <= 1000"}; + } + return {.valid = true, .reason = {}}; +} + struct Renderer::Impl { RendererConfig config{}; + RendererLifecycleState lifecycle{RendererLifecycleState::Uninitialized}; RendererBackend selected_backend{RendererBackend::Auto}; - bool initialized{false}; std::uint32_t reset_flags{BGFX_RESET_NONE}; + std::uint32_t backbuffer_width{0}; + std::uint32_t backbuffer_height{0}; + std::uint32_t pending_width{0}; + std::uint32_t pending_height{0}; + bool has_pending_resize{false}; + bool frame_active{false}; + bool device_lost{false}; + bool bgfx_initialized{false}; + std::uint64_t frame_count{0}; + std::chrono::steady_clock::time_point frame_begin_time{}; + std::uint32_t last_frame_time_ms{0}; + + [[nodiscard]] bool is_initialized() const noexcept { + return bgfx_initialized; + } + + [[nodiscard]] bool can_render() const noexcept { + return lifecycle == RendererLifecycleState::Ready && backbuffer_width > 0U && backbuffer_height > 0U; + } - [[nodiscard]] static std::uint32_t reset_flags_from_config(const RendererConfig& config) { - std::uint32_t flags = BGFX_RESET_NONE; + [[nodiscard]] std::uint32_t reset_flags_from_config() const { + std::uint32_t flags = config.reset_flags; if (config.vsync) { flags |= BGFX_RESET_VSYNC; + } else { + flags &= ~BGFX_RESET_VSYNC; } return flags; } + + void set_lifecycle(const RendererLifecycleState state) { + lifecycle = state; + platform::log::info(std::string{"Renderer state -> "} + to_string(lifecycle)); + } + + void queue_resize(const std::uint32_t width, const std::uint32_t height) { + const std::uint32_t clamped_width = width; + const std::uint32_t clamped_height = height; + if (has_pending_resize && pending_width == clamped_width && pending_height == clamped_height) { + return; + } + + pending_width = clamped_width; + pending_height = clamped_height; + has_pending_resize = true; + } + + void apply_resize_if_needed() { + if (!has_pending_resize) { + return; + } + + has_pending_resize = false; + if (pending_width == 0U || pending_height == 0U) { + backbuffer_width = pending_width; + backbuffer_height = pending_height; + platform::log::info("Renderer resize deferred while window is minimized (zero dimension)"); + return; + } + + set_lifecycle(RendererLifecycleState::Resizing); + backbuffer_width = pending_width; + backbuffer_height = pending_height; + bgfx::reset(backbuffer_width, backbuffer_height, reset_flags); + + std::ostringstream resize_log; + resize_log << "Renderer resize applied: " << backbuffer_width << "x" << backbuffer_height; + platform::log::info(resize_log.str()); + set_lifecycle(RendererLifecycleState::Ready); + } + + RendererStatus make_status() const { + RendererStatus status{}; + status.state = lifecycle; + status.selected_backend = selected_backend; + status.frame.frame_count = frame_count; + status.frame.backbuffer_width = backbuffer_width; + status.frame.backbuffer_height = backbuffer_height; + status.frame.last_frame_time_ms = last_frame_time_ms; + status.frame.vsync_enabled = (reset_flags & BGFX_RESET_VSYNC) != 0U; + status.frame.frame_active = frame_active; + status.device_lost = device_lost; + status.can_render = can_render(); + return status; + } }; Renderer::Renderer() : impl_(new Impl{}) {} @@ -141,12 +262,29 @@ Renderer::~Renderer() { } bool Renderer::initialize(const RendererConfig& config, const platform::PlatformRuntime& runtime) { - if (impl_->initialized) { + if (impl_->lifecycle == RendererLifecycleState::Initializing) { + platform::log::warn("Renderer initialization requested while already initializing"); + return false; + } + + if (impl_->lifecycle == RendererLifecycleState::Ready) { + platform::log::warn("Renderer initialization requested while already ready"); return true; } + const RendererConfigValidation validation = validate_renderer_config(config); + if (!validation.valid) { + platform::log::error(std::string{"Renderer init failed: invalid config: "} + validation.reason); + impl_->set_lifecycle(RendererLifecycleState::Failed); + return false; + } + + impl_->set_lifecycle(RendererLifecycleState::Initializing); impl_->config = config; - impl_->reset_flags = Impl::reset_flags_from_config(config); + impl_->reset_flags = impl_->reset_flags_from_config(); + impl_->device_lost = false; + impl_->frame_active = false; + impl_->has_pending_resize = false; bgfx::Init init{}; init.type = to_bgfx_renderer_type(config.backend); @@ -157,25 +295,45 @@ bool Renderer::initialize(const RendererConfig& config, const platform::Platform auto* sdl_window = static_cast(runtime.native_window_handle()); if (sdl_window == nullptr) { platform::log::error("Renderer init failed: runtime window handle was null"); + impl_->set_lifecycle(RendererLifecycleState::Failed); return false; } set_platform_data_from_sdl(init, sdl_window); + std::ostringstream request_log; + request_log << "Renderer init requested backend=" << to_string(config.backend) + << " size=" << config.width << "x" << config.height + << " vsync=" << (config.vsync ? "on" : "off") + << " debug=" << (config.debug ? "on" : "off"); + platform::log::info(request_log.str()); + if (!bgfx::init(init)) { platform::log::error("Renderer init failed: bgfx::init returned false"); + impl_->set_lifecycle(RendererLifecycleState::Failed); return false; } + impl_->bgfx_initialized = true; impl_->selected_backend = to_backend(bgfx::getRendererType()); + impl_->backbuffer_width = config.width; + impl_->backbuffer_height = config.height; const std::uint32_t debug_flags = config.debug ? BGFX_DEBUG_TEXT : BGFX_DEBUG_NONE; bgfx::setDebug(debug_flags); std::ostringstream backend_log; - backend_log << "Renderer initialized with backend: " << renderer_name(bgfx::getRendererType()); + backend_log << "Renderer backend selected requested=" << to_string(config.backend) + << " actual=" << renderer_name(bgfx::getRendererType()); platform::log::info(backend_log.str()); + if (is_explicit_backend_request(config.backend) && config.backend != impl_->selected_backend) { + std::ostringstream mismatch_log; + mismatch_log << "Requested backend " << to_string(config.backend) + << " not selected, running on " << to_string(impl_->selected_backend); + platform::log::warn(mismatch_log.str()); + } + if (const bgfx::Caps* caps = bgfx::getCaps(); caps != nullptr) { std::ostringstream caps_log; caps_log << "Renderer caps: maxViews=" << caps->limits.maxViews << ", maxTextureSize=" << caps->limits.maxTextureSize @@ -184,25 +342,61 @@ bool Renderer::initialize(const RendererConfig& config, const platform::Platform platform::log::info(caps_log.str()); } - impl_->initialized = true; + bgfx::setViewRect(kMainView, 0, 0, static_cast(minimum_dimension(config.width)), static_cast(minimum_dimension(config.height))); + bgfx::setViewClear(kMainView, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH, 0x1f2233ff, 1.0F, 0); + + impl_->set_lifecycle(RendererLifecycleState::Ready); return true; } void Renderer::shutdown() { - if (impl_ == nullptr || !impl_->initialized) { + if (impl_ == nullptr || impl_->lifecycle == RendererLifecycleState::Uninitialized) { return; } - bgfx::shutdown(); - impl_->initialized = false; + impl_->set_lifecycle(RendererLifecycleState::ShuttingDown); + if (impl_->frame_active) { + platform::log::warn("Renderer shutdown while a frame is active; ending frame implicitly"); + bgfx::frame(); + impl_->frame_active = false; + } + + if (impl_->is_initialized()) { + bgfx::shutdown(); + impl_->bgfx_initialized = false; + } + + impl_->selected_backend = RendererBackend::Auto; + impl_->backbuffer_width = 0; + impl_->backbuffer_height = 0; + impl_->has_pending_resize = false; + impl_->set_lifecycle(RendererLifecycleState::Uninitialized); +} + +bool Renderer::is_initialized() const noexcept { + return impl_->lifecycle != RendererLifecycleState::Uninitialized; +} + +bool Renderer::is_ready() const noexcept { + return impl_->lifecycle == RendererLifecycleState::Ready; } -bool Renderer::is_initialized() const noexcept { return impl_->initialized; } +bool Renderer::can_render() const noexcept { + return impl_->can_render(); +} + +RendererLifecycleState Renderer::state() const noexcept { + return impl_->lifecycle; +} + +RendererStatus Renderer::status() const noexcept { + return impl_->make_status(); +} RendererCaps Renderer::capabilities() const { RendererCaps caps{}; - if (!impl_->initialized) { + if (impl_->lifecycle != RendererLifecycleState::Ready) { return caps; } @@ -220,40 +414,114 @@ RendererCaps Renderer::capabilities() const { RendererBackend Renderer::backend() const noexcept { return impl_->selected_backend; } -void Renderer::begin_frame() { - if (!impl_->initialized) { - return; +bool Renderer::begin_frame() { + if (impl_->lifecycle == RendererLifecycleState::Failed) { + if (impl_->config.allow_automatic_recovery) { + return try_recover(); + } + return false; + } + + if (impl_->lifecycle != RendererLifecycleState::Ready) { + platform::log::warn(std::string{"begin_frame ignored in state "} + to_string(impl_->lifecycle)); + return false; + } + + if (impl_->frame_active) { + platform::log::warn("begin_frame called while frame already active"); + return false; + } + + impl_->apply_resize_if_needed(); + if (!impl_->can_render()) { + return false; } + impl_->frame_active = true; + impl_->frame_begin_time = std::chrono::steady_clock::now(); + if (impl_->config.debug) { bgfx::dbgTextClear(); bgfx::dbgTextPrintf(0, 0, 0x0f, "render :: backend %s", renderer_name(bgfx::getRendererType())); } + + return true; } -void Renderer::end_frame() { - if (!impl_->initialized) { - return; +bool Renderer::end_frame() { + if (impl_->lifecycle != RendererLifecycleState::Ready) { + return false; + } + + if (!impl_->frame_active) { + platform::log::warn("end_frame called without begin_frame"); + return false; } + bgfx::frame(); + impl_->frame_active = false; + impl_->frame_count += 1; + + const auto now = std::chrono::steady_clock::now(); + impl_->last_frame_time_ms = static_cast( + std::chrono::duration_cast(now - impl_->frame_begin_time).count()); + + if (impl_->config.min_frame_time_ms > 0U && impl_->last_frame_time_ms < impl_->config.min_frame_time_ms) { + const std::uint32_t sleep_ms = impl_->config.min_frame_time_ms - impl_->last_frame_time_ms; + platform::PlatformRuntime::sleep_for_milliseconds(sleep_ms); + impl_->last_frame_time_ms = impl_->config.min_frame_time_ms; + } + + return true; } -void Renderer::resize(const std::uint32_t width, const std::uint32_t height) { - if (!impl_->initialized) { +void Renderer::request_resize(const std::uint32_t width, const std::uint32_t height) { + if (impl_->lifecycle != RendererLifecycleState::Ready && impl_->lifecycle != RendererLifecycleState::Recovering) { + return; + } + + if (width == impl_->backbuffer_width && height == impl_->backbuffer_height) { return; } - bgfx::reset(width, height, impl_->reset_flags); + + impl_->queue_resize(width, height); +} + +bool Renderer::try_recover() { + if (impl_->lifecycle != RendererLifecycleState::Failed && impl_->lifecycle != RendererLifecycleState::Recovering) { + return impl_->lifecycle == RendererLifecycleState::Ready; + } + + impl_->set_lifecycle(RendererLifecycleState::Recovering); + impl_->device_lost = true; + + if (impl_->backbuffer_width == 0U || impl_->backbuffer_height == 0U) { + platform::log::warn("Renderer recovery delayed because backbuffer dimensions are zero"); + impl_->set_lifecycle(RendererLifecycleState::Failed); + return false; + } + + bgfx::reset(impl_->backbuffer_width, impl_->backbuffer_height, impl_->reset_flags); + impl_->device_lost = false; + impl_->set_lifecycle(RendererLifecycleState::Ready); + platform::log::info("Renderer recovery succeeded using bgfx::reset"); + return true; +} + +void Renderer::resize(const std::uint32_t width, const std::uint32_t height) { + request_resize(width, height); + impl_->apply_resize_if_needed(); } void Renderer::set_debug_enabled(const bool enabled) { impl_->config.debug = enabled; - if (impl_->initialized) { + if (impl_->lifecycle == RendererLifecycleState::Ready) { bgfx::setDebug(enabled ? BGFX_DEBUG_TEXT : BGFX_DEBUG_NONE); } } void Renderer::set_view(const ViewId view, const ViewDescription& desc) { - if (!impl_->initialized) { + if (!impl_->can_render()) { return; } @@ -277,14 +545,14 @@ void Renderer::set_view_transform( const ViewId view, const std::span view_transform, const std::span projection) { - if (!impl_->initialized) { + if (!impl_->can_render()) { return; } bgfx::setViewTransform(view.value, view_transform.data(), projection.data()); } VertexBufferHandle Renderer::create_vertex_buffer(const VertexBufferDescription& desc) { - if (!impl_->initialized || desc.data.empty()) { + if (!impl_->can_render() || desc.data.empty()) { return {}; } @@ -306,7 +574,7 @@ VertexBufferHandle Renderer::create_vertex_buffer(const VertexBufferDescription& } IndexBufferHandle Renderer::create_index_buffer(const IndexBufferDescription& desc) { - if (!impl_->initialized || desc.data.empty()) { + if (!impl_->can_render() || desc.data.empty()) { return {}; } @@ -316,7 +584,7 @@ IndexBufferHandle Renderer::create_index_buffer(const IndexBufferDescription& de } ProgramHandle Renderer::create_program(const ShaderProgramDescription& desc) { - if (!impl_->initialized) { + if (!impl_->can_render()) { return {}; } @@ -348,7 +616,7 @@ ProgramHandle Renderer::create_program(const ShaderProgramDescription& desc) { } TextureHandle Renderer::create_solid_color_texture(const SolidColorTextureDescription& desc) { - if (!impl_->initialized) { + if (!impl_->can_render()) { return {}; } @@ -385,31 +653,31 @@ TextureHandle Renderer::create_solid_color_texture(const SolidColorTextureDescri } void Renderer::destroy_buffer(const VertexBufferHandle handle) { - if (impl_->initialized && handle.idx != kInvalidHandle) { + if (impl_->lifecycle == RendererLifecycleState::Ready && handle.idx != kInvalidHandle) { bgfx::destroy(bgfx::VertexBufferHandle{handle.idx}); } } void Renderer::destroy_buffer(const IndexBufferHandle handle) { - if (impl_->initialized && handle.idx != kInvalidHandle) { + if (impl_->lifecycle == RendererLifecycleState::Ready && handle.idx != kInvalidHandle) { bgfx::destroy(bgfx::IndexBufferHandle{handle.idx}); } } void Renderer::destroy_program(const ProgramHandle handle) { - if (impl_->initialized && handle.idx != kInvalidHandle) { + if (impl_->lifecycle == RendererLifecycleState::Ready && handle.idx != kInvalidHandle) { bgfx::destroy(bgfx::ProgramHandle{handle.idx}); } } void Renderer::destroy_texture(const TextureHandle handle) { - if (impl_->initialized && handle.idx != kInvalidHandle) { + if (impl_->lifecycle == RendererLifecycleState::Ready && handle.idx != kInvalidHandle) { bgfx::destroy(bgfx::TextureHandle{handle.idx}); } } void Renderer::submit(const ViewId view, const MeshSubmission& mesh_submission) { - if (!impl_->initialized) { + if (!impl_->can_render()) { return; } diff --git a/engine/render/renderer.hpp b/engine/render/renderer.hpp index c8e0ea3..fad736f 100644 --- a/engine/render/renderer.hpp +++ b/engine/render/renderer.hpp @@ -25,11 +25,17 @@ class Renderer { void shutdown(); [[nodiscard]] bool is_initialized() const noexcept; + [[nodiscard]] bool is_ready() const noexcept; + [[nodiscard]] bool can_render() const noexcept; + [[nodiscard]] RendererLifecycleState state() const noexcept; + [[nodiscard]] RendererStatus status() const noexcept; [[nodiscard]] RendererCaps capabilities() const; [[nodiscard]] RendererBackend backend() const noexcept; - void begin_frame(); - void end_frame(); + [[nodiscard]] bool begin_frame(); + [[nodiscard]] bool end_frame(); + void request_resize(std::uint32_t width, std::uint32_t height); + [[nodiscard]] bool try_recover(); void resize(std::uint32_t width, std::uint32_t height); void set_debug_enabled(bool enabled); diff --git a/engine/render/renderer_types.hpp b/engine/render/renderer_types.hpp index ba7f478..c3f15e4 100644 --- a/engine/render/renderer_types.hpp +++ b/engine/render/renderer_types.hpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace render::platform { @@ -22,12 +23,30 @@ enum class RendererBackend : std::uint8_t { OpenGL, }; +enum class RendererLifecycleState : std::uint8_t { + Uninitialized = 0, + Initializing, + Ready, + Resizing, + Recovering, + ShuttingDown, + Failed, +}; + +enum class RendererResetFlag : std::uint32_t { + None = 0, + VSync = 1U << 0U, +}; + struct RendererConfig { RendererBackend backend{RendererBackend::Auto}; std::uint32_t width{1280}; std::uint32_t height{720}; bool debug{false}; bool vsync{true}; + std::uint32_t reset_flags{0}; + std::uint32_t min_frame_time_ms{0}; + bool allow_automatic_recovery{true}; }; struct RendererCaps { @@ -39,6 +58,40 @@ struct RendererCaps { bool origin_bottom_left{false}; }; +struct RendererFrameStats { + std::uint64_t frame_count{0}; + std::uint32_t backbuffer_width{0}; + std::uint32_t backbuffer_height{0}; + std::uint32_t last_frame_time_ms{0}; + bool vsync_enabled{true}; + bool frame_active{false}; +}; + +struct RendererStatus { + RendererLifecycleState state{RendererLifecycleState::Uninitialized}; + RendererBackend selected_backend{RendererBackend::Auto}; + RendererFrameStats frame{}; + bool device_lost{false}; + bool can_render{false}; +}; + +struct RendererConfigValidation { + bool valid{false}; + std::string reason{}; +}; + +[[nodiscard]] constexpr std::uint32_t to_mask(const RendererResetFlag flag) noexcept { + return static_cast(flag); +} + +[[nodiscard]] constexpr bool is_explicit_backend_request(const RendererBackend backend) noexcept { + return backend != RendererBackend::Auto; +} + +[[nodiscard]] const char* to_string(RendererBackend backend) noexcept; +[[nodiscard]] const char* to_string(RendererLifecycleState state) noexcept; +[[nodiscard]] RendererConfigValidation validate_renderer_config(const RendererConfig& config); + struct ViewId { std::uint16_t value{0}; }; diff --git a/engine/shell/main.cpp b/engine/shell/main.cpp index f689bd9..56c18de 100644 --- a/engine/shell/main.cpp +++ b/engine/shell/main.cpp @@ -75,7 +75,7 @@ int main(int argc, char** argv) { render::platform::RuntimeConfig platform_config{}; platform_config.app_name = "render-shell"; platform_config.org_name = "render"; - platform_config.window.title = "render :: Statement 4 shell"; + platform_config.window.title = "render :: Statement 9 shell"; platform_config.window.width = 1280; platform_config.window.height = 720; platform_config.window.resizable = true; @@ -154,11 +154,16 @@ int main(int argc, char** argv) { runtime.pump_events(); const render::platform::WindowState& window = runtime.window_state(); - if (window.resized_this_frame && window.width > 0 && window.height > 0) { - renderer.resize(window.width, window.height); + if (window.resized_this_frame) { + renderer.request_resize(window.width, window.height); } - renderer.begin_frame(); + const bool frame_started = renderer.begin_frame(); + if (!frame_started) { + runtime.end_frame(); + render::platform::PlatformRuntime::sleep_for_milliseconds(1); + continue; + } render::rendering::ViewDescription view{}; view.rect.width = static_cast(window.width); @@ -186,8 +191,6 @@ int main(int argc, char** argv) { renderer.end_frame(); runtime.end_frame(); - - render::platform::PlatformRuntime::sleep_for_milliseconds(1); } renderer.destroy_program(program); diff --git a/tests/render/renderer_lifecycle_tests.cpp b/tests/render/renderer_lifecycle_tests.cpp new file mode 100644 index 0000000..948c8f6 --- /dev/null +++ b/tests/render/renderer_lifecycle_tests.cpp @@ -0,0 +1,38 @@ +#include "engine/render/renderer.hpp" + +#include + +int main() { + using namespace render::rendering; + + { + RendererConfig config{}; + config.width = 0; + config.height = 720; + const RendererConfigValidation validation = validate_renderer_config(config); + assert(!validation.valid); + } + + { + RendererConfig config{}; + config.width = 1280; + config.height = 720; + config.min_frame_time_ms = 16; + const RendererConfigValidation validation = validate_renderer_config(config); + assert(validation.valid); + } + + assert(is_explicit_backend_request(RendererBackend::Vulkan)); + assert(!is_explicit_backend_request(RendererBackend::Auto)); + assert(to_string(RendererBackend::OpenGL) != nullptr); + assert(to_string(RendererLifecycleState::Ready) != nullptr); + + Renderer renderer; + assert(renderer.state() == RendererLifecycleState::Uninitialized); + assert(!renderer.is_ready()); + assert(!renderer.can_render()); + assert(!renderer.begin_frame()); + assert(!renderer.end_frame()); + + return 0; +}