Centralize legacy recording bridge

This commit is contained in:
2026-06-04 12:58:27 +02:00
parent 65e9fdf1b9
commit a9ed201adf
9 changed files with 351 additions and 40 deletions

View File

@@ -68,6 +68,8 @@ set(PP_LEGACY_APP_SOURCES
src/legacy_document_layer_services.h
src/legacy_history_services.cpp
src/legacy_history_services.h
src/legacy_recording_services.cpp
src/legacy_recording_services.h
src/pch.cpp
)

View File

@@ -476,6 +476,11 @@ Known local toolchain state:
stop, clear, platform cleanup, frame-count reset, and export progress-total
planning as JSON; the live recording controls consume those contracts before
reaching legacy recording threads, PBO readback, and MP4 encoder execution.
- `src/legacy_recording_services.*` is the current app-shell bridge for
recording start/stop/clear and MP4 export execution. It keeps those live paths
on the `pp_app_core` contracts while legacy recording thread ownership, PBO
readback, progress UI, platform cleanup, and `MP4Encoder` execution remain
tracked by `DEBT-0037`.
- `pano_cli plan-share-file` exposes `pp_app_core` share availability planning
as JSON for unsaved and saved document paths; the live platform share command
consumes the same contract before reaching iOS/macOS sharing bridges or

View File

@@ -54,6 +54,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0034 | Open | Modernization | About menu command planning and execution dispatch now consume pure `pp_app_core` through `App::init_menu_about`, `pano_cli plan-about-menu`, and the `AboutMenuServices` boundary, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy About/manual/what's-new dialogs, invokes the injected crash hook, and runs the legacy Canvas stroke performance test directly | Preserve About menu behavior while dialogs and diagnostics move toward app/UI/platform services | `pp_app_core_about_menu_tests`; `pano_cli plan-about-menu --command news --version-major 2 --version-minor 5 --version-fix 7`; `pano_cli plan-about-menu --command performance --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | About/manual/what's-new dialog dispatch, crash-test dispatch, and performance-test execution are owned by injected app/UI/platform services with `App::init_menu_about` acting only as a UI adapter and no legacy About adapter |
| DEBT-0035 | Open | Modernization | Main toolbar/status command planning and execution dispatch now consume pure `pp_app_core` through `App::init_toolbar_main`, `pano_cli plan-main-toolbar`, and the `MainToolbarServices` boundary, history/canvas commands now hand off through `HistoryUiServices` and `DocumentCanvasClearServices`, and live execution is centralized in `src/legacy_app_shell_services.*`, but the bridge still opens legacy open/save/settings/message-box dialogs and delegates to legacy history/canvas adapters | Preserve reachable toolbar/status behavior while app shell commands move toward app/document/UI services | `pp_app_core_main_toolbar_tests`; `pano_cli plan-main-toolbar --command undo --undo-count 2`; `pano_cli plan-main-toolbar --command clear-canvas --no-canvas`; `ctest --preset desktop-fast --build-config Debug` | Open/save/settings/message-box routing, undo/redo/clear-history execution, and canvas-clear execution are owned by injected app/document/UI services with `App::init_toolbar_main` acting only as a UI adapter and no legacy toolbar adapter |
| DEBT-0036 | Open | Modernization | `pp_renderer_api`, `pp_paint_renderer`, `pano_cli plan-paint-feedback`, and `pano_cli plan-stroke-composite` can choose backend-neutral complex paint feedback strategies for fixed-function blending, framebuffer-fetch-capable renderers, or ping-pong render targets. OpenGL extension detection now stores `pp::renderer::RenderDeviceFeatures` through `ShaderManager`, using `pp_renderer_gl::render_device_features` as the backend conversion point. `pp_paint_renderer::plan_canvas_blend_gate` owns the compatibility mapping from persisted layer/brush blend indices to the extracted stroke-composite planner, and live `Canvas::draw_merge` plus `NodeCanvas` panorama rendering both call it with the stored renderer-neutral feature set for their existing shader-blend gates and destination-copy versus framebuffer-fetch decisions. `pp_paint_renderer::plan_canvas_stroke_feedback` also owns the current destination-feedback decision, and live `Canvas::stroke_draw`, thumbnail layer blending, and `NodeStrokePreview` brush-preview rendering use it for framebuffer-fetch versus destination-copy decisions. Actual live stroke rasterization, dual-brush compositing, pattern feedback math, thumbnail layer compositing, and brush-preview compositing still use legacy OpenGL canvas/UI execution | Preserve current painting behavior while the renderer boundary matures for OpenGL parity and later Vulkan/Metal experiments | `pp_renderer_api_tests`; `pp_renderer_gl_capabilities_tests`; `pp_paint_renderer_compositor_tests`; `pano_cli plan-paint-feedback --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-paint-feedback --texture-copy`; `pano_cli plan-stroke-composite --stroke-blend 10 --framebuffer-fetch --explicit-transitions --render-only`; `pano_cli plan-stroke-composite --layer-blend 4 --dual-blend --texture-copy`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Live stroke/layer compositing chooses its feedback path through `pp_paint_renderer` and renderer services, with OpenGL golden parity and Vulkan/Metal lab tests covering framebuffer-fetch and ping-pong behavior |
| DEBT-0037 | Open | Modernization | Recording lifecycle/export planning and execution dispatch now consume pure `pp_app_core` through `App::rec_start`, `App::rec_stop`, `App::rec_clear`, `App::rec_export`, `pano_cli plan-recording-session`, and the `RecordingServices` boundary; live execution is centralized in `src/legacy_recording_services.*`, but the bridge still owns legacy recording thread startup/shutdown, platform recorded-file cleanup, progress UI, PBO readback through `App::rec_loop`, and `MP4Encoder::write_mp4` execution | Preserve current timelapse/MP4 behavior while recording moves toward app/document/renderer/video services | `pp_app_core_document_recording_tests`; `pano_cli plan-recording-session --running --frame-count 12`; `pano_cli plan-recording-session --platform-clears-files`; `ctest --preset desktop-fast --build-config Debug` | Recording thread lifecycle, frame readback, platform cleanup, progress reporting, and MP4 writing are owned by injected app/renderer/video services with `App` methods acting only as adapters |
## Closed Debt

