diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 278ebe72..fe74cbf7 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -18,6 +18,22 @@ agent or engineer to remove them without reconstructing context from chat. ## Reductions +- 2026-06-16: `DEBT-0003` was narrowed again. The Windows main-loop run-state + and VR worker coordination flags in `src/main.cpp` now use `std::atomic` + instead of unsynchronized globals; retained Win32 entrypoint ownership, + global app singleton reach, and broader runtime coupling remain. +- 2026-06-16: `DEBT-0017` was narrowed again. Retained Apple ObjC handles plus + storage paths now live behind one local helper in + `src/platform_legacy/legacy_platform_services.cpp`, and the iOS SonarPen + bridge now starts through that retained Apple state instead of reading + `App::I` inside the bridge body; the retained Apple fallback adapter and + broader platform-to-app singleton reach remain. +- 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` smoothing-mask face + shader setup plus per-face draw execution now route through + `execute_legacy_canvas_draw_merge_smask_faces(...)` in + `src/legacy_canvas_draw_merge_services.h` instead of spelling that pass out + inline in `NodeCanvas::draw()`; broader canvas draw orchestration and + retained GL resource ownership remain. - 2026-06-16: `DEBT-0036` was narrowed again. `NodeCanvas` merged-path per-plane merged-texture draw execution now routes through `execute_legacy_canvas_draw_merge_layer_texture(...)` instead of spelling out diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 1ce49a12..6fdd30bf 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -102,7 +102,9 @@ Current architecture mismatches that must be treated as real blockers: context hooks plus iOS main-render-target binding now route through the same bridge style, as do Apple crash-test, app-close, and iOS SonarPen hooks, while Linux/Web GLFW render-context acquire/present and Linux app-close now - route through retained local GLFW callback hooks. + route through retained local GLFW callback hooks, and retained Apple ObjC + handles plus storage paths now sit behind one local `platform_legacy` + helper instead of being re-read through `App::I` in each touched path. - `src/platform_legacy/legacy_platform_services.*` is still part of the live app shell. - `pp_panopainter_ui` still depends on `pp_legacy_app`. @@ -112,8 +114,8 @@ Current architecture mismatches that must be treated as real blockers: sequencing, per-layer/per-plane retained draw execution, and shared checkerboard background setup now route through retained draw-merge helpers, with the cache-to-screen checkerboard-plane callback setup also reduced and - the merged-path per-plane merged-texture draw now routed through the same - retained helper family. + the merged-path per-plane merged-texture draw plus the smoothing-mask face + shader/draw pass now routed through the same retained helper family. - `app_layout.cpp` and `app_dialogs.cpp` are still mixed shell/controller files rather than thin composition/binding surfaces. - `App`, `Canvas`, `Node`, retained workers, and platform entrypoints still use @@ -124,8 +126,9 @@ Current architecture mismatches that must be treated as real blockers: `AppRuntime` now owns render/UI workers with explicit `std::jthread` shutdown semantics while the Windows splash-dialog and HMD renderer workers also use owned `std::jthread` lifecycle, `LogRemote` now uses the same - ownership model, and the Windows VR device now has explicit `std::unique_ptr` - ownership instead of raw global lifetime. + ownership model, the Windows VR device now has explicit `std::unique_ptr` + ownership instead of raw global lifetime, and the Windows main-loop/VR + coordination flags now use `std::atomic` instead of unsynchronized globals. - Modern C++23 usage exists in extracted components, especially `std::span`, explicit result/status objects, and a few concepts, but the live app still does not consistently express ownership, thread affinity, or renderer diff --git a/docs/modernization/tasks.md b/docs/modernization/tasks.md index 89d44f84..2d4bc1c1 100644 --- a/docs/modernization/tasks.md +++ b/docs/modernization/tasks.md @@ -143,6 +143,10 @@ Current slice: draw, heightmap draw, and current-mode draw now also route through `execute_legacy_canvas_draw_merge_post_draw(...)`, but broader canvas draw orchestration is still inline. +- `NodeCanvas` smoothing-mask face shader setup plus per-face draw execution + now also route through + `execute_legacy_canvas_draw_merge_smask_faces(...)`, but the node still owns + the broader canvas draw flow and renderer-state sequencing around that seam. - `NodeCanvas` non-`draw_merged` per-layer/per-plane retained draw execution now also routes through `execute_legacy_canvas_draw_merge_layer_plane(...)`, but the node still owns substantial live layer traversal and renderer state @@ -335,6 +339,8 @@ Current slice: live in `AppRuntime` - `AppRuntime` render/UI worker ownership now also uses `std::jthread` plus explicit stop requests instead of raw `std::thread` +- Windows main-loop run-state and VR worker coordination flags in `main.cpp` + now use `std::atomic` ownership instead of unsynchronized globals - retained `App` composition, task call sites, and platform/runtime entrypoint coupling are still not fully reduced behind the runtime contract @@ -604,6 +610,9 @@ Current slice: - Apple crash-test, app-close, and iOS SonarPen hooks now also route through explicit Apple bridge callbacks instead of direct `App::I` calls in `LegacyPlatformServices` +- retained Apple ObjC handles plus storage paths now live in one local + `platform_legacy` helper, and the iOS SonarPen bridge now starts through + that retained Apple state instead of reading `App::I` inside the bridge body - Linux/Web GLFW render-context acquire/present hooks and Linux app-close now also route through retained local GLFW callback hooks instead of direct method-body `App::I` access in `LegacyPlatformServices` diff --git a/src/legacy_canvas_draw_merge_services.h b/src/legacy_canvas_draw_merge_services.h index 534aa46e..53f08855 100644 --- a/src/legacy_canvas_draw_merge_services.h +++ b/src/legacy_canvas_draw_merge_services.h @@ -2,6 +2,7 @@ #include "shader.h" +#include #include namespace pp::panopainter { @@ -238,6 +239,14 @@ struct LegacyCanvasDrawMergePostDrawExecution { std::function draw_current_modes; }; +struct LegacyCanvasDrawMergeSmaskFacesExecution { + std::function set_active_texture_unit; + std::function enable_blend; + std::function bind_face_texture; + std::function draw_face; + std::function unbind_face_texture; +}; + [[nodiscard]] inline LegacyCanvasDrawMergeShaderExecution legacy_shader_manager_draw_merge_execution() noexcept { return { @@ -631,4 +640,29 @@ inline void execute_legacy_canvas_draw_merge_post_draw( execution.draw_current_modes(); } +inline void execute_legacy_canvas_draw_merge_smask_faces( + const LegacyCanvasDrawMergeTextureMaskUniforms& uniforms, + const glm::mat4& proj, + const glm::mat4& camera, + float layer_scale, + const std::array& plane_transform, + const LegacyCanvasDrawMergeSmaskFacesExecution& execution) +{ + setup_legacy_canvas_draw_merge_texture_mask_shader(uniforms); + execution.set_active_texture_unit(); + execution.enable_blend(); + + for (int plane_index = 0; plane_index < 6; ++plane_index) { + auto plane_mvp = proj * camera * + glm::scale(glm::vec3(layer_scale)) * + plane_transform[plane_index] * + glm::translate(glm::vec3(0, 0, -1.f)); + + apply_legacy_canvas_draw_merge_mvp(plane_mvp); + execution.bind_face_texture(plane_index); + execution.draw_face(); + execution.unbind_face_texture(plane_index); + } +} + } // namespace pp::panopainter diff --git a/src/main.cpp b/src/main.cpp index 9c350eaa..0e45771e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #define WM_USER_CLOSE (WM_USER + 1) @@ -50,9 +51,9 @@ std::map vkey_map; static wchar_t window_title[512]; std::jthread hmd_renderer; -int vr_frames = 0; -int running = -1; -int vr_running = 0; +std::atomic vr_frames{0}; +std::atomic running{-1}; +std::atomic_bool vr_running{false}; int gl_count = 0; std::deque> main_tasklist; @@ -233,8 +234,9 @@ void win32_update_stylus(float dt) void win32_update_fps(int frames) { static wchar_t title_fps[512]; + const int vr_fps = vr_frames.load(std::memory_order_relaxed); if (App::I->vr_active) - swprintf_s(title_fps, L"%s - %d fps - %d vr fps", window_title, frames, vr_frames); + swprintf_s(title_fps, L"%s - %d fps - %d vr fps", window_title, frames, vr_fps); else swprintf_s(title_fps, L"%s - %d fps", window_title, frames); @@ -547,7 +549,7 @@ bool win32_vr_start() BT_SetTerminate(); LOG("start hmd render thread"); App::I->has_vr = true; - vr_running = true; + vr_running.store(true, std::memory_order_relaxed); vive->on_analog_button = std::bind(&App::vr_analog, App::I, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4); @@ -562,7 +564,7 @@ bool win32_vr_start() auto t0 = GetTickCount64(); float one_sec_timer = 0; int frames = 0; - while (!stop_token.stop_requested() && vr_running && running == 1 && vive->Valid()) + while (!stop_token.stop_requested() && vr_running.load(std::memory_order_relaxed) && running.load(std::memory_order_relaxed) == 1 && vive->Valid()) { std::unique_lock lock(hmd_render_mutex); auto t1 = GetTickCount64(); @@ -572,7 +574,7 @@ bool win32_vr_start() if (one_sec_timer >= 1.f) { one_sec_timer = 0; - vr_frames = frames; + vr_frames.store(frames, std::memory_order_relaxed); frames = 0; } frames++; @@ -583,7 +585,7 @@ bool win32_vr_start() App::I->vr_head = vive->m_pose; App::I->vr_update(dt); - if (vr_running && vive->m_active) + if (vr_running.load(std::memory_order_relaxed) && vive->m_active) { App::I->render_task([] { vive->Draw(); @@ -597,7 +599,7 @@ bool win32_vr_start() } App::I->vr_active = false; App::I->has_vr = false; - vr_running = false; + vr_running.store(false, std::memory_order_relaxed); vive->Terminate(); LOG("hmd renderer terminated"); }); @@ -608,7 +610,7 @@ void win32_vr_stop() { if (vive) { - vr_running = false; + vr_running.store(false, std::memory_order_relaxed); if (hmd_renderer.joinable()) { hmd_renderer.request_stop(); @@ -937,7 +939,7 @@ int main(int argc, char** argv) wglMakeCurrent(NULL, NULL); - running = 1; + running.store(1, std::memory_order_relaxed); App::I->runtime().render_thread_start(*App::I); App::I->runtime().ui_thread_start(*App::I); @@ -985,14 +987,14 @@ int main(int argc, char** argv) MSG msg; LOG("start main loop"); - while (running == 1) + while (running.load(std::memory_order_relaxed) == 1) { // If there any message in the queue process it auto present = App::I->animate || App::I->redraw ? PeekMessage(&msg, 0, 0, 0, PM_REMOVE) : GetMessage(&msg, 0, 0, 0); if (msg.message == WM_QUIT) - running = 0; + running.store(0, std::memory_order_relaxed); if (present) { @@ -1038,7 +1040,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) switch (msg) { case WM_USER_CLOSE: - running = 0; + running.store(0, std::memory_order_relaxed); if (hmd_renderer.joinable()) { hmd_renderer.request_stop(); @@ -1071,7 +1073,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { auto w = (float)LOWORD(lp); auto h = (float)HIWORD(lp); - if (h != 0 && running == 1) + if (h != 0 && running.load(std::memory_order_relaxed) == 1) { App::I->ui_task_async([=] { diff --git a/src/node_canvas.cpp b/src/node_canvas.cpp index 9ed383e6..faa1b515 100644 --- a/src/node_canvas.cpp +++ b/src/node_canvas.cpp @@ -738,27 +738,32 @@ void NodeCanvas::draw() m_canvas->modes[(int)kCanvasMode::MaskLine][0]->on_Draw(ortho_proj, proj, camera); }, .draw_smask_faces = [&] { - pp::panopainter::setup_legacy_canvas_draw_merge_texture_mask_shader( + pp::panopainter::execute_legacy_canvas_draw_merge_smask_faces( pp::panopainter::LegacyCanvasDrawMergeTextureMaskUniforms { .texture_slot = 0, .pattern_offset = m_outline_pan, + }, + proj, + camera, + m_canvas->m_layers.size() + 500.f, + m_canvas->m_plane_transform, + { + .set_active_texture_unit = [&] { + set_active_texture_unit(0); + }, + .enable_blend = [&] { + apply_node_canvas_capability(pp::renderer::gl::blend_state(), true); + }, + .bind_face_texture = [&](int plane_index) { + m_canvas->m_smask.rtt(plane_index).bindTexture(); + }, + .draw_face = [&] { + m_face_plane.draw_fill(); + }, + .unbind_face_texture = [&](int plane_index) { + m_canvas->m_smask.rtt(plane_index).unbindTexture(); + }, }); - set_active_texture_unit(0); - apply_node_canvas_capability(pp::renderer::gl::blend_state(), true); - - //draw the cube faces - for (int plane_index = 0; plane_index < 6; plane_index++) - { - auto plane_mvp = proj * camera * - glm::scale(glm::vec3(m_canvas->m_layers.size() + 500.f)) * - m_canvas->m_plane_transform[plane_index] * - glm::translate(glm::vec3(0, 0, -1.f)); - - pp::panopainter::apply_legacy_canvas_draw_merge_mvp(plane_mvp); - m_canvas->m_smask.rtt(plane_index).bindTexture(); - m_face_plane.draw_fill(); - m_canvas->m_smask.rtt(plane_index).unbindTexture(); - } }, .draw_grid_modes = [&] { for (auto& mode : Canvas::modes[(int)kCanvasMode::Grid]) diff --git a/src/platform_legacy/legacy_platform_services.cpp b/src/platform_legacy/legacy_platform_services.cpp index 425f4d18..5ae96551 100644 --- a/src/platform_legacy/legacy_platform_services.cpp +++ b/src/platform_legacy/legacy_platform_services.cpp @@ -132,25 +132,62 @@ struct RetainedLegacyGlfwWindowHooks final { return types; } +struct RetainedLegacyAppleState final { +#ifdef __IOS__ + decltype(App::I->ios_view) ios_view = nullptr; + decltype(App::I->ios_app) ios_app = nullptr; +#elif defined(__OSX__) + decltype(App::I->osx_view) osx_view = nullptr; + decltype(App::I->osx_app) osx_app = nullptr; +#endif + pp::platform::PlatformStoragePaths storage_paths; +}; + +[[nodiscard]] RetainedLegacyAppleState& active_legacy_apple_state() +{ + static RetainedLegacyAppleState state = [] { + RetainedLegacyAppleState retained; +#ifdef __IOS__ + retained.ios_view = App::I->ios_view; + retained.ios_app = App::I->ios_app; + retained.storage_paths = { + App::I->data_path, + App::I->work_path, + App::I->rec_path, + App::I->tmp_path, + }; +#elif defined(__OSX__) + retained.osx_view = App::I->osx_view; + retained.osx_app = App::I->osx_app; + retained.storage_paths = { + App::I->data_path, + App::I->work_path, + App::I->rec_path, + App::I->tmp_path, + }; +#endif + return retained; + }(); + return state; +} + [[nodiscard]] pp::platform::PlatformStoragePaths prepare_legacy_apple_storage_paths() { + const auto& apple_state = active_legacy_apple_state(); #ifdef __IOS__ - [App::I->ios_view init_dirs]; + [apple_state.ios_view init_dirs]; #elif defined(__OSX__) - [App::I->osx_app init_dirs]; + [apple_state.osx_app init_dirs]; #endif - return { - App::I->data_path, - App::I->work_path, - App::I->rec_path, - App::I->tmp_path, - }; + return apple_state.storage_paths; } [[nodiscard]] pp::platform::apple::AppleDocumentPlatformServices& active_apple_document_platform_services() { #ifdef __IOS__ - auto* const ios_view = App::I->ios_view; + const auto& apple_state = active_legacy_apple_state(); + auto* const ios_view = apple_state.ios_view; + auto* const ios_app = apple_state.ios_app; static pp::platform::apple::AppleDocumentPlatformServices services( pp::platform::PlatformFamily::ios, [ios_view] { @@ -173,8 +210,8 @@ struct RetainedLegacyGlfwWindowHooks final { bridge.trigger_crash_test = [ios_view] { [ios_view crash]; }; - bridge.start_sonarpen = [ios_view] { - [App::I->ios_app sonarpen_start]; + bridge.start_sonarpen = [ios_app] { + [ios_app sonarpen_start]; }; bridge.acquire_render_context = [ios_view] { [ios_view async_lock]; @@ -224,8 +261,9 @@ struct RetainedLegacyGlfwWindowHooks final { }()); return services; #else - auto* const osx_view = App::I->osx_view; - auto* const osx_app = App::I->osx_app; + const auto& apple_state = active_legacy_apple_state(); + auto* const osx_view = apple_state.osx_view; + auto* const osx_app = apple_state.osx_app; static pp::platform::apple::AppleDocumentPlatformServices services( pp::platform::PlatformFamily::macos, [osx_view, osx_app] {