Advance app runtime ownership and modernization docs
This commit is contained in:
16
AGENTS.md
16
AGENTS.md
@@ -30,11 +30,12 @@ Read these first:
|
||||
- Commit and push each verified successful progress slice.
|
||||
- Prefer larger coherent slices over tiny checkpoints, but keep docs/debt updated
|
||||
with each slice.
|
||||
- After a verified slice is committed and pushed, reset conversation context
|
||||
before starting the next slice when practical, especially if the thread is
|
||||
approaching automatic compaction. Record all needed resume state in committed
|
||||
code/docs first so the next thread can restart from `AGENTS.md`, roadmap/debt,
|
||||
and git history instead of relying on chat transcript context.
|
||||
- Treat automatic compaction as a failure mode to avoid. Keep active context
|
||||
small, commit and push before the thread grows large, and reset conversation
|
||||
context between verified slices when practical instead of carrying excess
|
||||
history forward. Record all needed resume state in committed code/docs first
|
||||
so the next thread can restart from `AGENTS.md`, roadmap/debt, and git
|
||||
history instead of relying on chat transcript context.
|
||||
- Do not revert user changes. Unrelated untracked notes, such as
|
||||
`docs/human-review-notes.md`, should be left alone unless explicitly requested.
|
||||
- Use CMake as the source of truth. Legacy Visual Studio project files are not the
|
||||
@@ -42,8 +43,9 @@ Read these first:
|
||||
- Use `apply_patch` for manual source/doc edits.
|
||||
- For delegated work, follow `docs/modernization/director-workflow.md`: the
|
||||
coordinator keeps integration locally, assigns direct worker tasks, uses
|
||||
`gpt-5.3-codex-spark` workers by default, and gives them the exact project
|
||||
context needed so they do not spend tokens re-reading repo docs.
|
||||
`gpt-5.4-mini` workers by default, and gives them a minimal task packet with
|
||||
only the build, test, and code-exploration context needed so they do not
|
||||
spend tokens re-reading repo docs.
|
||||
|
||||
## Build And Test
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ set(PP_PLATFORM_LINUX_SOURCES
|
||||
|
||||
set(PP_PANOPAINTER_APP_SOURCES
|
||||
src/app.cpp
|
||||
src/app_runtime.cpp
|
||||
src/app_cloud.cpp
|
||||
src/app_commands.cpp
|
||||
src/app_dialogs.cpp
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# PanoPainter Capability Map
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-05
|
||||
Last updated: 2026-06-16
|
||||
|
||||
This map is the preservation checklist for the modernization. When a component
|
||||
is extracted, update the relevant rows with the owning component, test label,
|
||||
@@ -59,6 +59,7 @@ and validation command.
|
||||
| Yoga layout | `Node` | `pp_ui_core` | Deterministic geometry fixtures |
|
||||
| Generic controls | `NodeButton`, sliders, text, images | `pp_ui_core` | Event dispatch, layout, ownership-handle, callback-disconnect, and destroy-during-callback tests |
|
||||
| PanoPainter panels/dialogs | `NodePanel*`, `NodeDialog*` | `pp_panopainter_ui`, `pp_ui_core` | UI automation scripts, command-dispatch view models, pure overlay lifetime tests, retained overlay-adapter build coverage, retained popup/dialog lifetime tests |
|
||||
| UI ownership and thread affinity | `Node`, `LayoutManager`, `App` UI queue, retained callbacks | `pp_ui_core`, app runtime service, `pp_panopainter_ui` | Checked-handle dispatch, scoped callback disconnect, destroy-during-callback, close-during-dispatch, and UI-thread post/drain/shutdown coverage |
|
||||
| Canvas viewport UI | `NodeCanvas` | `pp_panopainter_ui`, `pp_paint_renderer` | Input-to-command automation |
|
||||
| Settings UI | `Settings`, `NodeSettings` | `pp_assets`, `pp_panopainter_ui` | Round-trip settings |
|
||||
|
||||
@@ -67,6 +68,7 @@ and validation command.
|
||||
| Capability | Current Area | Target Owner | Required Tests |
|
||||
| --- | --- | --- | --- |
|
||||
| Mouse/keyboard/touch/gestures/cursor | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*`, app | Cursor visibility decision tests, platform service dispatch tests, synthetic event playback |
|
||||
| Render/UI task dispatch and worker shutdown | `App`, `Canvas`, retained worker threads, platform entrypoints | app runtime service, `pp_foundation`, `pp_platform_*` | Render/UI queue order, same-thread dispatch, cancellation, shutdown drain, and no-detached-worker ownership coverage |
|
||||
| Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback |
|
||||
| Clipboard/file picker/share/display | `App` platform methods | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Clipboard read/write, share saved-path, picked-path, and display-file decision tests, platform service display/share/picker dispatch tests, platform smoke or mocked service |
|
||||
| Virtual keyboard | `App`, platform entrypoints | `pp_app_core`, `pp_platform_api`, `pp_platform_*` | Keyboard visibility decision tests, platform service dispatch tests, platform smoke |
|
||||
|
||||
@@ -335,6 +335,10 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
routes the stroke preset/brush/dual-brush/pattern popup open-close flow through
|
||||
checked overlay handles and explicit partial-open cleanup; migration is still
|
||||
pending in remaining panel families.
|
||||
- 2026-06-16: `DEBT-0063` was narrowed again. `src/node_panel_stroke.cpp` now
|
||||
routes stroke popup close/reset through a shared overlay helper, and
|
||||
`src/node_popup_menu.cpp` now keeps the popup alive through selection dispatch
|
||||
with scoped ownership instead of raw `this` callbacks.
|
||||
- 2026-06-14: `DEBT-0063` was narrowed again. `src/node_panel_brush.cpp` now
|
||||
routes brush popup-menu open-close flow through checked overlay handles and
|
||||
handle-based close on selection.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Modernization Coordinator Workflow
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-06-14
|
||||
Last updated: 2026-06-16
|
||||
|
||||
Use this workflow when the user explicitly asks for subagents, delegation, a
|
||||
coordinator, or parallel agent work. Do not spawn subagents just because a task
|
||||
@@ -16,10 +16,16 @@ layer.
|
||||
|
||||
- Save main-thread tokens by keeping implementation and focused lookup out of
|
||||
the coordinator context.
|
||||
- Treat thread compaction as wasted budget. Prefer small active context,
|
||||
committed resume state, and fresh follow-on threads over carrying long stale
|
||||
history.
|
||||
- Keep each implementation slice measurable, validated, committed, and pushed.
|
||||
- Avoid merge conflicts by giving every worker a disjoint task and file scope.
|
||||
- Keep workers stateless between tasks: when a worker finishes, integrate or
|
||||
reject the result, then clear that worker context before the next assignment.
|
||||
- Do not leave workers idle. Either give a worker another coherent follow-on
|
||||
task while its context is still useful, or close it promptly.
|
||||
- Reuse worker context only for closely related follow-on tasks in the same
|
||||
local area; otherwise close the worker and start a fresh one with a new
|
||||
minimal packet.
|
||||
- Keep prompts dense with the exact project/task context workers need so they do
|
||||
not spend tokens re-reading repo docs or re-parsing broad areas.
|
||||
- Keep communication terse: no fillers, no cheerleading, no narrative padding.
|
||||
@@ -40,7 +46,8 @@ The coordinator is the main agent in the user thread. The coordinator owns:
|
||||
- running final validation
|
||||
- updating docs/debt/tasks
|
||||
- committing and pushing the verified slice
|
||||
- resetting worker context after each completed or rejected task
|
||||
- either assigning coherent follow-on work to an active worker or closing that
|
||||
worker once its useful context window is over
|
||||
|
||||
The coordinator should keep local work minimal. It may do a quick blocking
|
||||
check before delegation, such as reading task rows, checking git status, or
|
||||
@@ -49,13 +56,16 @@ code or test work, delegate it directly to a worker whenever the scope can be
|
||||
made clear.
|
||||
|
||||
The coordinator must front-load context. Workers should not be told to "read
|
||||
the roadmap" or "inspect the repo" unless that is the task. The coordinator is
|
||||
responsible for extracting and passing:
|
||||
the roadmap", "read AGENTS.md", or "inspect the repo" unless that is the task.
|
||||
The coordinator is responsible for extracting and passing:
|
||||
|
||||
- task ids and done checks
|
||||
- debt ids and removal conditions that matter
|
||||
- exact write scope and allowed read scope
|
||||
- required validation commands
|
||||
- the specific build/test preset or target names the worker may need
|
||||
- the exact code-exploration tools to use for the slice, such as `rg` or the
|
||||
compiler-aware `clangd_nav.py` helper
|
||||
- relevant file paths, code references, and current behavior notes
|
||||
- any repo rules or user constraints that materially affect the task
|
||||
|
||||
@@ -68,6 +78,11 @@ Workers do not own repo discovery. They start from the coordinator-provided
|
||||
context packet and stay inside the assigned scope unless they hit a blocker that
|
||||
requires a narrow follow-up question.
|
||||
|
||||
Workers may be kept alive for more than one assignment only when the next task
|
||||
is coherent with the current one: same subsystem, overlapping read scope,
|
||||
similar validation path, and no avoidable context rebuild. Do not keep workers
|
||||
alive just because a slot is available.
|
||||
|
||||
Every worker and explorer must be told:
|
||||
|
||||
- this repository may have other agents working in parallel
|
||||
@@ -80,13 +95,13 @@ Every worker and explorer must be told:
|
||||
|
||||
| Work Type | Model | Reasoning Effort | Use |
|
||||
| --- | --- | --- | --- |
|
||||
| Coordinator orchestration and integration | inherited main-thread model or `gpt-5.4-mini` | `low` or `medium` | Scope selection, task routing, conflict checks, validation, docs/debt updates, commits, pushes. |
|
||||
| Direct worker coding task | `gpt-5.3-codex-spark` | `medium` | Bounded implementation in known files with coordinator-supplied context. |
|
||||
| Direct worker lookup or inventory | `gpt-5.3-codex-spark` | `low` or `medium` | `rg` inventory, file ownership map, simple grep-based answers. |
|
||||
| Mechanical docs cleanup | `gpt-5.3-codex-spark` | `low` | Formatting, table updates, command normalization. |
|
||||
| Coordinator orchestration and integration | `gpt-5.4` | `low` or `medium` | Scope selection, task routing, conflict checks, validation, docs/debt updates, commits, pushes, and worker packet preparation. |
|
||||
| Direct worker coding task | `gpt-5.4-mini` | `medium` | Bounded implementation in known files with coordinator-supplied context. |
|
||||
| Direct worker lookup or inventory | `gpt-5.4-mini` | `low` or `medium` | `rg` inventory, file ownership map, simple grep-based answers. |
|
||||
| Mechanical docs cleanup | `gpt-5.4-mini` | `low` | Formatting, table updates, command normalization. |
|
||||
| Coordinator-only escalation | inherited higher model only when explicitly justified | inherited | Resolve architecture ambiguity, conflict integration, or task decomposition failures without adding a captain layer. |
|
||||
|
||||
Workers default to `gpt-5.3-codex-spark`. If a task looks too broad or risky
|
||||
Workers default to `gpt-5.4-mini`. If a task looks too broad or risky
|
||||
for that model, the coordinator should decompose it further or keep the narrow
|
||||
integration step locally instead of inserting an extra management tier.
|
||||
|
||||
@@ -96,33 +111,46 @@ integration step locally instead of inserting an extra management tier.
|
||||
ids, validation commands, and only the necessary excerpts.
|
||||
- Use `fork_context=true` only when prior conversation details are essential and
|
||||
not already captured in the worker prompt.
|
||||
- Do not let the coordinator thread drift toward compaction. Once a verified
|
||||
slice is committed and pushed, prefer a fresh thread for the next slice if
|
||||
the remaining context is no longer tight.
|
||||
- Do not paste large logs into prompts. Point workers at log paths and ask for
|
||||
the smallest relevant excerpt.
|
||||
- Do not ask workers to broadly read `AGENTS.md`, the roadmap, or the debt log.
|
||||
Summarize the exact rules and rows they need.
|
||||
- Do not ask workers to broadly read `AGENTS.md`, the roadmap, the debt log, or
|
||||
other repo-wide docs. Summarize the exact rules and rows they need.
|
||||
- Default worker context to a minimal operating packet: task id, assigned file
|
||||
scope, build command, test command, and code-exploration command hints.
|
||||
- Keep worker prompts compact but complete. Shorter is good only if it still
|
||||
removes the need for worker-side repo rediscovery.
|
||||
- Ask for compact final reports: changed files, result, validation, blockers,
|
||||
next recommendation.
|
||||
- Close completed workers after their results are integrated or rejected.
|
||||
- Keep active workers busy with another coherent task when that is cheaper than
|
||||
restarting context; otherwise close them immediately after integration.
|
||||
- Close workers that are done, blocked, or no longer have a strong context
|
||||
advantage so they do not accumulate and saturate worker slots.
|
||||
- Prefer the smallest number of concurrent workers that keeps disjoint work
|
||||
moving.
|
||||
- Use rolling integration: wait for whichever worker finishes first, process the
|
||||
result, then launch the next disjoint worker with a fresh context window.
|
||||
result, then either reuse that worker for the next coherent slice or close it
|
||||
before launching a fresh worker for unrelated work.
|
||||
- Prefer committed repo state over chat history as the handoff mechanism between
|
||||
slices so worker and coordinator prompts stay short.
|
||||
|
||||
## Delegation Flow
|
||||
|
||||
1. Coordinator picks one or more `Ready` tasks from
|
||||
`docs/modernization/tasks.md` with disjoint write scopes.
|
||||
2. Coordinator splits each task into direct worker-sized units.
|
||||
2. Coordinator splits each task into direct worker-sized units, grouping
|
||||
coherent follow-on work when one worker can finish it efficiently without
|
||||
broadening scope.
|
||||
3. Coordinator prepares a context packet for each worker with the exact task
|
||||
requirements, file scope, validation commands, and relevant project details.
|
||||
4. Coordinator assigns the task directly to a `gpt-5.3-codex-spark` worker or
|
||||
4. Coordinator assigns the task directly to a `gpt-5.4-mini` worker or
|
||||
explorer.
|
||||
5. Worker returns changed files, validation, blockers, and any narrow
|
||||
integration notes.
|
||||
6. Coordinator reviews for scope conflicts, integrates the result, and clears
|
||||
that worker context before assigning the next task.
|
||||
6. Coordinator reviews for scope conflicts, integrates the result, and decides
|
||||
whether to give that same worker another coherent task or close it.
|
||||
7. Coordinator runs the listed validation command or the quiet checkpoint
|
||||
wrapper for each integrated slice.
|
||||
8. Coordinator updates `tasks.md`, `debt.md`, and `roadmap.md` if task state or
|
||||
@@ -132,7 +160,7 @@ integration step locally instead of inserting an extra management tier.
|
||||
## Coordinator Prompt Template For A Worker
|
||||
|
||||
```text
|
||||
You are a `gpt-5.3-codex-spark` worker on PanoPainter. Other agents may be
|
||||
You are a `gpt-5.4-mini` worker on PanoPainter. Other agents may be
|
||||
editing nearby files; do not revert unrelated changes.
|
||||
|
||||
Task source: docs/modernization/tasks.md task(s) <TASK-ID-LIST>.
|
||||
@@ -142,15 +170,24 @@ Debt ids: <DEBT-LIST>.
|
||||
Write scope: <FILES/DIRS ONLY>.
|
||||
Read scope: <FILES/DIRS>.
|
||||
Validation: <COMMANDS>.
|
||||
Code exploration: <RG OR CLANGD_NAV COMMANDS TO USE>.
|
||||
|
||||
Repo constraints you must follow:
|
||||
- <ONLY THE RELEVANT RULES>
|
||||
|
||||
Current context you should rely on instead of broad repo/doc review:
|
||||
Minimal context you should rely on instead of broad repo/doc review:
|
||||
- <CURRENT BEHAVIOR NOTE>
|
||||
- <RELEVANT FILE OR SYMBOL NOTE>
|
||||
- <WHY THIS SLICE IS SAFE / WHAT MUST NOT CHANGE>
|
||||
|
||||
Do not read repo-wide docs unless this task explicitly requires it. Use only
|
||||
the supplied context, the listed file scope, and the build/test/code-exploration
|
||||
commands above.
|
||||
|
||||
If the coordinator gives you a second task, accept it only when it is coherent
|
||||
with the current scope and does not require broad repo rediscovery. Otherwise
|
||||
say that a fresh worker should be used.
|
||||
|
||||
Make the smallest behavior-preserving change that satisfies the done checks.
|
||||
Do not spend tokens on broad document review or inventory outside the assigned
|
||||
scope unless the task explicitly requires it. If the task is larger than
|
||||
@@ -187,6 +224,11 @@ Return only:
|
||||
- Focused validation for the task passed or the failure is documented.
|
||||
- `docs/modernization/debt.md` changed when debt was narrowed or closed.
|
||||
- `docs/modernization/tasks.md` score changed only for `Done` tasks.
|
||||
- Each worker result was integrated before that worker context was reused.
|
||||
- Each worker result was integrated before that worker was reused.
|
||||
- Reused workers only handled coherent follow-on tasks with a real context
|
||||
advantage.
|
||||
- Done or blocked workers were closed instead of being left idle.
|
||||
- The coordinator did not carry unnecessary stale history when a fresh thread
|
||||
would have been cheaper than compaction.
|
||||
- The commit contains one coherent slice.
|
||||
- The branch was pushed.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
5007
docs/modernization/tasks-done.md
Normal file
5007
docs/modernization/tasks-done.md
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
245
src/app.cpp
245
src/app.cpp
@@ -32,10 +32,6 @@
|
||||
|
||||
App* App::I = nullptr; // singleton
|
||||
|
||||
std::deque<AppTask> App::render_tasklist;
|
||||
std::mutex App::render_task_mutex;
|
||||
std::condition_variable App::render_cv;
|
||||
|
||||
namespace {
|
||||
|
||||
pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept
|
||||
@@ -130,17 +126,6 @@ void apply_app_scissor_test(bool enabled)
|
||||
}
|
||||
|
||||
}
|
||||
std::thread App::render_thread;
|
||||
std::thread::id App::render_thread_id;
|
||||
bool App::render_running = false;
|
||||
|
||||
std::deque<AppTask> App::ui_tasklist;
|
||||
std::mutex App::ui_task_mutex;
|
||||
std::condition_variable App::ui_cv;
|
||||
std::thread App::ui_thread;
|
||||
std::thread::id App::ui_thread_id;
|
||||
bool App::ui_running = false;
|
||||
|
||||
void App::create()
|
||||
{
|
||||
const auto initial_surface = pp::app::plan_app_initial_surface();
|
||||
@@ -353,7 +338,7 @@ void App::async_redraw()
|
||||
if (plan.set_redraw)
|
||||
redraw = true;
|
||||
if (plan.notify_ui)
|
||||
ui_cv.notify_all();
|
||||
runtime_.notify_ui_worker();
|
||||
}
|
||||
|
||||
void App::async_end()
|
||||
@@ -699,252 +684,40 @@ void App::rec_loop()
|
||||
|
||||
void App::render_thread_tick()
|
||||
{
|
||||
static uint32_t count = 0;
|
||||
render_thread_id = std::this_thread::get_id();
|
||||
std::deque<AppTask> working_list;
|
||||
pp::app::AppQueueDrainPlan drain_plan;
|
||||
|
||||
// move the task list locally to free the queue for other threads
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(render_task_mutex);
|
||||
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist.size());
|
||||
render_running = drain_plan.mark_running;
|
||||
if (!drain_plan.drain_tasks)
|
||||
return;
|
||||
working_list = std::move(render_tasklist);
|
||||
}
|
||||
|
||||
// execute the tasks
|
||||
if (drain_plan.wrap_in_render_context)
|
||||
{
|
||||
async_start();
|
||||
while (!working_list.empty())
|
||||
{
|
||||
//LOG("render task %d", count);
|
||||
//LOG("task %s", working_list.front().name.c_str());
|
||||
count++;
|
||||
working_list.front()();
|
||||
working_list.pop_front();
|
||||
}
|
||||
async_end();
|
||||
}
|
||||
runtime_.render_thread_tick(*this);
|
||||
}
|
||||
|
||||
void App::render_thread_main()
|
||||
{
|
||||
BT_SetTerminate();
|
||||
|
||||
uint32_t count = 0;
|
||||
render_thread_id = std::this_thread::get_id();
|
||||
render_running = pp::app::plan_app_thread_start().mark_running;
|
||||
while (render_running)
|
||||
{
|
||||
std::deque<AppTask> working_list;
|
||||
pp::app::AppQueueDrainPlan drain_plan;
|
||||
|
||||
// move the task list locally to free the queue for other threads
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(render_task_mutex);
|
||||
render_cv.wait(lock, [this] { return render_tasklist.empty() && render_running ? false : true; });
|
||||
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist.size());
|
||||
working_list = std::move(render_tasklist);
|
||||
}
|
||||
|
||||
// execute the tasks
|
||||
if (drain_plan.wrap_in_render_context)
|
||||
{
|
||||
async_start();
|
||||
while (!working_list.empty())
|
||||
{
|
||||
//LOG("render task %d", count);
|
||||
//LOG("task %s", working_list.front().name.c_str());
|
||||
count++;
|
||||
working_list.front()();
|
||||
working_list.pop_front();
|
||||
}
|
||||
async_end();
|
||||
}
|
||||
}
|
||||
runtime_.render_thread_main(*this);
|
||||
}
|
||||
|
||||
void App::ui_thread_tick()
|
||||
{
|
||||
ui_thread_id = std::this_thread::get_id();
|
||||
|
||||
std::deque<AppTask> working_list;
|
||||
pp::app::AppUiTickPlan tick_plan;
|
||||
|
||||
// move the task list locally to free the queue for other threads
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(ui_task_mutex);
|
||||
tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist.size(), redraw);
|
||||
ui_running = tick_plan.mark_running;
|
||||
working_list = std::move(ui_tasklist);
|
||||
}
|
||||
|
||||
// execute the tasks
|
||||
if (tick_plan.execute_tasks)
|
||||
{
|
||||
while (!working_list.empty())
|
||||
{
|
||||
//LOG("ui task %d", count);
|
||||
working_list.front()();
|
||||
working_list.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
if (tick_plan.tick_app)
|
||||
tick(0);
|
||||
|
||||
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(redraw, 0);
|
||||
if (redraw_plan.enqueue_render_frame)
|
||||
{
|
||||
if (redraw_plan.update_before_render)
|
||||
update(0);
|
||||
render_task([this]
|
||||
{
|
||||
bind_default_render_target();
|
||||
clear();
|
||||
draw(0);
|
||||
async_swap();
|
||||
});
|
||||
}
|
||||
runtime_.ui_thread_tick(*this);
|
||||
}
|
||||
|
||||
void App::ui_thread_main()
|
||||
{
|
||||
BT_SetTerminate();
|
||||
|
||||
uint32_t count = 0;
|
||||
ui_thread_id = std::this_thread::get_id();
|
||||
ui_running = pp::app::plan_app_thread_start().mark_running;
|
||||
|
||||
attach_ui_thread();
|
||||
|
||||
LOG("ui thread init()");
|
||||
init();
|
||||
|
||||
auto t_start = std::chrono::high_resolution_clock::now();
|
||||
float t_frame = 0;
|
||||
float t_fps_counter = 0;
|
||||
float t_reloader = 0;
|
||||
int rendered_frames = 0;
|
||||
while (ui_running)
|
||||
{
|
||||
std::deque<AppTask> working_list;
|
||||
|
||||
// move the task list locally to free the queue for other threads
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(ui_task_mutex);
|
||||
ui_cv.wait_for(lock, std::chrono::milliseconds(idle_ms),
|
||||
[this] { return ui_tasklist.empty() && ui_running ? false : true; });
|
||||
working_list = std::move(ui_tasklist);
|
||||
}
|
||||
|
||||
// execute the tasks
|
||||
if (!working_list.empty())
|
||||
{
|
||||
while (!working_list.empty())
|
||||
{
|
||||
//LOG("ui task %d", count);
|
||||
count++;
|
||||
working_list.front()();
|
||||
working_list.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
auto t_now = std::chrono::high_resolution_clock::now();
|
||||
float dt = std::chrono::duration<float>(t_now - t_start).count();
|
||||
t_start = t_now;
|
||||
|
||||
const auto timer_plan = pp::app::plan_app_ui_loop_timers(
|
||||
dt,
|
||||
t_frame,
|
||||
t_fps_counter,
|
||||
t_reloader,
|
||||
rendered_frames,
|
||||
platform_enables_live_asset_reloading());
|
||||
if (timer_plan) {
|
||||
if (timer_plan.value().update_platform_frame)
|
||||
update_platform_frame(dt);
|
||||
t_frame = timer_plan.value().frame_accumulator;
|
||||
t_fps_counter = timer_plan.value().fps_accumulator;
|
||||
t_reloader = timer_plan.value().reload_accumulator;
|
||||
rendered_frames = timer_plan.value().rendered_frames_after_report;
|
||||
|
||||
if (timer_plan.value().report_rendered_frames)
|
||||
report_rendered_frames(timer_plan.value().reported_frame_count);
|
||||
|
||||
if (timer_plan.value().check_live_asset_reload) {
|
||||
if (ShaderManager::reload())
|
||||
{
|
||||
stroke->update_controls();
|
||||
redraw = true;
|
||||
}
|
||||
if (layout.reload())
|
||||
redraw = true;
|
||||
if (layout_designer.reload())
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(redraw, rendered_frames);
|
||||
if (redraw_plan.tick_app)
|
||||
tick(dt);
|
||||
|
||||
if (redraw_plan.enqueue_render_frame)
|
||||
{
|
||||
if (redraw_plan.update_before_render)
|
||||
update(t_frame);
|
||||
render_task([this, t_frame]
|
||||
{
|
||||
bind_default_render_target();
|
||||
clear();
|
||||
draw(t_frame);
|
||||
async_swap();
|
||||
});
|
||||
if (redraw_plan.reset_frame_accumulator)
|
||||
t_frame = 0;
|
||||
rendered_frames = redraw_plan.rendered_frames;
|
||||
}
|
||||
}
|
||||
detach_ui_thread();
|
||||
runtime_.ui_thread_main(*this);
|
||||
}
|
||||
|
||||
void App::render_thread_start()
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_start();
|
||||
if (plan.start_thread)
|
||||
render_thread = std::thread(&App::render_thread_main, this);
|
||||
render_running = plan.mark_running;
|
||||
runtime_.render_thread_start(*this);
|
||||
}
|
||||
|
||||
void App::render_thread_stop()
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_stop(render_thread.joinable());
|
||||
if (plan.mark_not_running)
|
||||
render_running = false;
|
||||
if (plan.notify_worker)
|
||||
render_cv.notify_all();
|
||||
if (plan.join_thread)
|
||||
render_thread.join();
|
||||
runtime_.render_thread_stop();
|
||||
}
|
||||
|
||||
void App::ui_thread_start()
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_start();
|
||||
if (plan.start_thread)
|
||||
ui_thread = std::thread(&App::ui_thread_main, this);
|
||||
ui_running = plan.mark_running;
|
||||
runtime_.ui_thread_start(*this);
|
||||
}
|
||||
|
||||
void App::ui_thread_stop()
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_stop(ui_thread.joinable());
|
||||
if (plan.mark_not_running)
|
||||
ui_running = false;
|
||||
if (plan.notify_worker)
|
||||
ui_cv.notify_all();
|
||||
if (plan.join_thread)
|
||||
ui_thread.join();
|
||||
runtime_.ui_thread_stop();
|
||||
}
|
||||
|
||||
169
src/app.h
169
src/app.h
@@ -25,6 +25,7 @@
|
||||
#include "layout.h"
|
||||
#include "app_core/document_session.h"
|
||||
#include "app_core/app_thread.h"
|
||||
#include "app_runtime.h"
|
||||
|
||||
namespace pp::platform {
|
||||
class PlatformServices;
|
||||
@@ -76,21 +77,6 @@ struct VRController
|
||||
virtual float get_trigger_value() const { return 1.f; }
|
||||
};
|
||||
|
||||
struct AppTask : public std::packaged_task<void()>
|
||||
{
|
||||
size_t task_id;
|
||||
#ifdef _DEBUG
|
||||
std::string name;
|
||||
#endif
|
||||
template<typename F> AppTask(F f) : std::packaged_task<void()>(f)
|
||||
{
|
||||
task_id = typeid(f).hash_code();
|
||||
#ifdef _DEBUG
|
||||
name = typeid(f).name();
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
class App
|
||||
{
|
||||
public:
|
||||
@@ -345,16 +331,13 @@ public:
|
||||
|
||||
void cmd_convert(std::string pano_path, std::string out_path);
|
||||
|
||||
AppRuntime& runtime() noexcept { return runtime_; }
|
||||
const AppRuntime& runtime() const noexcept { return runtime_; }
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// RENDER THREAD
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static std::deque<AppTask> render_tasklist;
|
||||
static std::mutex render_task_mutex;
|
||||
static std::condition_variable render_cv;
|
||||
static std::thread render_thread;
|
||||
static std::thread::id render_thread_id;
|
||||
static bool render_running;
|
||||
void render_thread_tick();
|
||||
void render_thread_main();
|
||||
void render_thread_start();
|
||||
@@ -362,94 +345,30 @@ public:
|
||||
|
||||
bool is_render_thread()
|
||||
{
|
||||
return std::this_thread::get_id() == render_thread_id;
|
||||
return runtime_.is_render_thread();
|
||||
}
|
||||
|
||||
// don't capture a reference to this ptr as the object may be destroyed
|
||||
// by the time the task is executed
|
||||
template<typename T>
|
||||
std::future<void> render_task_async(T task, bool unique = false)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_render_thread(),
|
||||
unique,
|
||||
0U,
|
||||
render_running,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex);
|
||||
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
render_tasklist.size(),
|
||||
render_running,
|
||||
false,
|
||||
false);
|
||||
// remove any previously queued task from the same lambda
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
render_tasklist.erase(std::remove_if(render_tasklist.begin(), render_tasklist.end(),
|
||||
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist.end());
|
||||
render_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
render_cv.notify_all();
|
||||
}
|
||||
return f;
|
||||
return runtime_.render_task_async(std::move(task), unique);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void render_task(T task)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_render_thread(),
|
||||
false,
|
||||
0U,
|
||||
render_running,
|
||||
true,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex);
|
||||
render_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
render_cv.notify_all();
|
||||
}
|
||||
if (dispatch.wait_for_completion)
|
||||
f.get();
|
||||
runtime_.render_task(std::move(task));
|
||||
}
|
||||
|
||||
void render_sync()
|
||||
{
|
||||
render_task([] {});
|
||||
runtime_.render_sync();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// UI THREAD
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static std::deque<AppTask> ui_tasklist;
|
||||
static std::mutex ui_task_mutex;
|
||||
static std::condition_variable ui_cv;
|
||||
static std::thread ui_thread;
|
||||
static std::thread::id ui_thread_id;
|
||||
static bool ui_running;
|
||||
void ui_thread_tick();
|
||||
void ui_thread_main();
|
||||
void ui_thread_start();
|
||||
@@ -457,83 +376,29 @@ public:
|
||||
|
||||
bool is_ui_thread()
|
||||
{
|
||||
return std::this_thread::get_id() == ui_thread_id;
|
||||
return runtime_.is_ui_thread();
|
||||
}
|
||||
|
||||
// don't capture a reference to this ptr as the object may be destroyed
|
||||
// by the time the task is executed
|
||||
template<typename T>
|
||||
std::future<void> ui_task_async(T task, bool unique = false)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_ui_thread(),
|
||||
unique,
|
||||
0U,
|
||||
ui_running,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex);
|
||||
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
ui_tasklist.size(),
|
||||
ui_running,
|
||||
false,
|
||||
false);
|
||||
// remove any previously queued task from the same lambda
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
ui_tasklist.erase(std::remove_if(ui_tasklist.begin(), ui_tasklist.end(),
|
||||
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist.end());
|
||||
ui_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
ui_cv.notify_all();
|
||||
}
|
||||
return f;
|
||||
return runtime_.ui_task_async(std::move(task), unique);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void ui_task(T task)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_ui_thread(),
|
||||
false,
|
||||
0U,
|
||||
ui_running,
|
||||
true,
|
||||
true);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex);
|
||||
ui_tasklist.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
ui_cv.notify_all();
|
||||
}
|
||||
if (dispatch.wait_for_completion)
|
||||
f.get();
|
||||
if (dispatch.request_redraw)
|
||||
runtime_.ui_task(std::move(task));
|
||||
if (runtime_.request_redraw())
|
||||
redraw = true;
|
||||
runtime_.clear_request_redraw();
|
||||
}
|
||||
|
||||
void ui_sync()
|
||||
{
|
||||
ui_task([] {});
|
||||
runtime_.ui_sync();
|
||||
}
|
||||
|
||||
private:
|
||||
AppRuntime runtime_;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ void App::cloud_upload()
|
||||
|
||||
void App::cloud_upload_all()
|
||||
{
|
||||
std::thread([this] {
|
||||
pp::panopainter::queue_legacy_cloud_worker_task([this] {
|
||||
BT_SetTerminate();
|
||||
|
||||
auto names = Asset::list_files(data_path, ".*\\.ppi");
|
||||
@@ -28,7 +28,7 @@ void App::cloud_upload_all()
|
||||
const auto status = pp::panopainter::execute_legacy_cloud_bulk_upload_plan(*this, plan);
|
||||
if (!status.ok())
|
||||
LOG("Cloud bulk upload action failed: %s", status.message);
|
||||
}).detach();
|
||||
});
|
||||
}
|
||||
|
||||
void App::cloud_browse()
|
||||
|
||||
258
src/app_runtime.cpp
Normal file
258
src/app_runtime.cpp
Normal file
@@ -0,0 +1,258 @@
|
||||
#include "pch.h"
|
||||
#include "app_runtime.h"
|
||||
|
||||
#include "app.h"
|
||||
|
||||
bool AppRuntime::is_render_thread() const noexcept
|
||||
{
|
||||
return std::this_thread::get_id() == render_thread_id_;
|
||||
}
|
||||
|
||||
bool AppRuntime::is_ui_thread() const noexcept
|
||||
{
|
||||
return std::this_thread::get_id() == ui_thread_id_;
|
||||
}
|
||||
|
||||
void AppRuntime::notify_render_worker() noexcept
|
||||
{
|
||||
render_cv_.notify_all();
|
||||
}
|
||||
|
||||
void AppRuntime::notify_ui_worker() noexcept
|
||||
{
|
||||
ui_cv_.notify_all();
|
||||
}
|
||||
|
||||
void AppRuntime::render_thread_tick(App& app)
|
||||
{
|
||||
static uint32_t count = 0;
|
||||
render_thread_id_ = std::this_thread::get_id();
|
||||
std::deque<AppTask> working_list;
|
||||
pp::app::AppQueueDrainPlan drain_plan;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(render_task_mutex_);
|
||||
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size());
|
||||
render_running_ = drain_plan.mark_running;
|
||||
if (!drain_plan.drain_tasks)
|
||||
return;
|
||||
working_list = std::move(render_tasklist_);
|
||||
}
|
||||
|
||||
if (drain_plan.wrap_in_render_context)
|
||||
{
|
||||
app.async_start();
|
||||
while (!working_list.empty())
|
||||
{
|
||||
count++;
|
||||
working_list.front()();
|
||||
working_list.pop_front();
|
||||
}
|
||||
app.async_end();
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::render_thread_main(App& app)
|
||||
{
|
||||
BT_SetTerminate();
|
||||
|
||||
render_thread_id_ = std::this_thread::get_id();
|
||||
render_running_ = pp::app::plan_app_thread_start().mark_running;
|
||||
while (render_running_)
|
||||
{
|
||||
std::deque<AppTask> working_list;
|
||||
pp::app::AppQueueDrainPlan drain_plan;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(render_task_mutex_);
|
||||
render_cv_.wait(lock, [this] { return render_tasklist_.empty() && render_running_ ? false : true; });
|
||||
drain_plan = pp::app::plan_app_render_queue_drain(render_tasklist_.size());
|
||||
working_list = std::move(render_tasklist_);
|
||||
}
|
||||
|
||||
if (drain_plan.wrap_in_render_context)
|
||||
{
|
||||
app.async_start();
|
||||
while (!working_list.empty())
|
||||
{
|
||||
working_list.front()();
|
||||
working_list.pop_front();
|
||||
}
|
||||
app.async_end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::ui_thread_tick(App& app)
|
||||
{
|
||||
ui_thread_id_ = std::this_thread::get_id();
|
||||
|
||||
std::deque<AppTask> working_list;
|
||||
pp::app::AppUiTickPlan tick_plan;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(ui_task_mutex_);
|
||||
tick_plan = pp::app::plan_app_ui_thread_tick(ui_tasklist_.size(), app.redraw);
|
||||
ui_running_ = tick_plan.mark_running;
|
||||
working_list = std::move(ui_tasklist_);
|
||||
}
|
||||
|
||||
if (tick_plan.execute_tasks)
|
||||
{
|
||||
while (!working_list.empty())
|
||||
{
|
||||
working_list.front()();
|
||||
working_list.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
if (tick_plan.tick_app)
|
||||
app.tick(0);
|
||||
|
||||
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(app.redraw, 0);
|
||||
if (redraw_plan.enqueue_render_frame)
|
||||
{
|
||||
if (redraw_plan.update_before_render)
|
||||
app.update(0);
|
||||
app.render_task([&app]
|
||||
{
|
||||
app.bind_default_render_target();
|
||||
app.clear();
|
||||
app.draw(0);
|
||||
app.async_swap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void AppRuntime::ui_thread_main(App& app)
|
||||
{
|
||||
BT_SetTerminate();
|
||||
|
||||
ui_thread_id_ = std::this_thread::get_id();
|
||||
ui_running_ = pp::app::plan_app_thread_start().mark_running;
|
||||
|
||||
app.attach_ui_thread();
|
||||
|
||||
LOG("ui thread init()");
|
||||
app.init();
|
||||
|
||||
auto t_start = std::chrono::high_resolution_clock::now();
|
||||
float t_frame = 0;
|
||||
float t_fps_counter = 0;
|
||||
float t_reloader = 0;
|
||||
int rendered_frames = 0;
|
||||
while (ui_running_)
|
||||
{
|
||||
std::deque<AppTask> working_list;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(ui_task_mutex_);
|
||||
ui_cv_.wait_for(lock, std::chrono::milliseconds(app.idle_ms),
|
||||
[this] { return ui_tasklist_.empty() && ui_running_ ? false : true; });
|
||||
working_list = std::move(ui_tasklist_);
|
||||
}
|
||||
|
||||
if (!working_list.empty())
|
||||
{
|
||||
while (!working_list.empty())
|
||||
{
|
||||
working_list.front()();
|
||||
working_list.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
auto t_now = std::chrono::high_resolution_clock::now();
|
||||
float dt = std::chrono::duration<float>(t_now - t_start).count();
|
||||
t_start = t_now;
|
||||
|
||||
const auto timer_plan = pp::app::plan_app_ui_loop_timers(
|
||||
dt,
|
||||
t_frame,
|
||||
t_fps_counter,
|
||||
t_reloader,
|
||||
rendered_frames,
|
||||
app.platform_enables_live_asset_reloading());
|
||||
if (timer_plan) {
|
||||
if (timer_plan.value().update_platform_frame)
|
||||
app.update_platform_frame(dt);
|
||||
t_frame = timer_plan.value().frame_accumulator;
|
||||
t_fps_counter = timer_plan.value().fps_accumulator;
|
||||
t_reloader = timer_plan.value().reload_accumulator;
|
||||
rendered_frames = timer_plan.value().rendered_frames_after_report;
|
||||
|
||||
if (timer_plan.value().report_rendered_frames)
|
||||
app.report_rendered_frames(timer_plan.value().reported_frame_count);
|
||||
|
||||
if (timer_plan.value().check_live_asset_reload) {
|
||||
if (ShaderManager::reload())
|
||||
{
|
||||
app.stroke->update_controls();
|
||||
app.redraw = true;
|
||||
}
|
||||
if (app.layout.reload())
|
||||
app.redraw = true;
|
||||
if (app.layout_designer.reload())
|
||||
app.redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
const auto redraw_plan = pp::app::plan_app_ui_loop_redraw(app.redraw, rendered_frames);
|
||||
if (redraw_plan.tick_app)
|
||||
app.tick(dt);
|
||||
|
||||
if (redraw_plan.enqueue_render_frame)
|
||||
{
|
||||
if (redraw_plan.update_before_render)
|
||||
app.update(t_frame);
|
||||
app.render_task([&app, t_frame]
|
||||
{
|
||||
app.bind_default_render_target();
|
||||
app.clear();
|
||||
app.draw(t_frame);
|
||||
app.async_swap();
|
||||
});
|
||||
if (redraw_plan.reset_frame_accumulator)
|
||||
t_frame = 0;
|
||||
rendered_frames = redraw_plan.rendered_frames;
|
||||
}
|
||||
}
|
||||
app.detach_ui_thread();
|
||||
}
|
||||
|
||||
void AppRuntime::render_thread_start(App& app)
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_start();
|
||||
if (plan.start_thread)
|
||||
render_thread_ = std::thread(&AppRuntime::render_thread_main, this, std::ref(app));
|
||||
render_running_ = plan.mark_running;
|
||||
}
|
||||
|
||||
void AppRuntime::render_thread_stop()
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_stop(render_thread_.joinable());
|
||||
if (plan.mark_not_running)
|
||||
render_running_ = false;
|
||||
if (plan.notify_worker)
|
||||
render_cv_.notify_all();
|
||||
if (plan.join_thread)
|
||||
render_thread_.join();
|
||||
}
|
||||
|
||||
void AppRuntime::ui_thread_start(App& app)
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_start();
|
||||
if (plan.start_thread)
|
||||
ui_thread_ = std::thread(&AppRuntime::ui_thread_main, this, std::ref(app));
|
||||
ui_running_ = plan.mark_running;
|
||||
}
|
||||
|
||||
void AppRuntime::ui_thread_stop()
|
||||
{
|
||||
const auto plan = pp::app::plan_app_thread_stop(ui_thread_.joinable());
|
||||
if (plan.mark_not_running)
|
||||
ui_running_ = false;
|
||||
if (plan.notify_worker)
|
||||
ui_cv_.notify_all();
|
||||
if (plan.join_thread)
|
||||
ui_thread_.join();
|
||||
}
|
||||
216
src/app_runtime.h
Normal file
216
src/app_runtime.h
Normal file
@@ -0,0 +1,216 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_core/app_thread.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <typeinfo>
|
||||
#include <utility>
|
||||
#include <thread>
|
||||
|
||||
class App;
|
||||
|
||||
struct AppTask : public std::packaged_task<void()>
|
||||
{
|
||||
size_t task_id;
|
||||
#ifdef _DEBUG
|
||||
std::string name;
|
||||
#endif
|
||||
template<typename F> AppTask(F f) : std::packaged_task<void()>(f)
|
||||
{
|
||||
task_id = typeid(f).hash_code();
|
||||
#ifdef _DEBUG
|
||||
name = typeid(f).name();
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
class AppRuntime
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] bool is_render_thread() const noexcept;
|
||||
[[nodiscard]] bool is_ui_thread() const noexcept;
|
||||
[[nodiscard]] bool request_redraw() const noexcept { return request_redraw_; }
|
||||
void clear_request_redraw() noexcept { request_redraw_ = false; }
|
||||
|
||||
void notify_render_worker() noexcept;
|
||||
void notify_ui_worker() noexcept;
|
||||
|
||||
void render_thread_tick(App& app);
|
||||
void render_thread_main(App& app);
|
||||
void render_thread_start(App& app);
|
||||
void render_thread_stop();
|
||||
|
||||
void ui_thread_tick(App& app);
|
||||
void ui_thread_main(App& app);
|
||||
void ui_thread_start(App& app);
|
||||
void ui_thread_stop();
|
||||
|
||||
template<typename T>
|
||||
std::future<void> render_task_async(T task, bool unique = false)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_render_thread(),
|
||||
unique,
|
||||
0U,
|
||||
render_running_,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex_);
|
||||
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
render_tasklist_.size(),
|
||||
render_running_,
|
||||
false,
|
||||
false);
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
render_tasklist_.erase(std::remove_if(render_tasklist_.begin(), render_tasklist_.end(),
|
||||
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), render_tasklist_.end());
|
||||
render_tasklist_.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
render_cv_.notify_all();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void render_task(T task)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_render_thread(),
|
||||
false,
|
||||
0U,
|
||||
render_running_,
|
||||
true,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(render_task_mutex_);
|
||||
render_tasklist_.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
render_cv_.notify_all();
|
||||
}
|
||||
if (dispatch.wait_for_completion)
|
||||
f.get();
|
||||
}
|
||||
|
||||
void render_sync()
|
||||
{
|
||||
render_task([] {});
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::future<void> ui_task_async(T task, bool unique = false)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_ui_thread(),
|
||||
unique,
|
||||
0U,
|
||||
ui_running_,
|
||||
false,
|
||||
false);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex_);
|
||||
const auto queue_dispatch = pp::app::plan_app_task_dispatch(
|
||||
false,
|
||||
unique,
|
||||
ui_tasklist_.size(),
|
||||
ui_running_,
|
||||
false,
|
||||
false);
|
||||
if (queue_dispatch.remove_matching_unique_task)
|
||||
ui_tasklist_.erase(std::remove_if(ui_tasklist_.begin(), ui_tasklist_.end(),
|
||||
[id = pt.task_id](AppTask const& t){ return t.task_id == id; }), ui_tasklist_.end());
|
||||
ui_tasklist_.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
ui_cv_.notify_all();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void ui_task(T task)
|
||||
{
|
||||
AppTask pt(task);
|
||||
auto f = pt.get_future();
|
||||
const auto dispatch = pp::app::plan_app_task_dispatch(
|
||||
is_ui_thread(),
|
||||
false,
|
||||
0U,
|
||||
ui_running_,
|
||||
true,
|
||||
true);
|
||||
if (dispatch.execute_immediately)
|
||||
{
|
||||
pt();
|
||||
}
|
||||
else if (dispatch.queue_task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(ui_task_mutex_);
|
||||
ui_tasklist_.push_back(std::move(pt));
|
||||
}
|
||||
if (dispatch.notify_worker)
|
||||
ui_cv_.notify_all();
|
||||
}
|
||||
if (dispatch.wait_for_completion)
|
||||
f.get();
|
||||
if (dispatch.request_redraw)
|
||||
request_redraw_ = true;
|
||||
}
|
||||
|
||||
void ui_sync()
|
||||
{
|
||||
ui_task([] {});
|
||||
}
|
||||
|
||||
private:
|
||||
std::deque<AppTask> render_tasklist_;
|
||||
std::mutex render_task_mutex_;
|
||||
std::condition_variable render_cv_;
|
||||
std::thread render_thread_;
|
||||
std::thread::id render_thread_id_;
|
||||
bool render_running_ = false;
|
||||
|
||||
std::deque<AppTask> ui_tasklist_;
|
||||
std::mutex ui_task_mutex_;
|
||||
std::condition_variable ui_cv_;
|
||||
std::thread ui_thread_;
|
||||
std::thread::id ui_thread_id_;
|
||||
bool ui_running_ = false;
|
||||
bool request_redraw_ = false;
|
||||
};
|
||||
@@ -21,6 +21,79 @@ pp::foundation::Status execute_legacy_cloud_download_selection_action(
|
||||
|
||||
namespace {
|
||||
|
||||
class LegacyCloudWorker final {
|
||||
public:
|
||||
LegacyCloudWorker()
|
||||
: worker_([this](std::stop_token stop_token) {
|
||||
run(stop_token);
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
~LegacyCloudWorker()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
void post(std::function<void()> task)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (stopping_)
|
||||
return;
|
||||
tasks_.push_back(std::move(task));
|
||||
}
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
private:
|
||||
void shutdown()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
void run(std::stop_token stop_token)
|
||||
{
|
||||
for (;;) {
|
||||
std::function<void()> task;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
cv_.wait(lock, [&] {
|
||||
return stopping_ || stop_token.stop_requested() || !tasks_.empty();
|
||||
});
|
||||
if ((stopping_ || stop_token.stop_requested()) && tasks_.empty())
|
||||
break;
|
||||
task = std::move(tasks_.front());
|
||||
tasks_.pop_front();
|
||||
}
|
||||
|
||||
if (task) {
|
||||
try {
|
||||
task();
|
||||
} catch (...) {
|
||||
LOG("cloud worker task failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
std::deque<std::function<void()>> tasks_;
|
||||
bool stopping_ = false;
|
||||
std::jthread worker_;
|
||||
};
|
||||
|
||||
LegacyCloudWorker& cloud_worker()
|
||||
{
|
||||
static LegacyCloudWorker worker;
|
||||
return worker;
|
||||
}
|
||||
|
||||
#if WITH_CURL
|
||||
int progress_callback_download(void* clientp, curl_off_t dltotal,
|
||||
curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
|
||||
@@ -233,28 +306,12 @@ void execute_cloud_download_thread(
|
||||
execute_cloud_download_flow(app, request);
|
||||
}
|
||||
|
||||
void launch_cloud_download_thread(
|
||||
App& app,
|
||||
const pp::app::CloudDownloadRequest& request)
|
||||
{
|
||||
std::thread([app = &app, request] {
|
||||
execute_cloud_download_thread(*app, request);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void execute_cloud_publish_worker(App& app, bool save_before_upload)
|
||||
{
|
||||
BT_SetTerminate();
|
||||
execute_cloud_publish_transfer_and_success_prompt(app, save_before_upload);
|
||||
}
|
||||
|
||||
void launch_cloud_publish_thread(App& app, bool save_before_upload)
|
||||
{
|
||||
std::thread([app = &app, save_before_upload] {
|
||||
execute_cloud_publish_worker(*app, save_before_upload);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void wire_cloud_publish_prompt_buttons(
|
||||
const std::shared_ptr<NodeMessageBox>& dialog,
|
||||
std::function<void()> upload_thread)
|
||||
@@ -273,7 +330,9 @@ void setup_cloud_publish_prompt(App& app, bool save_before_upload)
|
||||
const auto prompt_plan = pp::app::plan_cloud_publish_prompt();
|
||||
auto dialog = app.message_box(prompt_plan.title, prompt_plan.message, prompt_plan.show_cancel);
|
||||
wire_cloud_publish_prompt_buttons(dialog, [&app, save_before_upload] {
|
||||
launch_cloud_publish_thread(app, save_before_upload);
|
||||
queue_legacy_cloud_worker_task([app = &app, save_before_upload] {
|
||||
execute_cloud_publish_worker(*app, save_before_upload);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -379,7 +438,9 @@ public:
|
||||
|
||||
void start_download(const pp::app::CloudDownloadRequest& request) override
|
||||
{
|
||||
launch_cloud_download_thread(app_, request);
|
||||
queue_legacy_cloud_worker_task([app = &app_, request] {
|
||||
execute_cloud_download_thread(*app, request);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -395,6 +456,11 @@ void show_cloud_save_required_warning(App& app)
|
||||
|
||||
} // namespace
|
||||
|
||||
void queue_legacy_cloud_worker_task(std::function<void()> task)
|
||||
{
|
||||
cloud_worker().post(std::move(task));
|
||||
}
|
||||
|
||||
pp::foundation::Status execute_legacy_cloud_upload_plan(
|
||||
App& app,
|
||||
const pp::app::CloudUploadPlan& plan)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "app_core/document_cloud.h"
|
||||
#include "foundation/result.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
class App;
|
||||
class NodeDialogCloud;
|
||||
|
||||
@@ -22,4 +24,6 @@ namespace pp::panopainter {
|
||||
pp::app::CloudDownloadSelectionAction action,
|
||||
NodeDialogCloud& dialog);
|
||||
|
||||
void queue_legacy_cloud_worker_task(std::function<void()> task);
|
||||
|
||||
} // namespace pp::panopainter
|
||||
|
||||
@@ -174,6 +174,25 @@ void close_legacy_overlay_handle_ignoring_status(
|
||||
(void)close_legacy_overlay_node(anchor, overlay);
|
||||
}
|
||||
|
||||
void close_legacy_overlay_handle_and_reset(
|
||||
Node& anchor,
|
||||
pp::ui::NodeHandle& overlay) noexcept
|
||||
{
|
||||
if (overlay.valid()) {
|
||||
close_legacy_overlay_handle_ignoring_status(anchor, overlay);
|
||||
overlay = {};
|
||||
}
|
||||
}
|
||||
|
||||
void close_legacy_overlay_handles_and_reset(
|
||||
Node& anchor,
|
||||
pp::ui::NodeHandle& popup_overlay,
|
||||
pp::ui::NodeHandle& tick_overlay) noexcept
|
||||
{
|
||||
close_legacy_overlay_handle_and_reset(anchor, popup_overlay);
|
||||
close_legacy_overlay_handle_and_reset(anchor, tick_overlay);
|
||||
}
|
||||
|
||||
void close_legacy_overlay_handles_if_open(
|
||||
Node& anchor,
|
||||
const pp::foundation::Result<pp::ui::NodeHandle>& popup_overlay,
|
||||
|
||||
@@ -111,6 +111,13 @@ void close_legacy_popup_overlay(Node& node) noexcept;
|
||||
void close_legacy_overlay_handle_ignoring_status(
|
||||
Node& anchor,
|
||||
pp::ui::NodeHandle overlay) noexcept;
|
||||
void close_legacy_overlay_handle_and_reset(
|
||||
Node& anchor,
|
||||
pp::ui::NodeHandle& overlay) noexcept;
|
||||
void close_legacy_overlay_handles_and_reset(
|
||||
Node& anchor,
|
||||
pp::ui::NodeHandle& popup_overlay,
|
||||
pp::ui::NodeHandle& tick_overlay) noexcept;
|
||||
void close_legacy_overlay_handles_if_open(
|
||||
Node& anchor,
|
||||
const pp::foundation::Result<pp::ui::NodeHandle>& popup_overlay,
|
||||
|
||||
@@ -932,8 +932,8 @@ int main(int argc, char** argv)
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
|
||||
running = 1;
|
||||
App::I->render_thread_start();
|
||||
App::I->ui_thread_start();
|
||||
App::I->runtime().render_thread_start(*App::I);
|
||||
App::I->runtime().ui_thread_start(*App::I);
|
||||
|
||||
#ifdef _DEBUG
|
||||
glad_set_pre_callback(_pre_call_callback);
|
||||
@@ -1035,8 +1035,8 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
running = 0;
|
||||
if (hmd_renderer.joinable())
|
||||
hmd_renderer.join();
|
||||
App::I->ui_thread_stop();
|
||||
App::I->render_thread_stop();
|
||||
App::I->runtime().ui_thread_stop();
|
||||
App::I->runtime().render_thread_stop();
|
||||
App::I->terminate();
|
||||
delete App::I;
|
||||
PostQuitMessage(0);
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
void Node::app_redraw()
|
||||
{
|
||||
App::I->redraw = true;
|
||||
App::I->render_cv.notify_all();
|
||||
App::I->runtime().notify_render_worker();
|
||||
}
|
||||
|
||||
void Node::watch(std::function<bool(Node*)> observer)
|
||||
|
||||
@@ -68,7 +68,7 @@ void NodeDialogCloud::init_controls()
|
||||
btn_cancel = find<NodeButton>("btn-cancel");
|
||||
pp::panopainter::bind_legacy_click_destroys_node(*btn_cancel, *this);
|
||||
container = find<Node>("files-list");
|
||||
std::thread(&NodeDialogCloud::load_thumbs_thread, this).detach();
|
||||
load_thumbs_worker_ = std::jthread(&NodeDialogCloud::load_thumbs_thread, this);
|
||||
}
|
||||
|
||||
void NodeDialogCloud::loaded()
|
||||
@@ -79,6 +79,7 @@ void NodeDialogCloud::removed(Node* parent)
|
||||
{
|
||||
NodeBorder::removed(parent);
|
||||
closed = true;
|
||||
load_thumbs_worker_.request_stop();
|
||||
}
|
||||
|
||||
NodeText* NodeDialogCloud::create_loading_status_text()
|
||||
@@ -143,24 +144,30 @@ std::vector<NodeDialogCloudItem*> NodeDialogCloud::create_cloud_file_items(const
|
||||
return nodes;
|
||||
}
|
||||
|
||||
void NodeDialogCloud::load_thumbs_thread()
|
||||
void NodeDialogCloud::load_thumbs_thread(std::stop_token stop)
|
||||
{
|
||||
#if WITH_CURL
|
||||
BT_SetTerminate();
|
||||
CURL *curl = curl_easy_init();
|
||||
auto curl = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>(curl_easy_init(), curl_easy_cleanup);
|
||||
std::string res;
|
||||
if (curl)
|
||||
{
|
||||
if (stop.stop_requested() || closed)
|
||||
return;
|
||||
|
||||
auto* text = create_loading_status_text();
|
||||
auto* align = text->m_parent;
|
||||
|
||||
if (!load_cloud_file_list(curl, res, *text))
|
||||
if (!load_cloud_file_list(curl.get(), res, *text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pp::panopainter::destroy_legacy_node(*align);
|
||||
|
||||
if (stop.stop_requested() || closed)
|
||||
return;
|
||||
|
||||
LOG("CLOUD LIST: %s", res.c_str());
|
||||
|
||||
auto names = split(res, ',');
|
||||
@@ -171,13 +178,12 @@ void NodeDialogCloud::load_thumbs_thread()
|
||||
{
|
||||
const auto& n = names[i];
|
||||
auto* node = nodes[i];
|
||||
if (closed)
|
||||
if (stop.stop_requested() || closed)
|
||||
break;
|
||||
|
||||
if (!load_cloud_thumb(curl, n, node, res))
|
||||
if (!load_cloud_thumb(curl.get(), n, node, res))
|
||||
break;
|
||||
}
|
||||
curl_easy_cleanup(curl);
|
||||
}
|
||||
#endif //CURL
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "node_text_input.h"
|
||||
|
||||
#include <vector>
|
||||
#include <stop_token>
|
||||
#include <thread>
|
||||
|
||||
class NodeDialogCloudItem : public NodeBorder
|
||||
{
|
||||
@@ -46,8 +48,11 @@ public:
|
||||
void init_controls();
|
||||
virtual void loaded() override;
|
||||
virtual void removed(Node* parent) override;
|
||||
void load_thumbs_thread();
|
||||
void load_thumbs_thread(std::stop_token stop);
|
||||
NodeText* create_loading_status_text();
|
||||
bool load_cloud_file_list(CURL* curl, std::string& response, NodeText& status_text);
|
||||
std::vector<NodeDialogCloudItem*> create_cloud_file_items(const std::vector<std::string>& names);
|
||||
|
||||
private:
|
||||
std::jthread load_thumbs_worker_;
|
||||
};
|
||||
|
||||
@@ -722,14 +722,10 @@ void NodePanelStroke::execute_stroke_control_plan(const pp::app::BrushStrokeCont
|
||||
|
||||
void NodePanelStroke::close_popup_overlay_handles() noexcept
|
||||
{
|
||||
if (m_popup_overlay_handle.valid()) {
|
||||
pp::panopainter::close_legacy_overlay_handle_ignoring_status(*this, m_popup_overlay_handle);
|
||||
m_popup_overlay_handle = {};
|
||||
}
|
||||
if (m_tick_overlay_handle.valid()) {
|
||||
pp::panopainter::close_legacy_overlay_handle_ignoring_status(*this, m_tick_overlay_handle);
|
||||
m_tick_overlay_handle = {};
|
||||
}
|
||||
pp::panopainter::close_legacy_overlay_handles_and_reset(
|
||||
*this,
|
||||
m_popup_overlay_handle,
|
||||
m_tick_overlay_handle);
|
||||
}
|
||||
|
||||
kEventResult NodePanelStroke::handle_event(Event* e)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "node_popup_menu.h"
|
||||
#include "node_button_custom.h"
|
||||
#include "app.h"
|
||||
#include <memory>
|
||||
|
||||
Node* NodePopupMenu::clone_instantiate() const
|
||||
{
|
||||
@@ -33,19 +34,22 @@ kEventResult NodePopupMenu::handle_event(Event* e)
|
||||
case kEventType::MouseDownL:
|
||||
break;
|
||||
case kEventType::MouseUpL:
|
||||
if (m_mouse_inside)
|
||||
{
|
||||
for (int i = 0; i < m_children.size(); i++)
|
||||
auto self = std::static_pointer_cast<NodePopupMenu>(shared_from_this());
|
||||
if (m_mouse_inside)
|
||||
{
|
||||
if (m_children[i]->m_mouse_inside)
|
||||
for (int i = 0; i < m_children.size(); i++)
|
||||
{
|
||||
if (on_select)
|
||||
on_select(this, i);
|
||||
break;
|
||||
if (m_children[i]->m_mouse_inside)
|
||||
{
|
||||
if (on_select)
|
||||
on_select(self.get(), i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
close_popup();
|
||||
}
|
||||
close_popup();
|
||||
break;
|
||||
default:
|
||||
return kEventResult::Available;
|
||||
@@ -61,13 +65,19 @@ void NodePopupMenu::added(Node* parent)
|
||||
m_mouse_ignore = false;
|
||||
m_flood_events = true;
|
||||
m_capture_children = false;
|
||||
auto self = std::static_pointer_cast<NodePopupMenu>(shared_from_this());
|
||||
for (int i = 0; i < m_children.size(); i++)
|
||||
{
|
||||
if (auto b = std::dynamic_pointer_cast<NodeButtonCustom>(m_children[i]))
|
||||
{
|
||||
b->on_click = [this, i](Node* target) {
|
||||
if (on_select)
|
||||
on_select(this, i);
|
||||
std::weak_ptr<NodePopupMenu> weak_self = self;
|
||||
b->on_click = [weak_self, i](Node* target) {
|
||||
auto self = weak_self.lock();
|
||||
if (!self) {
|
||||
return;
|
||||
}
|
||||
if (self->on_select)
|
||||
self->on_select(self.get(), i);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user