View File

@@ -463,8 +463,10 @@ plus MP4 animation and timelapse export dialogs before they call legacy
canvas/recording export execution.
`pano_cli plan-recording-session` exposes the app-core recording start, stop,
clear, platform recorded-file cleanup, frame reset, and export progress-total
decisions used by the live recording controls before legacy recording threads,
PBO readback, and MP4 encoder execution continue.
decisions used by the live recording controls. Recording lifecycle and MP4
export execution now dispatch through `RecordingServices` in
`src/legacy_recording_services.*` before legacy recording threads, PBO
readback, and MP4 encoder execution continue.
`pano_cli plan-share-file` exposes the app-core saved-path decision used by the
live platform share command before iOS/macOS sharing bridges or retained no-op
platform branches execute.

View File

@@ -11,6 +11,7 @@
#include "app_core/document_route.h"
#include "app_core/document_session.h"
#include "legacy_history_services.h"
#include "legacy_recording_services.h"
#include "platform_api/platform_services.h"
#include "renderer_gl/opengl_capabilities.h"
@@ -767,56 +768,30 @@ void App::rec_clear()
rec_running,
platform_deletes_recorded_files_on_clear()
);
if (plan.stop_running_recording)
rec_stop();
if (plan.delete_recorded_files)
clear_platform_recorded_files(rec_path);
rec_count = plan.frame_count_after_clear;
update_rec_frames();
const auto status = pp::panopainter::execute_legacy_recording_clear_plan(*this, plan);
if (!status.ok())
LOG("Recording clear action failed: %s", status.message);
}
void App::rec_start()
{
const auto plan = pp::app::plan_recording_start(rec_running);
switch (plan)
{
case pp::app::RecordingStartAction::start_thread:
break;
case pp::app::RecordingStartAction::no_op_already_running:
return;
}
update_rec_frames();
rec_thread = std::thread(&App::rec_loop, this);
const auto status = pp::panopainter::execute_legacy_recording_start_action(*this, plan);
if (!status.ok())
LOG("Recording start action failed: %s", status.message);
}
void App::rec_stop()
{
const auto plan = pp::app::plan_recording_stop(rec_running);
switch (plan)
{
case pp::app::RecordingStopAction::stop_thread:
break;
case pp::app::RecordingStopAction::no_op_not_running:
return;
}
rec_running = false;
rec_cv.notify_all();
if (rec_thread.joinable())
rec_thread.join();
update_rec_frames();
const auto status = pp::panopainter::execute_legacy_recording_stop_action(*this, plan);
if (!status.ok())
LOG("Recording stop action failed: %s", status.message);
}
void App::rec_export(std::string path)
{
const auto plan = pp::app::plan_recording_export(static_cast<std::size_t>(rec_count));
auto pb = layout[main_id]->add_child<NodeProgressBar>();
pb->m_progress->SetWidthP(0);
pb->m_title->set_text("Exporting MP4 movie");
pb->m_total = plan.progress_total;
pb->m_count = 0;
/*
#if defined(__IOS__) || defined(__OSX__)
export_mp4(rec_path, width, height, rec_count, ^(float) {
@@ -824,9 +799,9 @@ void App::rec_export(std::string path)
});
#endif
*/
Canvas::I->m_encoder->write_mp4(path);
pb->destroy();
const auto status = pp::panopainter::execute_legacy_recording_export_plan(*this, plan, path);
if (!status.ok())
LOG("Recording export action failed: %s", status.message);
}
void App::rec_loop()

