Route render context lifecycle through platform services

This commit is contained in:
2026-06-03 04:50:42 +02:00
parent f3925f8423
commit 7a9b14a86f
10 changed files with 134 additions and 49 deletions

View File

@@ -447,8 +447,8 @@ Known local toolchain state:
- `pp_platform_api` exposes the SDK-free `PlatformServices` interface for - `pp_platform_api` exposes the SDK-free `PlatformServices` interface for
clipboard text, cursor visibility, virtual-keyboard visibility, external clipboard text, cursor visibility, virtual-keyboard visibility, external
file display, file sharing, native app/window close, UI-thread lifecycle file display, file sharing, native app/window close, UI-thread lifecycle
hooks, per-frame platform hooks, picker callbacks, and prepared-file hooks, render-context lifecycle hooks, per-frame platform hooks, picker
save/download handoff; Windows callbacks, and prepared-file save/download handoff; Windows
live app execution now uses injected live app execution now uses injected
`WindowsPlatformServices` from `WindowsPlatformServices` from
`src/platform_windows/windows_platform_services.*` in `pp_platform_windows`, `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`,

View File

@@ -35,7 +35,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix |
| DEBT-0015 | Open | Modernization | Cursor visibility requests now consume pure `pp_app_core` planning through `pano_cli plan-cursor-visibility`, `App::show_cursor`/`App::hide_cursor` dispatch through `PlatformServices` without platform guards, and Windows live execution uses injected `WindowsPlatformServices`, but macOS cursor execution still reaches the retained fallback adapter | Keep canvas cursor behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-cursor-visibility --visible`; `ctest --preset desktop-fast --build-config Debug` | Cursor visibility execution is owned by injected `pp_platform_*` services for every supported platform | | DEBT-0015 | Open | Modernization | Cursor visibility requests now consume pure `pp_app_core` planning through `pano_cli plan-cursor-visibility`, `App::show_cursor`/`App::hide_cursor` dispatch through `PlatformServices` without platform guards, and Windows live execution uses injected `WindowsPlatformServices`, but macOS cursor execution still reaches the retained fallback adapter | Keep canvas cursor behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-cursor-visibility --visible`; `ctest --preset desktop-fast --build-config Debug` | Cursor visibility execution is owned by injected `pp_platform_*` services for every supported platform |
| DEBT-0016 | Open | Modernization | Clipboard get/set requests now consume pure `pp_app_core` planning through `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write`, and Windows live execution uses injected `WindowsPlatformServices`, but Apple/Android clipboard execution still reaches retained fallback adapter branches from `App::clipboard_get_text` and `App::clipboard_set_text` | Keep picker/color text clipboard behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-clipboard-write --text #ff00aa`; `ctest --preset desktop-fast --build-config Debug` | Clipboard execution is owned by injected `pp_platform_*` services for every supported platform | | DEBT-0016 | Open | Modernization | Clipboard get/set requests now consume pure `pp_app_core` planning through `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write`, and Windows live execution uses injected `WindowsPlatformServices`, but Apple/Android clipboard execution still reaches retained fallback adapter branches from `App::clipboard_get_text` and `App::clipboard_set_text` | Keep picker/color text clipboard behavior stable while platform shells are extracted incrementally | `pp_app_core_document_platform_io_tests`; `pano_cli plan-clipboard-write --text #ff00aa`; `ctest --preset desktop-fast --build-config Debug` | Clipboard execution is owned by injected `pp_platform_*` services for every supported platform |
| DEBT-0017 | Open | Modernization | `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, native app/window close, UI-thread lifecycle hooks, per-frame platform hooks, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, and prepared-file save/download handoff now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell | | DEBT-0017 | Open | Modernization | `App::clipboard_get_text`, `App::clipboard_set_text`, `App::show_cursor`, `App::hide_cursor`, `App::showKeyboard`, `App::hideKeyboard`, `App::display_file`, `App::share_file`, native app/window close, UI-thread lifecycle hooks, render-context acquire/release/present hooks, per-frame platform hooks, `App::pick_image`, `App::pick_file`, the non-writer `App::pick_file_save`, `App::pick_dir`, and prepared-file save/download handoff now call the SDK-free `pp::platform::PlatformServices` interface, and Windows injects `WindowsPlatformServices` from `src/platform_windows/windows_platform_services.*`; non-Windows live implementations still use `src/platform_legacy/legacy_platform_services.*`, a named fallback adapter that forwards to retained Apple/Android/Linux/Web bridge functions and retained no-op branches | Preserve behavior while moving platform execution behind a testable service boundary before platform shell implementations are injected | `pp_platform_api_tests`; `pp_app_core_document_platform_io_tests`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Replace `src/platform_legacy/legacy_platform_services.*` with injected `pp_platform_*` service implementations owned by each non-Windows platform shell |
## Closed Debt ## Closed Debt

View File

@@ -470,8 +470,8 @@ app-core clipboard text decisions used by live clipboard get/set requests
before retained platform clipboard bridges continue. before retained platform clipboard bridges continue.
`pp_platform_api` now owns a headless `PlatformServices` interface for `pp_platform_api` now owns a headless `PlatformServices` interface for
clipboard text, cursor visibility, virtual-keyboard visibility, UI-thread clipboard text, cursor visibility, virtual-keyboard visibility, UI-thread
lifecycle hooks, external file display, file sharing, image/file/save-file lifecycle hooks, render-context acquire/release/present hooks, external file
pickers, and directory pickers. display, file sharing, image/file/save-file pickers, and directory pickers.
Windows installs an injected `WindowsPlatformServices` implementation from Windows installs an injected `WindowsPlatformServices` implementation from
`src/platform_windows/windows_platform_services.*` in `pp_platform_windows`; `src/platform_windows/windows_platform_services.*` in `pp_platform_windows`;
other platforms still route through the debt-tracked legacy fallback adapter other platforms still route through the debt-tracked legacy fallback adapter
@@ -495,6 +495,11 @@ The UI thread's platform attach/detach hooks now also dispatch through
`PlatformServices`, preserving Android JNI attach/detach behavior in the `PlatformServices`, preserving Android JNI attach/detach behavior in the
legacy adapter while removing direct Android lifecycle calls from the main app legacy adapter while removing direct Android lifecycle calls from the main app
loop. loop.
The app's render context acquire/release/present path now dispatches through
`PlatformServices` as well. Windows owns WGL acquisition, default framebuffer
rebinding, and swap in `WindowsPlatformServices`; Apple, Android, Linux, and
WebGL behavior is preserved behind the legacy adapter until their platform
shells are injected.
`pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by `pano_cli plan-cloud-upload` exposes the app-core cloud upload decision used by
the live cloud upload command for missing-canvas, new-document warning, publish the live cloud upload command for missing-canvas, new-document warning, publish
prompt, and dirty-document save-before-upload states before legacy UI, canvas, prompt, and dirty-document save-before-upload states before legacy UI, canvas,
@@ -1009,9 +1014,9 @@ Results:
interface for clipboard read/write, empty clipboard writes, cursor interface for clipboard read/write, empty clipboard writes, cursor
visibility dispatch, virtual-keyboard visibility dispatch, external file visibility dispatch, virtual-keyboard visibility dispatch, external file
display dispatch, file sharing dispatch, native app/window close dispatch, display dispatch, file sharing dispatch, native app/window close dispatch,
UI-thread lifecycle dispatch, per-frame platform hook dispatch, picker UI-thread lifecycle dispatch, render-context lifecycle dispatch, per-frame
callback dispatch, and prepared-file save/download callback dispatch. The platform hook dispatch, picker callback dispatch, and prepared-file
live Windows app now save/download callback dispatch. The live Windows app now
consumes this interface through an injected consumes this interface through an injected
`WindowsPlatformServices` instance isolated in `WindowsPlatformServices` instance isolated in
`src/platform_windows/windows_platform_services.*`; other platforms still `src/platform_windows/windows_platform_services.*`; other platforms still

