Harden runtime flags and thin Apple/canvas seams

This commit is contained in:
2026-06-16 08:00:36 +02:00
parent 34e2747867
commit 2948e907bc
7 changed files with 157 additions and 50 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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`

View File

@@ -2,6 +2,7 @@
#include "shader.h"
#include <array>
#include <functional>
namespace pp::panopainter {
@@ -238,6 +239,14 @@ struct LegacyCanvasDrawMergePostDrawExecution {
std::function<void()> draw_current_modes;
};
struct LegacyCanvasDrawMergeSmaskFacesExecution {
std::function<void()> set_active_texture_unit;
std::function<void()> enable_blend;
std::function<void(int plane_index)> bind_face_texture;
std::function<void()> draw_face;
std::function<void(int plane_index)> 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<glm::mat4, 6>& 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

View File

@@ -31,6 +31,7 @@
#include <ctime>
#include <sstream>
#include <memory>
#include <atomic>
#include <stop_token>
#define WM_USER_CLOSE (WM_USER + 1)
@@ -50,9 +51,9 @@ std::map<kKey, int> 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<int> vr_frames{0};
std::atomic<int> running{-1};
std::atomic_bool vr_running{false};
int gl_count = 0;
std::deque<std::packaged_task<void()>> 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<std::mutex> 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([=]
{

View File

@@ -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,
});
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++)
},
proj,
camera,
m_canvas->m_layers.size() + 500.f,
m_canvas->m_plane_transform,
{
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);
.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();
}
},
});
},
.draw_grid_modes = [&] {
for (auto& mode : Canvas::modes[(int)kCanvasMode::Grid])

View File

@@ -132,25 +132,62 @@ struct RetainedLegacyGlfwWindowHooks final {
return types;
}
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_legacy_apple_storage_paths()
{
struct RetainedLegacyAppleState final {
#ifdef __IOS__
[App::I->ios_view init_dirs];
decltype(App::I->ios_view) ios_view = nullptr;
decltype(App::I->ios_app) ios_app = nullptr;
#elif defined(__OSX__)
[App::I->osx_app init_dirs];
decltype(App::I->osx_view) osx_view = nullptr;
decltype(App::I->osx_app) osx_app = nullptr;
#endif
return {
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__
[apple_state.ios_view init_dirs];
#elif defined(__OSX__)
[apple_state.osx_app init_dirs];
#endif
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] {