View File

@@ -1,7 +1,10 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <limits>
#include <string_view>
namespace pp::app {
@@ -26,6 +29,20 @@ struct RecordingExportPlan {
int progress_total = 0;
};
class RecordingServices {
public:
virtual ~RecordingServices() = default;
virtual void start_thread() = 0;
virtual void stop_thread() = 0;
virtual void delete_recorded_files() = 0;
virtual void set_frame_count(int frame_count) = 0;
virtual void update_frame_label() = 0;
virtual void begin_export(int progress_total) = 0;
virtual void write_mp4(std::string_view path) = 0;
virtual void end_export() = 0;
};
[[nodiscard]] constexpr RecordingStartAction plan_recording_start(bool is_running) noexcept
{
return is_running
@@ -60,4 +77,60 @@ struct RecordingExportPlan {
};
}
[[nodiscard]] inline pp::foundation::Status execute_recording_start_action(
RecordingStartAction action,
RecordingServices& services)
{
switch (action) {
case RecordingStartAction::start_thread:
services.start_thread();
return pp::foundation::Status::success();
case RecordingStartAction::no_op_already_running:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown recording start action");
}
[[nodiscard]] inline pp::foundation::Status execute_recording_stop_action(
RecordingStopAction action,
RecordingServices& services)
{
switch (action) {
case RecordingStopAction::stop_thread:
services.stop_thread();
return pp::foundation::Status::success();
case RecordingStopAction::no_op_not_running:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown recording stop action");
}
[[nodiscard]] inline pp::foundation::Status execute_recording_clear_plan(
const RecordingClearPlan& plan,
RecordingServices& services)
{
if (plan.stop_running_recording) {
services.stop_thread();
}
if (plan.delete_recorded_files) {
services.delete_recorded_files();
}
services.set_frame_count(plan.frame_count_after_clear);
services.update_frame_label();
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_recording_export_plan(
const RecordingExportPlan& plan,
RecordingServices& services,
std::string_view path)
{
services.begin_export(plan.progress_total);
services.write_mp4(path);
services.end_export();
return pp::foundation::Status::success();
}
}

View File

@@ -0,0 +1,110 @@
#include "pch.h"
#include "legacy_recording_services.h"
#include "app.h"
#include "canvas.h"
#include "node_progress_bar.h"
namespace pp::panopainter {
namespace {
class LegacyRecordingServices final : public pp::app::RecordingServices {
public:
explicit LegacyRecordingServices(App& app) noexcept
: app_(app)
{
}
void start_thread() override
{
app_.update_rec_frames();
app_.rec_thread = std::thread(&App::rec_loop, &app_);
}
void stop_thread() override
{
app_.rec_running = false;
app_.rec_cv.notify_all();
if (app_.rec_thread.joinable())
app_.rec_thread.join();
app_.update_rec_frames();
}
void delete_recorded_files() override
{
app_.clear_platform_recorded_files(app_.rec_path);
}
void set_frame_count(int frame_count) override
{
app_.rec_count = frame_count;
}
void update_frame_label() override
{
app_.update_rec_frames();
}
void begin_export(int progress_total) override
{
progress_ = app_.layout[app_.main_id]->add_child<NodeProgressBar>();
progress_->m_progress->SetWidthP(0);
progress_->m_title->set_text("Exporting MP4 movie");
progress_->m_total = progress_total;
progress_->m_count = 0;
}
void write_mp4(std::string_view path) override
{
Canvas::I->m_encoder->write_mp4(std::string(path));
}
void end_export() override
{
if (progress_)
progress_->destroy();
progress_ = nullptr;
}
private:
App& app_;
NodeProgressBar* progress_ = nullptr;
};
} // namespace
pp::foundation::Status execute_legacy_recording_start_action(
App& app,
pp::app::RecordingStartAction action)
{
LegacyRecordingServices services(app);
return pp::app::execute_recording_start_action(action, services);
}
pp::foundation::Status execute_legacy_recording_stop_action(
App& app,
pp::app::RecordingStopAction action)
{
LegacyRecordingServices services(app);
return pp::app::execute_recording_stop_action(action, services);
}
pp::foundation::Status execute_legacy_recording_clear_plan(
App& app,
const pp::app::RecordingClearPlan& plan)
{
LegacyRecordingServices services(app);
return pp::app::execute_recording_clear_plan(plan, services);
}
pp::foundation::Status execute_legacy_recording_export_plan(
App& app,
const pp::app::RecordingExportPlan& plan,
std::string_view path)
{
LegacyRecordingServices services(app);
return pp::app::execute_recording_export_plan(plan, services, path);
}
} // namespace pp::panopainter

View File

@@ -0,0 +1,26 @@
#pragma once
#include "app_core/document_recording.h"
#include "foundation/result.h"
#include <string_view>
class App;
namespace pp::panopainter {
[[nodiscard]] pp::foundation::Status execute_legacy_recording_start_action(
App& app,
pp::app::RecordingStartAction action);
[[nodiscard]] pp::foundation::Status execute_legacy_recording_stop_action(
App& app,
pp::app::RecordingStopAction action);
[[nodiscard]] pp::foundation::Status execute_legacy_recording_clear_plan(
App& app,
const pp::app::RecordingClearPlan& plan);
[[nodiscard]] pp::foundation::Status execute_legacy_recording_export_plan(
App& app,
const pp::app::RecordingExportPlan& plan,
std::string_view path);
} // namespace pp::panopainter

View File

@@ -2,9 +2,75 @@
#include "test_harness.h"
#include <limits>
#include <string>
namespace {
class FakeRecordingServices final : public pp::app::RecordingServices {
public:
void start_thread() override
{
starts += 1;
call_order += "start;";
}
void stop_thread() override
{
stops += 1;
call_order += "stop;";
}
void delete_recorded_files() override
{
deletes += 1;
call_order += "delete;";
}
void set_frame_count(int frame_count) override
{
frame_count_value = frame_count;
call_order += "count;";
}
void update_frame_label() override
{
label_updates += 1;
call_order += "label;";
}
void begin_export(int progress_total) override
{
export_begins += 1;
export_progress_total = progress_total;
call_order += "begin;";
}
void write_mp4(std::string_view path) override
{
export_writes += 1;
export_path = std::string(path);
call_order += "write;";
}
void end_export() override
{
export_ends += 1;
call_order += "end;";
}
int starts = 0;
int stops = 0;
int deletes = 0;
int label_updates = 0;
int frame_count_value = -1;
int export_begins = 0;
int export_writes = 0;
int export_ends = 0;
int export_progress_total = 0;
std::string export_path;
std::string call_order;
};
void recording_start_only_starts_when_not_running(pp::tests::Harness& harness)
{
PP_EXPECT(
@@ -53,6 +119,55 @@ void recording_export_clamps_progress_total(pp::tests::Harness& harness)
PP_EXPECT(harness, plan.progress_total == std::numeric_limits<int>::max());
}
void executor_dispatches_recording_lifecycle(pp::tests::Harness& harness)
{
FakeRecordingServices services;
PP_EXPECT(
harness,
pp::app::execute_recording_start_action(pp::app::RecordingStartAction::start_thread, services).ok());
PP_EXPECT(
harness,
pp::app::execute_recording_stop_action(pp::app::RecordingStopAction::stop_thread, services).ok());
PP_EXPECT(
harness,
pp::app::execute_recording_start_action(
pp::app::RecordingStartAction::no_op_already_running,
services)
.ok());
PP_EXPECT(
harness,
pp::app::execute_recording_stop_action(pp::app::RecordingStopAction::no_op_not_running, services).ok());
PP_EXPECT(harness, services.starts == 1);
PP_EXPECT(harness, services.stops == 1);
PP_EXPECT(harness, services.call_order == "start;stop;");
}
void executor_dispatches_recording_clear_and_export(pp::tests::Harness& harness)
{
FakeRecordingServices services;
const auto clear = pp::app::plan_recording_clear(true, true);
PP_EXPECT(harness, pp::app::execute_recording_clear_plan(clear, services).ok());
const auto export_plan = pp::app::plan_recording_export(12);
PP_EXPECT(
harness,
pp::app::execute_recording_export_plan(export_plan, services, "D:/Paint/out.mp4").ok());
PP_EXPECT(harness, services.stops == 1);
PP_EXPECT(harness, services.deletes == 1);
PP_EXPECT(harness, services.frame_count_value == 0);
PP_EXPECT(harness, services.label_updates == 1);
PP_EXPECT(harness, services.export_begins == 1);
PP_EXPECT(harness, services.export_progress_total == 12);
PP_EXPECT(harness, services.export_writes == 1);
PP_EXPECT(harness, services.export_path == "D:/Paint/out.mp4");
PP_EXPECT(harness, services.export_ends == 1);
PP_EXPECT(harness, services.call_order == "stop;delete;count;label;begin;write;end;");
}
}
int main()
@@ -65,5 +180,7 @@ int main()
recording_clear_resets_frames_and_preserves_platform_delete_flag);
harness.run("recording export tracks frame count", recording_export_tracks_frame_count);
harness.run("recording export clamps progress total", recording_export_clamps_progress_total);
harness.run("executor dispatches recording lifecycle", executor_dispatches_recording_lifecycle);
harness.run("executor dispatches recording clear and export", executor_dispatches_recording_clear_and_export);
return harness.finish();
}