View File

@@ -16,15 +16,7 @@
#endif #endif
#include "settings.h" #include "settings.h"
#ifdef __ANDROID__ #ifdef _WIN32
void android_async_lock();
void android_async_swap();
void android_async_unlock();
#elif _WIN32
bool async_lock_try();
void async_lock();
void win32_async_swap();
void async_unlock();
void win32_renderdoc_frame_start(); void win32_renderdoc_frame_start();
void win32_renderdoc_frame_end(); void win32_renderdoc_frame_end();
#elif __LINUX__ #elif __LINUX__
@@ -609,18 +601,7 @@ void App::init()
void App::async_start() void App::async_start()
{ {
#if __OSX__ acquire_render_context();
[osx_view async_lock];
#elif __IOS__
[ios_view async_lock];
#elif __ANDROID__
android_async_lock();
#elif _WIN32
async_lock();
glBindFramebuffer(framebuffer_target(), default_framebuffer_id());
#elif __LINUX__ || __WEB__
glfwMakeContextCurrent(glfw_window);
#endif
} }
void App::async_redraw() void App::async_redraw()
@@ -631,30 +612,12 @@ void App::async_redraw()
void App::async_end() void App::async_end()
{ {
#if __OSX__ release_render_context();
[osx_view async_unlock];
#elif __IOS__
[ios_view async_unlock];
#elif __ANDROID__
android_async_unlock();
#elif _WIN32
async_unlock();
#endif
} }
void App::async_swap() void App::async_swap()
{ {
#if __OSX__ present_render_context();
[osx_view async_swap];
#elif __IOS__
[ios_view async_swap];
#elif __ANDROID__
android_async_swap();
#elif _WIN32
win32_async_swap();
#elif __LINUX__ || __WEB__
glfwSwapBuffers(glfw_window);
#endif
} }
bool App::update_ui_observer(Node *n) bool App::update_ui_observer(Node *n)

View File

@@ -192,6 +192,9 @@ public:
void request_app_close(); void request_app_close();
void attach_ui_thread(); void attach_ui_thread();
void detach_ui_thread(); void detach_ui_thread();
void acquire_render_context();
void release_render_context();
void present_render_context();
void update_platform_frame(float delta_time_seconds); void update_platform_frame(float delta_time_seconds);
void report_rendered_frames(int frames); void report_rendered_frames(int frames);
void save_prepared_file( void save_prepared_file(

View File

@@ -229,6 +229,21 @@ void App::detach_ui_thread()
active_platform_services().detach_ui_thread(); active_platform_services().detach_ui_thread();
} }
void App::acquire_render_context()
{
active_platform_services().acquire_render_context();
}
void App::release_render_context()
{
active_platform_services().release_render_context();
}
void App::present_render_context()
{
active_platform_services().present_render_context();
}
void App::update_platform_frame(float delta_time_seconds) void App::update_platform_frame(float delta_time_seconds)
{ {
active_platform_services().update_platform_frame(delta_time_seconds); active_platform_services().update_platform_frame(delta_time_seconds);

View File

@@ -20,6 +20,9 @@ public:
virtual void set_virtual_keyboard_visible(bool visible) = 0; virtual void set_virtual_keyboard_visible(bool visible) = 0;
virtual void attach_ui_thread() = 0; virtual void attach_ui_thread() = 0;
virtual void detach_ui_thread() = 0; virtual void detach_ui_thread() = 0;
virtual void acquire_render_context() = 0;
virtual void release_render_context() = 0;
virtual void present_render_context() = 0;
virtual void update_platform_frame(float delta_time_seconds) = 0; virtual void update_platform_frame(float delta_time_seconds) = 0;
virtual void report_rendered_frames(int frames) = 0; virtual void report_rendered_frames(int frames) = 0;
virtual void display_file(std::string_view path) = 0; virtual void display_file(std::string_view path) = 0;

View File

@@ -6,6 +6,9 @@
#ifdef __ANDROID__ #ifdef __ANDROID__
void displayKeyboard(bool pShow); void displayKeyboard(bool pShow);
void android_async_lock();
void android_async_swap();
void android_async_unlock();
void android_attach_jni(); void android_attach_jni();
void android_detach_jni(); void android_detach_jni();
void android_pick_file(std::function<void(std::string)> callback); void android_pick_file(std::function<void(std::string)> callback);
@@ -104,6 +107,43 @@ public:
#endif #endif
} }
void acquire_render_context() override
{
#if __OSX__
[App::I->osx_view async_lock];
#elif __IOS__
[App::I->ios_view async_lock];
#elif __ANDROID__
android_async_lock();
#elif __LINUX__ || __WEB__
glfwMakeContextCurrent(App::I->glfw_window);
#endif
}
void release_render_context() override
{
#if __OSX__
[App::I->osx_view async_unlock];
#elif __IOS__
[App::I->ios_view async_unlock];
#elif __ANDROID__
android_async_unlock();
#endif
}
void present_render_context() override
{
#if __OSX__
[App::I->osx_view async_swap];
#elif __IOS__
[App::I->ios_view async_swap];
#elif __ANDROID__
android_async_swap();
#elif __LINUX__ || __WEB__
glfwSwapBuffers(App::I->glfw_window);
#endif
}
void update_platform_frame(float delta_time_seconds) override void update_platform_frame(float delta_time_seconds) override
{ {
(void)delta_time_seconds; (void)delta_time_seconds;

View File

@@ -1,6 +1,8 @@
#include "pch.h" #include "pch.h"
#include "platform_windows/windows_platform_services.h" #include "platform_windows/windows_platform_services.h"
#include "renderer_gl/opengl_capabilities.h"
#include <deque> #include <deque>
extern HWND hWnd; extern HWND hWnd;
@@ -8,6 +10,9 @@ extern std::deque<std::packaged_task<void()>> main_tasklist;
extern std::mutex main_task_mutex; extern std::mutex main_task_mutex;
void destroy_window(); void destroy_window();
void async_lock();
void async_unlock();
void win32_async_swap();
void win32_update_fps(int frames); void win32_update_fps(int frames);
void win32_update_stylus(float dt); void win32_update_stylus(float dt);
@@ -178,6 +183,24 @@ public:
{ {
} }
void acquire_render_context() override
{
async_lock();
glBindFramebuffer(
static_cast<GLenum>(pp::renderer::gl::framebuffer_target()),
static_cast<GLuint>(pp::renderer::gl::default_framebuffer_id()));
}
void release_render_context() override
{
async_unlock();
}
void present_render_context() override
{
win32_async_swap();
}
void update_platform_frame(float delta_time_seconds) override void update_platform_frame(float delta_time_seconds) override
{ {
win32_update_stylus(delta_time_seconds); win32_update_stylus(delta_time_seconds);

View File

@@ -50,6 +50,21 @@ public:
++ui_thread_detaches; ++ui_thread_detaches;
} }
void acquire_render_context() override
{
++render_context_acquires;
}
void release_render_context() override
{
++render_context_releases;
}
void present_render_context() override
{
++render_context_presents;
}
void update_platform_frame(float delta_time_seconds) override void update_platform_frame(float delta_time_seconds) override
{ {
++platform_frame_updates; ++platform_frame_updates;
@@ -122,6 +137,9 @@ public:
int keyboard_updates = 0; int keyboard_updates = 0;
int ui_thread_attaches = 0; int ui_thread_attaches = 0;
int ui_thread_detaches = 0; int ui_thread_detaches = 0;
int render_context_acquires = 0;
int render_context_releases = 0;
int render_context_presents = 0;
int platform_frame_updates = 0; int platform_frame_updates = 0;
int frame_reports = 0; int frame_reports = 0;
int display_file_requests = 0; int display_file_requests = 0;
@@ -201,6 +219,20 @@ void platform_services_dispatch_ui_thread_lifecycle(pp::tests::Harness& harness)
PP_EXPECT(harness, fake.ui_thread_detaches == 1); PP_EXPECT(harness, fake.ui_thread_detaches == 1);
} }
void platform_services_dispatch_render_context_lifecycle(pp::tests::Harness& harness)
{
FakePlatformServices fake("unused");
pp::platform::PlatformServices& services = fake;
services.acquire_render_context();
services.present_render_context();
services.release_render_context();
PP_EXPECT(harness, fake.render_context_acquires == 1);
PP_EXPECT(harness, fake.render_context_presents == 1);
PP_EXPECT(harness, fake.render_context_releases == 1);
}
void platform_services_dispatch_frame_hooks(pp::tests::Harness& harness) void platform_services_dispatch_frame_hooks(pp::tests::Harness& harness)
{ {
FakePlatformServices fake("unused"); FakePlatformServices fake("unused");
@@ -288,6 +320,7 @@ int main()
harness.run("platform services preserve empty clipboard writes", platform_services_preserve_empty_clipboard_writes); harness.run("platform services preserve empty clipboard writes", platform_services_preserve_empty_clipboard_writes);
harness.run("platform services dispatch visibility updates", platform_services_dispatch_visibility_updates); harness.run("platform services dispatch visibility updates", platform_services_dispatch_visibility_updates);
harness.run("platform services dispatch UI thread lifecycle", platform_services_dispatch_ui_thread_lifecycle); harness.run("platform services dispatch UI thread lifecycle", platform_services_dispatch_ui_thread_lifecycle);
harness.run("platform services dispatch render context lifecycle", platform_services_dispatch_render_context_lifecycle);
harness.run("platform services dispatch frame hooks", platform_services_dispatch_frame_hooks); harness.run("platform services dispatch frame hooks", platform_services_dispatch_frame_hooks);
harness.run("platform services dispatch file actions", platform_services_dispatch_file_actions); harness.run("platform services dispatch file actions", platform_services_dispatch_file_actions);
harness.run("platform services dispatch picker callbacks", platform_services_dispatch_picker_callbacks); harness.run("platform services dispatch picker callbacks", platform_services_dispatch_picker_callbacks);