Compare commits

...

245 Commits

Author SHA1 Message Date
cee5f141a3 Add animation panel action planner 2026-06-03 16:57:56 +02:00
603bb0c4e7 Add animation playback toggle boundary 2026-06-03 16:47:02 +02:00
5752bc6ae9 Extend animation panel frame dispatch 2026-06-03 16:39:14 +02:00
93f3037410 Add animation panel service boundary 2026-06-03 14:04:36 +02:00
9c7c89fed4 Add canvas tool service boundary 2026-06-03 13:42:34 +02:00
45a7d49d40 Add quick panel service boundary 2026-06-03 13:36:48 +02:00
de9bca8bb5 Add brush UI service boundary 2026-06-03 13:28:50 +02:00
6427f218e7 Add history command service boundary 2026-06-03 13:22:16 +02:00
6d0cc4eb15 Add image import service boundary 2026-06-03 13:17:31 +02:00
a6306c2759 Add canvas clear service boundary 2026-06-03 13:13:43 +02:00
7c76703355 Add document resize service boundary 2026-06-03 13:09:12 +02:00
9c3f56954e Add export menu service boundary 2026-06-03 13:04:00 +02:00
e880f23040 Add file menu service boundary 2026-06-03 13:00:22 +02:00
defa9fc212 Add layer menu service boundary 2026-06-03 12:56:25 +02:00
ea96f38875 Add tools menu service boundary 2026-06-03 12:52:46 +02:00
b67f3d63cf Add about menu service boundary 2026-06-03 12:47:15 +02:00
fb111dcdc9 Add main toolbar service boundary 2026-06-03 12:42:23 +02:00
62561624ed Extract main toolbar action planning 2026-06-03 12:37:32 +02:00
b5bd6d42f7 Extract about menu action planning 2026-06-03 12:27:47 +02:00
c640519772 Extract tools menu planning 2026-06-03 12:15:57 +02:00
fb844f79fd Extract layer menu action planning 2026-06-03 12:05:13 +02:00
6dac909869 Extract file menu action planning 2026-06-03 11:56:14 +02:00
65b262207c Extract export menu action planning 2026-06-03 11:48:34 +02:00
ef50f4a361 Extract image import route planning 2026-06-03 11:41:28 +02:00
888e94a77c Extract canvas clear command planning 2026-06-03 11:35:20 +02:00
c56d301b29 Route canvas input mode switching through planner 2026-06-03 11:28:41 +02:00
91e1c2c9a3 Extract canvas toolbar state planning 2026-06-03 11:26:58 +02:00
2087505921 Extract canvas tool UI planning 2026-06-03 11:20:56 +02:00
58afa672c7 Extract history UI operation planning 2026-06-03 11:13:57 +02:00
8dc476d205 Extract quick UI operation planning 2026-06-03 11:01:01 +02:00
73fac0f8e4 Extract grid UI operation planning 2026-06-03 10:52:51 +02:00
a487b0ba48 Mute unused parameter warnings 2026-06-03 10:46:25 +02:00
efd568a416 Extract brush UI operation planning 2026-06-03 10:40:12 +02:00
4f0909f30c Extract animation operation planning 2026-06-03 10:32:06 +02:00
fdc1defaba Extract layer operation planning 2026-06-03 10:20:37 +02:00
07ed23c2d1 Extract layer rename planning 2026-06-03 10:10:08 +02:00
5d5bb24711 Extract document resize planning 2026-06-03 10:03:34 +02:00
21c448d6f1 Report package smoke readiness matrix 2026-06-03 09:57:12 +02:00
cd9206344d Align platform build presets and component matrix 2026-06-03 09:51:14 +02:00
4d06608cc9 Extract app status planning into app core 2026-06-03 09:45:12 +02:00
a64a63def7 Extract app preference planning into app core 2026-06-03 09:36:38 +02:00
19cb14b5dc Debt-track MSVC unreferenced parameter warning mute 2026-06-03 08:45:29 +02:00
4de6f496ad Route font mesh operations through renderer GL 2026-06-03 07:29:09 +02:00
e1cce05bd6 Route Shape mesh operations through renderer GL 2026-06-03 07:19:14 +02:00
1ae79ab3c1 Route shader creation through renderer GL 2026-06-03 07:03:50 +02:00
acdaf3bb8e Route shader runtime calls through renderer GL 2026-06-03 06:53:51 +02:00
f20595aff6 Route cube textures and samplers through renderer GL 2026-06-03 06:46:06 +02:00
779d6b0387 Route RTT framebuffer binding through renderer GL 2026-06-03 06:35:47 +02:00
3128a0d309 Route RTT blit readback through renderer GL 2026-06-03 06:31:41 +02:00
ae69f7437f Route Texture2D through renderer GL 2026-06-03 06:24:56 +02:00
9971b2b7f2 Route GL state snapshot through renderer GL 2026-06-03 06:15:51 +02:00
3e15b2f46c Route VR render state through renderer GL 2026-06-03 06:03:28 +02:00
7dcf76c3aa Route VR UI viewport scissor through renderer GL 2026-06-03 05:59:36 +02:00
155e67fcec Route app viewport scissor through renderer GL 2026-06-03 05:56:57 +02:00
2a030318b1 Route default clear through renderer GL 2026-06-03 05:50:36 +02:00
103fe4fb12 Route GL runtime info through renderer GL 2026-06-03 05:45:08 +02:00
b2335b1656 Apply startup GL state through renderer GL 2026-06-03 05:41:27 +02:00
692fe08d9f Move initial GL state policy into renderer GL 2026-06-03 05:35:39 +02:00
8b12ae35d4 Route render debug callback through platform services 2026-06-03 05:30:54 +02:00
87b1851d59 Route render platform hints through platform services 2026-06-03 05:20:24 +02:00
389cd93e68 Route render target binding through platform services 2026-06-03 05:17:25 +02:00
6652127545 Route diagnostic hooks through platform services 2026-06-03 05:12:00 +02:00
e152616d7f Route live reload policy through platform services 2026-06-03 05:08:00 +02:00
ac4d065c78 Route recording cleanup through platform services 2026-06-03 05:04:14 +02:00
578b1f6082 Route startup storage paths through platform services 2026-06-03 04:59:23 +02:00
beb7f717f1 Route render capture hooks through platform services 2026-06-03 04:54:29 +02:00
7a9b14a86f Route render context lifecycle through platform services 2026-06-03 04:50:42 +02:00
f3925f8423 Route UI thread lifecycle through platform services 2026-06-03 04:45:02 +02:00
dd641c047b Route platform frame hooks through services 2026-06-03 04:41:01 +02:00
22006eaf47 Route native close through platform services 2026-06-03 04:36:10 +02:00
537f0dcb2f Dispatch visibility through platform services 2026-06-03 04:31:30 +02:00
2ea850cbcc Route prepared file saves through platform services 2026-06-03 04:29:58 +02:00
e10e16f491 Extract legacy platform services adapter 2026-06-03 04:25:01 +02:00
6369c3c969 Extract Windows platform services 2026-06-03 04:20:14 +02:00
ead7f58285 Inject Windows platform services 2026-06-03 04:14:47 +02:00
0e77ca6ba8 Route picker callbacks through platform services 2026-06-03 04:07:09 +02:00
1e0500a3f7 Route file actions through platform services 2026-06-03 04:03:25 +02:00
4ed72ebc80 Introduce platform services interface 2026-06-03 03:59:59 +02:00
6960bd3410 Plan clipboard text actions in app core 2026-06-03 03:52:00 +02:00
5ee2dd271c Plan cursor visibility in app core 2026-06-03 03:47:28 +02:00
5ac807c6bd Plan keyboard visibility in app core 2026-06-03 03:42:47 +02:00
4af55a7d3f Plan display file actions in app core 2026-06-03 03:38:14 +02:00
712c28068d Plan picked path callbacks in app core 2026-06-03 03:33:33 +02:00
777723b68c Plan document share decisions in app core 2026-06-02 23:53:09 +02:00
cc3490d9d8 Plan recording session decisions in app core 2026-06-02 23:49:13 +02:00
d9be3f910a Plan cloud bulk upload progress in app core 2026-06-02 23:42:27 +02:00
8a7db3bca8 Plan cloud browse decisions in app core 2026-06-02 23:39:03 +02:00
3a78361aea Plan cloud upload decisions in app core 2026-06-02 23:34:58 +02:00
6e3296469a Route video export start through app core 2026-06-02 23:27:41 +02:00
561193b2ab Plan export start decisions in app core 2026-06-02 23:24:44 +02:00
8de9dadf1d Plan save-as file writes in app core 2026-06-02 23:18:19 +02:00
853307697a Plan new document creation in app core 2026-06-02 23:14:35 +02:00
fd1772a417 Plan document open actions in app core 2026-06-02 23:06:36 +02:00
1df506a176 Plan save-version targets in app core 2026-06-02 22:58:28 +02:00
b349f24931 Plan app export targets in app core 2026-06-02 22:50:42 +02:00
5841878df9 Plan document file saves in app core 2026-06-02 22:42:51 +02:00
c8d769c02c Route document workflow prompts through app core 2026-06-02 22:36:05 +02:00
d28aa25358 Route app save decisions through app core 2026-06-02 22:26:58 +02:00
76808d60e3 Extract app document session decisions 2026-06-02 22:21:08 +02:00
9dd53f9212 Expose app document routing in pano cli 2026-06-02 22:16:08 +02:00
0e03e5940a Add app document route core 2026-06-02 22:10:50 +02:00
e15894e4ea Contain retained assets and paint document paths 2026-06-02 22:05:19 +02:00
37b1cf82f3 Contain retained base UI controls 2026-06-02 22:00:56 +02:00
39444af84e Contain retained vendor source bundle 2026-06-02 21:58:27 +02:00
da584ce0f0 Contain retained OpenGL runtime sources 2026-06-02 21:56:22 +02:00
455c91bf29 Split retained legacy engine target 2026-06-02 21:53:17 +02:00
6fda4d4a90 Move app orchestration into app target 2026-06-02 21:50:07 +02:00
b84dfc049d Move app UI workflows into UI target 2026-06-02 21:48:17 +02:00
3a1ca7a8e6 Extract first PanoPainter UI target 2026-06-02 21:45:49 +02:00
b80bd759aa Move version metadata into app target 2026-06-02 21:40:30 +02:00
a2e47c862e Extract Windows runtime payload deployment 2026-06-02 21:36:31 +02:00
7b882896f1 Move Windows link ownership to platform shell 2026-06-02 21:29:29 +02:00
def1a170dc Split Windows app shell target 2026-06-02 21:27:46 +02:00
6a3cd867f0 Validate OpenGL command pass barriers 2026-06-02 21:16:58 +02:00
55b725e876 Validate OpenGL texture binding slots 2026-06-02 21:14:07 +02:00
d664e9fc39 Validate OpenGL command plan dependencies 2026-06-02 21:06:44 +02:00
1dcd96ab36 Plan OpenGL shader command metadata 2026-06-02 21:02:24 +02:00
b6a25474ff Plan OpenGL texture command metadata 2026-06-02 20:59:05 +02:00
b4c2117992 Expose OpenGL command plans in pano_cli 2026-06-02 20:52:53 +02:00
9a4c595f64 Plan recorded OpenGL command streams 2026-06-02 20:48:49 +02:00
ce33eaaef2 Plan recorded renderer commands for OpenGL 2026-06-02 20:45:03 +02:00
cc33fbdde2 Map render pass clear values to OpenGL 2026-06-02 20:35:28 +02:00
c18297f221 Map renderer sampler state to OpenGL 2026-06-02 20:33:20 +02:00
2f8f12a8fd Map renderer viewport and scissor to OpenGL 2026-06-02 20:29:30 +02:00
728116da8f Map renderer blend state to OpenGL 2026-06-02 20:26:55 +02:00
36f9e73dd4 Map renderer depth state to OpenGL 2026-06-02 20:23:49 +02:00
9b6c5b0849 Map render pass clear masks to OpenGL 2026-06-02 20:19:54 +02:00
cc4eaef3e6 Map renderer color write masks to OpenGL 2026-06-02 20:15:56 +02:00
77c2a68cc5 Map renderer blit filters to OpenGL 2026-06-02 18:30:46 +02:00
647dd81992 Map renderer sampler tokens to OpenGL 2026-06-02 18:28:25 +02:00
c5c31f0a56 Map renderer depth compares to OpenGL 2026-06-02 18:25:25 +02:00
b6c66f3e41 Map renderer blend tokens to OpenGL 2026-06-02 18:21:52 +02:00
1065183e75 Map renderer primitive topologies to OpenGL 2026-06-02 18:18:34 +02:00
dc03491b0d Map renderer texture formats to OpenGL 2026-06-02 18:14:40 +02:00
8c99454bf5 Check OpenGL readback byte counts 2026-06-02 18:09:45 +02:00
0fc73d51d2 Validate renderer blit descriptors first 2026-06-02 18:06:23 +02:00
831e5deeae Validate renderer readback descriptors 2026-06-02 18:03:42 +02:00
22dfde8e7c Validate shader resource label bounds 2026-06-02 18:00:30 +02:00
9a7f4bc0d2 Expose recording renderer clear reset automation 2026-06-02 17:57:51 +02:00
860e5ad31e Reset recording renderer state on clear 2026-06-02 17:53:17 +02:00
9b00acec6f Reject non-finite CLI float inputs 2026-06-02 17:48:13 +02:00
53fc5f9a57 Reject duplicate document snapshot payloads 2026-06-02 17:45:29 +02:00
f6780d183c Expose fuzz and stress test presets 2026-06-02 17:42:23 +02:00
48fdfd849d Reject unsupported parsed PPI blend modes 2026-06-02 17:39:45 +02:00
52da64fc96 Reject non-finite PPI layer opacity 2026-06-02 17:37:32 +02:00
9759abde44 Harden binary stream overlapping writes 2026-06-02 17:35:00 +02:00
06a44705d0 Harden stroke sampler edge tests 2026-06-02 17:32:12 +02:00
3ae84de123 Expose paint blend references in CLI 2026-06-02 17:27:41 +02:00
8c0784f9c3 Add stroke alpha blend reference tests 2026-06-02 17:23:44 +02:00
995752da75 Add renderer backend feature reporting 2026-06-02 17:18:48 +02:00
18617cdbd2 Add renderer texture transition contract 2026-06-02 17:13:44 +02:00
56cb9eaacb Expose renderer mipmap command in CLI 2026-06-02 17:06:31 +02:00
a5dbf05ab5 Add renderer resource label contract 2026-06-02 17:01:10 +02:00
bbe3db1747 Add renderer trace scope contract 2026-06-02 16:55:23 +02:00
07293c0590 Add renderer mipmap command contract 2026-06-02 16:47:44 +02:00
901aff1051 Add renderer texture usage contract 2026-06-02 16:42:53 +02:00
75dd5cfdc9 Add renderer texture copy command 2026-06-02 16:35:38 +02:00
483bbb4a9c Add renderer draw descriptor contract 2026-06-02 16:27:28 +02:00
58f163788b Add renderer render pass clear contract 2026-06-02 16:23:02 +02:00
8232b0efc8 Add renderer shader uniform command 2026-06-02 16:15:23 +02:00
23c308db1b Add renderer resource factory contract 2026-06-02 16:09:52 +02:00
881b5271a2 Capture renderer draw mesh inputs 2026-06-02 16:01:29 +02:00
952a00e7d3 Add renderer sampler state contract 2026-06-02 15:56:26 +02:00
b68ddc42c6 Add renderer depth state contract 2026-06-02 15:50:59 +02:00
9a7e1c4def Add renderer scissor state contract 2026-06-02 15:46:03 +02:00
5226746c1a Add renderer blend state contract 2026-06-02 15:40:43 +02:00
5dbeb0504d Add renderer texture binding contract 2026-06-02 15:34:57 +02:00
ee3fb36047 Add renderer blit command contract 2026-06-02 15:31:01 +02:00
1c40602744 Add renderer texture upload contract 2026-06-02 15:25:31 +02:00
818014127a Add renderer frame capture contract 2026-06-02 15:18:04 +02:00
d37145660a Harden recording renderer CLI bounds 2026-06-02 15:12:59 +02:00
c58b9a3718 Add renderer readback command contract 2026-06-02 15:10:44 +02:00
a6a4e7b249 Add stroke script CLI edge coverage 2026-06-02 11:34:17 +02:00
2b50c2157f Add stroke script document automation 2026-06-02 11:31:08 +02:00
0eded78c4c Add document export file automation 2026-06-02 11:25:07 +02:00
99b2eeb99d Add document export CLI automation 2026-06-02 11:18:19 +02:00
7b14c356db Validate snapshot face payloads 2026-06-02 11:14:41 +02:00
bad2670f87 Add document PPI export boundary 2026-06-02 11:11:01 +02:00
b3710498f3 Add explicit PPI project writer 2026-06-02 11:05:08 +02:00
1bc90d88b4 Add targeted PPI payload automation 2026-06-02 11:00:29 +02:00
ddca24779e Add document selection mask automation 2026-06-02 10:55:12 +02:00
1ab2a9b846 Add PPI layer metadata save automation 2026-06-02 10:48:03 +02:00
9c6b52eb8e Add image export roundtrip automation 2026-06-02 10:41:34 +02:00
9d05d193a7 Add positive import image automation 2026-06-02 10:35:40 +02:00
e6e80b94ba Add multi-layer PPI save automation 2026-06-02 10:32:10 +02:00
4e70c90ca8 Add multi-frame PPI save automation 2026-06-02 10:25:30 +02:00
a8faa82b70 Add PPI dirty-face payload save automation 2026-06-02 10:18:35 +02:00
4f4ac380ac Update PPI save modernization debt 2026-06-02 10:11:36 +02:00
374cb5b075 Add metadata-only PPI save automation 2026-06-02 10:10:30 +02:00
b0445382dd Add file-driven image import automation 2026-06-02 10:04:48 +02:00
3701fd2a71 Expose image import simulation through pano cli 2026-06-02 09:59:42 +02:00
1e4b4cad73 Expose document edits through pano cli 2026-06-02 09:56:12 +02:00
a6aa31da79 Expose document history through pano cli 2026-06-02 09:52:25 +02:00
b82cc1e4bd Expose recording renderer through pano cli 2026-06-02 09:47:09 +02:00
1d44036933 Add headless recording renderer api 2026-06-02 09:44:04 +02:00
61f86f5aae Add renderer boundary automation guard 2026-06-02 09:37:57 +02:00
acd8ef6658 Move Windows WGL context attributes to renderer gl 2026-06-02 09:32:27 +02:00
c22f2e7fa2 Route remaining canvas GL constants through renderer gl 2026-06-02 09:27:42 +02:00
b7d9dfbf31 Move canvas import export GL mappings to renderer gl 2026-06-02 09:24:37 +02:00
92338a0911 Move canvas layer merge GL mappings to renderer gl 2026-06-02 09:22:35 +02:00
f7d32f2835 Move canvas merge GL mappings to renderer gl 2026-06-02 09:20:18 +02:00
737c29cca4 Move canvas stroke commit GL mappings to renderer gl 2026-06-02 09:15:55 +02:00
37a59c01ac Move early canvas GL mappings to renderer gl 2026-06-02 09:12:39 +02:00
7ae37038b3 Move canvas resource formats to renderer gl 2026-06-02 09:09:22 +02:00
bbe8378630 Move NodeCanvas GL mappings to renderer gl 2026-06-02 09:04:28 +02:00
466c1d0cc0 Move canvas mode GL mappings to renderer gl 2026-06-02 09:00:53 +02:00
4a7eff24bf Move VR render state mapping to renderer gl 2026-06-02 08:58:07 +02:00
f968488e34 Move Windows GL info mapping to renderer gl 2026-06-02 08:54:51 +02:00
a12a3454c4 Move texture defaults behind renderer gl 2026-06-02 08:51:34 +02:00
8a92bc973b Move stroke preview render mapping to renderer gl 2026-06-02 08:47:27 +02:00
dbaf50cb6e Move GL state utility mapping to renderer gl 2026-06-02 08:43:42 +02:00
92e9de0441 Move grid panel render mapping to renderer gl 2026-06-02 08:39:54 +02:00
7280678593 Move canvas layer pixel mapping to renderer gl 2026-06-02 08:36:43 +02:00
b7c087617b Move simple UI blend mapping to renderer gl 2026-06-02 08:31:31 +02:00
d8e958769b Move color wheel state mapping to renderer gl 2026-06-02 08:28:24 +02:00
b85c530df7 Move image node state mapping to renderer gl 2026-06-02 08:24:58 +02:00
3823a612ae Move image texture node state mapping to renderer gl 2026-06-02 08:15:36 +02:00
2a3402e991 Move viewport render state mapping to renderer gl 2026-06-02 08:13:03 +02:00
bbb85bb133 Move canvas action pixel mapping to renderer gl 2026-06-02 08:08:49 +02:00
d0b0dc3865 Move font render mapping to renderer gl 2026-06-02 07:36:09 +02:00
6fc8b9e5d2 Move extension query mapping to renderer gl 2026-06-02 07:33:40 +02:00
217450e161 Move app render state mapping to renderer gl 2026-06-02 07:01:07 +02:00
3930f39b14 Move app command state mapping to renderer gl 2026-06-02 06:58:30 +02:00
19f815e3d2 Move app init state mapping to renderer gl 2026-06-02 06:54:43 +02:00
9e0a88726c Move shader state mapping to renderer gl 2026-06-02 06:51:31 +02:00
47eb1ec0b2 Move shape buffer mapping to renderer gl 2026-06-02 06:47:29 +02:00
0d2a1bd0ae Move texture cube mapping to renderer gl 2026-06-02 06:43:51 +02:00
85a5d19a3e Move texture2d mapping to renderer gl 2026-06-02 06:41:00 +02:00
02f14f1bf5 Move render target clear mapping to renderer gl 2026-06-02 06:38:32 +02:00
e00eec30d4 Move framebuffer setup mapping to renderer gl 2026-06-02 06:35:07 +02:00
43e3a74c42 Move pixel buffer mapping to renderer gl 2026-06-02 06:31:15 +02:00
75dfc85978 Move readback format mapping to renderer gl 2026-06-02 06:27:29 +02:00
9ce49ef19c Move framebuffer blit mapping to renderer gl 2026-06-02 06:25:14 +02:00
36fea6b870 Move sampler border parameter to renderer gl 2026-06-02 06:22:41 +02:00
8130a922d0 Move sampler parameter mapping to renderer gl 2026-06-01 18:13:55 +02:00
f1e2743d58 Move render target texture parameters to renderer gl 2026-06-01 18:11:44 +02:00
7d80afce2f Move cube face target mapping to renderer gl 2026-06-01 18:09:12 +02:00
4212387b70 Extract OpenGL shader uniform catalog 2026-06-01 18:06:14 +02:00
bdcd44b340 Extract OpenGL shader attribute bindings 2026-06-01 17:58:09 +02:00
05064b3a0d Move OpenGL shape draw mappings 2026-06-01 17:52:59 +02:00
aa32c47e18 Move OpenGL framebuffer diagnostics 2026-06-01 17:50:51 +02:00
2e0ebd0e13 Move OpenGL image format mapping 2026-06-01 17:48:30 +02:00
2754df9f46 Move OpenGL texture upload mapping 2026-06-01 17:46:48 +02:00
9ab73a0354 Add OpenGL renderer capability target 2026-06-01 17:44:00 +02:00
d61c7f37c3 Extract renderer shader catalog 2026-06-01 17:36:25 +02:00
162 changed files with 42001 additions and 2800 deletions

View File

@@ -20,6 +20,7 @@ endif()
include(PanoPainterWarnings)
include(PanoPainterSources)
include(PanoPainterVersion)
include(PanoPainterRuntime)
if(PP_ENABLE_CLANG_TIDY)
find_program(PP_CLANG_TIDY_EXE NAMES clang-tidy)
@@ -136,6 +137,7 @@ target_link_libraries(pp_paint
add_library(pp_document STATIC
src/document/document.cpp
src/document/ppi_export.cpp
src/document/ppi_import.cpp)
target_include_directories(pp_document
PUBLIC
@@ -150,7 +152,9 @@ target_link_libraries(pp_document
pp_project_warnings)
add_library(pp_renderer_api STATIC
src/renderer_api/renderer_api.cpp)
src/renderer_api/recording_renderer.cpp
src/renderer_api/renderer_api.cpp
src/renderer_api/shader_catalog.cpp)
target_include_directories(pp_renderer_api
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
@@ -161,6 +165,22 @@ target_link_libraries(pp_renderer_api
PRIVATE
pp_project_warnings)
if(PP_ENABLE_OPENGL)
add_library(pp_renderer_gl STATIC
src/renderer_gl/command_plan.cpp
src/renderer_gl/opengl_capabilities.cpp
src/renderer_gl/shader_bindings.cpp)
target_include_directories(pp_renderer_gl
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_renderer_gl
PUBLIC
pp_renderer_api
pp_project_options
PRIVATE
pp_project_warnings)
endif()
add_library(pp_paint_renderer STATIC
src/paint_renderer/compositor.cpp)
target_include_directories(pp_paint_renderer
@@ -190,6 +210,52 @@ target_link_libraries(pp_ui_core
pp_xml_tinyxml2
pp_project_warnings)
add_library(pp_platform_api STATIC
src/platform_api/platform_services.cpp
src/platform_api/platform_services.h)
target_include_directories(pp_platform_api
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_platform_api
PUBLIC
pp_project_options
PRIVATE
pp_project_warnings)
add_library(pp_app_core STATIC
src/app_core/about_menu.h
src/app_core/app_preferences.h
src/app_core/app_status.h
src/app_core/brush_ui.h
src/app_core/canvas_tool_ui.h
src/app_core/document_animation.h
src/app_core/document_canvas.h
src/app_core/document_cloud.h
src/app_core/document_export.cpp
src/app_core/document_import.h
src/app_core/document_layer.h
src/app_core/document_platform_io.h
src/app_core/document_recording.h
src/app_core/document_resize.h
src/app_core/document_route.cpp
src/app_core/document_sharing.h
src/app_core/document_session.cpp
src/app_core/file_menu.h
src/app_core/grid_ui.h
src/app_core/history_ui.h
src/app_core/main_toolbar.h
src/app_core/quick_ui.h
src/app_core/tools_menu.h)
target_include_directories(pp_app_core
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(pp_app_core
PUBLIC
pp_foundation
pp_project_options
PRIVATE
pp_project_warnings)
if(PP_BUILD_TOOLS)
add_subdirectory(tools/pano_cli)
endif()
@@ -201,15 +267,207 @@ endif()
if(PP_BUILD_APP)
if(WIN32)
add_library(pp_legacy_app STATIC
${PP_LEGACY_APP_SOURCES}
add_library(pp_legacy_vendor OBJECT
${PP_VENDOR_SOURCES})
target_link_libraries(pp_legacy_app
target_link_libraries(pp_legacy_vendor
PUBLIC
pp_project_options
PRIVATE
pp_project_warnings)
target_include_directories(pp_legacy_vendor
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_vendor
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_vendor PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
add_library(pp_legacy_renderer_gl OBJECT
${PP_LEGACY_RENDERER_GL_SOURCES})
target_link_libraries(pp_legacy_renderer_gl
PUBLIC
pp_project_options
PRIVATE
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_renderer_gl PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_renderer_gl
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_renderer_gl
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_renderer_gl PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_renderer_gl PRIVATE src/pch.h)
add_library(pp_legacy_assets_io OBJECT
${PP_LEGACY_ASSETS_IO_SOURCES})
target_link_libraries(pp_legacy_assets_io
PUBLIC
pp_project_options
PRIVATE
pp_assets
pp_project_warnings)
target_include_directories(pp_legacy_assets_io
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_assets_io
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_assets_io PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_assets_io PRIVATE src/pch.h)
add_library(pp_legacy_paint_document OBJECT
${PP_LEGACY_PAINT_DOCUMENT_SOURCES})
target_link_libraries(pp_legacy_paint_document
PUBLIC
pp_project_options
PRIVATE
pp_assets
pp_document
pp_paint
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_paint_document PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_paint_document
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_paint_document
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_paint_document PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_paint_document PRIVATE src/pch.h)
add_library(pp_legacy_engine STATIC
${PP_LEGACY_ENGINE_SOURCES}
$<TARGET_OBJECTS:pp_legacy_assets_io>
$<TARGET_OBJECTS:pp_legacy_paint_document>
$<TARGET_OBJECTS:pp_legacy_renderer_gl>
$<TARGET_OBJECTS:pp_legacy_vendor>)
target_link_libraries(pp_legacy_engine
PUBLIC
pp_project_options
PRIVATE
pp_assets
pp_document
pp_paint
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_engine PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_engine
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_engine
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_engine PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_engine PRIVATE src/pch.h)
add_library(pp_legacy_ui_core OBJECT
${PP_LEGACY_UI_CORE_SOURCES})
target_link_libraries(pp_legacy_ui_core
PUBLIC
pp_app_core
pp_legacy_engine
pp_project_options
PRIVATE
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_ui_core PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_ui_core
PUBLIC
${PP_LEGACY_INCLUDE_DIRS})
target_compile_definitions(pp_legacy_ui_core
PUBLIC
ENUM_BITFIELDS_NOT_SUPPORTED
UNICODE
_UNICODE
_CRT_SECURE_NO_WARNINGS
_SCL_SECURE_NO_WARNINGS
_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
_CONSOLE
WITH_CURL=1)
set_target_properties(pp_legacy_ui_core PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_ui_core PRIVATE src/pch.h)
add_library(pp_legacy_app STATIC
${PP_LEGACY_APP_SOURCES}
$<TARGET_OBJECTS:pp_legacy_ui_core>)
target_link_libraries(pp_legacy_app
PUBLIC
pp_legacy_engine
pp_legacy_ui_core
pp_project_options
PRIVATE
pp_renderer_api
pp_project_warnings)
if(TARGET pp_renderer_gl)
target_link_libraries(pp_legacy_app PRIVATE pp_renderer_gl)
endif()
target_include_directories(pp_legacy_app
PUBLIC
@@ -230,19 +488,40 @@ if(PP_BUILD_APP)
VS_GLOBAL_CharacterSet "Unicode")
target_precompile_headers(pp_legacy_app PRIVATE src/pch.h)
set_source_files_properties(${PP_VENDOR_SOURCES}
PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
set_source_files_properties(src/version.cpp
PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
add_executable(PanoPainter WIN32
${PP_WINDOWS_APP_SOURCES})
target_link_libraries(PanoPainter
PRIVATE
pp_project_options
pp_project_warnings
add_library(pp_panopainter_ui STATIC
${PP_PANOPAINTER_UI_SOURCES})
target_link_libraries(pp_panopainter_ui
PUBLIC
pp_legacy_app
pp_project_options
PRIVATE
pp_project_warnings)
target_precompile_headers(pp_panopainter_ui REUSE_FROM pp_legacy_app)
set_target_properties(pp_panopainter_ui PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
add_library(panopainter_app STATIC
${PP_PANOPAINTER_APP_SOURCES})
target_include_directories(panopainter_app
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
target_link_libraries(panopainter_app
PUBLIC
pp_app_core
pp_legacy_app
pp_panopainter_ui
pp_platform_api
pp_project_options
PRIVATE
pp_project_warnings)
pp_add_version_generation(panopainter_app "$<IF:$<CONFIG:Debug>,debug,release>")
add_library(pp_platform_windows OBJECT
${PP_WINDOWS_PLATFORM_SOURCES})
target_link_libraries(pp_platform_windows
PUBLIC
panopainter_app
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.lib"
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.lib>"
"$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.lib>"
@@ -257,34 +536,28 @@ if(PP_BUILD_APP)
shell32
shlwapi
user32
wbemuuid)
wbemuuid
PRIVATE
pp_project_options
pp_project_warnings)
target_precompile_headers(pp_platform_windows REUSE_FROM pp_legacy_app)
set_target_properties(pp_platform_windows PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
add_executable(PanoPainter WIN32
${PP_WINDOWS_APP_SOURCES}
$<TARGET_OBJECTS:pp_platform_windows>)
target_link_libraries(PanoPainter
PRIVATE
pp_project_options
pp_project_warnings
pp_platform_windows)
target_precompile_headers(PanoPainter REUSE_FROM pp_legacy_app)
set_target_properties(PanoPainter PROPERTIES
VS_GLOBAL_CharacterSet "Unicode")
pp_add_version_generation(PanoPainter "$<IF:$<CONFIG:Debug>,debug,release>")
add_custom_command(TARGET PanoPainter POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/data"
"$<TARGET_FILE_DIR:PanoPainter>/data"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.dll"
"$<TARGET_FILE_DIR:PanoPainter>/BugTrapU-x64.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.dll>$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.dll>"
"$<TARGET_FILE_DIR:PanoPainter>/"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/libyuv.dll"
"$<TARGET_FILE_DIR:PanoPainter>/libyuv.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.dll"
"$<TARGET_FILE_DIR:PanoPainter>/libmp4v2.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.dll"
"$<TARGET_FILE_DIR:PanoPainter>/openh264-2.0.0-win64.dll"
VERBATIM)
pp_configure_windows_runtime_payloads(PanoPainter)
else()
message(WARNING "PP_BUILD_APP is enabled, but the root CMake app target is currently Windows-only. Platform alignment is tracked in Phase 6.")
endif()

View File

@@ -200,6 +200,22 @@
{
"name": "android-focus-arm64",
"configurePreset": "android-focus-arm64"
},
{
"name": "emscripten",
"configurePreset": "emscripten"
},
{
"name": "macos",
"configurePreset": "macos"
},
{
"name": "ios-device",
"configurePreset": "ios-device"
},
{
"name": "ios-simulator",
"configurePreset": "ios-simulator"
}
],
"testPresets": [
@@ -250,6 +266,30 @@
"label": "gpu"
}
}
},
{
"name": "fuzz",
"configurePreset": "windows-msvc-default",
"output": {
"outputOnFailure": true
},
"filter": {
"include": {
"label": "fuzz"
}
}
},
{
"name": "stress",
"configurePreset": "windows-msvc-default",
"output": {
"outputOnFailure": true
},
"filter": {
"include": {
"label": "stress"
}
}
}
]
}

View File

@@ -0,0 +1,26 @@
function(pp_configure_windows_runtime_payloads target_name)
if(NOT TARGET "${target_name}")
message(FATAL_ERROR "pp_configure_windows_runtime_payloads target does not exist: ${target_name}")
endif()
add_custom_command(TARGET "${target_name}" POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/data"
"$<TARGET_FILE_DIR:${target_name}>/data"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/bugtrap-client/lib/BugTrapU-x64.dll"
"$<TARGET_FILE_DIR:${target_name}>/BugTrapU-x64.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"$<$<CONFIG:Debug>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-debug-x64/libcurl_debug.dll>$<$<NOT:$<CONFIG:Debug>>:${CMAKE_CURRENT_SOURCE_DIR}/libs/curl-win/lib/dll-release-x64/libcurl.dll>"
"$<TARGET_FILE_DIR:${target_name}>/"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/libyuv/lib/win/libyuv.dll"
"$<TARGET_FILE_DIR:${target_name}>/libyuv.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/mp4v2/lib/win/libmp4v2.dll"
"$<TARGET_FILE_DIR:${target_name}>/libmp4v2.dll"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/libs/openh264/lib/openh264-2.0.0-win64.dll"
"$<TARGET_FILE_DIR:${target_name}>/openh264-2.0.0-win64.dll"
VERBATIM)
endfunction()

View File

@@ -1,6 +1,67 @@
set(PP_LEGACY_APP_SOURCES
set(PP_LEGACY_ENGINE_SOURCES
src/hmd.cpp
src/log.cpp
src/mp4enc.cpp
src/util.cpp
src/wacom.cpp
)
set(PP_LEGACY_ASSETS_IO_SOURCES
src/abr.cpp
src/asset.cpp
src/binary_stream.cpp
src/image.cpp
src/serializer.cpp
src/settings.cpp
)
set(PP_LEGACY_PAINT_DOCUMENT_SOURCES
src/action.cpp
src/bezier.cpp
src/brush.cpp
src/canvas.cpp
src/canvas_actions.cpp
src/canvas_layer.cpp
src/event.cpp
)
set(PP_LEGACY_RENDERER_GL_SOURCES
src/font.cpp
src/rtt.cpp
src/shader.cpp
src/shape.cpp
src/texture.cpp
)
set(PP_LEGACY_UI_CORE_SOURCES
src/layout.cpp
src/node.cpp
src/node_border.cpp
src/node_button.cpp
src/node_button_custom.cpp
src/node_checkbox.cpp
src/node_combobox.cpp
src/node_icon.cpp
src/node_image.cpp
src/node_image_texture.cpp
src/node_input_box.cpp
src/node_popup_menu.cpp
src/node_progress_bar.cpp
src/node_remote_page.cpp
src/node_scroll.cpp
src/node_settings.cpp
src/node_shorcuts.cpp
src/node_slider.cpp
src/node_text.cpp
src/node_text_input.cpp
)
set(PP_LEGACY_APP_SOURCES
src/canvas_modes.cpp
src/pch.cpp
)
set(PP_PANOPAINTER_APP_SOURCES
src/app.cpp
src/app_cloud.cpp
src/app_commands.cpp
@@ -9,32 +70,17 @@ set(PP_LEGACY_APP_SOURCES
src/app_layout.cpp
src/app_shaders.cpp
src/app_vr.cpp
src/asset.cpp
src/bezier.cpp
src/binary_stream.cpp
src/brush.cpp
src/canvas.cpp
src/canvas_actions.cpp
src/canvas_layer.cpp
src/canvas_modes.cpp
src/event.cpp
src/font.cpp
src/hmd.cpp
src/image.cpp
src/layout.cpp
src/log.cpp
src/mp4enc.cpp
src/node.cpp
src/platform_legacy/legacy_platform_services.cpp
src/platform_legacy/legacy_platform_services.h
src/version.cpp
)
set(PP_PANOPAINTER_UI_SOURCES
src/node_about.cpp
src/node_border.cpp
src/node_button.cpp
src/node_button_custom.cpp
src/node_canvas.cpp
src/node_changelog.cpp
src/node_checkbox.cpp
src/node_color_quad.cpp
src/node_colorwheel.cpp
src/node_combobox.cpp
src/node_dialog_browse.cpp
src/node_dialog_cloud.cpp
src/node_dialog_export_ppbr.cpp
@@ -42,10 +88,6 @@ set(PP_LEGACY_APP_SOURCES
src/node_dialog_open.cpp
src/node_dialog_picker.cpp
src/node_dialog_resize.cpp
src/node_icon.cpp
src/node_image.cpp
src/node_image_texture.cpp
src/node_input_box.cpp
src/node_message_box.cpp
src/node_metadata.cpp
src/node_panel_animation.cpp
@@ -56,33 +98,19 @@ set(PP_LEGACY_APP_SOURCES
src/node_panel_layer.cpp
src/node_panel_quick.cpp
src/node_panel_stroke.cpp
src/node_popup_menu.cpp
src/node_progress_bar.cpp
src/node_remote_page.cpp
src/node_scroll.cpp
src/node_settings.cpp
src/node_shorcuts.cpp
src/node_slider.cpp
src/node_stroke_preview.cpp
src/node_text.cpp
src/node_text_input.cpp
src/node_tool_bucket.cpp
src/node_usermanual.cpp
src/node_viewport.cpp
src/pch.cpp
src/rtt.cpp
src/serializer.cpp
src/settings.cpp
src/shader.cpp
src/shape.cpp
src/texture.cpp
src/util.cpp
src/version.cpp
src/wacom.cpp
)
set(PP_WINDOWS_PLATFORM_SOURCES
src/main.cpp
src/platform_windows/windows_platform_services.cpp
src/platform_windows/windows_platform_services.h
)
set(PP_WINDOWS_APP_SOURCES
src/main.cpp
PanoPainter.rc
)
@@ -140,4 +168,3 @@ set(PP_LEGACY_INCLUDE_DIRS
"${CMAKE_CURRENT_SOURCE_DIR}/libs/wacom"
"${CMAKE_CURRENT_SOURCE_DIR}/libs/yoga"
)

View File

@@ -4,7 +4,9 @@ function(pp_configure_project_warnings target)
/W4
/permissive-
/Zc:__cplusplus
/Zc:preprocessor)
/Zc:preprocessor
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
/wd4100)
if(PP_ENABLE_MSVC_ANALYZE)
target_compile_options(${target} INTERFACE /analyze)
endif()
@@ -15,7 +17,9 @@ function(pp_configure_project_warnings target)
-Wpedantic
-Wconversion
-Wshadow
-Wnull-dereference)
-Wnull-dereference
# DEBT-0019: remove once legacy callback/interface parameters are either named intentionally or consumed.
-Wno-unused-parameter)
endif()
if(PP_ENABLE_ASAN)

View File

@@ -1,7 +1,7 @@
# Build And Platform Inventory
Status: live
Last updated: 2026-06-01
Last updated: 2026-06-03
This inventory records the known build surfaces during the CMake migration.
Keep it updated as platform paths move to shared CMake targets.
@@ -10,7 +10,7 @@ Keep it updated as platform paths move to shared CMake targets.
| Platform/Target | Current Entrypoint | Notes |
| --- | --- | --- |
| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022 |
| Windows desktop | Root `CMakeLists.txt`, preset `windows-msvc-default`; target preset `windows-vs2026-x64` retained for VS 2026 | Raw `.sln/.vcxproj` files removed on 2026-05-31; local machine currently uses Visual Studio 17 2022; `PanoPainter` now links through `pp_platform_windows` and `panopainter_app`, with Windows/vendor link dependencies owned by the platform shell, runtime payload deployment in `cmake/PanoPainterRuntime.cmake`, tested app-level document-open routing plus open/close/save session decisions owned by `pp_app_core`, SDK-free clipboard/cursor/virtual-keyboard/display/share/picker service contracts owned by `pp_platform_api`, and injected `WindowsPlatformServices` now isolated in `src/platform_windows/windows_platform_services.*`; retained third-party source dependencies contained by `pp_legacy_vendor`, retained asset/file/serialization sources contained by `pp_legacy_assets_io`, retained paint/document/canvas sources contained by `pp_legacy_paint_document`, retained OpenGL runtime sources contained by `pp_legacy_renderer_gl` and folded into `pp_legacy_engine`, retained runtime shell sources contained by `pp_legacy_engine`, retained base UI controls contained by `pp_legacy_ui_core` and folded into `pp_legacy_app`, app orchestration/version metadata owned by `panopainter_app`, and app-specific modal/dialog/panel/canvas workflow nodes owned by `pp_panopainter_ui` |
| Windows AppX | `PanoPainterPackage/Package.appxmanifest`, `.wapproj` referenced by solution | Distribution packaging |
| macOS | `PanoPainter-OSX/` project files and `Info.plist` | Uses `NSOpenGLView` today |
| iOS | `PanoPainter/Info.plist`, related Apple sources | Uses OpenGL ES today |
@@ -61,6 +61,8 @@ These commands are the current local baseline.
cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
ctest --preset desktop-fast --build-config Debug
ctest --preset fuzz --build-config Debug
ctest --preset stress --build-config Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
cmake --build --preset windows-msvc-default --target panopainter_validate_shaders
@@ -88,40 +90,452 @@ Known local toolchain state:
treating `windows-clangcl-asan` as a passing sanitizer gate.
- Android arm64 headless configure/build passes through root CMake and the
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_renderer_gl`,
`pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
including foundation event/logging/task queue coverage, PNG metadata and
decode, PPI header/layout, settings document, document
snapshot/per-layer-frame/move/duration/face-pixel coverage, paint
brush/stroke/stroke-script coverage, renderer shader descriptor coverage,
UI color parsing, and layout XML parse coverage.
including foundation binary-stream/event/logging/task queue coverage, PNG metadata and
decode, PPI header/layout/non-finite opacity and blend-mode rejection, settings document, document
snapshot/per-layer-frame/move/duration/face-pixel/PPI export coverage,
snapshot-embedded duplicate/invalid face-payload and selection-mask rejection, paint brush/final-blend/
stroke-alpha-blend/stroke spacing/stroke stress/stroke-script coverage,
renderer shader descriptor and OpenGL capability coverage, UI
color parsing, and layout XML parse coverage.
- Root CMake exposes named `fuzz` and `stress` CTest presets. `fuzz` currently
runs deterministic parser/serializer edge tests for binary streams, image
metadata, PPI, stroke scripts, and layout XML; `stress` currently runs the
stroke sampler stress coverage.
- `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by
`pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture.
- `pp_assets_image_pixels_tests` decodes PNG payloads, encodes RGBA8 pixels to
PNG, round-trips encoded pixels back through the decoder, and rejects corrupt
or malformed image payloads.
- `pano_cli import-image` accepts a PNG path, decodes RGBA8 pixels through
`pp_assets`, attaches them to a pure `pp_document` face payload, and is
covered for checked-in decodable PNG import by `pano_cli_import_image_smoke`
and metadata-valid truncated PNG rejection by
`pano_cli_import_image_rejects_truncated_png`.
- `pano_cli export-image` writes a deterministic RGBA8 PNG through `pp_assets`
and is covered by `pano_cli_export_image_roundtrip_smoke`, which imports the
generated file back through `pano_cli import-image`.
- `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout,
body summary fields, layer/frame descriptors, and dirty-face PNG payload
metadata, and is covered by `pano_cli_inspect_project_layout_smoke` with a
minimal PPI fixture.
- `pp_assets_image_pixels_tests` decodes PNG payloads to RGBA8 and rejects
corrupt image payloads.
- `pp_document_ppi_import_tests` attaches decoded PPI dirty-face payloads to
`pp_document` layer/frame storage and rejects payloads outside document
layers.
- `pp_document_ppi_export_tests` exports pure `pp_document` metadata,
per-layer frame durations, and RGBA8 face payloads to PPI bytes through
`pp_assets`, then decodes and reimports them for round-trip coverage.
- `pano_cli simulate-document-export` exposes the same pure document-to-PPI
export, asset-level decode, and document reimport path through JSON
automation and is covered by `pano_cli_simulate_document_export_smoke`.
- `pano_cli save-document-project` writes that pure document export to a PPI
file and is covered by `pano_cli_save_document_project_roundtrip_smoke`,
which inspects and loads the generated file.
- `pano_cli load-project` creates a `pp_document` projection with per-layer
frame counts, durations, and decoded face-pixel payloads when present; the
metadata-only minimal fixture remains covered by
`pano_cli_load_project_metadata_smoke`.
- `pp_assets::create_ppi_project` writes generated multi-layer, multi-frame
PPI files with explicit per-layer names, opacity, blend mode, alpha lock,
visibility, per-layer frame durations, and targeted dirty-face layer/frame
payloads. `pano_cli save-project` exposes that path for automation and is
covered by `pano_cli_save_project_roundtrip_smoke` and
`pano_cli_save_project_payload_roundtrip_smoke`, which reload generated
metadata-only and targeted dirty-face-payload projects through
`pano_cli load-project`, plus
`pano_cli_save_project_rejects_non_finite_opacity`, which verifies rejected
automation floats do not create output files.
- `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and
is covered by `pano_cli_create_animation_document_smoke`.
- `pano_cli simulate-document-edits` exercises pure document layer/frame edit
operations, renderer-free face payloads, and renderer-free selection masks,
and is covered by `pano_cli_simulate_document_edits_smoke`.
- `pano_cli simulate-document-history` exercises pure document history
apply/undo/redo behavior and is covered by
`pano_cli_simulate_document_history_smoke`.
- `pano_cli simulate-image-import` decodes an embedded tiny PNG through
`pp_assets`, attaches it to `pp_document`, and is covered by
`pano_cli_simulate_image_import_smoke`.
- `pano_cli simulate-blend` exposes deterministic final RGBA and stroke-alpha
blend reference vectors through JSON automation and is covered by
`pano_cli_simulate_blend_smoke`.
- `pano_cli simulate-stroke` exposes the pure stroke sampler for scripted
automation and is covered by `pano_cli_simulate_stroke_smoke`.
- `pano_cli simulate-stroke-script` loads a text stroke script fixture and is
covered by `pano_cli_simulate_stroke_script_smoke`.
- `pano_cli apply-stroke-script` parses a text stroke script fixture, samples
every stroke through `pp_paint`, maps the samples into a bounded
`pp_document` RGBA8 face payload, writes a PPI file, and is covered by
`pano_cli_apply_stroke_script_roundtrip_smoke`, which inspects the dirty-face
box and loads the generated file back as decoded document pixel data, plus
`pano_cli_apply_stroke_script_rejects_tiny_canvas` for invalid dimension
rejection.
- `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes.
- `pp_renderer_api` owns the canonical PanoPainter shader catalog consumed by
the legacy OpenGL app initialization path; `pp_renderer_api_tests` validates
catalog size, key entries, duplicate rejection, and bad path rejection.
- `pp_renderer_gl` owns headless OpenGL runtime capability detection consumed
by the legacy app initialization path; `pp_renderer_gl_capabilities_tests`
validates framebuffer fetch, map-buffer alignment, desktop GL float support,
GLES float/half-float extensions, WebGL exclusion behavior, and the
upload-type mapping used by legacy `Texture2D` and `RTT` creation, plus the
RGBA pixel-format mapping used by `RTT` texture allocation. It also validates
image channel-count to OpenGL texture format mapping, including
invalid channel counts rejected by `Texture2D::create(Image)`, renderer API
texture-format to OpenGL internal/pixel/component token mapping including
depth-stencil formats, RGBA8/RGBA32F
readback formats, checked byte-count math, and PBO pixel-buffer target/usage/access
mapping used by `RTT` and `PBO` readbacks, and framebuffer status naming
used by `RTT` and `Texture2D` diagnostics. It also owns the 2D texture target,
framebuffer setup, readback format, mipmap target, and update component-type
tokens used by `Texture2D`, plus cube-map binding and allocation face targets
used by `TextureCube`. It also owns and
validates framebuffer blit color mask and linear/nearest filters used by
`RTT::resize` and `RTT::copy`, renderer API blit-filter to OpenGL token
mapping, plus the default linear clamp-to-edge
render-target texture parameters, texture/renderbuffer targets, depth format,
framebuffer targets, binding queries, attachment points, and completion
status used by `RTT::create` and framebuffer bind/restore paths, plus RTT
clear color/depth masks, renderer API render-pass color/depth/stencil
clear-mask and clear-value mapping, and color-write-mask query tokens. `RTT` no longer
spells GL enum names directly. It also
validates renderer API primitive-topology to OpenGL draw-mode mapping, Shape
index-type, fill/stroke primitive-mode, buffer target, static upload usage,
and vertex attribute component/normalization mapping used by
the legacy mesh draw path. Legacy `Shape` mesh buffer/VAO creation, zero-byte
dynamic-buffer creation, dynamic vertex/index uploads, fill/stroke draw
calls, and buffer/VAO deletion now consume tested dispatch contracts here,
plus the PanoPainter cube-face to OpenGL
texture-target mapping used by `TextureCube`.
It also owns and validates sampler wrap S/T/R, min/mag filter, and desktop
border-color parameter mapping used by legacy `Sampler`, plus renderer API
sampler filter/address-mode to OpenGL token mapping including mirrored-repeat
and aggregate renderer API sampler-state to OpenGL min/mag/wrap mapping.
Legacy `TextureCube` allocation/bind/delete and legacy `Sampler`
create/configure/border/bind/unbind calls now consume those resource dispatch
contracts directly from the retained app utilities.
The PanoPainter
shader attribute binding catalog, shader stage tokens, compile/link status
queries, active-uniform count query, and matrix-uniform transpose token used
by legacy `Shader` creation also live here. Renderer API blend factor/op to
OpenGL token mapping is tested here with explicit support flags so `GL_ZERO`
stays distinguishable from unsupported enum values. Aggregate renderer API
blend-state to OpenGL enable/factor/equation/color-mask mapping, depth
compare-op to OpenGL depth-function mapping, and aggregate renderer API
depth-state to OpenGL enable/write/compare mapping are tested here too.
`Shader` no longer spells GL enum
names directly. It also owns the PanoPainter shader uniform catalog and legacy hash
mapping used by `Shader` active-uniform discovery and the uniform uniqueness
check. Legacy `Shader` program use/delete, uniform writes, and
attribute-location lookup now consume tested dispatch contracts here.
Legacy shader source compilation, shader deletion, program attach/link,
attribute rebinding, active-uniform count/enumeration, and uniform-location
discovery now consume tested dispatch contracts as well, leaving the retained
shader utility with thin GL adapter functions.
App OpenGL initialization debug severity, debug output, GL info string,
renderer API viewport/scissor rect conversion, default depth/program-point/
line-smooth state, blend factor/equation, and UI render-target RGBA8 format
tokens are cataloged and tested here too, including the legacy convert command
and resize path. App clear color-buffer masks, default framebuffer binding,
scissor state, and sampler filter/wrap tokens also consume the backend mapping.
OpenGL extension enumeration query tokens
used before runtime capability detection are cataloged here. Legacy font
atlas texture formats, text mesh buffer targets, attribute component and
normalization tokens, draw primitive/index type, upload usage, and active
texture unit selection also consume the backend mapping. Text mesh
buffer/VAO creation, deferred index/vertex uploads, and indexed draw calls
now consume tested `pp_renderer_gl` mesh dispatch contracts too. Canvas undo/redo
dirty-region texture updates and readbacks also consume the backend-owned 2D
texture target, RGBA pixel format, and unsigned-byte component mapping.
`NodeViewport` preview rendering also consumes backend-owned viewport query,
clear-color query, color-buffer clear mask, and blend-state tokens.
`NodeImageTexture` preview drawing also consumes backend-owned fallback 2D
texture bind and blend-state tokens.
`NodeImage` drawing and remote-image texture creation also consume
backend-owned mipmapped sampler filters, blend-state tokens, and RGBA8/RGBA
texture format mapping.
`NodeColorWheel` triangle-buffer setup and draw-state handling also consume
backend-owned array-buffer, static-upload, vertex-attribute, primitive-mode,
and blend-state tokens.
Simple UI text, text-input, border, scroll, and animation timeline draw
paths also consume backend-owned blend-state tokens.
Canvas layer cube/equirect generation, clear, restore, and snapshot paths
also consume backend-owned cube/2D texture targets, active texture units,
blend/clear state, and RGBA8 read/write pixel mapping.
`NodePanelGrid` heightmap preview and lightmap baking also consume
backend-owned texture readback formats, sampler filters, depth/blend state,
depth clears, viewport queries, color-mask booleans, active texture units,
and float render-target formats.
Legacy `util.cpp` OpenGL error naming and `gl_state` save/restore also
consume backend-owned error codes, state queries, framebuffer targets,
texture binding targets, and active texture units.
`NodeStrokePreview` brush preview rendering also consumes backend-owned
depth/scissor/blend state, viewport/clear-color queries, active texture
units, 2D texture targets, copy targets, and sampler filters/wraps.
Legacy `Texture2D`, `TextureManager`, `Sampler`, and `RTT` public headers no
longer expose raw OpenGL enum defaults; default texture formats, sampler
filters/wraps, and render-target formats resolve through backend-owned
overloads.
The Windows entrypoint also consumes backend-owned generic OpenGL
error-code/info-string tokens and WGL core-context/pixel-format attribute
catalogs.
The headless OpenGL command planner consumes `pp_renderer_api` recorded
commands and maps render-pass clear masks/values, viewport/scissor state,
blend/depth/sampler state, texture formats, primitive modes, draw counts, and
blit filters into GL-facing planned command data while rejecting unsupported
enum tokens before a real GL context is needed. It also plans whole recorded
command streams, preserving per-command planned data while counting render
passes, draws, shader binds, shader uniforms, texture/sampler binds, texture
uploads, mipmap generation, texture transitions, texture copies, texture
readbacks, frame captures, passthrough commands, trace commands, unsupported
commands, and render-pass ordering errors such as state changes outside a
pass, nested passes, texture I/O or blits inside a pass, and unclosed passes.
It also validates executable command dependencies, including
shader-before-uniform and shader-plus-mesh before draw within each render
pass, and rejects invalid texture/sampler bind slots in malformed recorded
streams. `pano_cli record-render` emits the OpenGL plan texture/sampler bind
counts so automation can assert backend interpretation without an OpenGL
context.
Desktop VR drawing also consumes backend-owned scissor/depth/blend state,
depth clear masks, active texture units, and fallback 2D texture unbind
targets while retaining the existing VR SDK/platform bridge shape.
Canvas mode overlay, mask, and transform paths also consume backend-owned
blend/depth state, active texture units, 2D texture copy targets, and RGBA8
readback format tokens.
`NodeCanvas` panorama UI rendering also consumes backend-owned sampler
defaults, viewport/clear-state queries, blend/depth/scissor state, color
clear masks, active texture units, fallback 2D texture unbind targets, copy
targets, and RGBA8 render-target formats.
Canvas resource setup also consumes backend-owned stroke-buffer
RGBA8/RGBA16F/RGBA32F formats, flood-fill texture upload format/type,
brush/stencil/mix sampler filters and wraps, and image channel-count texture
formats for cube-strip imports. Clamp-to-border sampler wrap is now part of
the backend capability catalog and test coverage.
Early canvas draw helpers also consume backend-owned pick readback
format/type, stroke mixer depth/scissor/blend state, saved viewport and
clear-state queries, active texture units, fallback 2D texture unbind
targets, and stroke background copy targets.
Canvas stroke commit also consumes backend-owned saved viewport/clear/blend
state, history readback format/type, active texture units, fallback 2D
texture unbind targets, and layer compositing copy targets.
Canvas layer merge rendering and explicit layer-merge compositing also consume
backend-owned depth/blend state, active texture units, fallback 2D texture
unbind targets, and merge framebuffer copy targets.
Canvas equirectangular import drawing and depth export rendering also consume
backend-owned depth/blend state and active texture units.
Canvas thumbnail generation and object-drawing helpers also consume
backend-owned saved viewport/clear/blend state, active texture units,
readback format/type, framebuffer copy targets, and renderbuffer/depth
attachment parameters; `src/canvas.cpp` no longer contains raw `GL_*`
constants.
Windows desktop OpenGL context creation now consumes a tested
`windows_wgl_core_context_3_3_config()` catalog from `pp_renderer_gl` instead
of owning active WGL context/pixel-format attribute literals in `main.cpp`.
- `windows-msvc-vcpkg-headless` validates manifest install/configure/build/test
for the current headless component matrix; see DEBT-0007 for remaining app
and platform triplet migration.
- `scripts/automation/analyze.*` runs shader validation plus a
renderer-boundary guard that reports JSON and fails if active non-backend
source code reintroduces raw `GL_*`/`WGL_*` constants outside the allowed
legacy OpenGL implementation files.
- `pp_renderer_api` exposes a headless `RecordingRenderDevice` that reports
renderer feature flags and validates backend-owned resource creation,
explicit texture usage flags, command order,
render-pass color/depth/stencil clear intent, scissor state, depth state,
blend state, texture-slot binding, sampler-state binding, texture-upload byte
counts, texture mip-level counts, texture/mesh/shader resource debug labels, mipmap-generation commands,
texture-state transitions, shader-uniform writes, explicit draw descriptor ranges, texture-copy regions,
readback/frame-capture/blit descriptor validation, readback bounds, destination buffer sizes, and
render-target blit regions, records
render-pass-clear/scissor/depth/blend/shader-uniform/texture-bind/
sampler-bind/draw/upload/mipmap-generation/texture-transition/texture-copy/readback/
frame-capture/blit commands, draw mesh inputs, explicit draw ranges, and
records trace markers and scopes without a window or GL context. Recorder
`clear()` also resets active render-pass and trace-scope state so automation
can reuse the same recording device after an interrupted frame.
- `pano_cli record-render` exposes the recording renderer through JSON
automation, including backend feature flags, render-pass/depth-clear counts, scissor/depth/blend/
shader-uniform/texture-bind/sampler-bind/upload/mipmap-generation/texture-transition/texture-copy/readback/
frame-capture/blit command and byte totals, trace marker/scope counts,
labeled descriptor counts, backend resource creation counts, plus draw
descriptor vertex/index totals. When `pp_renderer_gl` is available, it also
emits an `openGlPlan` JSON object with the planned command count, support
status, render-pass/draw/shader-bind/uniform/texture-upload/mipmap/
transition/copy/readback/capture/passthrough/trace counts, unsupported
command count, render-pass order error count, dependency error count, and
unclosed-pass state. Its
`--exercise-clear` mode verifies
interrupted-frame recorder clear/reuse behavior and reports the result in
JSON, and is covered by `pano_cli_record_render_smoke`,
`pano_cli_record_render_exercises_clear_reset`, plus
`pano_cli_record_render_rejects_oversized_target`.
- `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory`
apply/undo/redo state through JSON automation and is covered by
`pano_cli_simulate_document_history_smoke`.
- `pano_cli simulate-document-edits` exposes `pp_document` layer metadata,
frame order, active-index, tiny face-payload state, and selection-mask state
through JSON automation and is covered by
`pano_cli_simulate_document_edits_smoke`.
- `pano_cli simulate-image-import` exposes embedded PNG decode and document
face-payload attachment through JSON automation and is covered by
`pano_cli_simulate_image_import_smoke`.
- `pano_cli import-image` exposes file-driven PNG decode and document
face-payload attachment through JSON automation and is covered by
`pano_cli_import_image_smoke` and
`pano_cli_import_image_rejects_truncated_png`.
- `pano_cli export-image` exposes deterministic RGBA8 PNG writing through JSON
automation and is covered by `pano_cli_export_image_roundtrip_smoke`; full
legacy canvas export remains a future CLI task.
- `pano_cli save-project` exposes generated multi-layer, multi-frame PPI
writing with layer metadata and targeted dirty-face layer/frame payloads
through JSON automation and is covered by metadata-only and
dirty-face-payload round-trip smoke tests; full legacy canvas save parity
remains tracked by DEBT-0013.
- `pp_document::export_ppi_project_document` exposes pure document-to-PPI byte
export through CTest coverage; legacy Canvas save integration remains a
future DEBT-0010/DEBT-0013 task.
- `pano_cli simulate-document-export` exposes document export round-trip state
through JSON automation for agent-driven checks.
- `pano_cli save-document-project` exposes file-writing document export
automation for inspect/load round trips.
- `pano_cli apply-stroke-script` exposes file-driven stroke-script application
to a pure document face payload and writes a PPI artifact for inspect/load
round-trip automation.
- `pano_cli classify-open` exposes the `pp_app_core` document-open route
contract as JSON and is covered for project files, ABR imports, PPBR
imports, and malformed path rejection.
- `pano_cli plan-open-route` exposes `pp_app_core` document-open action
planning as JSON and is covered for clean project open, dirty project
discard-prompt, and ABR import-prompt states.
- `pano_cli plan-new-document` exposes `pp_app_core` new-document target,
legacy resolution-index mapping, and overwrite-prompt planning as JSON and is
covered for save-now, existing-target overwrite, and invalid-resolution
states.
- `pano_cli plan-document-file` exposes `pp_app_core` document-name
validation, legacy `.ppi` path construction, and overwrite-prompt decisions
as JSON through the same combined save-file plan consumed by the live save-as
dialog; it is covered for save-now and existing-target overwrite states.
- `pano_cli plan-document-version` exposes `pp_app_core` save-version suffix
parsing, candidate path generation, collision skipping, and no-slot failure
behavior as JSON and is covered for first-version and existing-path skip
states.
- `pano_cli plan-export-target` exposes `pp_app_core` export target planning
for image file exports, layer/frame collection directories, picked-directory
stems, and MP4 suggested names as JSON and is covered for file, collection,
and suggested-name states.
- `pano_cli plan-export-start` exposes `pp_app_core` export availability
planning for license-gated, demo-blocked, and missing-canvas states as JSON;
the live image, layer, animation-frame, depth, and cube-face export dialogs
plus MP4 animation and timelapse export dialogs consume the same start
contract before reaching legacy canvas/recording export execution.
- `pano_cli plan-recording-session` exposes `pp_app_core` recording start,
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.
- `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
retained no-op platform branches.
- `pano_cli plan-picked-path` exposes `pp_app_core` selected-path planning as
JSON for empty and non-empty file picker results; live image/file/save/
directory picker branches consume the same contract before invoking retained
platform callbacks or legacy picker bridges.
- `pano_cli plan-display-file` exposes `pp_app_core` external file presentation
planning as JSON for empty and non-empty paths; the live display-file command
consumes the same contract before retained platform open-file bridges.
- `pano_cli plan-keyboard-visibility` exposes `pp_app_core` virtual keyboard
visibility planning as JSON for hidden and visible states; live show/hide
keyboard requests consume the same contract before retained mobile platform
keyboard bridges.
- `pano_cli plan-cursor-visibility` exposes `pp_app_core` cursor visibility
planning as JSON for hidden and visible states; live canvas cursor requests
consume the same contract before retained desktop platform cursor bridges.
- `pano_cli plan-clipboard-read` and `pano_cli plan-clipboard-write` expose
`pp_app_core` clipboard text planning as JSON, including empty text writes;
live clipboard get/set requests consume the same contracts before retained
platform clipboard bridges.
- `pp_platform_api` exposes the SDK-free `PlatformServices` interface for
startup storage path preparation, clipboard text, cursor visibility,
virtual-keyboard visibility, external file display, file sharing, native
app/window close, UI-thread lifecycle hooks, render-context lifecycle hooks,
render-target binding hooks, render platform hint hooks, render-capture frame
hooks, render debug callback hooks, per-frame platform hooks, picker
callbacks, and recording cleanup, live asset/layout reload policy, diagnostic
stacktrace/crash hooks, prepared-file save/download handoff;
Windows
live app execution now uses injected
`WindowsPlatformServices` from
`src/platform_windows/windows_platform_services.*` in `pp_platform_windows`,
while non-Windows platforms still reach retained platform bridges through
the debt-tracked adapter isolated in
`src/platform_legacy/legacy_platform_services.*`.
- `pp_renderer_gl` owns the tested `OpenGlInitialState` startup depth/blend
policy and dispatch application consumed by `App::init`, tested runtime
version/vendor/renderer/GLSL string query dispatch, tested default clear
color/buffer dispatch consumed by `App::clear`, tested app UI
viewport/scissor dispatch consumed by `App::draw` and `App::vr_draw_ui`,
tested generic capability/buffer-clear dispatch consumed by VR draw state
setup, tested saved-state snapshot/restore dispatch consumed by the retained
`gl_state` utility, tested texture lifecycle/readback dispatch consumed by
the retained `Texture2D` utility, tested framebuffer blit/readback dispatch
consumed by retained `RTT` resize/copy/readback paths, tested framebuffer
bind/restore dispatch consumed by retained `RTT` render-target pass entry
and exit paths, plus renderer API to OpenGL token mapping and command-planning
contracts used by the OpenGL parity work.
- `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability,
new-document warning, publish prompt, and save-before-upload planning as JSON;
the live cloud upload command consumes the same start contract before
reaching legacy UI, canvas save, and network upload execution.
- `pano_cli plan-cloud-upload-all` exposes bulk cloud upload file-count,
progress UI availability, and progress-total clamping as JSON; the live
upload-all command consumes the same contract before reaching legacy asset
file listing, OpenGL context guard, progress UI, and network upload
execution.
- `pano_cli plan-cloud-browse` exposes `pp_app_core` cloud browse availability
and selected-file download planning as JSON; the live cloud browse command
consumes those contracts before reaching legacy dialog, network download,
canvas project-open, layer UI, and action-history execution.
- `pano_cli simulate-app-session` exposes `pp_app_core` project-open,
app-close, save, save-as, save-version, and save-before-workflow decisions
as JSON and is covered for clean, dirty, already-prompting, missing-canvas,
new-document, save-as, save-version, and dirty-save-version states.
- `pp_app_core_document_route_tests` covers the app document-open route
contract for PPI/project files, ABR imports, PPBR imports, inner-dot names,
and malformed paths before the live `App::open_document` performs UI or
legacy canvas work.
- `pp_app_core_document_export_tests` covers export file targets, collection
directory/stem targets, picked-directory stems, MP4 suggested names, and
invalid export naming inputs, plus export-start license/canvas availability
decisions.
- `pp_app_core_document_recording_tests` covers recording start/stop, clear,
platform recorded-file cleanup, frame-count reset, export progress totals,
and oversized progress-total clamping.
- `pp_app_core_document_sharing_tests` covers saved-path gating before platform
share execution.
- `pp_app_core_document_platform_io_tests` covers empty selected-path filtering
and non-empty picked-path callback planning, plus empty/non-empty display-file
planning before platform picker/display callbacks, plus virtual keyboard
show/hide planning before platform keyboard callbacks, plus cursor visibility
planning before platform cursor callbacks, plus clipboard read/write
planning before platform clipboard callbacks.
- `pp_platform_api_tests` covers service dispatch for clipboard read/write,
empty clipboard writes, cursor visibility, virtual-keyboard visibility,
external file display, file sharing, and picker callbacks without platform
SDK headers or a window.
- `pp_app_core_document_cloud_tests` covers cloud upload no-canvas,
new-document warning, clean publish prompt, and dirty save-before-upload
decisions, plus cloud browse no-canvas/show-browser and selected-download
decisions, plus bulk upload progress visibility, zero-file, and clamped
progress-total decisions.
- `pp_app_core_document_session_tests` covers clean and dirty app session,
document-open action planning, save-request, save-before-workflow,
new-document target/resolution/overwrite planning, document file target,
combined save-file overwrite planning, and save-version target decisions
without requiring a window, canvas, or message box.
- `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON`
through the vcpkg preset; default and Android validation still use the
retained vendored fallback tracked by DEBT-0012.
@@ -129,6 +543,40 @@ Known local toolchain state:
Known warnings after the current CMake app build:
- Legacy code/vendor warnings under `/W4`.
- `pp_legacy_vendor` intentionally owns retained third-party source builds for
now, including JPEG, SQLite, Yoga, poly2tri, GLAD, fmt, Wacom utilities, and
other patched/embedded sources. Each dependency should either move to vcpkg,
an SDK import target, or a documented permanent vendored target.
- `pp_legacy_assets_io` is an object-library containment boundary for retained
ABR, asset/file, binary stream, image, serializer, and settings code. It
should shrink as app I/O consumes `pp_assets` directly.
- `pp_legacy_paint_document` is an object-library containment boundary for
retained action, bezier, brush, canvas, canvas-layer, and event code. It
should shrink as app painting and document behavior consume `pp_paint` and
`pp_document` directly.
- `pp_legacy_engine` intentionally contains retained legacy runtime shell
sources for now, so it concentrates existing legacy tablet, video, HMD, log,
and low-level utility warnings until those paths move to cleaner component
ownership.
- `pp_legacy_renderer_gl` is an object-library containment boundary because
the retained OpenGL runtime classes still include legacy app/engine headers
and are still consumed directly by canvas and UI classes. It should become a
normal backend library once those call sites depend on `pp_renderer_api`.
- `pp_legacy_ui_core` is an object-library containment boundary because the
retained base `Node` controls still depend on legacy renderer and app
headers. It should shrink as layout parsing, colors, generic controls, and
text/image primitives move to `pp_ui_core`.
- `pp_panopainter_ui` currently surfaces existing legacy `Node`/`Serializer`
header and static-analysis warnings while it still depends on
`pp_legacy_app`; these should be reduced as the UI core/app UI boundary is
tightened instead of suppressed globally.
- `pp_app_core` is the first pure app-engine target consumed by
`panopainter_app`; it should grow only with UI-free command routing,
validation, and app service contracts that can be tested without a window.
- `panopainter_app` currently surfaces existing app orchestration, GLM,
base64, VR, and serializer warnings now that app sources live in the
composition target; warning cleanup should follow component ownership rather
than be hidden with target-wide suppressions.
- Visual Studio vcpkg manifest warning because manifest mode is not enabled.
- `LNK4099` missing `yuv.pdb` for retained libyuv binaries.
- `LNK4098` runtime library conflict from retained vendor binaries.

View File

@@ -1,7 +1,7 @@
# PanoPainter Capability Map
Status: live
Last updated: 2026-05-31
Last updated: 2026-06-02
This map is the preservation checklist for the modernization. When a component
is extracted, update the relevant rows with the owning component, test label,
@@ -12,17 +12,19 @@ and validation command.
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| PPI open/save | `Canvas`, serializer, dialogs | `pp_document`, `pp_assets`, `pano_cli` | Round-trip tiny project, old-version fixture, corrupt/truncated fixture |
| Open-document routing | `App::open_document` | `pp_app_core`, `pano_cli`, `pp_panopainter_ui`, `pp_document`, `pp_assets` | Project/ABR/PPBR route tests, malformed path tests, open-action plan tests, CLI route/action smoke, app open smoke |
| Document session decisions | `App::open_document`, `App::request_close`, save hotkeys, file menu, dialogs | `pp_app_core`, `pano_cli`, `pp_panopainter_ui` | Clean/dirty/prompt-open/save/save-as/save-version/save-before-workflow/name/new-document resolution/overwrite/version-target decision tests, CLI session, new-document, document-file, and document-version smoke, app close/open/save/new/browse smoke |
| Version metadata | `scripts/pre-build.py`, `version.*` | build system, `pp_foundation` | Generated header smoke test, missing-tag behavior |
| Thumbnail generation/read | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden thumbnail, corrupt input |
| Save-as, overwrite prompts | App/dialogs | `pp_panopainter_ui`, `pp_platform_*` | UI automation and platform smoke |
| Save-as, overwrite prompts | App/dialogs | `pp_app_core`, `pp_panopainter_ui`, `pp_platform_*` | Decision tests, UI automation, and platform smoke |
## Image And Export
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| PNG/JPEG import | `Image`, `Canvas` import paths | `pp_assets`, `pp_document` | Fixture import, malformed file |
| PNG/JPEG export | `Canvas`, `Image` | `pp_assets`, `pp_paint_renderer` | Golden output tolerance |
| Equirectangular import/export | `Canvas`, shaders, RTT | `pp_paint_renderer` | Tiny cube/equirect golden |
| PNG/JPEG export | `Canvas`, `Image`, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core` | Golden output tolerance, export start/target planning tests |
| Equirectangular import/export | `Canvas`, shaders, RTT, export dialogs | `pp_paint_renderer`, `pp_app_core` | Tiny cube/equirect golden, app-core file target tests |
| Cube face export | `Canvas` | `pp_paint_renderer` | Six-face golden set |
| Depth export | `Canvas`, grid tools | `pp_paint_renderer` | Float/readback validation |
@@ -34,8 +36,8 @@ and validation command.
| ABR import | `ABR`, `Brush` | `pp_assets`, `pp_paint` | Sample ABR and malformed ABR |
| PPBR import/export | brush panel/dialog | `pp_assets`, `pp_panopainter_ui` | Round-trip fixture |
| Stroke sampling | `Stroke`, `Canvas` | `pp_paint` | Property tests for spacing, pressure, jitter |
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | CPU reference and GPU golden |
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | CPU reference vectors and GPU parity |
| Dual brush/pattern behavior | `Brush`, shaders | `pp_paint`, `pp_paint_renderer` | Stroke-alpha CPU reference and GPU golden |
| Blend modes | GLSL include files, layer rendering | `pp_paint`, `pp_paint_renderer` | Final RGBA and stroke-alpha CPU reference vectors plus GPU parity |
| Erase/flood fill/masks | `Canvas`, modes, shaders | `pp_document`, `pp_paint_renderer` | Edge masks, alpha lock, dirty rects |
## Layers And Animation
@@ -46,7 +48,7 @@ and validation command.
| Blend/opacity/visibility/alpha lock | `Layer`, UI panels, shaders | `pp_document`, `pp_paint_renderer` | CPU model and render golden |
| Selection mask | `Canvas` mask layer | `pp_document`, `pp_paint_renderer` | Mask apply/clear edge cases |
| Animation frames | `LayerFrame`, animation panel | `pp_document`, `pp_panopainter_ui` | Duration, duplicate, remove, seek |
| MP4/timelapse export | `MP4Encoder`, recording thread | `pp_assets`, `pp_paint_renderer`, app | Smoke export and cancellation |
| MP4/timelapse export | `MP4Encoder`, recording thread, export dialogs | `pp_assets`, `pp_paint_renderer`, `pp_app_core`, app | Recording lifecycle/progress decision tests, smoke export, cancellation, suggested-name tests |
## UI And Workflow
@@ -63,10 +65,10 @@ and validation command.
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Mouse/keyboard/touch/gestures | `App`, platform entrypoints | `pp_platform_*`, app | Synthetic event playback |
| 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 |
| Wacom pressure | `WacomTablet` | `pp_platform_windows` | Adapter smoke with fallback |
| Clipboard/file picker/share | `App` platform methods | `pp_platform_*` | Platform smoke or mocked service |
| Virtual keyboard | platform entrypoints | `pp_platform_*` | Platform smoke |
| 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 |
| OpenVR desktop | `HMD`, `Vive`, `app_vr` | `pp_platform_vr`, app | Compile gate and mocked pose tests |
| Quest/OVR | Android Quest files | `pp_platform_android_quest` | Compile/package gate |
| Focus/Wave | Android Focus files | `pp_platform_android_wave` | Compile/package gate |
@@ -75,9 +77,8 @@ and validation command.
| Capability | Current Area | Target Owner | Required Tests |
| --- | --- | --- | --- |
| Upload/download/browse | `app_cloud`, CURL helpers | app service, `pp_platform_*` | Mocked HTTP and timeout tests |
| Upload/download/browse | `app_cloud`, CURL helpers | `pp_app_core`, app service, `pp_platform_*` | Upload prompt/new-doc/no-canvas decision tests, bulk-upload progress decision tests, browse/selection decision tests, mocked HTTP and timeout tests |
| License/check flows | app/cloud code | app service | Mocked response tests |
| Logging/crash reporting | `log`, BugTrap/AppCenter | `pp_foundation`, platform wrappers | Log formatting and platform compile |
| Headless automation | none yet | `tools/pano_cli` | JSON command fixtures |
| Tracing | none yet | `pp_foundation` | Span nesting/timing tests |

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -19,16 +19,25 @@ if ($NoApp) {
& cmake @argsList
$configureExitCode = $LASTEXITCODE
$shaderExitCode = 0
$rendererBoundaryExitCode = 0
if ($configureExitCode -eq 0) {
& cmake --build --preset $Preset --target panopainter_validate_shaders
$shaderExitCode = $LASTEXITCODE
}
if ($configureExitCode -eq 0) {
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "check-renderer-boundary.ps1")
$rendererBoundaryExitCode = $LASTEXITCODE
}
$exitCode = $configureExitCode
if ($exitCode -eq 0 -and $shaderExitCode -ne 0) {
$exitCode = $shaderExitCode
}
if ($exitCode -eq 0 -and $rendererBoundaryExitCode -ne 0) {
$exitCode = $rendererBoundaryExitCode
}
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
@@ -44,6 +53,10 @@ $elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
name = "shader-validation"
exitCode = $shaderExitCode
},
[ordered]@{
name = "renderer-boundary"
exitCode = $rendererBoundaryExitCode
}
)
elapsedMs = $elapsed

View File

@@ -1,20 +1,29 @@
#!/usr/bin/env sh
set -u
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
preset="${1:-linux-clang}"
start="$(date +%s)"
cmake --preset "$preset" -DPP_ENABLE_CLANG_TIDY=ON -DPP_ENABLE_CPPCHECK=ON
configure_exit_code="$?"
shader_exit_code="0"
renderer_boundary_exit_code="0"
if [ "$configure_exit_code" -eq 0 ]; then
cmake --build --preset "$preset" --target panopainter_validate_shaders
shader_exit_code="$?"
fi
if [ "$configure_exit_code" -eq 0 ]; then
"$script_dir/check-renderer-boundary.sh"
renderer_boundary_exit_code="$?"
fi
exit_code="$configure_exit_code"
if [ "$exit_code" -eq 0 ] && [ "$shader_exit_code" -ne 0 ]; then
exit_code="$shader_exit_code"
fi
if [ "$exit_code" -eq 0 ] && [ "$renderer_boundary_exit_code" -ne 0 ]; then
exit_code="$renderer_boundary_exit_code"
fi
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"analyze","preset":"%s","exitCode":%s,"checks":[{"name":"configure","exitCode":%s},{"name":"shader-validation","exitCode":%s}],"elapsedMs":%s}\n' "$preset" "$exit_code" "$configure_exit_code" "$shader_exit_code" "$elapsed_ms"
printf '{"command":"analyze","preset":"%s","exitCode":%s,"checks":[{"name":"configure","exitCode":%s},{"name":"shader-validation","exitCode":%s},{"name":"renderer-boundary","exitCode":%s}],"elapsedMs":%s}\n' "$preset" "$exit_code" "$configure_exit_code" "$shader_exit_code" "$renderer_boundary_exit_code" "$elapsed_ms"
exit "$exit_code"

View File

@@ -0,0 +1,62 @@
[CmdletBinding()]
param(
[string]$Root = ""
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($Root)) {
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
}
$started = Get-Date
$pattern = '\b(?:GL|WGL)_[A-Z0-9_]+\b'
$allowed = @(
"src/renderer_gl/",
"src/rtt.cpp",
"src/texture.cpp"
)
$violations = @()
$files = Get-ChildItem -Path (Join-Path $Root "src") -Recurse -File -Include *.c,*.cc,*.cpp,*.h,*.hpp
foreach ($file in $files) {
$relative = $file.FullName.Substring($Root.Length).TrimStart('\', '/').Replace('\', '/')
$isAllowed = $false
foreach ($prefix in $allowed) {
if ($relative.StartsWith($prefix)) {
$isAllowed = $true
break
}
}
if ($isAllowed) {
continue
}
$lineNumber = 0
foreach ($line in Get-Content -LiteralPath $file.FullName) {
$lineNumber += 1
$trimmed = $line.TrimStart()
if ($trimmed.StartsWith("//")) {
continue
}
$matches = [regex]::Matches($line, $pattern)
foreach ($match in $matches) {
$violations += [ordered]@{
file = $relative
line = $lineNumber
token = $match.Value
}
}
}
}
$exitCode = if ($violations.Count -eq 0) { 0 } else { 1 }
$elapsed = [int]((Get-Date) - $started).TotalMilliseconds
[ordered]@{
command = "check-renderer-boundary"
exitCode = $exitCode
violationCount = $violations.Count
violations = @($violations | Select-Object -First 50)
elapsedMs = $elapsed
} | ConvertTo-Json -Compress -Depth 4
exit $exitCode

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env sh
set -u
script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
root="${1:-$(CDPATH= cd -- "$script_dir/../.." && pwd)}"
start="$(date +%s)"
tmp="${TMPDIR:-/tmp}/panopainter-renderer-boundary-$$.txt"
find "$root/src" -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) | while IFS= read -r file; do
rel="${file#"$root"/}"
case "$rel" in
src/renderer_gl/*|src/rtt.cpp|src/texture.cpp)
continue
;;
esac
awk -v rel="$rel" '
/^[[:space:]]*\/\// { next }
match($0, /\<(GL|WGL)_[A-Z0-9_]+\>/) {
print rel ":" FNR ":" substr($0, RSTART, RLENGTH)
}
' "$file"
done > "$tmp"
count="$(wc -l < "$tmp" | tr -d '[:space:]')"
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
exit_code="0"
if [ "$count" -ne 0 ]; then
exit_code="1"
fi
printf '{"command":"check-renderer-boundary","exitCode":%s,"violationCount":%s,"elapsedMs":%s}\n' "$exit_code" "$count" "$elapsed_ms"
rm -f "$tmp"
exit "$exit_code"

View File

@@ -2,11 +2,210 @@
param(
[string]$Preset = "windows-msvc-default",
[string]$Configuration = "Debug",
[string]$Target = "PanoPainter"
[string]$Target = "PanoPainter",
[string[]]$PackageKinds = @(
"windows-appx",
"android-standard-apk",
"android-quest-apk",
"android-focus-apk",
"apple-bundle",
"webgl"
)
)
$ErrorActionPreference = "Stop"
$started = Get-Date
$root = (Get-Location).Path
function Test-CommandAvailable {
param([string]$Name)
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
}
function New-ArtifactCheck {
param(
[string]$Name,
[string]$Path,
[string]$PathType = "Any"
)
$exists = if ($PathType -eq "Container") {
Test-Path -LiteralPath $Path -PathType Container
} elseif ($PathType -eq "Leaf") {
Test-Path -LiteralPath $Path -PathType Leaf
} else {
Test-Path -LiteralPath $Path
}
[ordered]@{
name = $Name
path = $Path
pathType = $PathType
exists = $exists
}
}
function New-Prerequisite {
param(
[string]$Name,
[bool]$Available,
[string]$Detail
)
[ordered]@{
name = $Name
available = $Available
detail = $Detail
}
}
function New-PackageReadiness {
param(
[string]$Kind,
[string]$Status,
[string]$Reason,
[object[]]$Prerequisites,
[object[]]$Artifacts,
[string]$ValidationCommand
)
[ordered]@{
kind = $Kind
status = $Status
reason = $Reason
debt = "DEBT-0011"
validationCommand = $ValidationCommand
prerequisites = $Prerequisites
artifacts = $Artifacts
}
}
function Get-PackageReadiness {
param([string[]]$Kinds)
$readiness = @()
foreach ($kind in $Kinds) {
switch ($kind) {
"windows-appx" {
$wapproj = Join-Path $root "PanoPainterPackage/PanoPainterPackage.wapproj"
$manifest = Join-Path $root "PanoPainterPackage/Package.appxmanifest"
$appPackages = Join-Path $root "PanoPainterPackage/AppPackages"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-wapproj-present-but-root-cmake-package-target-missing" `
-ValidationCommand "msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=$Configuration /p:Platform=x64" `
-Prerequisites @(
(New-Prerequisite -Name "legacy-wapproj" -Available (Test-Path -LiteralPath $wapproj -PathType Leaf) -Detail $wapproj),
(New-Prerequisite -Name "appx-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
(New-Prerequisite -Name "makeappx" -Available (Test-CommandAvailable "makeappx") -Detail "Windows SDK packaging tool"),
(New-Prerequisite -Name "signtool" -Available (Test-CommandAvailable "signtool") -Detail "Windows SDK signing tool"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "app-packages" -Path $appPackages -PathType "Container")
)
}
"android-standard-apk" {
$gradle = Join-Path $root "android/android/build.gradle"
$manifest = Join-Path $root "android/android/src/main/AndroidManifest.xml"
$apkDir = Join-Path $root "android/android/build/outputs/apk"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
-ValidationCommand "gradle -p android/android assembleDebug" `
-Prerequisites @(
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-arm64/android-x64"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
)
}
"android-quest-apk" {
$gradle = Join-Path $root "android/quest/build.gradle"
$manifest = Join-Path $root "android/quest/src/main/AndroidManifest.xml"
$apkDir = Join-Path $root "android/quest/build/outputs/apk"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
-ValidationCommand "gradle -p android/quest assembleDebug" `
-Prerequisites @(
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-quest-arm64"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
)
}
"android-focus-apk" {
$gradle = Join-Path $root "android/focus/build.gradle"
$manifest = Join-Path $root "android/focus/src/main/AndroidManifest.xml"
$apkDir = Join-Path $root "android/focus/build/outputs/apk"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-gradle-package-not-consuming-root-cmake-targets" `
-ValidationCommand "gradle -p android/focus assembleDebug" `
-Prerequisites @(
(New-Prerequisite -Name "gradle-build" -Available (Test-Path -LiteralPath $gradle -PathType Leaf) -Detail $gradle),
(New-Prerequisite -Name "android-manifest" -Available (Test-Path -LiteralPath $manifest -PathType Leaf) -Detail $manifest),
(New-Prerequisite -Name "gradle" -Available (Test-CommandAvailable "gradle") -Detail "Android package builder"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "android-focus-arm64"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "apk-output" -Path $apkDir -PathType "Container")
)
}
"apple-bundle" {
$xcodeProject = Join-Path $root "PanoPainter.xcodeproj/project.pbxproj"
$bundleDir = Join-Path $root "out/package/apple"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target" `
-ValidationCommand "xcodebuild -project PanoPainter.xcodeproj -configuration $Configuration" `
-Prerequisites @(
(New-Prerequisite -Name "legacy-xcode-project" -Available (Test-Path -LiteralPath $xcodeProject -PathType Leaf) -Detail $xcodeProject),
(New-Prerequisite -Name "xcodebuild" -Available (Test-CommandAvailable "xcodebuild") -Detail "Apple package builder"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "macos/ios-device/ios-simulator"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "apple-package-output" -Path $bundleDir -PathType "Container")
)
}
"webgl" {
$webDir = Join-Path $root "out/package/webgl"
$readiness += New-PackageReadiness `
-Kind $kind `
-Status "blocked" `
-Reason "emscripten-preset-exists-but-webgl-package-target-missing" `
-ValidationCommand "cmake --build --preset emscripten --target PanoPainter" `
-Prerequisites @(
(New-Prerequisite -Name "emcc" -Available (Test-CommandAvailable "emcc") -Detail "Emscripten compiler"),
(New-Prerequisite -Name "emcmake" -Available (Test-CommandAvailable "emcmake") -Detail "Emscripten CMake wrapper"),
(New-Prerequisite -Name "root-cmake-preset" -Available $true -Detail "emscripten"),
(New-Prerequisite -Name "root-cmake-package-target" -Available $false -Detail "Not migrated yet")
) `
-Artifacts @(
(New-ArtifactCheck -Name "webgl-output" -Path $webDir -PathType "Container")
)
}
}
}
return $readiness
}
& cmake --build --preset $Preset --config $Configuration --target $Target
$buildExitCode = $LASTEXITCODE
@@ -20,6 +219,7 @@ if ($buildExitCode -ne 0) {
stage = "build"
exitCode = $buildExitCode
elapsedMs = $elapsed
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
} | ConvertTo-Json -Compress
exit $buildExitCode
}
@@ -43,6 +243,7 @@ $elapsedMs = [int]((Get-Date) - $started).TotalMilliseconds
exitCode = $exitCode
elapsedMs = $elapsedMs
checks = $checks
packageReadiness = Get-PackageReadiness -Kinds $PackageKinds
} | ConvertTo-Json -Compress -Depth 5
exit $exitCode

View File

@@ -6,13 +6,91 @@ configuration="${2:-Debug}"
target="${3:-PanoPainter}"
artifact="${4:-out/build/$preset/$target}"
start="$(date +%s)"
root="$(pwd)"
json_string() {
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
}
json_bool() {
if [ "$1" = "1" ]; then
printf true
else
printf false
fi
}
command_available() {
command -v "$1" >/dev/null 2>&1
}
file_available() {
[ -f "$1" ]
}
dir_available() {
[ -d "$1" ]
}
package_readiness_json() {
windows_wapproj="$root/PanoPainterPackage/PanoPainterPackage.wapproj"
windows_manifest="$root/PanoPainterPackage/Package.appxmanifest"
windows_output="$root/PanoPainterPackage/AppPackages"
android_standard_gradle="$root/android/android/build.gradle"
android_standard_manifest="$root/android/android/src/main/AndroidManifest.xml"
android_standard_output="$root/android/android/build/outputs/apk"
android_quest_gradle="$root/android/quest/build.gradle"
android_quest_manifest="$root/android/quest/src/main/AndroidManifest.xml"
android_quest_output="$root/android/quest/build/outputs/apk"
android_focus_gradle="$root/android/focus/build.gradle"
android_focus_manifest="$root/android/focus/src/main/AndroidManifest.xml"
android_focus_output="$root/android/focus/build/outputs/apk"
apple_project="$root/PanoPainter.xcodeproj/project.pbxproj"
apple_output="$root/out/package/apple"
webgl_output="$root/out/package/webgl"
file_available "$windows_wapproj"; windows_wapproj_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$windows_manifest"; windows_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available makeappx; makeappx_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available signtool; signtool_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$windows_output"; windows_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_standard_gradle"; android_standard_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_standard_manifest"; android_standard_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_quest_gradle"; android_quest_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_quest_manifest"; android_quest_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_focus_gradle"; android_focus_gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$android_focus_manifest"; android_focus_manifest_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available gradle; gradle_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$android_standard_output"; android_standard_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$android_quest_output"; android_quest_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$android_focus_output"; android_focus_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
file_available "$apple_project"; apple_project_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available xcodebuild; xcodebuild_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$apple_output"; apple_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available emcc; emcc_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
command_available emcmake; emcmake_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
dir_available "$webgl_output"; webgl_output_exists="$([ "$?" -eq 0 ] && printf 1 || printf 0)"
printf '['
printf '{"kind":"windows-appx","status":"blocked","reason":"legacy-wapproj-present-but-root-cmake-package-target-missing","debt":"DEBT-0011","validationCommand":"msbuild PanoPainterPackage/PanoPainterPackage.wapproj /p:Configuration=%s /p:Platform=x64","prerequisites":[{"name":"legacy-wapproj","available":%s,"detail":%s},{"name":"appx-manifest","available":%s,"detail":%s},{"name":"makeappx","available":%s,"detail":"Windows SDK packaging tool"},{"name":"signtool","available":%s,"detail":"Windows SDK signing tool"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"app-packages","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$windows_wapproj_exists")" "$(json_string "$windows_wapproj")" "$(json_bool "$windows_manifest_exists")" "$(json_string "$windows_manifest")" "$(json_bool "$makeappx_exists")" "$(json_bool "$signtool_exists")" "$(json_string "$windows_output")" "$(json_bool "$windows_output_exists")"
printf ',{"kind":"android-standard-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/android assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-arm64/android-x64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_standard_gradle_exists")" "$(json_string "$android_standard_gradle")" "$(json_bool "$android_standard_manifest_exists")" "$(json_string "$android_standard_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_standard_output")" "$(json_bool "$android_standard_output_exists")"
printf ',{"kind":"android-quest-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/quest assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-quest-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_quest_gradle_exists")" "$(json_string "$android_quest_gradle")" "$(json_bool "$android_quest_manifest_exists")" "$(json_string "$android_quest_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_quest_output")" "$(json_bool "$android_quest_output_exists")"
printf ',{"kind":"android-focus-apk","status":"blocked","reason":"legacy-gradle-package-not-consuming-root-cmake-targets","debt":"DEBT-0011","validationCommand":"gradle -p android/focus assembleDebug","prerequisites":[{"name":"gradle-build","available":%s,"detail":%s},{"name":"android-manifest","available":%s,"detail":%s},{"name":"gradle","available":%s,"detail":"Android package builder"},{"name":"root-cmake-preset","available":true,"detail":"android-focus-arm64"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apk-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$android_focus_gradle_exists")" "$(json_string "$android_focus_gradle")" "$(json_bool "$android_focus_manifest_exists")" "$(json_string "$android_focus_manifest")" "$(json_bool "$gradle_exists")" "$(json_string "$android_focus_output")" "$(json_bool "$android_focus_output_exists")"
printf ',{"kind":"apple-bundle","status":"blocked","reason":"legacy-xcode-project-and-host-toolchain-not-aligned-with-root-cmake-package-target","debt":"DEBT-0011","validationCommand":"xcodebuild -project PanoPainter.xcodeproj -configuration %s","prerequisites":[{"name":"legacy-xcode-project","available":%s,"detail":%s},{"name":"xcodebuild","available":%s,"detail":"Apple package builder"},{"name":"root-cmake-preset","available":true,"detail":"macos/ios-device/ios-simulator"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"apple-package-output","path":%s,"pathType":"Container","exists":%s}]}' "$configuration" "$(json_bool "$apple_project_exists")" "$(json_string "$apple_project")" "$(json_bool "$xcodebuild_exists")" "$(json_string "$apple_output")" "$(json_bool "$apple_output_exists")"
printf ',{"kind":"webgl","status":"blocked","reason":"emscripten-preset-exists-but-webgl-package-target-missing","debt":"DEBT-0011","validationCommand":"cmake --build --preset emscripten --target PanoPainter","prerequisites":[{"name":"emcc","available":%s,"detail":"Emscripten compiler"},{"name":"emcmake","available":%s,"detail":"Emscripten CMake wrapper"},{"name":"root-cmake-preset","available":true,"detail":"emscripten"},{"name":"root-cmake-package-target","available":false,"detail":"Not migrated yet"}],"artifacts":[{"name":"webgl-output","path":%s,"pathType":"Container","exists":%s}]}' "$(json_bool "$emcc_exists")" "$(json_bool "$emcmake_exists")" "$(json_string "$webgl_output")" "$(json_bool "$webgl_output_exists")"
printf ']'
}
cmake --build --preset "$preset" --config "$configuration" --target "$target"
build_exit="$?"
if [ "$build_exit" -ne 0 ]; then
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms"
readiness="$(package_readiness_json)"
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","stage":"build","exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$build_exit" "$elapsed_ms" "$readiness"
exit "$build_exit"
fi
@@ -24,5 +102,6 @@ fi
end="$(date +%s)"
elapsed_ms="$(( (end - start) * 1000 ))"
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms"
readiness="$(package_readiness_json)"
printf '{"command":"package-smoke","preset":"%s","configuration":"%s","target":"%s","artifact":"%s","exists":%s,"exitCode":%s,"elapsedMs":%s,"packageReadiness":%s}\n' "$preset" "$configuration" "$target" "$artifact" "$([ "$exit_code" -eq 0 ] && printf true || printf false)" "$exit_code" "$elapsed_ms" "$readiness"
exit "$exit_code"

View File

@@ -1,7 +1,54 @@
[CmdletBinding()]
param(
[string[]]$Presets = @("android-arm64"),
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_image_metadata_tests", "pp_assets_image_pixels_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_paint_stroke_script_tests", "pp_document_tests", "pp_document_ppi_import_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_color_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
[string[]]$Targets = @(
"pp_foundation",
"pp_assets",
"pp_paint",
"pp_document",
"pp_renderer_api",
"pp_renderer_gl",
"pp_paint_renderer",
"pp_ui_core",
"pp_platform_api",
"pp_app_core",
"pano_cli",
"pp_foundation_binary_stream_tests",
"pp_foundation_event_tests",
"pp_foundation_log_tests",
"pp_foundation_parse_tests",
"pp_foundation_task_queue_tests",
"pp_foundation_trace_tests",
"pp_assets_image_format_tests",
"pp_assets_image_metadata_tests",
"pp_assets_image_pixels_tests",
"pp_assets_ppi_header_tests",
"pp_assets_settings_document_tests",
"pp_paint_brush_tests",
"pp_paint_blend_tests",
"pp_paint_stroke_tests",
"pp_paint_stroke_script_tests",
"pp_document_tests",
"pp_document_ppi_import_tests",
"pp_document_ppi_export_tests",
"pp_renderer_api_tests",
"pp_renderer_gl_capabilities_tests",
"pp_renderer_gl_command_plan_tests",
"pp_paint_renderer_compositor_tests",
"pp_platform_api_tests",
"pp_ui_core_color_tests",
"pp_ui_core_layout_value_tests",
"pp_ui_core_layout_xml_tests",
"pp_app_core_document_route_tests",
"pp_app_core_document_export_tests",
"pp_app_core_document_cloud_tests",
"pp_app_core_document_platform_io_tests",
"pp_app_core_document_recording_tests",
"pp_app_core_app_preferences_tests",
"pp_app_core_app_status_tests",
"pp_app_core_document_sharing_tests",
"pp_app_core_document_session_tests"
)
)
$ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}"
shift || true
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_renderer_gl pp_paint_renderer pp_ui_core pp_platform_api pp_app_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_document_ppi_export_tests pp_renderer_api_tests pp_renderer_gl_capabilities_tests pp_renderer_gl_command_plan_tests pp_paint_renderer_compositor_tests pp_platform_api_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pp_app_core_document_route_tests pp_app_core_document_export_tests pp_app_core_document_cloud_tests pp_app_core_document_platform_io_tests pp_app_core_document_recording_tests pp_app_core_app_preferences_tests pp_app_core_app_status_tests pp_app_core_document_sharing_tests pp_app_core_document_session_tests}"
start="$(date +%s)"
cmake --preset "$preset"

View File

@@ -5,6 +5,13 @@
#include "node_dialog_open.h"
#include "node_progress_bar.h"
#include "mp4enc.h"
#include "app_core/app_status.h"
#include "app_core/canvas_tool_ui.h"
#include "app_core/document_recording.h"
#include "app_core/document_route.h"
#include "app_core/document_session.h"
#include "platform_api/platform_services.h"
#include "renderer_gl/opengl_capabilities.h"
#ifdef __APPLE__
#include <Foundation/Foundation.h>
@@ -12,32 +19,156 @@
#endif
#include "settings.h"
#ifdef __ANDROID__
void android_async_lock();
void android_async_swap();
void android_async_unlock();
void android_attach_jni();
void android_detach_jni();
#elif _WIN32
bool async_lock_try();
void async_lock();
void win32_async_swap();
void async_unlock();
void destroy_window();
void win32_renderdoc_frame_start();
void win32_renderdoc_frame_end();
#elif __LINUX__
std::string linux_home_path();
int mkpath(const std::string& dir, mode_t mode = DEFFILEMODE);
#elif __WEB__
#include <unistd.h>
#endif
App* App::I = nullptr; // singleton
std::deque<AppTask> App::render_tasklist;
std::mutex App::render_task_mutex;
std::condition_variable App::render_cv;
namespace {
[[nodiscard]] const char* query_opengl_string(std::uint32_t name) noexcept
{
return reinterpret_cast<const char*>(glGetString(static_cast<GLenum>(name)));
}
void enable_opengl_state(std::uint32_t state) noexcept
{
glEnable(static_cast<GLenum>(state));
}
pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept
{
switch (mode) {
case kCanvasMode::Draw:
return pp::app::CanvasToolMode::draw;
case kCanvasMode::Erase:
return pp::app::CanvasToolMode::erase;
case kCanvasMode::Line:
return pp::app::CanvasToolMode::line;
case kCanvasMode::Camera:
return pp::app::CanvasToolMode::camera;
case kCanvasMode::Grid:
return pp::app::CanvasToolMode::grid;
case kCanvasMode::Copy:
return pp::app::CanvasToolMode::copy;
case kCanvasMode::Cut:
return pp::app::CanvasToolMode::cut;
case kCanvasMode::Fill:
return pp::app::CanvasToolMode::fill;
case kCanvasMode::MaskFree:
return pp::app::CanvasToolMode::mask_free;
case kCanvasMode::MaskLine:
return pp::app::CanvasToolMode::mask_line;
case kCanvasMode::FloodFill:
return pp::app::CanvasToolMode::flood_fill;
default:
return pp::app::CanvasToolMode::draw;
}
}
void disable_opengl_state(std::uint32_t state) noexcept
{
glDisable(static_cast<GLenum>(state));
}
void set_opengl_blend_func(std::uint32_t source_factor, std::uint32_t destination_factor) noexcept
{
glBlendFunc(static_cast<GLenum>(source_factor), static_cast<GLenum>(destination_factor));
}
void set_opengl_blend_equation_separate(std::uint32_t color_equation, std::uint32_t alpha_equation) noexcept
{
glBlendEquationSeparate(static_cast<GLenum>(color_equation), static_cast<GLenum>(alpha_equation));
}
void clear_opengl_color(float r, float g, float b, float a) noexcept
{
glClearColor(r, g, b, a);
}
void clear_opengl_buffers(std::uint32_t mask) noexcept
{
glClear(static_cast<GLbitfield>(mask));
}
void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}
void set_opengl_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
glScissor(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}
void apply_app_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
{
const auto status = pp::renderer::gl::apply_opengl_viewport(
viewport,
pp::renderer::gl::OpenGlViewportDispatch {
.viewport = set_opengl_viewport,
});
if (!status.ok())
LOG("OpenGL viewport failed: %s", status.message);
}
void apply_app_scissor(pp::renderer::gl::OpenGlScissorRect scissor)
{
const auto status = pp::renderer::gl::apply_opengl_scissor_rect(
scissor,
pp::renderer::gl::OpenGlScissorDispatch {
.enable = enable_opengl_state,
.disable = disable_opengl_state,
.scissor = set_opengl_scissor,
});
if (!status.ok())
LOG("OpenGL scissor failed: %s", status.message);
}
void apply_app_scissor_test(bool enabled)
{
const auto status = pp::renderer::gl::apply_opengl_scissor_test(
enabled,
pp::renderer::gl::OpenGlScissorTestDispatch {
.enable = enable_opengl_state,
.disable = disable_opengl_state,
});
if (!status.ok())
LOG("OpenGL scissor test failed: %s", status.message);
}
[[nodiscard]] GLint rgba8_internal_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}
[[nodiscard]] GLenum linear_texture_filter() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::linear_texture_filter());
}
[[nodiscard]] GLenum nearest_texture_filter() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::nearest_texture_filter());
}
[[nodiscard]] GLenum repeat_texture_wrap() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::repeat_texture_wrap());
}
[[nodiscard]] GLenum framebuffer_target() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::framebuffer_target());
}
[[nodiscard]] GLuint default_framebuffer_id() noexcept
{
return static_cast<GLuint>(pp::renderer::gl::default_framebuffer_id());
}
}
std::thread App::render_thread;
std::thread::id App::render_thread_id;
bool App::render_running = false;
@@ -57,15 +188,14 @@ void App::create()
void App::open_document(std::string path)
{
std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
std::smatch m;
if (!std::regex_search(path, m, r))
const auto route = pp::app::classify_document_open_path(path);
if (!route)
return;
std::string base = m[1].str();
std::string name = m[2].str();
std::string ext = m[3].str();
if (str_iequals(ext, "abr"))
const bool has_unsaved_project =
route.value().kind == pp::app::DocumentOpenKind::open_project && Canvas::I->m_unsaved;
const auto open_plan = pp::app::plan_document_open(route.value().kind, has_unsaved_project);
if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_abr)
{
auto mb = message_box("Import ABR", "Would you like to import the brushes?", true);
mb->on_submit = [this, path] (Node* target) {
@@ -73,7 +203,7 @@ void App::open_document(std::string path)
target->destroy();
};
}
else if (str_iequals(ext, "ppbr"))
else if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_ppbr)
{
auto mb = message_box("Import PPBR", "Would you like to import the brushes?", true);
mb->on_submit = [this, path] (Node* target) {
@@ -83,6 +213,8 @@ void App::open_document(std::string path)
}
else
{
const std::string base = route.value().directory;
const std::string name = route.value().name;
auto open_action = [this, path, base, name] {
doc_name = name;
doc_dir = base;
@@ -109,7 +241,7 @@ void App::open_document(std::string path)
});
ActionManager::clear();
};
if (!Canvas::I->m_unsaved)
if (open_plan == pp::app::DocumentOpenPlanAction::open_project_now)
{
open_action();
}
@@ -127,25 +259,19 @@ void App::open_document(std::string path)
bool App::request_close()
{
static bool dialog_already_opened = false;
if (!Canvas::I->m_unsaved)
const auto close_decision = pp::app::plan_close_request(
Canvas::I->m_unsaved,
dialog_already_opened);
if (close_decision == pp::app::CloseRequestDecision::close_now)
return true;
if (!dialog_already_opened)
if (close_decision == pp::app::CloseRequestDecision::show_unsaved_prompt)
{
auto* m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Do you want to close without saving?");
m->btn_ok->m_text->set_text("Yes");
m->btn_ok->on_click = [this](Node*) {
#ifdef _WIN32
destroy_window();
//PostQuitMessage(0);
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
[osx_view close];
});
#elif __LINUX__
glfwSetWindowShouldClose(glfw_window, GLFW_TRUE);
#endif
request_app_close();
Canvas::I->m_unsaved = false;
};
m->btn_cancel->m_text->set_text("No");
@@ -160,8 +286,13 @@ bool App::request_close()
void App::clear()
{
glClearColor(.1f, .1f, .1f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
const auto status = pp::renderer::gl::clear_panopainter_default_target(
pp::renderer::gl::OpenGlClearDispatch {
.clear_color = clear_opengl_color,
.clear = clear_opengl_buffers,
});
if (!status.ok())
LOG("OpenGL clear failed: %s", status.message);
}
void App::initAssets()
@@ -170,9 +301,9 @@ void App::initAssets()
FontManager::init();
LOG("initializing assets create sampler");
sampler.create(GL_NEAREST);
sampler_stencil.create(GL_LINEAR, GL_REPEAT);
sampler_linear.create(GL_LINEAR);
sampler.create(nearest_texture_filter());
sampler_stencil.create(linear_texture_filter(), repeat_texture_wrap());
sampler_linear.create(linear_texture_filter());
m_face_plane.create<1>(2, 2);
sphere.create<8, 8>(1);
LOG("initializing assets load uvs texture");
@@ -181,78 +312,16 @@ void App::initAssets()
void App::initLog()
{
#if defined(__IOS__)
[ios_view init_dirs];
#elif defined(__OSX__)
[osx_app init_dirs];
#elif defined(_WIN32)
//CHAR my_documents[MAX_PATH];
//HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
//HMODULE hModule = GetModuleHandle(NULL);
//CHAR path[MAX_PATH];
//GetModuleFileNameA(hModule, path, MAX_PATH);
//CHAR out_drive[MAX_PATH];
//CHAR out_path[MAX_PATH];
//_splitpath(path, out_drive, out_path, nullptr, nullptr);
//sprintf_s(path, "%s%s", out_drive, out_path);
//data_path = path;
CHAR my_documents[MAX_PATH];
HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
if (SUCCEEDED(result))
{
std::string path = std::string(my_documents) + "\\PanoPainter";
if (!PathFileExistsA(path.c_str()))
CreateDirectoryA(path.c_str(), NULL);
data_path = path;
}
else
{
CHAR path[MAX_PATH];
GetCurrentDirectoryA(sizeof(path), path);
data_path = path;
}
rec_path = data_path + "\\frames";
if (!PathFileExistsA(rec_path.c_str()))
CreateDirectoryA(rec_path.c_str(), NULL);
if (!PathFileExistsA((data_path + "\\brushes").c_str()))
CreateDirectoryA((data_path + "\\brushes").c_str(), NULL);
if (!PathFileExistsA((data_path + "\\brushes\\thumbs").c_str()))
CreateDirectoryA((data_path + "\\brushes\\thumbs").c_str(), NULL);
if (!PathFileExistsA((data_path + "\\patterns").c_str()))
CreateDirectoryA((data_path + "\\patterns").c_str(), NULL);
if (!PathFileExistsA((data_path + "\\patterns\\thumbs").c_str()))
CreateDirectoryA((data_path + "\\patterns\\thumbs").c_str(), NULL);
if (!PathFileExistsA((data_path + "\\settings").c_str()))
CreateDirectoryA((data_path + "\\settings").c_str(), NULL);
#elif __LINUX__
data_path = linux_home_path() + "/PanoPainter";
mkpath(data_path + "/brushes");
mkpath(data_path + "/brushes/thumbs");
mkpath(data_path + "/patterns");
mkpath(data_path + "/patterns/thumbs");
mkpath(data_path + "/settings");
mkpath(data_path + "/frames");
#elif __WEB__
data_path = "/PanoPainter";
mkdir(data_path.c_str(), 0777);
mkdir((data_path + "/brushes").c_str(), 0777);
mkdir((data_path + "/brushes/thumbs").c_str(), 0777);
mkdir((data_path + "/patterns").c_str(), 0777);
mkdir((data_path + "/patterns/thumbs").c_str(), 0777);
mkdir((data_path + "/settings").c_str(), 0777);
mkdir((data_path + "/frames").c_str(), 0777);
#endif
const auto paths = prepare_storage_paths();
if (!paths.data_path.empty())
data_path = paths.data_path;
if (!paths.recording_path.empty())
rec_path = paths.recording_path;
if (!paths.temporary_path.empty())
tmp_path = paths.temporary_path;
// TODO: save this path somewhere in the settings, don't overwrite every start
work_path = data_path;
work_path = paths.work_path.empty() ? data_path : paths.work_path;
//LogRemote::I.start();
LogRemote::I.file_init();
@@ -385,56 +454,29 @@ void App::upload(std::string filename, std::string name, std::function<void(floa
#endif //CURL
}
#ifdef _WIN32
static CONSOLE_SCREEN_BUFFER_INFO info;
void handle_gl_callback(GLenum source, GLenum type, GLuint id,
GLenum severity, GLsizei length, const GLchar* message, const void* userParam)
{
static std::map<GLenum, int> colors = {
{ GL_DEBUG_SEVERITY_NOTIFICATION, 8 },
{ GL_DEBUG_SEVERITY_LOW, 8 },
{ GL_DEBUG_SEVERITY_MEDIUM, FOREGROUND_GREEN | FOREGROUND_INTENSITY },
{ GL_DEBUG_SEVERITY_HIGH, FOREGROUND_RED | FOREGROUND_INTENSITY },
};
if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM || severity == GL_DEBUG_SEVERITY_LOW)
{
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colors[severity]);
LOG("OPENGL: %.*s", length, message);
FlushConsoleInputBuffer(GetStdHandle(STD_OUTPUT_HANDLE));
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), info.wAttributes);
#ifdef _DEBUG
if (severity == GL_DEBUG_SEVERITY_HIGH)
__debugbreak();
#endif
}
}
#endif
void App::init()
{
#ifdef _WIN32
if (glDebugMessageCallback)
{
// colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
render_task([]
{
glDebugMessageCallback(handle_gl_callback, nullptr);
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
});
}
#endif
LOG("Screen Resolution: %dx%d", (int)width, (int)height);
render_task([]
{
LOG("GL version: %s", glGetString(GL_VERSION));
LOG("GL vendor: %s", glGetString(GL_VENDOR));
LOG("GL renderer: %s", glGetString(GL_RENDERER));
LOG("GLSL version: %s", glGetString(GL_SHADING_LANGUAGE_VERSION));
App::I->install_render_debug_callback();
const auto runtime_info_result = pp::renderer::gl::query_opengl_runtime_info(
pp::renderer::gl::OpenGlRuntimeInfoDispatch {
.get_string = query_opengl_string,
});
if (runtime_info_result.ok())
{
const auto& runtime_info = runtime_info_result.value();
LOG("GL version: %s", runtime_info.version);
LOG("GL vendor: %s", runtime_info.vendor);
LOG("GL renderer: %s", runtime_info.renderer);
LOG("GLSL version: %s", runtime_info.shading_language_version);
}
else
{
LOG("OpenGL runtime info failed: %s", runtime_info_result.status().message);
}
//GLint n_exts;
//glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
@@ -447,13 +489,16 @@ void App::init()
// }
//}
glDisable(GL_DEPTH_TEST);
#if defined(_WIN32) || defined(__OSX__)
glEnable(GL_PROGRAM_POINT_SIZE);
glEnable(GL_LINE_SMOOTH);
#endif
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX);
App::I->apply_render_platform_hints();
const auto startup_state_status = pp::renderer::gl::apply_panopainter_initial_state(
pp::renderer::gl::OpenGlStateDispatch {
.enable = enable_opengl_state,
.disable = disable_opengl_state,
.blend_func = set_opengl_blend_func,
.blend_equation_separate = set_opengl_blend_equation_separate,
});
if (!startup_state_status.ok())
LOG("OpenGL startup state failed: %s", startup_state_status.message);
});
int run_counter = Settings::value<Serializer::Integer>("run_counter") + 1;
@@ -468,7 +513,7 @@ void App::init()
initLayout();
title_update();
uirtt.create(width, height, -1, GL_RGBA8, true);
uirtt.create(width, height, -1, rgba8_internal_format(), true);
if (Settings::value_or<Serializer::Boolean>("auto-timelapse", true))
rec_start();
@@ -482,18 +527,7 @@ void App::init()
void App::async_start()
{
#if __OSX__
[osx_view async_lock];
#elif __IOS__
[ios_view async_lock];
#elif __ANDROID__
android_async_lock();
#elif _WIN32
async_lock();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#elif __LINUX__ || __WEB__
glfwMakeContextCurrent(glfw_window);
#endif
acquire_render_context();
}
void App::async_redraw()
@@ -504,30 +538,12 @@ void App::async_redraw()
void App::async_end()
{
#if __OSX__
[osx_view async_unlock];
#elif __IOS__
[ios_view async_unlock];
#elif __ANDROID__
android_async_unlock();
#elif _WIN32
async_unlock();
#endif
release_render_context();
}
void App::async_swap()
{
#if __OSX__
[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
present_render_context();
}
bool App::update_ui_observer(Node *n)
@@ -569,7 +585,13 @@ bool App::update_ui_observer(Node *n)
n->m_on_screen = true;
}
glm::ivec4 c = glm::vec4(box.x - 1, (height / zoom - box.y - box.w - 1), box.z + 2, box.w + 2) * zoom;
glScissor(floorf(c.x + off_x), floorf(c.y + off_y), ceilf(c.z), ceilf(c.w));
apply_app_scissor(pp::renderer::gl::OpenGlScissorRect {
.enabled = 1U,
.x = static_cast<std::int32_t>(floorf(c.x + off_x)),
.y = static_cast<std::int32_t>(floorf(c.y + off_y)),
.width = static_cast<std::int32_t>(ceilf(c.z)),
.height = static_cast<std::int32_t>(ceilf(c.w)),
});
n->draw();
return true;
}
@@ -588,32 +610,36 @@ void App::draw(float dt)
{
uirtt.bindFramebuffer();
uirtt.clear();
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
glEnable(GL_SCISSOR_TEST);
apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
.width = static_cast<std::int32_t>(uirtt.getWidth()),
.height = static_cast<std::int32_t>(uirtt.getHeight()),
});
apply_app_scissor_test(true);
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
layout_designer[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
glDisable(GL_SCISSOR_TEST);
apply_app_scissor_test(false);
uirtt.unbindFramebuffer();
}
if (!vr_only)
{
#if __IOS__
[ios_view->glview bindDrawable];
#else
glBindFramebuffer(GL_FRAMEBUFFER, 0);
#endif
glViewport(off_x, off_y, (GLsizei)width, (GLsizei)height);
glEnable(GL_SCISSOR_TEST);
bind_main_render_target();
apply_app_viewport(pp::renderer::gl::OpenGlViewportRect {
.x = static_cast<std::int32_t>(off_x),
.y = static_cast<std::int32_t>(off_y),
.width = static_cast<std::int32_t>(width),
.height = static_cast<std::int32_t>(height),
});
apply_app_scissor_test(true);
for (int i = 0; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++)
layout_designer[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
glDisable(GL_SCISSOR_TEST);
apply_app_scissor_test(false);
}
redraw = false;
@@ -636,25 +662,26 @@ void App::update(float dt)
main->update(width, height, zoom);
{
static glm::vec4 color_button_normal{ .1, .1, .1, 1 };
static glm::vec4 color_button_hlight{ 1, .0, .0, 1 };
auto mode = Canvas::I->m_current_mode;
CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0];
layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(mode == kCanvasMode::Draw && pm->m_picking);
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(canvas->m_canvas->m_touch_lock);
const auto toolbar = pp::app::plan_canvas_tool_button_state(
canvas_tool_mode_from_canvas_mode(mode),
pm && pm->m_picking,
canvas->m_canvas->m_touch_lock);
layout[main_id]->find<NodeButtonCustom>("btn-pick")->set_active(toolbar.pick_active);
layout[main_id]->find<NodeButtonCustom>("btn-touchlock")->set_active(toolbar.touch_lock_active);
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(mode == kCanvasMode::Draw);
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(mode == kCanvasMode::Erase);
layout[main_id]->find<NodeButton>("btn-cam")->set_active(mode == kCanvasMode::Camera);
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(mode == kCanvasMode::Line);
layout[main_id]->find<NodeButton>("btn-grid")->set_active(mode == kCanvasMode::Grid);
layout[main_id]->find<NodeButton>("btn-copy")->set_active(mode == kCanvasMode::Copy);
layout[main_id]->find<NodeButton>("btn-cut")->set_active(mode == kCanvasMode::Cut);
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(mode == kCanvasMode::MaskFree);
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(mode == kCanvasMode::MaskLine);
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(mode == kCanvasMode::FloodFill);
layout[main_id]->find<NodeButtonCustom>("btn-pen")->set_active(toolbar.pen_active);
layout[main_id]->find<NodeButtonCustom>("btn-erase")->set_active(toolbar.erase_active);
layout[main_id]->find<NodeButton>("btn-cam")->set_active(toolbar.camera_active);
layout[main_id]->find<NodeButtonCustom>("btn-line")->set_active(toolbar.line_active);
layout[main_id]->find<NodeButton>("btn-grid")->set_active(toolbar.grid_active);
layout[main_id]->find<NodeButton>("btn-copy")->set_active(toolbar.copy_active);
layout[main_id]->find<NodeButton>("btn-cut")->set_active(toolbar.cut_active);
layout[main_id]->find<NodeButtonCustom>("btn-mask-free")->set_active(toolbar.mask_free_active);
layout[main_id]->find<NodeButtonCustom>("btn-mask-line")->set_active(toolbar.mask_line_active);
layout[main_id]->find<NodeButtonCustom>("btn-bucket")->set_active(toolbar.flood_fill_active);
}
}
@@ -688,9 +715,8 @@ void App::update_memory_usage(size_t bytes)
{
if (auto txt = layout[main_id]->find<NodeText>("txt-memory"))
{
static char buffer[128];
sprintf(buffer, "History memory: %.2f Mb", bytes / 1024.f / 1024.f);
txt->set_text(buffer);
const auto label = pp::app::make_history_memory_label(bytes);
txt->set_text(label.c_str());
}
}
@@ -698,91 +724,102 @@ void App::update_rec_frames()
{
if (auto txt = layout[main_id]->find<NodeText>("txt-rec"))
{
if (rec_running && Canvas::I->m_encoder)
{
static char buffer[128];
sprintf(buffer, "Recorded %d frames", Canvas::I->m_encoder->frames_count());
txt->set_text(buffer);
}
else
{
txt->set_text("");
}
const auto label = pp::app::make_recording_frame_label(
rec_running,
Canvas::I->m_encoder != nullptr,
Canvas::I->m_encoder ? Canvas::I->m_encoder->frames_count() : 0);
txt->set_text(label.text.c_str());
}
}
int App::res_from_index(int i)
{
return res_map[i];
const auto resolution = pp::app::display_resolution_from_index(i);
return resolution ? resolution.value() : pp::app::document_resolution_values.front();
}
int App::res_to_index(int res)
{
return (int)std::distance(res_map.begin(), std::find(res_map.begin(), res_map.end(), res));
const auto index = pp::app::document_resolution_to_index(res);
return index ? static_cast<int>(index.value()) : static_cast<int>(pp::app::document_resolution_values.size());
}
std::string App::res_to_string(int res)
{
return res_map_str[res_to_index(res)];
const auto label = pp::app::document_resolution_label(res);
return label ? std::string(label.value()) : std::string("unknown");
}
void App::renderdoc_frame_start()
{
#if __WIN__
win32_renderdoc_frame_start();
#endif
begin_render_capture_frame();
}
void App::renderdoc_frame_end()
{
#if __WIN__
win32_renderdoc_frame_end();
#endif
end_render_capture_frame();
}
void App::rec_clear()
{
const auto plan = pp::app::plan_recording_clear(
rec_running,
platform_deletes_recorded_files_on_clear()
);
if (plan.stop_running_recording)
rec_stop();
#if defined(__IOS__) || defined(__OSX__)
delete_all_files_in_path(rec_path);
#endif
rec_count = 0;
if (plan.delete_recorded_files)
clear_platform_recorded_files(rec_path);
rec_count = plan.frame_count_after_clear;
update_rec_frames();
}
void App::rec_start()
{
if (!rec_running)
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);
}
}
void App::rec_stop()
{
if (rec_running)
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();
}
}
void App::rec_export(std::string path)
{
int progress = 0;
int tot = rec_count;
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) {
pb->m_progress->SetWidthP((float)progress / tot * 100.f);
pb->increment();
});
#endif
*/
@@ -915,7 +952,7 @@ void App::ui_thread_tick()
update(0);
render_task([this]
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
bind_default_render_target();
clear();
draw(0);
async_swap();
@@ -931,9 +968,7 @@ void App::ui_thread_main()
ui_thread_id = std::this_thread::get_id();
ui_running = true;
#if __ANDROID__
android_attach_jni();
#endif
attach_ui_thread();
LOG("ui thread init()");
init();
@@ -971,10 +1006,7 @@ void App::ui_thread_main()
float dt = std::chrono::duration<float>(t_now - t_start).count();
t_start = t_now;
#ifdef _WIN32
extern void win32_update_stylus(float dt);
win32_update_stylus(dt);
#endif
update_platform_frame(dt);
// increment timers
t_frame += dt;
@@ -982,18 +1014,13 @@ void App::ui_thread_main()
if (t_fps_counter > 1.f)
{
#ifdef _WIN32
extern void win32_update_fps(int frames);
win32_update_fps(rendered_frames);
#elif __LINUX__
extern void linux_update_fps(int frames);
linux_update_fps(rendered_frames);
#endif
report_rendered_frames(rendered_frames);
t_fps_counter = 0;
rendered_frames = 0;
}
#if /*_DEBUG &&*/ (_WIN32 || __OSX__)
if (platform_enables_live_asset_reloading())
{
t_reloader += dt;
if (t_reloader > 1.0)
{
@@ -1008,7 +1035,7 @@ void App::ui_thread_main()
if (layout_designer.reload())
redraw = true;
}
#endif
}
tick(dt);
@@ -1017,7 +1044,7 @@ void App::ui_thread_main()
update(t_frame);
render_task([this, t_frame]
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
bind_default_render_target();
clear();
draw(t_frame);
async_swap();
@@ -1026,9 +1053,7 @@ void App::ui_thread_main()
rendered_frames++;
}
}
#if __ANDROID__
android_detach_jni();
#endif
detach_ui_thread();
}
void App::render_thread_start()

View File

@@ -24,6 +24,12 @@
#include "node_input_box.h"
#include "node_panel_animation.h"
#include "layout.h"
#include "app_core/document_session.h"
namespace pp::platform {
class PlatformServices;
struct PlatformStoragePaths;
}
#if defined(__OBJC__) && defined(__IOS__)
#import <Foundation/Foundation.h>
@@ -155,6 +161,7 @@ public:
float display_density = 1.f;
float zoom = 1.f;
int idle_ms = 100;
pp::platform::PlatformServices* platform_services_ = nullptr;
#if defined(__IOS__) && defined(__OBJC__)
GameViewController* ios_view;
@@ -183,6 +190,30 @@ public:
void pick_dir(std::function<void(std::string path)> callback);
void display_file(std::string path);
void share_file(std::string path);
void request_app_close();
void attach_ui_thread();
void detach_ui_thread();
void acquire_render_context();
void release_render_context();
void present_render_context();
void bind_default_render_target();
void bind_main_render_target();
void apply_render_platform_hints();
void install_render_debug_callback();
void begin_render_capture_frame();
void end_render_capture_frame();
[[nodiscard]] bool platform_deletes_recorded_files_on_clear();
void clear_platform_recorded_files(std::string path);
[[nodiscard]] bool platform_enables_live_asset_reloading();
void update_platform_frame(float delta_time_seconds);
void report_rendered_frames(int frames);
void save_prepared_file(
std::string path,
std::string suggested_name,
std::function<void(const std::string& path, bool saved)> callback);
void set_platform_services(pp::platform::PlatformServices* services) noexcept;
[[nodiscard]] pp::platform::PlatformServices* platform_services() const noexcept;
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths();
void showKeyboard();
void hideKeyboard();
void initLog();
@@ -248,6 +279,8 @@ public:
void dialog_usermanual();
void dialog_changelog();
void dialog_about();
void save_document(pp::app::DocumentSaveIntent intent);
void continue_document_workflow_after_optional_save(std::function<void()> action);
void dialog_newdoc();
void dialog_save();
void dialog_save_ver();

View File

@@ -1,19 +1,29 @@
#include "pch.h"
#include "app.h"
#include "app_core/document_cloud.h"
#include "util.h"
#include "node_progress_bar.h"
#include "node_dialog_cloud.h"
void App::cloud_upload()
{
if (!canvas)
const bool has_canvas = canvas != nullptr;
const auto plan = pp::app::plan_cloud_upload(
has_canvas,
has_canvas && Canvas::I->m_newdoc,
has_canvas && Canvas::I->m_unsaved);
switch (plan.action)
{
case pp::app::CloudUploadAction::unavailable_no_canvas:
return;
if (Canvas::I->m_newdoc)
{
case pp::app::CloudUploadAction::show_save_required_warning:
message_box("Warning", "This document needs to be saved before upload.");
return;
case pp::app::CloudUploadAction::prompt_publish:
break;
}
else
{
auto upload_thread = [this] {
BT_SetTerminate();
@@ -42,7 +52,6 @@ void App::cloud_upload()
m->btn_cancel->on_click = [this, m, upload_thread](Node*) {
m->destroy();
};
}
}
void App::cloud_upload_all()
@@ -51,22 +60,23 @@ void App::cloud_upload_all()
BT_SetTerminate();
auto names = Asset::list_files(data_path, ".*\\.ppi");
const auto plan = pp::app::plan_cloud_bulk_upload(names.size(), layout.m_loaded);
gl_state gl;
std::shared_ptr<NodeProgressBar> pb;
if (layout.m_loaded)
pb = show_progress("Export Pano Image", names.size());
if (plan.show_progress)
pb = show_progress("Export Pano Image", plan.progress_total);
for (const auto& n : names)
{
std::string path = data_path + "/" + n;
upload(path);
if (layout.m_loaded)
if (plan.show_progress)
pb->increment();
}
if (layout.m_loaded)
if (plan.show_progress)
pb->destroy();
}).detach();
@@ -74,8 +84,14 @@ void App::cloud_upload_all()
void App::cloud_browse()
{
if (!canvas)
const auto browse_plan = pp::app::plan_cloud_browse(canvas != nullptr);
switch (browse_plan)
{
case pp::app::CloudBrowseAction::unavailable_no_canvas:
return;
case pp::app::CloudBrowseAction::show_browser:
break;
}
// load thumbnail test
auto dialog = std::make_shared<NodeDialogCloud>();
@@ -88,7 +104,8 @@ void App::cloud_browse()
dialog->btn_ok->on_click = [this, dialog](Node*)
{
if (dialog->selected_file.empty())
const auto selection_plan = pp::app::plan_cloud_download_selection(dialog->selected_file);
if (selection_plan == pp::app::CloudDownloadSelectionAction::wait_for_selection)
return;
dialog->destroy();
std::thread([this, dialog] {

View File

@@ -1,16 +1,46 @@
#include "pch.h"
#include "app.h"
#include "canvas.h"
#include "renderer_gl/opengl_capabilities.h"
namespace {
[[nodiscard]] GLenum depth_test_state() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
}
[[nodiscard]] GLenum program_point_size_state() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::program_point_size_state());
}
[[nodiscard]] GLenum source_alpha_blend_factor() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::source_alpha_blend_factor());
}
[[nodiscard]] GLenum one_minus_source_alpha_blend_factor() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::one_minus_source_alpha_blend_factor());
}
[[nodiscard]] GLenum add_blend_equation() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::add_blend_equation());
}
}
void App::cmd_convert(std::string pano_path, std::string out_path)
{
glDisable(GL_DEPTH_TEST);
glEnable(GL_PROGRAM_POINT_SIZE);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendEquation(GL_FUNC_ADD);
glDisable(depth_test_state());
glEnable(program_point_size_state());
glBlendFunc(source_alpha_blend_factor(), one_minus_source_alpha_blend_factor());
glBlendEquation(add_blend_equation());
Canvas* canvas = new Canvas;
canvas->create(CANVAS_RES, CANVAS_RES);
canvas->project_open_thread(pano_path);
canvas->export_equirectangular_thread(out_path);
Canvas* command_canvas = new Canvas;
command_canvas->create(CANVAS_RES, CANVAS_RES);
command_canvas->project_open_thread(pano_path);
command_canvas->export_equirectangular_thread(out_path);
}

126
src/app_core/about_menu.h Normal file
View File

@@ -0,0 +1,126 @@
#pragma once
#include "foundation/result.h"
#include <string>
namespace pp::app {
enum class AboutMenuCommand {
help_guide,
about_app,
whats_new,
induce_crash,
performance_test,
};
enum class AboutMenuAction {
show_user_manual,
show_about_dialog,
show_whats_new_dialog,
trigger_crash_test,
run_performance_test,
no_op_unavailable,
};
struct AboutMenuPlan {
AboutMenuCommand command = AboutMenuCommand::help_guide;
AboutMenuAction action = AboutMenuAction::show_user_manual;
std::string label;
bool closes_root_popup = true;
bool requires_canvas = false;
int performance_iterations = 0;
int performance_updates_per_iteration = 0;
};
class AboutMenuServices {
public:
virtual ~AboutMenuServices() = default;
virtual void show_user_manual() = 0;
virtual void show_about_dialog() = 0;
virtual void show_whats_new_dialog() = 0;
virtual void trigger_crash_test() = 0;
virtual void run_performance_test(const AboutMenuPlan& plan) = 0;
};
[[nodiscard]] inline AboutMenuPlan plan_about_menu_command(
AboutMenuCommand command,
int version_major,
int version_minor,
int version_fix,
bool diagnostics_available = true,
bool has_canvas = true)
{
AboutMenuPlan plan;
plan.command = command;
switch (command) {
case AboutMenuCommand::help_guide:
plan.action = AboutMenuAction::show_user_manual;
plan.label = "Help Guide";
break;
case AboutMenuCommand::about_app:
plan.action = AboutMenuAction::show_about_dialog;
plan.label = "About PanoPainter";
break;
case AboutMenuCommand::whats_new:
plan.action = AboutMenuAction::show_whats_new_dialog;
plan.label = "What's new in "
+ std::to_string(version_major)
+ "."
+ std::to_string(version_minor)
+ "."
+ std::to_string(version_fix)
+ "?";
break;
case AboutMenuCommand::induce_crash:
plan.label = "Induce crash";
plan.action = diagnostics_available
? AboutMenuAction::trigger_crash_test
: AboutMenuAction::no_op_unavailable;
plan.closes_root_popup = diagnostics_available;
break;
case AboutMenuCommand::performance_test:
plan.label = has_canvas ? "Performance test" : "Performance test (No canvas)";
plan.requires_canvas = true;
plan.performance_iterations = 100;
plan.performance_updates_per_iteration = 10;
plan.action = has_canvas
? AboutMenuAction::run_performance_test
: AboutMenuAction::no_op_unavailable;
plan.closes_root_popup = has_canvas;
break;
}
return plan;
}
[[nodiscard]] inline pp::foundation::Status execute_about_menu_plan(
const AboutMenuPlan& plan,
AboutMenuServices& services)
{
switch (plan.action) {
case AboutMenuAction::show_user_manual:
services.show_user_manual();
return pp::foundation::Status::success();
case AboutMenuAction::show_about_dialog:
services.show_about_dialog();
return pp::foundation::Status::success();
case AboutMenuAction::show_whats_new_dialog:
services.show_whats_new_dialog();
return pp::foundation::Status::success();
case AboutMenuAction::trigger_crash_test:
services.trigger_crash_test();
return pp::foundation::Status::success();
case AboutMenuAction::run_performance_test:
services.run_performance_test(plan);
return pp::foundation::Status::success();
case AboutMenuAction::no_op_unavailable:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown about menu action");
}
}

View File

@@ -0,0 +1,114 @@
#pragma once
#include <cstddef>
#include <span>
namespace pp::app {
enum class InterfaceDirection {
left_to_right,
right_to_left,
};
enum class TimelapseRecordingAction {
no_op,
start_recording,
stop_recording,
};
struct ScaleApplicationPlan {
float scale = 1.0F;
float display_density = 1.0F;
float font_scale = 1.0F;
};
struct ScaleOptionSelection {
bool has_selection = false;
std::size_t index = 0;
};
struct InterfaceDirectionPlan {
InterfaceDirection direction = InterfaceDirection::left_to_right;
};
struct TimelapsePreferencePlan {
bool enabled = true;
TimelapseRecordingAction recording_action = TimelapseRecordingAction::no_op;
};
struct StoredIntegerPreferencePlan {
int value = 0;
};
struct StoredBooleanPreferencePlan {
bool value = false;
};
[[nodiscard]] constexpr ScaleApplicationPlan plan_ui_scale(
float requested_scale,
float display_density) noexcept
{
return {
requested_scale,
display_density,
requested_scale * display_density,
};
}
[[nodiscard]] constexpr ScaleApplicationPlan plan_viewport_scale(
float requested_scale,
float display_density = 1.0F) noexcept
{
return {
requested_scale,
display_density,
requested_scale * display_density,
};
}
[[nodiscard]] constexpr ScaleOptionSelection plan_scale_option_selection(
float current_scale,
std::span<const float> options) noexcept
{
ScaleOptionSelection selection;
for (std::size_t index = 0; index < options.size(); ++index) {
if (current_scale >= options[index]) {
selection.has_selection = true;
selection.index = index;
}
}
return selection;
}
[[nodiscard]] constexpr InterfaceDirectionPlan plan_interface_direction(bool right_to_left) noexcept
{
return {
right_to_left ? InterfaceDirection::right_to_left : InterfaceDirection::left_to_right,
};
}
[[nodiscard]] constexpr TimelapsePreferencePlan plan_timelapse_preference(
bool enabled,
bool recording_running) noexcept
{
if (enabled && !recording_running) {
return { enabled, TimelapseRecordingAction::start_recording };
}
if (!enabled && recording_running) {
return { enabled, TimelapseRecordingAction::stop_recording };
}
return { enabled, TimelapseRecordingAction::no_op };
}
[[nodiscard]] constexpr StoredBooleanPreferencePlan plan_vr_controllers_preference(
bool enabled) noexcept
{
return { enabled };
}
[[nodiscard]] constexpr StoredIntegerPreferencePlan plan_canvas_cursor_mode(int mode) noexcept
{
return { mode };
}
}

119
src/app_core/app_status.h Normal file
View File

@@ -0,0 +1,119 @@
#pragma once
#include "foundation/result.h"
#include <array>
#include <cstdio>
#include <cstddef>
#include <string>
#include <string_view>
namespace pp::app {
inline constexpr std::array<int, 6> document_resolution_values {
512,
1024,
1536,
2048,
4096,
8192,
};
inline constexpr std::array<std::string_view, 6> document_resolution_labels {
"2K",
"4K",
"6K",
"8K",
"16K",
"32K",
};
struct RecordingFrameLabel {
bool visible = false;
std::string text;
};
[[nodiscard]] inline pp::foundation::Result<int> display_resolution_from_index(int index)
{
if (index < 0 || static_cast<std::size_t>(index) >= document_resolution_values.size()) {
return pp::foundation::Result<int>::failure(
pp::foundation::Status::out_of_range("document resolution index is out of range"));
}
return pp::foundation::Result<int>::success(
document_resolution_values[static_cast<std::size_t>(index)]);
}
[[nodiscard]] inline pp::foundation::Result<std::size_t> document_resolution_to_index(int resolution)
{
for (std::size_t index = 0; index < document_resolution_values.size(); ++index) {
if (document_resolution_values[index] == resolution) {
return pp::foundation::Result<std::size_t>::success(index);
}
}
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("document resolution is not supported"));
}
[[nodiscard]] inline pp::foundation::Result<std::string_view> document_resolution_label(int resolution)
{
const auto index = document_resolution_to_index(resolution);
if (!index) {
return pp::foundation::Result<std::string_view>::failure(index.status());
}
return pp::foundation::Result<std::string_view>::success(document_resolution_labels[index.value()]);
}
[[nodiscard]] inline std::string make_document_title(
std::string_view document_name,
bool has_unsaved_changes,
int resolution)
{
const auto label = document_resolution_label(resolution);
const auto resolution_label = label ? label.value() : std::string_view("unknown");
std::string title = "Panodoc: ";
title.append(document_name);
if (has_unsaved_changes) {
title.push_back('*');
}
title.append(" (");
title.append(resolution_label);
title.push_back(')');
return title;
}
[[nodiscard]] inline std::string make_dpi_label(float zoom)
{
char buffer[64] {};
std::snprintf(buffer, sizeof(buffer), "%.1fx-dpi", zoom);
return buffer;
}
[[nodiscard]] inline std::string make_history_memory_label(std::size_t bytes)
{
char buffer[128] {};
std::snprintf(
buffer,
sizeof(buffer),
"History memory: %.2f Mb",
static_cast<double>(bytes) / 1024.0 / 1024.0);
return buffer;
}
[[nodiscard]] inline RecordingFrameLabel make_recording_frame_label(
bool is_recording,
bool encoder_available,
int encoded_frames)
{
if (!is_recording || !encoder_available) {
return {};
}
char buffer[128] {};
std::snprintf(buffer, sizeof(buffer), "Recorded %d frames", encoded_frames);
return {
true,
buffer,
};
}
}

171
src/app_core/brush_ui.h Normal file
View File

@@ -0,0 +1,171 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
#include <string>
#include <string_view>
namespace pp::app {
enum class BrushUiTextureSlot {
tip,
pattern,
dual,
};
enum class BrushUiOperation {
set_tip_color,
set_texture,
replace_brush_from_preset,
stroke_settings_changed,
};
struct BrushUiPlan {
BrushUiOperation operation = BrushUiOperation::stroke_settings_changed;
BrushUiTextureSlot texture_slot = BrushUiTextureSlot::tip;
std::string path;
std::string thumbnail_path;
float r = 0.0F;
float g = 0.0F;
float b = 0.0F;
float a = 1.0F;
bool mutates_brush = false;
bool preserves_existing_color = false;
bool loads_brush_resources = false;
bool update_color_ui = false;
bool update_brush_ui = false;
};
class BrushUiServices {
public:
virtual ~BrushUiServices() = default;
virtual void set_tip_color(float r, float g, float b, float a) = 0;
virtual void set_texture(BrushUiTextureSlot slot, std::string_view path, std::string_view thumbnail_path) = 0;
virtual void replace_brush_from_preset(bool preserve_existing_color, bool load_resources) = 0;
virtual void refresh_brush_ui(bool update_color_ui, bool update_brush_ui) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_brush_ui_color_channel(float value) noexcept
{
if (!std::isfinite(value) || value < 0.0F || value > 1.0F) {
return pp::foundation::Status::out_of_range("brush color channels must be finite and within 0..1");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_color(
float r,
float g,
float b,
float a)
{
for (const auto value : { r, g, b, a }) {
const auto channel_status = validate_brush_ui_color_channel(value);
if (!channel_status.ok()) {
return pp::foundation::Result<BrushUiPlan>::failure(channel_status);
}
}
BrushUiPlan plan;
plan.operation = BrushUiOperation::set_tip_color;
plan.r = r;
plan.g = g;
plan.b = b;
plan.a = a;
plan.mutates_brush = true;
plan.update_color_ui = true;
return pp::foundation::Result<BrushUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_texture(
BrushUiTextureSlot slot,
std::string_view path,
std::string_view thumbnail_path)
{
if (path.empty()) {
return pp::foundation::Result<BrushUiPlan>::failure(
pp::foundation::Status::invalid_argument("brush texture path must not be empty"));
}
BrushUiPlan plan;
plan.operation = BrushUiOperation::set_texture;
plan.texture_slot = slot;
plan.path = std::string(path);
plan.thumbnail_path = std::string(thumbnail_path);
plan.mutates_brush = true;
plan.loads_brush_resources = true;
plan.update_color_ui = true;
plan.update_brush_ui = true;
return pp::foundation::Result<BrushUiPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<BrushUiPlan> plan_brush_ui_preset_replace(bool has_preset_brush)
{
if (!has_preset_brush) {
return pp::foundation::Result<BrushUiPlan>::failure(
pp::foundation::Status::invalid_argument("preset brush must be available"));
}
BrushUiPlan plan;
plan.operation = BrushUiOperation::replace_brush_from_preset;
plan.mutates_brush = true;
plan.preserves_existing_color = true;
plan.loads_brush_resources = true;
plan.update_color_ui = true;
plan.update_brush_ui = true;
return pp::foundation::Result<BrushUiPlan>::success(plan);
}
[[nodiscard]] inline constexpr BrushUiPlan plan_brush_ui_stroke_settings_changed() noexcept
{
BrushUiPlan plan;
plan.operation = BrushUiOperation::stroke_settings_changed;
plan.mutates_brush = true;
plan.update_color_ui = true;
plan.update_brush_ui = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Status execute_brush_ui_plan(
const BrushUiPlan& plan,
BrushUiServices& services)
{
switch (plan.operation) {
case BrushUiOperation::set_tip_color:
{
for (const auto value : { plan.r, plan.g, plan.b, plan.a }) {
const auto channel_status = validate_brush_ui_color_channel(value);
if (!channel_status.ok()) {
return channel_status;
}
}
services.set_tip_color(plan.r, plan.g, plan.b, plan.a);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
}
case BrushUiOperation::set_texture:
if (plan.path.empty()) {
return pp::foundation::Status::invalid_argument("brush texture path must not be empty");
}
services.set_texture(plan.texture_slot, plan.path, plan.thumbnail_path);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
case BrushUiOperation::replace_brush_from_preset:
services.replace_brush_from_preset(plan.preserves_existing_color, plan.loads_brush_resources);
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
case BrushUiOperation::stroke_settings_changed:
services.refresh_brush_ui(plan.update_color_ui, plan.update_brush_ui);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown brush UI operation");
}
} // namespace pp::app

View File

@@ -0,0 +1,180 @@
#pragma once
#include "foundation/result.h"
namespace pp::app {
enum class CanvasToolOperation {
select_mode,
toggle_picking,
toggle_touch_lock,
};
enum class CanvasToolMode {
draw,
erase,
line,
camera,
grid,
copy,
cut,
fill,
mask_free,
mask_line,
flood_fill,
};
enum class CanvasToolTransformAction {
none,
copy,
cut,
};
struct CanvasToolPlan {
CanvasToolOperation operation = CanvasToolOperation::select_mode;
CanvasToolMode mode = CanvasToolMode::draw;
CanvasToolTransformAction transform_action = CanvasToolTransformAction::none;
bool selects_toolbar_button = false;
bool updates_canvas_mode = false;
bool toggles_picking = false;
bool toggles_touch_lock = false;
bool requires_draw_mode = false;
bool no_op = false;
};
struct CanvasToolButtonState {
CanvasToolMode mode = CanvasToolMode::draw;
bool pick_active = false;
bool touch_lock_active = false;
bool pen_active = false;
bool erase_active = false;
bool line_active = false;
bool camera_active = false;
bool grid_active = false;
bool copy_active = false;
bool cut_active = false;
bool fill_active = false;
bool mask_free_active = false;
bool mask_line_active = false;
bool flood_fill_active = false;
};
class CanvasToolServices {
public:
virtual ~CanvasToolServices() = default;
virtual void select_toolbar_button(CanvasToolMode mode) = 0;
virtual void set_transform_action(CanvasToolTransformAction action) = 0;
virtual void set_canvas_mode(CanvasToolMode mode) = 0;
virtual void toggle_picking() = 0;
virtual void toggle_touch_lock() = 0;
};
[[nodiscard]] inline constexpr CanvasToolTransformAction transform_action_for_mode(CanvasToolMode mode) noexcept
{
if (mode == CanvasToolMode::copy) {
return CanvasToolTransformAction::copy;
}
if (mode == CanvasToolMode::cut) {
return CanvasToolTransformAction::cut;
}
return CanvasToolTransformAction::none;
}
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_select(CanvasToolMode mode) noexcept
{
CanvasToolPlan plan;
plan.operation = CanvasToolOperation::select_mode;
plan.mode = mode;
plan.transform_action = transform_action_for_mode(mode);
plan.selects_toolbar_button = true;
plan.updates_canvas_mode = true;
return plan;
}
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_pick_toggle(bool current_mode_is_draw) noexcept
{
CanvasToolPlan plan;
plan.operation = CanvasToolOperation::toggle_picking;
plan.mode = CanvasToolMode::draw;
plan.requires_draw_mode = true;
plan.toggles_picking = current_mode_is_draw;
plan.no_op = !current_mode_is_draw;
return plan;
}
[[nodiscard]] inline constexpr CanvasToolPlan plan_canvas_tool_touch_lock_toggle() noexcept
{
CanvasToolPlan plan;
plan.operation = CanvasToolOperation::toggle_touch_lock;
plan.toggles_touch_lock = true;
return plan;
}
[[nodiscard]] inline constexpr CanvasToolButtonState plan_canvas_tool_button_state(
CanvasToolMode mode,
bool picking,
bool touch_lock) noexcept
{
CanvasToolButtonState state;
state.mode = mode;
state.pick_active = mode == CanvasToolMode::draw && picking;
state.touch_lock_active = touch_lock;
state.pen_active = mode == CanvasToolMode::draw;
state.erase_active = mode == CanvasToolMode::erase;
state.line_active = mode == CanvasToolMode::line;
state.camera_active = mode == CanvasToolMode::camera;
state.grid_active = mode == CanvasToolMode::grid;
state.copy_active = mode == CanvasToolMode::copy;
state.cut_active = mode == CanvasToolMode::cut;
state.fill_active = mode == CanvasToolMode::fill;
state.mask_free_active = mode == CanvasToolMode::mask_free;
state.mask_line_active = mode == CanvasToolMode::mask_line;
state.flood_fill_active = mode == CanvasToolMode::flood_fill;
return state;
}
[[nodiscard]] inline pp::foundation::Status execute_canvas_tool_plan(
const CanvasToolPlan& plan,
CanvasToolServices& services)
{
switch (plan.operation) {
case CanvasToolOperation::select_mode:
if (!plan.selects_toolbar_button || !plan.updates_canvas_mode) {
return pp::foundation::Status::invalid_argument("canvas tool select plan must select toolbar and update mode");
}
if (plan.transform_action != transform_action_for_mode(plan.mode)) {
return pp::foundation::Status::invalid_argument("canvas tool select plan has mismatched transform action");
}
services.select_toolbar_button(plan.mode);
if (plan.transform_action != CanvasToolTransformAction::none) {
services.set_transform_action(plan.transform_action);
}
services.set_canvas_mode(plan.mode);
return pp::foundation::Status::success();
case CanvasToolOperation::toggle_picking:
if (!plan.requires_draw_mode) {
return pp::foundation::Status::invalid_argument("canvas pick plan must require draw mode");
}
if (plan.no_op) {
return pp::foundation::Status::success();
}
if (!plan.toggles_picking) {
return pp::foundation::Status::invalid_argument("canvas pick plan must toggle picking or be a no-op");
}
services.toggle_picking();
return pp::foundation::Status::success();
case CanvasToolOperation::toggle_touch_lock:
if (!plan.toggles_touch_lock || plan.no_op) {
return pp::foundation::Status::invalid_argument("canvas touch-lock plan must toggle touch lock");
}
services.toggle_touch_lock();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown canvas tool operation");
}
} // namespace pp::app

View File

@@ -0,0 +1,620 @@
#pragma once
#include "foundation/result.h"
#include <algorithm>
#include <cstdint>
#include <limits>
namespace pp::app {
inline constexpr int document_animation_default_frame_duration = 1;
enum class DocumentAnimationOperation {
add_frame,
duplicate_frame,
remove_frame,
adjust_duration,
move_frame,
select_frame,
goto_frame,
goto_next,
goto_previous,
playback_step,
toggle_playback,
set_onion_size,
};
enum class DocumentAnimationPanelAction {
goto_frame,
next_frame,
previous_frame,
playback_step,
toggle_playback,
};
struct DocumentAnimationPanelState {
int total_duration = 1;
int current_frame = 0;
bool playback_active = false;
};
struct DocumentAnimationOperationPlan {
DocumentAnimationOperation operation = DocumentAnimationOperation::goto_frame;
int frame_count = 1;
int current_frame = 0;
int selected_frame = 0;
int target_frame = 0;
int frame_duration = document_animation_default_frame_duration;
int duration_delta = 0;
int move_offset = 0;
int onion_size = 1;
int layer_index = 0;
std::uint32_t layer_id = 0;
int playback_idle_ms = 100;
bool requires_selected_frame = false;
bool mutates_document = false;
bool reloads_animation_layers = false;
bool updates_canvas_animation = false;
bool marks_unsaved = false;
bool playback_was_active = false;
bool playback_active = false;
bool resets_playback_timer = false;
};
class DocumentAnimationServices {
public:
virtual ~DocumentAnimationServices() = default;
virtual void add_frame() = 0;
virtual void duplicate_frame(int selected_frame) = 0;
virtual void remove_frame(int selected_frame, int target_frame) = 0;
virtual void set_frame_duration(int selected_frame, int duration) = 0;
virtual int move_frame(int selected_frame, int move_offset) = 0;
virtual void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) = 0;
virtual void select_layer(int layer_index) = 0;
virtual void goto_frame(int target_frame) = 0;
virtual void set_timeline_frame(int target_frame) = 0;
virtual void set_onion_size(int onion_size) = 0;
virtual void capture_playback_restore_mode() = 0;
virtual void enter_playback_camera_mode() = 0;
virtual void restore_playback_canvas_mode() = 0;
virtual void set_playback_active(bool active) = 0;
virtual void reset_playback_timer() = 0;
virtual void set_playback_idle_ms(int idle_ms) = 0;
virtual void update_canvas_animation() = 0;
virtual void update_frame_status() = 0;
virtual void reload_animation_layers() = 0;
virtual void mark_unsaved() = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_count(int frame_count) noexcept
{
if (frame_count <= 0) {
return pp::foundation::Status::invalid_argument("animation layer must contain at least one frame");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_index(
int frame_count,
int index) noexcept
{
const auto count_status = validate_animation_frame_count(frame_count);
if (!count_status.ok()) {
return count_status;
}
if (index < 0 || index >= frame_count) {
return pp::foundation::Status::out_of_range("animation frame index is outside the layer");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_animation_frame_duration(int duration) noexcept
{
if (duration < 1) {
return pp::foundation::Status::invalid_argument("animation frame duration must be at least 1");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_add_frame(
int frame_count,
int current_frame)
{
const auto count_status = validate_animation_frame_count(frame_count);
if (!count_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(count_status);
}
if (current_frame < 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("current animation frame must not be negative"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::add_frame;
plan.frame_count = frame_count;
plan.current_frame = current_frame;
plan.selected_frame = frame_count;
plan.target_frame = current_frame;
plan.mutates_document = true;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
plan.marks_unsaved = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_duplicate_frame(
int frame_count,
int selected_frame)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::duplicate_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = selected_frame + 1;
plan.requires_selected_frame = true;
plan.mutates_document = true;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
plan.marks_unsaved = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_remove_frame(
int frame_count,
int selected_frame)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
if (frame_count <= 1) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation layer must keep at least one frame"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::remove_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = std::min(selected_frame, frame_count - 2);
plan.requires_selected_frame = true;
plan.mutates_document = true;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
plan.marks_unsaved = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_adjust_duration(
int frame_count,
int selected_frame,
int current_duration,
int delta)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
const auto duration_status = validate_animation_frame_duration(current_duration);
if (!duration_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(duration_status);
}
if (delta == 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation frame duration delta must not be zero"));
}
if (delta > 0 && current_duration > std::numeric_limits<int>::max() - delta) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("animation frame duration would overflow"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::adjust_duration;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = selected_frame;
plan.frame_duration = std::max(current_duration + delta, 1);
plan.duration_delta = delta;
plan.requires_selected_frame = true;
plan.mutates_document = plan.frame_duration != current_duration;
plan.reloads_animation_layers = plan.mutates_document;
plan.updates_canvas_animation = plan.mutates_document;
plan.marks_unsaved = plan.mutates_document;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_move_frame(
int frame_count,
int selected_frame,
int offset)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
if (offset == 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation frame move offset must not be zero"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::move_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
const auto unclamped_target = static_cast<std::int64_t>(selected_frame) + static_cast<std::int64_t>(offset);
plan.target_frame = static_cast<int>(std::clamp<std::int64_t>(unclamped_target, 0, frame_count - 1));
plan.move_offset = offset;
plan.requires_selected_frame = true;
plan.mutates_document = plan.target_frame != selected_frame;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
plan.marks_unsaved = plan.mutates_document;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_select_frame(
int frame_count,
int layer_index,
std::uint32_t layer_id,
int selected_frame)
{
const auto index_status = validate_animation_frame_index(frame_count, selected_frame);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(index_status);
}
if (layer_index < 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("animation layer index must not be negative"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::select_frame;
plan.frame_count = frame_count;
plan.selected_frame = selected_frame;
plan.target_frame = selected_frame;
plan.layer_index = layer_index;
plan.layer_id = layer_id;
plan.requires_selected_frame = true;
plan.updates_canvas_animation = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_goto_frame(
int total_duration,
int frame)
{
if (total_duration <= 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation duration must be greater than zero"));
}
if (frame < 0 || frame >= total_duration) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::out_of_range("animation timeline frame is outside the document"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::goto_frame;
plan.frame_count = total_duration;
plan.current_frame = frame;
plan.target_frame = frame;
plan.reloads_animation_layers = true;
plan.updates_canvas_animation = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_step_frame(
int total_duration,
int current_frame,
int offset)
{
const auto current_status = plan_animation_goto_frame(total_duration, current_frame);
if (!current_status) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(current_status.status());
}
if (offset == 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation frame step offset must not be zero"));
}
DocumentAnimationOperationPlan plan;
plan.operation = offset > 0 ? DocumentAnimationOperation::goto_next : DocumentAnimationOperation::goto_previous;
plan.frame_count = total_duration;
plan.current_frame = current_frame;
auto target = (static_cast<std::int64_t>(current_frame) + static_cast<std::int64_t>(offset))
% static_cast<std::int64_t>(total_duration);
if (target < 0) {
target += total_duration;
}
plan.target_frame = static_cast<int>(target);
plan.updates_canvas_animation = true;
plan.reloads_animation_layers = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_step(
int total_duration,
int current_frame,
int offset)
{
const auto step = plan_animation_step_frame(total_duration, current_frame, offset);
if (!step) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(step.status());
}
auto plan = step.value();
plan.operation = DocumentAnimationOperation::playback_step;
plan.move_offset = offset;
plan.reloads_animation_layers = false;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_playback_toggle(
bool playback_active)
{
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::toggle_playback;
plan.playback_was_active = playback_active;
plan.playback_active = !playback_active;
plan.playback_idle_ms = playback_active ? 100 : 10;
plan.resets_playback_timer = !playback_active;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_onion_size(int onion_size)
{
if (onion_size < 0) {
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("animation onion size must not be negative"));
}
DocumentAnimationOperationPlan plan;
plan.operation = DocumentAnimationOperation::set_onion_size;
plan.onion_size = onion_size;
plan.updates_canvas_animation = true;
return pp::foundation::Result<DocumentAnimationOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentAnimationOperationPlan> plan_animation_panel_action(
DocumentAnimationPanelAction action,
const DocumentAnimationPanelState& state,
int target_frame = 0)
{
switch (action) {
case DocumentAnimationPanelAction::goto_frame:
return plan_animation_goto_frame(state.total_duration, target_frame);
case DocumentAnimationPanelAction::next_frame:
return plan_animation_step_frame(state.total_duration, state.current_frame, 1);
case DocumentAnimationPanelAction::previous_frame:
return plan_animation_step_frame(state.total_duration, state.current_frame, -1);
case DocumentAnimationPanelAction::playback_step:
return plan_animation_playback_step(state.total_duration, state.current_frame, 1);
case DocumentAnimationPanelAction::toggle_playback:
return plan_animation_playback_toggle(state.playback_active);
}
return pp::foundation::Result<DocumentAnimationOperationPlan>::failure(
pp::foundation::Status::invalid_argument("unknown animation panel action"));
}
[[nodiscard]] inline pp::foundation::Status validate_animation_operation_plan(
const DocumentAnimationOperationPlan& plan) noexcept
{
switch (plan.operation) {
case DocumentAnimationOperation::add_frame:
if (!plan.mutates_document || !plan.marks_unsaved) {
return pp::foundation::Status::invalid_argument("animation add plan must mutate the document");
}
return validate_animation_frame_count(plan.frame_count);
case DocumentAnimationOperation::duplicate_frame:
case DocumentAnimationOperation::remove_frame:
if (!plan.requires_selected_frame || !plan.mutates_document || !plan.marks_unsaved) {
return pp::foundation::Status::invalid_argument("animation selected-frame plan must mutate the document");
}
if (plan.operation == DocumentAnimationOperation::remove_frame && plan.frame_count <= 1) {
return pp::foundation::Status::invalid_argument("animation layer must keep at least one frame");
}
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
case DocumentAnimationOperation::adjust_duration:
if (!plan.requires_selected_frame) {
return pp::foundation::Status::invalid_argument("animation duration plan must require a selected frame");
}
{
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
if (!index_status.ok()) {
return index_status;
}
}
return validate_animation_frame_duration(plan.frame_duration);
case DocumentAnimationOperation::move_frame:
if (!plan.requires_selected_frame || plan.move_offset == 0) {
return pp::foundation::Status::invalid_argument("animation move plan must require selected frame and non-zero offset");
}
return validate_animation_frame_index(plan.frame_count, plan.selected_frame);
case DocumentAnimationOperation::select_frame:
if (!plan.requires_selected_frame || !plan.updates_canvas_animation || plan.layer_index < 0) {
return pp::foundation::Status::invalid_argument("animation frame select plan has invalid state");
}
{
const auto index_status = validate_animation_frame_index(plan.frame_count, plan.selected_frame);
if (!index_status.ok()) {
return index_status;
}
}
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
case DocumentAnimationOperation::goto_frame:
case DocumentAnimationOperation::goto_next:
case DocumentAnimationOperation::goto_previous:
case DocumentAnimationOperation::playback_step:
if (!plan.updates_canvas_animation) {
return pp::foundation::Status::invalid_argument("animation goto plan must update canvas animation");
}
if (plan.operation == DocumentAnimationOperation::playback_step && plan.move_offset == 0) {
return pp::foundation::Status::invalid_argument("animation playback step offset must not be zero");
}
return validate_animation_frame_index(plan.frame_count, plan.target_frame);
case DocumentAnimationOperation::toggle_playback:
if (plan.playback_active == plan.playback_was_active) {
return pp::foundation::Status::invalid_argument("animation playback toggle must change state");
}
if (plan.playback_idle_ms <= 0) {
return pp::foundation::Status::invalid_argument("animation playback idle interval must be positive");
}
if (plan.playback_active && !plan.resets_playback_timer) {
return pp::foundation::Status::invalid_argument("animation playback start must reset timer");
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::set_onion_size:
if (plan.onion_size < 0) {
return pp::foundation::Status::invalid_argument("animation onion size must not be negative");
}
if (!plan.updates_canvas_animation) {
return pp::foundation::Status::invalid_argument("animation onion plan must update canvas animation");
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown animation operation");
}
[[nodiscard]] inline pp::foundation::Status execute_animation_operation_plan(
const DocumentAnimationOperationPlan& plan,
DocumentAnimationServices& services)
{
const auto validation = validate_animation_operation_plan(plan);
if (!validation.ok()) {
return validation;
}
switch (plan.operation) {
case DocumentAnimationOperation::add_frame:
services.add_frame();
services.mark_unsaved();
services.update_canvas_animation();
services.reload_animation_layers();
return pp::foundation::Status::success();
case DocumentAnimationOperation::duplicate_frame:
services.duplicate_frame(plan.selected_frame);
services.mark_unsaved();
services.update_canvas_animation();
services.reload_animation_layers();
return pp::foundation::Status::success();
case DocumentAnimationOperation::remove_frame:
services.remove_frame(plan.selected_frame, plan.target_frame);
services.mark_unsaved();
if (plan.updates_canvas_animation) {
services.goto_frame(plan.target_frame);
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::adjust_duration:
if (plan.mutates_document) {
services.set_frame_duration(plan.selected_frame, plan.frame_duration);
services.mark_unsaved();
if (plan.updates_canvas_animation) {
services.update_canvas_animation();
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::move_frame:
{
const auto actual_target_frame = services.move_frame(plan.selected_frame, plan.move_offset);
if (plan.marks_unsaved) {
services.mark_unsaved();
}
if (plan.updates_canvas_animation) {
services.goto_frame(actual_target_frame);
}
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
}
case DocumentAnimationOperation::goto_frame:
case DocumentAnimationOperation::goto_next:
case DocumentAnimationOperation::goto_previous:
services.goto_frame(plan.target_frame);
if (plan.reloads_animation_layers) {
services.reload_animation_layers();
}
return pp::foundation::Status::success();
case DocumentAnimationOperation::select_frame:
services.select_frame(plan.layer_id, plan.layer_index, plan.selected_frame);
if (plan.updates_canvas_animation) {
services.goto_frame(plan.target_frame);
}
services.select_layer(plan.layer_index);
return pp::foundation::Status::success();
case DocumentAnimationOperation::playback_step:
services.goto_frame(plan.target_frame);
services.set_timeline_frame(plan.target_frame);
services.update_frame_status();
return pp::foundation::Status::success();
case DocumentAnimationOperation::toggle_playback:
if (plan.playback_active) {
services.capture_playback_restore_mode();
services.enter_playback_camera_mode();
if (plan.resets_playback_timer) {
services.reset_playback_timer();
}
} else {
services.restore_playback_canvas_mode();
}
services.set_playback_active(plan.playback_active);
services.set_playback_idle_ms(plan.playback_idle_ms);
return pp::foundation::Status::success();
case DocumentAnimationOperation::set_onion_size:
services.set_onion_size(plan.onion_size);
if (plan.updates_canvas_animation) {
services.update_canvas_animation();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown animation operation");
}
} // namespace pp::app

View File

@@ -0,0 +1,85 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
namespace pp::app {
struct DocumentCanvasClearPlan {
float r = 0.0F;
float g = 0.0F;
float b = 0.0F;
float a = 0.0F;
bool clears_canvas = false;
bool records_undo = false;
bool marks_unsaved = false;
bool no_op = true;
};
class DocumentCanvasClearServices {
public:
virtual ~DocumentCanvasClearServices() = default;
virtual void clear_current_canvas(float r, float g, float b, float a) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_clear_color_channel(float value) noexcept
{
if (!std::isfinite(value)) {
return pp::foundation::Status::invalid_argument("clear color channel must be finite");
}
if (value < 0.0F || value > 1.0F) {
return pp::foundation::Status::out_of_range("clear color channel must be within 0..1");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentCanvasClearPlan> plan_document_canvas_clear(
bool has_canvas,
float r = 0.0F,
float g = 0.0F,
float b = 0.0F,
float a = 0.0F) noexcept
{
const float channels[] { r, g, b, a };
for (const float channel : channels) {
const auto status = validate_clear_color_channel(channel);
if (!status.ok()) {
return pp::foundation::Result<DocumentCanvasClearPlan>::failure(status);
}
}
DocumentCanvasClearPlan plan;
plan.r = r;
plan.g = g;
plan.b = b;
plan.a = a;
plan.clears_canvas = has_canvas;
plan.records_undo = has_canvas;
plan.marks_unsaved = has_canvas;
plan.no_op = !has_canvas;
return pp::foundation::Result<DocumentCanvasClearPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_document_canvas_clear_plan(
const DocumentCanvasClearPlan& plan,
DocumentCanvasClearServices& services)
{
const float channels[] { plan.r, plan.g, plan.b, plan.a };
for (const float channel : channels) {
const auto status = validate_clear_color_channel(channel);
if (!status.ok()) {
return status;
}
}
if (plan.no_op || !plan.clears_canvas) {
return pp::foundation::Status::success();
}
services.clear_current_canvas(plan.r, plan.g, plan.b, plan.a);
return pp::foundation::Status::success();
}
} // namespace pp::app

View File

@@ -0,0 +1,79 @@
#pragma once
#include <cstddef>
#include <limits>
#include <string_view>
namespace pp::app {
enum class CloudUploadAction {
unavailable_no_canvas,
show_save_required_warning,
prompt_publish,
};
enum class CloudBrowseAction {
unavailable_no_canvas,
show_browser,
};
enum class CloudDownloadSelectionAction {
wait_for_selection,
start_download,
};
struct CloudUploadPlan {
CloudUploadAction action = CloudUploadAction::unavailable_no_canvas;
bool save_before_upload = false;
};
struct CloudBulkUploadPlan {
std::size_t file_count = 0;
int progress_total = 0;
bool show_progress = false;
};
[[nodiscard]] constexpr CloudUploadPlan plan_cloud_upload(
bool has_canvas,
bool is_new_document,
bool has_unsaved_changes) noexcept
{
if (!has_canvas) {
return { CloudUploadAction::unavailable_no_canvas, false };
}
if (is_new_document) {
return { CloudUploadAction::show_save_required_warning, false };
}
return { CloudUploadAction::prompt_publish, has_unsaved_changes };
}
[[nodiscard]] constexpr CloudBrowseAction plan_cloud_browse(bool has_canvas) noexcept
{
return has_canvas
? CloudBrowseAction::show_browser
: CloudBrowseAction::unavailable_no_canvas;
}
[[nodiscard]] constexpr CloudDownloadSelectionAction plan_cloud_download_selection(
std::string_view selected_file) noexcept
{
return selected_file.empty()
? CloudDownloadSelectionAction::wait_for_selection
: CloudDownloadSelectionAction::start_download;
}
[[nodiscard]] constexpr CloudBulkUploadPlan plan_cloud_bulk_upload(
std::size_t file_count,
bool progress_ui_available) noexcept
{
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
return {
file_count,
file_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(file_count),
progress_ui_available,
};
}
}

View File

@@ -0,0 +1 @@
#include "app_core/document_export.h"

View File

@@ -0,0 +1,283 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
#include <utility>
namespace pp::app {
struct DocumentExportFileTarget {
std::string path;
std::string suggested_name;
};
struct DocumentExportCollectionTarget {
std::string directory;
std::string stem_path;
};
struct DocumentExportStemTarget {
std::string stem_path;
};
struct DocumentExportSuggestedName {
std::string name;
};
enum class DocumentExportStartDecision {
start_now,
show_license_disabled,
unavailable_no_canvas,
};
enum class DocumentExportMenuKind {
jpeg,
png,
layers,
cube_faces,
depth,
animation_frames,
animation_mp4,
timelapse,
};
enum class DocumentExportMenuAction {
show_jpeg_dialog,
show_png_dialog,
show_layers_dialog,
show_cube_faces_dialog,
show_depth_dialog,
show_animation_frames_dialog,
show_animation_mp4_dialog,
show_timelapse_dialog,
show_license_disabled,
unavailable_no_canvas,
};
struct DocumentExportMenuPlan {
DocumentExportMenuKind kind = DocumentExportMenuKind::jpeg;
DocumentExportMenuAction action = DocumentExportMenuAction::show_jpeg_dialog;
bool opens_dialog = true;
};
class DocumentExportMenuServices {
public:
virtual ~DocumentExportMenuServices() = default;
virtual void show_jpeg_dialog() = 0;
virtual void show_png_dialog() = 0;
virtual void show_layers_dialog() = 0;
virtual void show_cube_faces_dialog() = 0;
virtual void show_depth_dialog() = 0;
virtual void show_animation_frames_dialog() = 0;
virtual void show_animation_mp4_dialog() = 0;
virtual void show_timelapse_dialog() = 0;
virtual void show_license_disabled() = 0;
};
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
bool requires_license,
bool license_valid,
bool has_canvas) noexcept
{
if (requires_license && !license_valid) {
return DocumentExportStartDecision::show_license_disabled;
}
return has_canvas
? DocumentExportStartDecision::start_now
: DocumentExportStartDecision::unavailable_no_canvas;
}
[[nodiscard]] constexpr bool document_export_menu_requires_license(
DocumentExportMenuKind kind) noexcept
{
switch (kind) {
case DocumentExportMenuKind::animation_mp4:
case DocumentExportMenuKind::timelapse:
return true;
case DocumentExportMenuKind::jpeg:
case DocumentExportMenuKind::png:
case DocumentExportMenuKind::layers:
case DocumentExportMenuKind::cube_faces:
case DocumentExportMenuKind::depth:
case DocumentExportMenuKind::animation_frames:
return false;
}
return false;
}
[[nodiscard]] constexpr DocumentExportMenuAction document_export_menu_dialog_action(
DocumentExportMenuKind kind) noexcept
{
switch (kind) {
case DocumentExportMenuKind::jpeg:
return DocumentExportMenuAction::show_jpeg_dialog;
case DocumentExportMenuKind::png:
return DocumentExportMenuAction::show_png_dialog;
case DocumentExportMenuKind::layers:
return DocumentExportMenuAction::show_layers_dialog;
case DocumentExportMenuKind::cube_faces:
return DocumentExportMenuAction::show_cube_faces_dialog;
case DocumentExportMenuKind::depth:
return DocumentExportMenuAction::show_depth_dialog;
case DocumentExportMenuKind::animation_frames:
return DocumentExportMenuAction::show_animation_frames_dialog;
case DocumentExportMenuKind::animation_mp4:
return DocumentExportMenuAction::show_animation_mp4_dialog;
case DocumentExportMenuKind::timelapse:
return DocumentExportMenuAction::show_timelapse_dialog;
}
return DocumentExportMenuAction::show_jpeg_dialog;
}
[[nodiscard]] constexpr DocumentExportMenuPlan plan_document_export_menu_action(
DocumentExportMenuKind kind,
bool has_canvas,
bool license_valid) noexcept
{
DocumentExportMenuPlan plan;
plan.kind = kind;
plan.action = document_export_menu_dialog_action(kind);
const auto start = plan_document_export_start(
document_export_menu_requires_license(kind),
license_valid,
has_canvas);
if (start == DocumentExportStartDecision::show_license_disabled) {
plan.action = DocumentExportMenuAction::show_license_disabled;
plan.opens_dialog = false;
} else if (start == DocumentExportStartDecision::unavailable_no_canvas) {
plan.action = DocumentExportMenuAction::unavailable_no_canvas;
plan.opens_dialog = false;
}
return plan;
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportFileTarget> make_document_export_file_target(
std::string_view work_directory,
std::string_view document_name,
std::string_view extension)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportFileTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
if (extension.empty()) {
return pp::foundation::Result<DocumentExportFileTarget>::failure(
pp::foundation::Status::invalid_argument("extension must not be empty"));
}
DocumentExportFileTarget target;
target.suggested_name.reserve(document_name.size() + extension.size());
target.suggested_name += document_name;
target.suggested_name += extension;
target.path.reserve(work_directory.size() + target.suggested_name.size() + 1);
target.path += work_directory;
target.path += "/";
target.path += target.suggested_name;
return pp::foundation::Result<DocumentExportFileTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportCollectionTarget> make_document_export_collection_target(
std::string_view work_directory,
std::string_view document_name,
std::string_view suffix)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportCollectionTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportCollectionTarget target;
target.directory.reserve(work_directory.size() + document_name.size() + suffix.size() + 1);
target.directory += work_directory;
target.directory += "/";
target.directory += document_name;
target.directory += suffix;
target.stem_path.reserve(target.directory.size() + document_name.size() + 1);
target.stem_path += target.directory;
target.stem_path += "/";
target.stem_path += document_name;
return pp::foundation::Result<DocumentExportCollectionTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportStemTarget> make_document_export_stem_target(
std::string_view directory,
std::string_view document_name)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportStemTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportStemTarget target;
target.stem_path.reserve(directory.size() + document_name.size() + 1);
target.stem_path += directory;
target.stem_path += "/";
target.stem_path += document_name;
return pp::foundation::Result<DocumentExportStemTarget>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportSuggestedName> make_document_export_suggested_name(
std::string_view document_name,
std::string_view suffix)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentExportSuggestedName>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentExportSuggestedName target;
target.name.reserve(document_name.size() + suffix.size());
target.name += document_name;
target.name += suffix;
return pp::foundation::Result<DocumentExportSuggestedName>::success(std::move(target));
}
[[nodiscard]] inline pp::foundation::Status execute_document_export_menu_plan(
const DocumentExportMenuPlan& plan,
DocumentExportMenuServices& services)
{
switch (plan.action) {
case DocumentExportMenuAction::show_jpeg_dialog:
services.show_jpeg_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_png_dialog:
services.show_png_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_layers_dialog:
services.show_layers_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_cube_faces_dialog:
services.show_cube_faces_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_depth_dialog:
services.show_depth_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_animation_frames_dialog:
services.show_animation_frames_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_animation_mp4_dialog:
services.show_animation_mp4_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_timelapse_dialog:
services.show_timelapse_dialog();
return pp::foundation::Status::success();
case DocumentExportMenuAction::show_license_disabled:
services.show_license_disabled();
return pp::foundation::Status::success();
case DocumentExportMenuAction::unavailable_no_canvas:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document export menu action");
}
}

View File

@@ -0,0 +1,88 @@
#pragma once
#include "foundation/result.h"
#include <string_view>
namespace pp::app {
enum class DocumentImageImportAction {
import_equirectangular,
place_transform,
};
struct DocumentImageImportPlan {
int width = 0;
int height = 0;
DocumentImageImportAction action = DocumentImageImportAction::place_transform;
bool imports_equirectangular = false;
bool enters_transform_mode = false;
};
class DocumentImageImportServices {
public:
virtual ~DocumentImageImportServices() = default;
virtual void import_equirectangular(std::string_view path) = 0;
virtual void enter_transform_import(std::string_view path) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_document_image_import_dimensions(
int width,
int height) noexcept
{
if (width <= 0 || height <= 0) {
return pp::foundation::Status::invalid_argument("image dimensions must be positive");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentImageImportPlan> plan_document_image_import(
int width,
int height) noexcept
{
const auto dimensions = validate_document_image_import_dimensions(width, height);
if (!dimensions.ok()) {
return pp::foundation::Result<DocumentImageImportPlan>::failure(dimensions);
}
const auto wide_equirect = static_cast<long long>(width) == static_cast<long long>(height) * 2LL;
const auto vertical_cube_strip = width == height / 6;
DocumentImageImportPlan plan;
plan.width = width;
plan.height = height;
plan.imports_equirectangular = wide_equirect || vertical_cube_strip;
plan.enters_transform_mode = !plan.imports_equirectangular;
plan.action = plan.imports_equirectangular
? DocumentImageImportAction::import_equirectangular
: DocumentImageImportAction::place_transform;
return pp::foundation::Result<DocumentImageImportPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_document_image_import_plan(
const DocumentImageImportPlan& plan,
std::string_view path,
DocumentImageImportServices& services)
{
const auto dimensions = validate_document_image_import_dimensions(plan.width, plan.height);
if (!dimensions.ok()) {
return dimensions;
}
if (path.empty()) {
return pp::foundation::Status::invalid_argument("image import path must not be empty");
}
switch (plan.action) {
case DocumentImageImportAction::import_equirectangular:
services.import_equirectangular(path);
return pp::foundation::Status::success();
case DocumentImageImportAction::place_transform:
services.enter_transform_import(path);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown image import action");
}
} // namespace pp::app

View File

@@ -0,0 +1,448 @@
#pragma once
#include "foundation/result.h"
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <utility>
namespace pp::app {
inline constexpr std::size_t document_layer_name_max_length = 128;
inline constexpr int document_layer_legacy_blend_mode_count = 5;
enum class DocumentLayerRenameAction {
no_op_same_name,
rename_and_record_undo,
};
enum class DocumentLayerOperation {
add,
duplicate,
select,
reorder,
remove,
set_opacity,
set_visibility,
set_alpha_lock,
set_blend_mode,
set_highlight,
};
enum class DocumentLayerMenuCommand {
clear,
rename,
merge_down,
};
enum class DocumentLayerMenuAction {
clear_current_layer,
show_rename_dialog,
merge_with_lower_layer,
show_merge_animated_not_supported,
no_op_select_layer,
no_op_select_upper_layer,
};
struct DocumentLayerRenamePlan {
std::string old_name;
std::string new_name;
DocumentLayerRenameAction action = DocumentLayerRenameAction::no_op_same_name;
};
struct DocumentLayerOperationPlan {
DocumentLayerOperation operation = DocumentLayerOperation::select;
int index = 0;
int from_index = 0;
int to_index = 0;
int insert_index = 0;
int source_index = 0;
std::string name;
float opacity = 1.0F;
bool flag = false;
int blend_mode = 0;
bool mutates_document = false;
bool marks_unsaved = false;
bool reloads_animation_layers = false;
bool updates_title = false;
};
struct DocumentLayerMenuPlan {
DocumentLayerMenuCommand command = DocumentLayerMenuCommand::clear;
DocumentLayerMenuAction action = DocumentLayerMenuAction::clear_current_layer;
std::string label;
int from_index = 0;
int to_index = 0;
};
class DocumentLayerMenuServices {
public:
virtual ~DocumentLayerMenuServices() = default;
virtual void clear_current_layer() = 0;
virtual void show_rename_dialog() = 0;
virtual void merge_with_lower_layer(int from_index, int to_index) = 0;
virtual void show_merge_animated_not_supported() = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_layer_index(
int layer_count,
int index) noexcept
{
if (layer_count <= 0) {
return pp::foundation::Status::invalid_argument("document must contain at least one layer");
}
if (index < 0 || index >= layer_count) {
return pp::foundation::Status::out_of_range("layer index is outside the document");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_layer_insert_index(
int layer_count,
int index) noexcept
{
if (layer_count < 0) {
return pp::foundation::Status::invalid_argument("layer count must not be negative");
}
if (index < 0 || index > layer_count) {
return pp::foundation::Status::out_of_range("layer insert index is outside the document");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerRenamePlan> plan_document_layer_rename(
std::string_view old_name,
std::string_view requested_name)
{
if (requested_name.empty()) {
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
pp::foundation::Status::invalid_argument("layer name must not be empty"));
}
if (requested_name.size() > document_layer_name_max_length) {
return pp::foundation::Result<DocumentLayerRenamePlan>::failure(
pp::foundation::Status::out_of_range("layer name length exceeds the configured limit"));
}
DocumentLayerRenamePlan plan;
plan.old_name = std::string(old_name);
plan.new_name = std::string(requested_name);
plan.action = old_name == requested_name
? DocumentLayerRenameAction::no_op_same_name
: DocumentLayerRenameAction::rename_and_record_undo;
return pp::foundation::Result<DocumentLayerRenamePlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_add(
int layer_count,
int insert_index,
std::string_view name)
{
const auto index_status = validate_layer_insert_index(layer_count, insert_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
const auto rename = plan_document_layer_rename({}, name);
if (!rename) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(rename.status());
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::add;
plan.insert_index = insert_index;
plan.name = std::string(name);
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.reloads_animation_layers = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_duplicate(
int layer_count,
int source_index)
{
const auto index_status = validate_layer_index(layer_count, source_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::duplicate;
plan.source_index = source_index;
plan.insert_index = source_index + 1;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.reloads_animation_layers = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_select(
int layer_count,
int index)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::select;
plan.index = index;
plan.reloads_animation_layers = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_reorder(
int layer_count,
int from_index,
int to_index)
{
auto index_status = validate_layer_index(layer_count, from_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
index_status = validate_layer_index(layer_count, to_index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::reorder;
plan.from_index = from_index;
plan.to_index = to_index;
plan.mutates_document = from_index != to_index;
plan.marks_unsaved = plan.mutates_document;
plan.reloads_animation_layers = plan.mutates_document;
plan.updates_title = plan.mutates_document;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_remove(
int layer_count,
int index)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
if (layer_count <= 1) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
pp::foundation::Status::invalid_argument("document must keep at least one layer"));
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::remove;
plan.index = index;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.reloads_animation_layers = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_opacity(
int layer_count,
int index,
float opacity)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1"));
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_opacity;
plan.index = index;
plan.opacity = opacity;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_visibility(
int layer_count,
int index,
bool visible)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_visibility;
plan.index = index;
plan.flag = visible;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.reloads_animation_layers = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_alpha_lock(
int layer_count,
int index,
bool locked)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_alpha_lock;
plan.index = index;
plan.flag = locked;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_blend_mode(
int layer_count,
int index,
int blend_mode)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
if (blend_mode < 0 || blend_mode >= document_layer_legacy_blend_mode_count) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(
pp::foundation::Status::out_of_range("layer blend mode is outside the supported range"));
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_blend_mode;
plan.index = index;
plan.blend_mode = blend_mode;
plan.mutates_document = true;
plan.marks_unsaved = true;
plan.updates_title = true;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerOperationPlan> plan_document_layer_highlight(
int layer_count,
int index,
bool highlight)
{
const auto index_status = validate_layer_index(layer_count, index);
if (!index_status.ok()) {
return pp::foundation::Result<DocumentLayerOperationPlan>::failure(index_status);
}
DocumentLayerOperationPlan plan;
plan.operation = DocumentLayerOperation::set_highlight;
plan.index = index;
plan.flag = highlight;
return pp::foundation::Result<DocumentLayerOperationPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<DocumentLayerMenuPlan> plan_document_layer_menu(
DocumentLayerMenuCommand command,
bool has_current_layer,
int current_index,
int animation_duration,
std::string_view current_layer_name,
std::string_view lower_layer_name)
{
if (current_index < 0) {
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
pp::foundation::Status::out_of_range("current layer index must not be negative"));
}
if (animation_duration < 0) {
return pp::foundation::Result<DocumentLayerMenuPlan>::failure(
pp::foundation::Status::out_of_range("animation duration must not be negative"));
}
DocumentLayerMenuPlan plan;
plan.command = command;
plan.from_index = current_index;
plan.to_index = current_index > 0 ? current_index - 1 : 0;
switch (command) {
case DocumentLayerMenuCommand::clear:
plan.action = has_current_layer
? DocumentLayerMenuAction::clear_current_layer
: DocumentLayerMenuAction::no_op_select_layer;
plan.label = has_current_layer
? "Clear Layer " + std::string(current_layer_name)
: "Clear Layer (Select a layer)";
break;
case DocumentLayerMenuCommand::rename:
plan.action = has_current_layer
? DocumentLayerMenuAction::show_rename_dialog
: DocumentLayerMenuAction::no_op_select_layer;
plan.label = has_current_layer
? "Rename Layer " + std::string(current_layer_name)
: "Rename Layer (Select a layer)";
break;
case DocumentLayerMenuCommand::merge_down:
if (!has_current_layer) {
plan.action = DocumentLayerMenuAction::no_op_select_layer;
plan.label = "Merge Layer (Select a layer)";
} else if (animation_duration > 1) {
plan.action = DocumentLayerMenuAction::show_merge_animated_not_supported;
plan.label = "Merge Layer (Animation not supported)";
} else if (current_index <= 0) {
plan.action = DocumentLayerMenuAction::no_op_select_upper_layer;
plan.label = "Merge Layer (Select upper layers)";
} else {
plan.action = DocumentLayerMenuAction::merge_with_lower_layer;
plan.label = "Merge with " + std::string(lower_layer_name);
}
break;
}
return pp::foundation::Result<DocumentLayerMenuPlan>::success(std::move(plan));
}
[[nodiscard]] inline pp::foundation::Status execute_document_layer_menu_plan(
const DocumentLayerMenuPlan& plan,
DocumentLayerMenuServices& services)
{
switch (plan.action) {
case DocumentLayerMenuAction::clear_current_layer:
services.clear_current_layer();
return pp::foundation::Status::success();
case DocumentLayerMenuAction::show_rename_dialog:
services.show_rename_dialog();
return pp::foundation::Status::success();
case DocumentLayerMenuAction::merge_with_lower_layer:
services.merge_with_lower_layer(plan.from_index, plan.to_index);
return pp::foundation::Status::success();
case DocumentLayerMenuAction::show_merge_animated_not_supported:
services.show_merge_animated_not_supported();
return pp::foundation::Status::success();
case DocumentLayerMenuAction::no_op_select_layer:
case DocumentLayerMenuAction::no_op_select_upper_layer:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown document layer menu action");
}
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include <string_view>
namespace pp::app {
enum class PickedPathAction {
ignore_empty_path,
invoke_callback,
};
enum class DisplayFileAction {
ignore_empty_path,
open_external_file,
};
enum class VirtualKeyboardAction {
show_keyboard,
hide_keyboard,
};
enum class CursorVisibilityAction {
show_cursor,
hide_cursor,
};
enum class ClipboardReadAction {
read_text,
};
enum class ClipboardWriteAction {
write_text,
};
[[nodiscard]] constexpr PickedPathAction plan_picked_path(std::string_view path) noexcept
{
return path.empty()
? PickedPathAction::ignore_empty_path
: PickedPathAction::invoke_callback;
}
[[nodiscard]] constexpr DisplayFileAction plan_display_file(std::string_view path) noexcept
{
return path.empty()
? DisplayFileAction::ignore_empty_path
: DisplayFileAction::open_external_file;
}
[[nodiscard]] constexpr VirtualKeyboardAction plan_virtual_keyboard(bool visible) noexcept
{
return visible
? VirtualKeyboardAction::show_keyboard
: VirtualKeyboardAction::hide_keyboard;
}
[[nodiscard]] constexpr CursorVisibilityAction plan_cursor_visibility(bool visible) noexcept
{
return visible
? CursorVisibilityAction::show_cursor
: CursorVisibilityAction::hide_cursor;
}
[[nodiscard]] constexpr ClipboardReadAction plan_clipboard_read() noexcept
{
return ClipboardReadAction::read_text;
}
[[nodiscard]] constexpr ClipboardWriteAction plan_clipboard_write(std::string_view) noexcept
{
return ClipboardWriteAction::write_text;
}
}

View File

@@ -0,0 +1,63 @@
#pragma once
#include <cstddef>
#include <limits>
namespace pp::app {
enum class RecordingStartAction {
start_thread,
no_op_already_running,
};
enum class RecordingStopAction {
stop_thread,
no_op_not_running,
};
struct RecordingClearPlan {
bool stop_running_recording = false;
bool delete_recorded_files = false;
int frame_count_after_clear = 0;
};
struct RecordingExportPlan {
std::size_t frame_count = 0;
int progress_total = 0;
};
[[nodiscard]] constexpr RecordingStartAction plan_recording_start(bool is_running) noexcept
{
return is_running
? RecordingStartAction::no_op_already_running
: RecordingStartAction::start_thread;
}
[[nodiscard]] constexpr RecordingStopAction plan_recording_stop(bool is_running) noexcept
{
return is_running
? RecordingStopAction::stop_thread
: RecordingStopAction::no_op_not_running;
}
[[nodiscard]] constexpr RecordingClearPlan plan_recording_clear(
bool is_running,
bool platform_deletes_recorded_files) noexcept
{
return {
is_running,
platform_deletes_recorded_files,
0,
};
}
[[nodiscard]] constexpr RecordingExportPlan plan_recording_export(std::size_t frame_count) noexcept
{
const auto max_progress_total = static_cast<std::size_t>(std::numeric_limits<int>::max());
return {
frame_count,
frame_count > max_progress_total ? std::numeric_limits<int>::max() : static_cast<int>(frame_count),
};
}
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include "app_core/app_status.h"
#include "foundation/result.h"
#include <string>
#include <string_view>
namespace pp::app {
struct DocumentResizeDialogState {
int current_resolution = 0;
std::string current_resolution_text;
int current_resolution_index = 0;
};
struct DocumentResizePlan {
int resolution = 0;
int width = 0;
int height = 0;
bool clears_history = false;
};
class DocumentResizeServices {
public:
virtual ~DocumentResizeServices() = default;
virtual void resize_document(int width, int height) = 0;
virtual void update_title() = 0;
virtual void clear_history() = 0;
};
[[nodiscard]] inline DocumentResizeDialogState make_document_resize_dialog_state(
int current_resolution)
{
const auto label = document_resolution_label(current_resolution);
const auto index = document_resolution_to_index(current_resolution);
std::string text = "Current: ";
text.append(label ? std::string_view(label.value()) : std::string_view("unknown"));
return {
current_resolution,
text,
index ? static_cast<int>(index.value()) : static_cast<int>(document_resolution_values.size()),
};
}
[[nodiscard]] inline pp::foundation::Result<DocumentResizePlan> plan_document_resize(
int selected_resolution_index)
{
const auto resolution = display_resolution_from_index(selected_resolution_index);
if (!resolution) {
return pp::foundation::Result<DocumentResizePlan>::failure(resolution.status());
}
const auto value = resolution.value();
return pp::foundation::Result<DocumentResizePlan>::success(
DocumentResizePlan {
value,
value,
value,
true,
});
}
[[nodiscard]] inline pp::foundation::Status execute_document_resize_plan(
const DocumentResizePlan& plan,
DocumentResizeServices& services)
{
if (plan.width <= 0 || plan.height <= 0) {
return pp::foundation::Status::out_of_range("resize dimensions must be positive");
}
services.resize_document(plan.width, plan.height);
services.update_title();
if (plan.clears_history) {
services.clear_history();
}
return pp::foundation::Status::success();
}
}

View File

@@ -0,0 +1,67 @@
#include "app_core/document_route.h"
#include <cctype>
#include <utility>
namespace pp::app {
namespace {
[[nodiscard]] bool is_extension_char(char value) noexcept
{
const auto ch = static_cast<unsigned char>(value);
return std::isalnum(ch) != 0 || value == '_';
}
[[nodiscard]] std::string lowercase_ascii(std::string_view value)
{
std::string lowered;
lowered.reserve(value.size());
for (const char ch : value) {
lowered.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
}
return lowered;
}
}
pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(std::string_view path)
{
const auto separator = path.find_last_of("/\\");
if (separator == std::string_view::npos || separator + 1U >= path.size()) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document path must include a directory and file name"));
}
const auto dot = path.find_last_of('.');
if (dot == std::string_view::npos || dot <= separator + 1U || dot + 1U >= path.size()) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document path must include a file extension"));
}
const std::string_view extension = path.substr(dot + 1U);
for (const char ch : extension) {
if (!is_extension_char(ch)) {
return pp::foundation::Result<DocumentOpenRoute>::failure(
pp::foundation::Status::invalid_argument("document extension contains unsupported characters"));
}
}
auto lowered_extension = lowercase_ascii(extension);
auto kind = DocumentOpenKind::open_project;
if (lowered_extension == "abr") {
kind = DocumentOpenKind::import_abr;
} else if (lowered_extension == "ppbr") {
kind = DocumentOpenKind::import_ppbr;
}
return pp::foundation::Result<DocumentOpenRoute>::success(
DocumentOpenRoute {
.kind = kind,
.path = std::string(path),
.directory = std::string(path.substr(0U, separator)),
.name = std::string(path.substr(separator + 1U, dot - separator - 1U)),
.extension = std::move(lowered_extension),
});
}
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
namespace pp::app {
enum class DocumentOpenKind {
import_abr,
import_ppbr,
open_project,
};
struct DocumentOpenRoute {
DocumentOpenKind kind = DocumentOpenKind::open_project;
std::string path;
std::string directory;
std::string name;
std::string extension;
};
[[nodiscard]] pp::foundation::Result<DocumentOpenRoute> classify_document_open_path(
std::string_view path);
}

View File

@@ -0,0 +1 @@
#include "app_core/document_session.h"

View File

@@ -0,0 +1,323 @@
#pragma once
#include "app_core/document_route.h"
#include "foundation/result.h"
#include <array>
#include <cctype>
#include <cstdio>
#include <string>
#include <string_view>
#include <utility>
namespace pp::app {
enum class ProjectOpenDecision {
open_now,
prompt_discard_unsaved,
};
enum class CloseRequestDecision {
close_now,
show_unsaved_prompt,
wait_for_existing_prompt,
};
enum class DocumentSaveIntent {
save,
save_as,
save_version,
save_dirty_version,
};
enum class DocumentSaveDecision {
no_op,
show_save_dialog,
save_existing,
save_version,
};
enum class DocumentWorkflowDecision {
unavailable,
continue_now,
prompt_save_before_continue,
};
enum class DocumentFileWriteDecision {
save_now,
prompt_overwrite,
};
enum class DocumentOpenPlanAction {
open_project_now,
prompt_discard_unsaved_project,
prompt_import_abr,
prompt_import_ppbr,
};
struct DocumentFileTarget {
std::string name;
std::string directory;
std::string path;
};
struct DocumentVersionTarget {
std::string name;
std::string path;
};
struct DocumentFileSavePlan {
DocumentFileTarget target;
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
};
struct NewDocumentPlan {
DocumentFileTarget target;
int resolution = 0;
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
};
[[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept
{
return has_unsaved_changes
? ProjectOpenDecision::prompt_discard_unsaved
: ProjectOpenDecision::open_now;
}
[[nodiscard]] constexpr DocumentOpenPlanAction plan_document_open(
DocumentOpenKind kind,
bool has_unsaved_changes) noexcept
{
switch (kind) {
case DocumentOpenKind::import_abr:
return DocumentOpenPlanAction::prompt_import_abr;
case DocumentOpenKind::import_ppbr:
return DocumentOpenPlanAction::prompt_import_ppbr;
case DocumentOpenKind::open_project:
return has_unsaved_changes
? DocumentOpenPlanAction::prompt_discard_unsaved_project
: DocumentOpenPlanAction::open_project_now;
}
return DocumentOpenPlanAction::open_project_now;
}
[[nodiscard]] constexpr CloseRequestDecision plan_close_request(
bool has_unsaved_changes,
bool close_prompt_already_open) noexcept
{
if (!has_unsaved_changes) {
return CloseRequestDecision::close_now;
}
return close_prompt_already_open
? CloseRequestDecision::wait_for_existing_prompt
: CloseRequestDecision::show_unsaved_prompt;
}
[[nodiscard]] constexpr DocumentSaveDecision plan_document_save(
bool is_new_document,
bool has_unsaved_changes,
DocumentSaveIntent intent) noexcept
{
switch (intent) {
case DocumentSaveIntent::save:
if (is_new_document) {
return DocumentSaveDecision::show_save_dialog;
}
return has_unsaved_changes
? DocumentSaveDecision::save_existing
: DocumentSaveDecision::no_op;
case DocumentSaveIntent::save_as:
return DocumentSaveDecision::show_save_dialog;
case DocumentSaveIntent::save_version:
return is_new_document
? DocumentSaveDecision::show_save_dialog
: DocumentSaveDecision::save_version;
case DocumentSaveIntent::save_dirty_version:
if (is_new_document) {
return DocumentSaveDecision::show_save_dialog;
}
return has_unsaved_changes
? DocumentSaveDecision::save_version
: DocumentSaveDecision::no_op;
}
return DocumentSaveDecision::no_op;
}
[[nodiscard]] constexpr DocumentWorkflowDecision plan_document_workflow(
bool has_canvas,
bool has_unsaved_changes) noexcept
{
if (!has_canvas) {
return DocumentWorkflowDecision::unavailable;
}
return has_unsaved_changes
? DocumentWorkflowDecision::prompt_save_before_continue
: DocumentWorkflowDecision::continue_now;
}
[[nodiscard]] inline pp::foundation::Result<DocumentFileTarget> make_document_file_target(
std::string_view work_directory,
std::string_view document_name)
{
if (document_name.empty()) {
return pp::foundation::Result<DocumentFileTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
DocumentFileTarget target;
target.name = std::string(document_name);
target.directory = std::string(work_directory);
target.path.reserve(target.directory.size() + target.name.size() + 5);
target.path += target.directory;
target.path += "/";
target.path += target.name;
target.path += ".ppi";
return pp::foundation::Result<DocumentFileTarget>::success(std::move(target));
}
[[nodiscard]] constexpr DocumentFileWriteDecision plan_document_file_write(
bool target_exists) noexcept
{
return target_exists
? DocumentFileWriteDecision::prompt_overwrite
: DocumentFileWriteDecision::save_now;
}
template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<DocumentFileSavePlan> plan_document_file_save(
std::string_view work_directory,
std::string_view document_name,
ExistsPredicate&& exists)
{
auto target = make_document_file_target(work_directory, document_name);
if (!target) {
return pp::foundation::Result<DocumentFileSavePlan>::failure(target.status());
}
DocumentFileSavePlan plan;
plan.target = std::move(target.value());
plan.write_decision = plan_document_file_write(exists(plan.target.path));
return pp::foundation::Result<DocumentFileSavePlan>::success(std::move(plan));
}
[[nodiscard]] constexpr pp::foundation::Result<int> document_resolution_from_index(int index) noexcept
{
constexpr std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
if (index < 0 || static_cast<std::size_t>(index) >= resolutions.size()) {
return pp::foundation::Result<int>::failure(
pp::foundation::Status::out_of_range("document resolution index is out of range"));
}
return pp::foundation::Result<int>::success(resolutions[static_cast<std::size_t>(index)]);
}
template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<NewDocumentPlan> plan_new_document(
std::string_view work_directory,
std::string_view document_name,
int resolution_index,
ExistsPredicate&& exists)
{
const auto resolution = document_resolution_from_index(resolution_index);
if (!resolution) {
return pp::foundation::Result<NewDocumentPlan>::failure(resolution.status());
}
auto save_plan = plan_document_file_save(
work_directory,
document_name,
std::forward<ExistsPredicate>(exists));
if (!save_plan) {
return pp::foundation::Result<NewDocumentPlan>::failure(save_plan.status());
}
NewDocumentPlan plan;
plan.target = std::move(save_plan.value().target);
plan.resolution = resolution.value();
plan.write_decision = save_plan.value().write_decision;
return pp::foundation::Result<NewDocumentPlan>::success(std::move(plan));
}
[[nodiscard]] inline bool has_legacy_two_character_version_suffix(std::string_view document_name) noexcept
{
const auto dot = document_name.rfind('.');
if (dot == std::string_view::npos || dot + 3 != document_name.size()) {
return false;
}
const auto is_word = [](char ch) noexcept {
return std::isalnum(static_cast<unsigned char>(ch)) != 0 || ch == '_';
};
return is_word(document_name[dot + 1]) && is_word(document_name[dot + 2]);
}
[[nodiscard]] inline int legacy_version_number(std::string_view suffix) noexcept
{
int value = 0;
for (const char ch : suffix) {
if (ch < '0' || ch > '9') {
break;
}
value = value * 10 + (ch - '0');
}
return value;
}
[[nodiscard]] inline std::string make_legacy_version_name(std::string_view base_name, int version)
{
char suffix[4] {};
std::snprintf(suffix, sizeof(suffix), ".%02d", version);
std::string name;
name.reserve(base_name.size() + 3);
name += base_name;
name += suffix;
return name;
}
template <typename ExistsPredicate>
[[nodiscard]] pp::foundation::Result<DocumentVersionTarget> find_next_document_version_target(
std::string_view directory,
std::string_view document_name,
ExistsPredicate&& exists)
{
if (directory.empty()) {
return pp::foundation::Result<DocumentVersionTarget>::failure(
pp::foundation::Status::invalid_argument("directory must not be empty"));
}
if (document_name.empty()) {
return pp::foundation::Result<DocumentVersionTarget>::failure(
pp::foundation::Status::invalid_argument("document name must not be empty"));
}
int current = 0;
std::string_view base = document_name;
if (has_legacy_two_character_version_suffix(document_name)) {
const auto dot = document_name.rfind('.');
base = document_name.substr(0, dot);
current = legacy_version_number(document_name.substr(dot + 1));
}
for (int version = current + 1; version < 99; ++version) {
DocumentVersionTarget target;
target.name = make_legacy_version_name(base, version);
target.path.reserve(directory.size() + target.name.size() + 5);
target.path += directory;
target.path += "/";
target.path += target.name;
target.path += ".ppi";
if (!exists(target.path)) {
return pp::foundation::Result<DocumentVersionTarget>::success(std::move(target));
}
}
return pp::foundation::Result<DocumentVersionTarget>::failure(
pp::foundation::Status::out_of_range("no available document version target"));
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <string_view>
namespace pp::app {
enum class DocumentShareAction {
show_save_required_warning,
share_now,
};
[[nodiscard]] constexpr DocumentShareAction plan_document_share(std::string_view path) noexcept
{
return path.empty()
? DocumentShareAction::show_save_required_warning
: DocumentShareAction::share_now;
}
}

209
src/app_core/file_menu.h Normal file
View File

@@ -0,0 +1,209 @@
#pragma once
#include "app_core/document_export.h"
#include "app_core/document_session.h"
#include "foundation/result.h"
#include <string_view>
namespace pp::app {
enum class FileMenuCommand {
new_document,
import_image,
open_project,
browse_cloud,
save,
save_as,
save_version,
export_jpeg,
export_submenu,
share,
resize,
cloud_upload,
cloud_browse,
};
enum class FileMenuAction {
show_new_document_dialog,
pick_image_for_import,
pick_project_file,
show_cloud_browser_dialog,
save_document,
show_export_jpeg_dialog,
show_export_submenu,
share_document,
show_resize_dialog,
upload_to_cloud,
browse_cloud_documents,
};
struct FileMenuPlan {
FileMenuCommand command = FileMenuCommand::new_document;
FileMenuAction action = FileMenuAction::show_new_document_dialog;
DocumentSaveIntent save_intent = DocumentSaveIntent::save;
DocumentExportMenuKind export_kind = DocumentExportMenuKind::jpeg;
};
class FileMenuServices {
public:
virtual ~FileMenuServices() = default;
virtual void show_new_document_dialog() = 0;
virtual void pick_image_for_import() = 0;
virtual void pick_project_file() = 0;
virtual void show_cloud_browser_dialog() = 0;
virtual void save_document(DocumentSaveIntent intent) = 0;
virtual void show_export_jpeg_dialog(DocumentExportMenuKind kind) = 0;
virtual void show_export_submenu() = 0;
virtual void share_document() = 0;
virtual void show_resize_dialog() = 0;
virtual void upload_to_cloud() = 0;
virtual void browse_cloud_documents() = 0;
};
[[nodiscard]] constexpr FileMenuPlan plan_file_menu_command(FileMenuCommand command) noexcept
{
FileMenuPlan plan;
plan.command = command;
switch (command) {
case FileMenuCommand::new_document:
plan.action = FileMenuAction::show_new_document_dialog;
break;
case FileMenuCommand::import_image:
plan.action = FileMenuAction::pick_image_for_import;
break;
case FileMenuCommand::open_project:
plan.action = FileMenuAction::pick_project_file;
break;
case FileMenuCommand::browse_cloud:
plan.action = FileMenuAction::show_cloud_browser_dialog;
break;
case FileMenuCommand::save:
plan.action = FileMenuAction::save_document;
plan.save_intent = DocumentSaveIntent::save;
break;
case FileMenuCommand::save_as:
plan.action = FileMenuAction::save_document;
plan.save_intent = DocumentSaveIntent::save_as;
break;
case FileMenuCommand::save_version:
plan.action = FileMenuAction::save_document;
plan.save_intent = DocumentSaveIntent::save_version;
break;
case FileMenuCommand::export_jpeg:
plan.action = FileMenuAction::show_export_jpeg_dialog;
plan.export_kind = DocumentExportMenuKind::jpeg;
break;
case FileMenuCommand::export_submenu:
plan.action = FileMenuAction::show_export_submenu;
break;
case FileMenuCommand::share:
plan.action = FileMenuAction::share_document;
break;
case FileMenuCommand::resize:
plan.action = FileMenuAction::show_resize_dialog;
break;
case FileMenuCommand::cloud_upload:
plan.action = FileMenuAction::upload_to_cloud;
break;
case FileMenuCommand::cloud_browse:
plan.action = FileMenuAction::browse_cloud_documents;
break;
}
return plan;
}
[[nodiscard]] inline pp::foundation::Result<FileMenuCommand> parse_file_menu_command(
std::string_view command) noexcept
{
if (command == "new" || command == "new-document") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::new_document);
}
if (command == "import" || command == "import-image") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::import_image);
}
if (command == "open" || command == "open-project") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::open_project);
}
if (command == "browse") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::browse_cloud);
}
if (command == "save") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save);
}
if (command == "save-as") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_as);
}
if (command == "save-version") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::save_version);
}
if (command == "export" || command == "export-jpeg") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_jpeg);
}
if (command == "export-submenu") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::export_submenu);
}
if (command == "share") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::share);
}
if (command == "resize") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::resize);
}
if (command == "cloud-upload") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_upload);
}
if (command == "cloud-browse") {
return pp::foundation::Result<FileMenuCommand>::success(FileMenuCommand::cloud_browse);
}
return pp::foundation::Result<FileMenuCommand>::failure(
pp::foundation::Status::invalid_argument("unknown file menu command"));
}
[[nodiscard]] inline pp::foundation::Status execute_file_menu_plan(
const FileMenuPlan& plan,
FileMenuServices& services)
{
switch (plan.action) {
case FileMenuAction::show_new_document_dialog:
services.show_new_document_dialog();
return pp::foundation::Status::success();
case FileMenuAction::pick_image_for_import:
services.pick_image_for_import();
return pp::foundation::Status::success();
case FileMenuAction::pick_project_file:
services.pick_project_file();
return pp::foundation::Status::success();
case FileMenuAction::show_cloud_browser_dialog:
services.show_cloud_browser_dialog();
return pp::foundation::Status::success();
case FileMenuAction::save_document:
services.save_document(plan.save_intent);
return pp::foundation::Status::success();
case FileMenuAction::show_export_jpeg_dialog:
services.show_export_jpeg_dialog(plan.export_kind);
return pp::foundation::Status::success();
case FileMenuAction::show_export_submenu:
services.show_export_submenu();
return pp::foundation::Status::success();
case FileMenuAction::share_document:
services.share_document();
return pp::foundation::Status::success();
case FileMenuAction::show_resize_dialog:
services.show_resize_dialog();
return pp::foundation::Status::success();
case FileMenuAction::upload_to_cloud:
services.upload_to_cloud();
return pp::foundation::Status::success();
case FileMenuAction::browse_cloud_documents:
services.browse_cloud_documents();
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown file menu action");
}
} // namespace pp::app

145
src/app_core/grid_ui.h Normal file
View File

@@ -0,0 +1,145 @@
#pragma once
#include "foundation/result.h"
#include <string>
#include <string_view>
#include <utility>
namespace pp::app {
enum class GridUiOperation {
request_heightmap_pick,
load_heightmap,
clear_heightmap,
reload_heightmap,
render_lightmap,
commit_heightmap,
};
struct GridUiPlan {
GridUiOperation operation = GridUiOperation::request_heightmap_pick;
std::string path;
int texture_resolution = 0;
int sample_count = 0;
bool opens_picker = false;
bool loads_heightmap = false;
bool clears_heightmap = false;
bool renders_lightmap = false;
bool commits_heightmap = false;
bool updates_preview = false;
bool updates_ground_opacity = false;
bool updates_shading_mode = false;
bool shows_unsupported_message = false;
bool shows_progress = false;
bool mutates_grid_state = false;
};
[[nodiscard]] inline pp::foundation::Status validate_grid_texture_resolution(int texture_resolution) noexcept
{
if (texture_resolution <= 0 || texture_resolution > 16384) {
return pp::foundation::Status::out_of_range("grid texture resolution must be within 1..16384");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_grid_lightmap_samples(int sample_count) noexcept
{
if (sample_count <= 0 || sample_count > 4096) {
return pp::foundation::Status::out_of_range("grid lightmap samples must be within 1..4096");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_pick() noexcept
{
GridUiPlan plan;
plan.operation = GridUiOperation::request_heightmap_pick;
plan.opens_picker = true;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_load(std::string_view path)
{
if (path.empty()) {
return pp::foundation::Result<GridUiPlan>::failure(
pp::foundation::Status::invalid_argument("heightmap path must not be empty"));
}
GridUiPlan plan;
plan.operation = GridUiOperation::load_heightmap;
plan.path = std::string(path);
plan.loads_heightmap = true;
plan.updates_preview = true;
plan.updates_ground_opacity = true;
plan.mutates_grid_state = true;
return pp::foundation::Result<GridUiPlan>::success(std::move(plan));
}
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_clear(bool has_heightmap) noexcept
{
GridUiPlan plan;
plan.operation = GridUiOperation::clear_heightmap;
plan.clears_heightmap = true;
plan.updates_preview = has_heightmap;
plan.mutates_grid_state = has_heightmap;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_heightmap_reload(std::string_view path)
{
auto plan = plan_grid_heightmap_load(path);
if (!plan) {
return pp::foundation::Result<GridUiPlan>::failure(plan.status());
}
plan.value().operation = GridUiOperation::reload_heightmap;
plan.value().updates_ground_opacity = false;
return plan;
}
[[nodiscard]] inline pp::foundation::Result<GridUiPlan> plan_grid_lightmap_render(
bool has_heightmap,
bool supports_float32,
bool supports_float16,
int texture_resolution,
int sample_count)
{
const auto texture_status = validate_grid_texture_resolution(texture_resolution);
if (!texture_status.ok()) {
return pp::foundation::Result<GridUiPlan>::failure(texture_status);
}
const auto sample_status = validate_grid_lightmap_samples(sample_count);
if (!sample_status.ok()) {
return pp::foundation::Result<GridUiPlan>::failure(sample_status);
}
GridUiPlan plan;
plan.operation = GridUiOperation::render_lightmap;
plan.texture_resolution = texture_resolution;
plan.sample_count = sample_count;
if (!supports_float32 && !supports_float16) {
plan.shows_unsupported_message = true;
return pp::foundation::Result<GridUiPlan>::success(plan);
}
plan.renders_lightmap = has_heightmap;
plan.shows_progress = has_heightmap;
plan.updates_shading_mode = has_heightmap;
plan.mutates_grid_state = has_heightmap;
return pp::foundation::Result<GridUiPlan>::success(plan);
}
[[nodiscard]] inline constexpr GridUiPlan plan_grid_heightmap_commit(bool has_canvas) noexcept
{
GridUiPlan plan;
plan.operation = GridUiOperation::commit_heightmap;
plan.commits_heightmap = has_canvas;
plan.updates_ground_opacity = has_canvas;
plan.mutates_grid_state = has_canvas;
return plan;
}
} // namespace pp::app

161
src/app_core/history_ui.h Normal file
View File

@@ -0,0 +1,161 @@
#pragma once
#include "foundation/result.h"
namespace pp::app {
enum class HistoryUiOperation {
undo,
redo,
clear,
};
struct HistoryUiPlan {
HistoryUiOperation operation = HistoryUiOperation::undo;
int undo_count = 0;
int redo_count = 0;
int memory_bytes = 0;
bool invokes_undo = false;
bool invokes_redo = false;
bool clears_history = false;
bool updates_memory_label = false;
bool updates_title = false;
bool no_op = false;
};
class HistoryUiServices {
public:
virtual ~HistoryUiServices() = default;
virtual void invoke_undo() = 0;
virtual void invoke_redo() = 0;
virtual void clear_history() = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_history_metric(int value, const char* message) noexcept
{
if (value < 0) {
return pp::foundation::Status::out_of_range(message);
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_undo(int undo_count)
{
const auto count_status = validate_history_metric(undo_count, "undo action count must not be negative");
if (!count_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::undo;
plan.undo_count = undo_count;
if (undo_count == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.invokes_undo = true;
plan.updates_memory_label = true;
plan.updates_title = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_redo(int redo_count)
{
const auto count_status = validate_history_metric(redo_count, "redo action count must not be negative");
if (!count_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(count_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::redo;
plan.redo_count = redo_count;
if (redo_count == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.invokes_redo = true;
plan.updates_memory_label = true;
plan.updates_title = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<HistoryUiPlan> plan_history_clear(
int undo_count,
int redo_count,
int memory_bytes)
{
const auto undo_status = validate_history_metric(undo_count, "undo action count must not be negative");
if (!undo_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(undo_status);
}
const auto redo_status = validate_history_metric(redo_count, "redo action count must not be negative");
if (!redo_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(redo_status);
}
const auto memory_status = validate_history_metric(memory_bytes, "history memory bytes must not be negative");
if (!memory_status.ok()) {
return pp::foundation::Result<HistoryUiPlan>::failure(memory_status);
}
HistoryUiPlan plan;
plan.operation = HistoryUiOperation::clear;
plan.undo_count = undo_count;
plan.redo_count = redo_count;
plan.memory_bytes = memory_bytes;
if (undo_count == 0 && redo_count == 0 && memory_bytes == 0) {
plan.no_op = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
plan.clears_history = true;
plan.updates_memory_label = true;
return pp::foundation::Result<HistoryUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_history_ui_plan(
const HistoryUiPlan& plan,
HistoryUiServices& services)
{
const auto undo_status = validate_history_metric(plan.undo_count, "undo action count must not be negative");
if (!undo_status.ok()) {
return undo_status;
}
const auto redo_status = validate_history_metric(plan.redo_count, "redo action count must not be negative");
if (!redo_status.ok()) {
return redo_status;
}
const auto memory_status = validate_history_metric(plan.memory_bytes, "history memory bytes must not be negative");
if (!memory_status.ok()) {
return memory_status;
}
if (plan.no_op) {
return pp::foundation::Status::success();
}
switch (plan.operation) {
case HistoryUiOperation::undo:
if (plan.invokes_undo) {
services.invoke_undo();
}
return pp::foundation::Status::success();
case HistoryUiOperation::redo:
if (plan.invokes_redo) {
services.invoke_redo();
}
return pp::foundation::Status::success();
case HistoryUiOperation::clear:
if (plan.clears_history) {
services.clear_history();
}
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown history operation");
}
} // namespace pp::app

202
src/app_core/main_toolbar.h Normal file
View File

@@ -0,0 +1,202 @@
#pragma once
#include "app_core/document_canvas.h"
#include "app_core/history_ui.h"
#include "foundation/result.h"
#include <string>
namespace pp::app {
enum class MainToolbarCommand {
open_document,
save_document,
undo,
redo,
clear_history,
clear_canvas,
show_message_box,
show_settings,
};
enum class MainToolbarAction {
show_open_dialog,
show_save_dialog,
invoke_undo,
invoke_redo,
clear_history,
clear_canvas,
show_message_box,
show_settings_dialog,
no_op_unavailable,
};
struct MainToolbarPlan {
MainToolbarCommand command = MainToolbarCommand::open_document;
MainToolbarAction action = MainToolbarAction::show_open_dialog;
std::string label;
bool requires_canvas = false;
bool updates_memory_label = false;
bool updates_title = false;
bool records_undo = false;
bool marks_unsaved = false;
bool no_op = false;
HistoryUiPlan history;
DocumentCanvasClearPlan canvas_clear;
};
class MainToolbarServices {
public:
virtual ~MainToolbarServices() = default;
virtual void show_open_dialog() = 0;
virtual void show_save_dialog() = 0;
virtual void invoke_undo(const HistoryUiPlan& plan) = 0;
virtual void invoke_redo(const HistoryUiPlan& plan) = 0;
virtual void clear_history(const HistoryUiPlan& plan) = 0;
virtual void clear_canvas(const DocumentCanvasClearPlan& plan) = 0;
virtual void show_message_box() = 0;
virtual void show_settings_dialog() = 0;
};
[[nodiscard]] inline pp::foundation::Result<MainToolbarPlan> plan_main_toolbar_command(
MainToolbarCommand command,
int undo_count = 0,
int redo_count = 0,
int memory_bytes = 0,
bool has_canvas = true)
{
MainToolbarPlan plan;
plan.command = command;
switch (command) {
case MainToolbarCommand::open_document:
plan.action = MainToolbarAction::show_open_dialog;
plan.label = "Open";
return pp::foundation::Result<MainToolbarPlan>::success(plan);
case MainToolbarCommand::save_document:
plan.action = MainToolbarAction::show_save_dialog;
plan.label = "Save";
return pp::foundation::Result<MainToolbarPlan>::success(plan);
case MainToolbarCommand::undo:
{
const auto history = plan_history_undo(undo_count);
if (!history) {
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
}
plan.action = history.value().invokes_undo
? MainToolbarAction::invoke_undo
: MainToolbarAction::no_op_unavailable;
plan.label = history.value().invokes_undo ? "Undo" : "Undo (No history)";
plan.updates_memory_label = history.value().updates_memory_label;
plan.updates_title = history.value().updates_title;
plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
case MainToolbarCommand::redo:
{
const auto history = plan_history_redo(redo_count);
if (!history) {
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
}
plan.action = history.value().invokes_redo
? MainToolbarAction::invoke_redo
: MainToolbarAction::no_op_unavailable;
plan.label = history.value().invokes_redo ? "Redo" : "Redo (No history)";
plan.updates_memory_label = history.value().updates_memory_label;
plan.updates_title = history.value().updates_title;
plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
case MainToolbarCommand::clear_history:
{
const auto history = plan_history_clear(undo_count, redo_count, memory_bytes);
if (!history) {
return pp::foundation::Result<MainToolbarPlan>::failure(history.status());
}
plan.action = history.value().clears_history
? MainToolbarAction::clear_history
: MainToolbarAction::no_op_unavailable;
plan.label = history.value().clears_history ? "Clear History" : "Clear History (Empty)";
plan.updates_memory_label = history.value().updates_memory_label;
plan.no_op = history.value().no_op;
plan.history = history.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
case MainToolbarCommand::clear_canvas:
{
const auto clear = plan_document_canvas_clear(has_canvas);
if (!clear) {
return pp::foundation::Result<MainToolbarPlan>::failure(clear.status());
}
plan.action = clear.value().clears_canvas
? MainToolbarAction::clear_canvas
: MainToolbarAction::no_op_unavailable;
plan.label = clear.value().clears_canvas ? "Clear Canvas" : "Clear Canvas (No canvas)";
plan.requires_canvas = true;
plan.records_undo = clear.value().records_undo;
plan.marks_unsaved = clear.value().marks_unsaved;
plan.no_op = clear.value().no_op;
plan.canvas_clear = clear.value();
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
case MainToolbarCommand::show_message_box:
plan.action = MainToolbarAction::show_message_box;
plan.label = "Show Message Box";
return pp::foundation::Result<MainToolbarPlan>::success(plan);
case MainToolbarCommand::show_settings:
plan.action = MainToolbarAction::show_settings_dialog;
plan.label = "Settings";
return pp::foundation::Result<MainToolbarPlan>::success(plan);
}
return pp::foundation::Result<MainToolbarPlan>::failure(
pp::foundation::Status::invalid_argument("unknown main toolbar command"));
}
[[nodiscard]] inline pp::foundation::Status execute_main_toolbar_plan(
const MainToolbarPlan& plan,
MainToolbarServices& services)
{
switch (plan.action) {
case MainToolbarAction::show_open_dialog:
services.show_open_dialog();
return pp::foundation::Status::success();
case MainToolbarAction::show_save_dialog:
services.show_save_dialog();
return pp::foundation::Status::success();
case MainToolbarAction::invoke_undo:
services.invoke_undo(plan.history);
return pp::foundation::Status::success();
case MainToolbarAction::invoke_redo:
services.invoke_redo(plan.history);
return pp::foundation::Status::success();
case MainToolbarAction::clear_history:
services.clear_history(plan.history);
return pp::foundation::Status::success();
case MainToolbarAction::clear_canvas:
services.clear_canvas(plan.canvas_clear);
return pp::foundation::Status::success();
case MainToolbarAction::show_message_box:
services.show_message_box();
return pp::foundation::Status::success();
case MainToolbarAction::show_settings_dialog:
services.show_settings_dialog();
return pp::foundation::Status::success();
case MainToolbarAction::no_op_unavailable:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown main toolbar action");
}
} // namespace pp::app

229
src/app_core/quick_ui.h Normal file
View File

@@ -0,0 +1,229 @@
#pragma once
#include "foundation/result.h"
namespace pp::app {
enum class QuickUiSlotKind {
brush,
color,
};
enum class QuickUiOperation {
select_slot,
open_slot_popup,
restore_state,
reset_state,
};
struct QuickUiPlan {
QuickUiOperation operation = QuickUiOperation::select_slot;
QuickUiSlotKind slot_kind = QuickUiSlotKind::brush;
int slot_index = 0;
int previous_index = 0;
int brush_index = 0;
int color_index = 0;
int slot_count = 0;
bool fire_event = false;
bool updates_selection = false;
bool opens_brush_popup = false;
bool opens_color_picker = false;
bool invokes_change_callback = false;
bool restores_slots = false;
bool resets_slots = false;
bool redraws_brush_previews = false;
bool mutates_quick_state = false;
};
class QuickUiServices {
public:
virtual ~QuickUiServices() = default;
virtual void select_slot(QuickUiSlotKind slot_kind, int slot_index, bool fire_event) = 0;
virtual void open_slot_popup(QuickUiSlotKind slot_kind, int slot_index) = 0;
virtual void restore_state(int brush_index, int color_index, bool fire_event) = 0;
virtual void reset_state(bool fire_event) = 0;
};
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_count(int slot_count) noexcept
{
if (slot_count <= 0) {
return pp::foundation::Status::out_of_range("quick slot count must be greater than zero");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status validate_quick_slot_index(int slot_index, int slot_count) noexcept
{
const auto count_status = validate_quick_slot_count(slot_count);
if (!count_status.ok()) {
return count_status;
}
if (slot_index < 0 || slot_index >= slot_count) {
return pp::foundation::Status::out_of_range("quick slot index is out of range");
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_slot_click(
QuickUiSlotKind slot_kind,
int current_index,
int clicked_index,
int slot_count)
{
const auto current_status = validate_quick_slot_index(current_index, slot_count);
if (!current_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(current_status);
}
const auto clicked_status = validate_quick_slot_index(clicked_index, slot_count);
if (!clicked_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(clicked_status);
}
QuickUiPlan plan;
plan.slot_kind = slot_kind;
plan.slot_index = clicked_index;
plan.previous_index = current_index;
plan.brush_index = slot_kind == QuickUiSlotKind::brush ? clicked_index : 0;
plan.color_index = slot_kind == QuickUiSlotKind::color ? clicked_index : 0;
plan.slot_count = slot_count;
if (clicked_index != current_index) {
plan.operation = QuickUiOperation::select_slot;
plan.updates_selection = true;
plan.invokes_change_callback = true;
plan.mutates_quick_state = true;
return pp::foundation::Result<QuickUiPlan>::success(plan);
}
plan.operation = QuickUiOperation::open_slot_popup;
plan.opens_brush_popup = slot_kind == QuickUiSlotKind::brush;
plan.opens_color_picker = slot_kind == QuickUiSlotKind::color;
return pp::foundation::Result<QuickUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_restore(
int brush_index,
int color_index,
int slot_count,
bool fire_event)
{
const auto brush_status = validate_quick_slot_index(brush_index, slot_count);
if (!brush_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(brush_status);
}
const auto color_status = validate_quick_slot_index(color_index, slot_count);
if (!color_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(color_status);
}
QuickUiPlan plan;
plan.operation = QuickUiOperation::restore_state;
plan.brush_index = brush_index;
plan.color_index = color_index;
plan.slot_count = slot_count;
plan.fire_event = fire_event;
plan.updates_selection = true;
plan.invokes_change_callback = fire_event;
plan.restores_slots = true;
plan.redraws_brush_previews = true;
plan.mutates_quick_state = true;
return pp::foundation::Result<QuickUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Result<QuickUiPlan> plan_quick_state_reset(
int slot_count,
bool fire_event)
{
const auto count_status = validate_quick_slot_count(slot_count);
if (!count_status.ok()) {
return pp::foundation::Result<QuickUiPlan>::failure(count_status);
}
QuickUiPlan plan;
plan.operation = QuickUiOperation::reset_state;
plan.brush_index = 0;
plan.color_index = 0;
plan.slot_count = slot_count;
plan.fire_event = fire_event;
plan.updates_selection = true;
plan.invokes_change_callback = fire_event;
plan.resets_slots = true;
plan.redraws_brush_previews = true;
plan.mutates_quick_state = true;
return pp::foundation::Result<QuickUiPlan>::success(plan);
}
[[nodiscard]] inline pp::foundation::Status execute_quick_ui_plan(
const QuickUiPlan& plan,
QuickUiServices& services)
{
switch (plan.operation) {
case QuickUiOperation::select_slot:
if (!plan.updates_selection) {
return pp::foundation::Status::invalid_argument("quick select plan must update selection");
}
{
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
if (!status.ok()) {
return status;
}
}
services.select_slot(plan.slot_kind, plan.slot_index, plan.invokes_change_callback);
return pp::foundation::Status::success();
case QuickUiOperation::open_slot_popup:
if (plan.slot_kind == QuickUiSlotKind::brush && !plan.opens_brush_popup) {
return pp::foundation::Status::invalid_argument("quick brush popup plan must open brush popup");
}
if (plan.slot_kind == QuickUiSlotKind::color && !plan.opens_color_picker) {
return pp::foundation::Status::invalid_argument("quick color popup plan must open color picker");
}
{
const auto status = validate_quick_slot_index(plan.slot_index, plan.slot_count);
if (!status.ok()) {
return status;
}
}
services.open_slot_popup(plan.slot_kind, plan.slot_index);
return pp::foundation::Status::success();
case QuickUiOperation::restore_state:
if (!plan.restores_slots) {
return pp::foundation::Status::invalid_argument("quick restore plan must restore slots");
}
{
const auto brush_status = validate_quick_slot_index(plan.brush_index, plan.slot_count);
if (!brush_status.ok()) {
return brush_status;
}
const auto color_status = validate_quick_slot_index(plan.color_index, plan.slot_count);
if (!color_status.ok()) {
return color_status;
}
}
services.restore_state(plan.brush_index, plan.color_index, plan.fire_event);
return pp::foundation::Status::success();
case QuickUiOperation::reset_state:
if (!plan.resets_slots) {
return pp::foundation::Status::invalid_argument("quick reset plan must reset slots");
}
{
const auto status = validate_quick_slot_count(plan.slot_count);
if (!status.ok()) {
return status;
}
}
services.reset_state(plan.fire_event);
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown quick UI operation");
}
} // namespace pp::app

185
src/app_core/tools_menu.h Normal file
View File

@@ -0,0 +1,185 @@
#pragma once
#include "foundation/result.h"
#include <string_view>
namespace pp::app {
enum class ToolsMenuCommand {
panels,
options,
clear_grids,
reset_camera,
shortcuts,
sonarpen,
};
enum class ToolsMenuAction {
show_panels_submenu,
show_options_submenu,
clear_grid_overlays,
reset_camera,
show_shortcuts_dialog,
start_sonarpen,
no_op_unavailable,
};
enum class ToolsPanel {
presets,
color,
color_advanced,
layers,
brush,
grids,
animation,
};
enum class ToolsPanelAction {
open_floating_panel,
no_op_already_visible,
};
struct ToolsMenuPlan {
ToolsMenuCommand command = ToolsMenuCommand::panels;
ToolsMenuAction action = ToolsMenuAction::show_panels_submenu;
std::string_view label;
bool closes_root_popup = false;
};
struct ToolsPanelPlan {
ToolsPanel panel = ToolsPanel::presets;
ToolsPanelAction action = ToolsPanelAction::open_floating_panel;
std::string_view title;
int width = 0;
int height = 0;
int min_width = 0;
int min_height = 0;
bool droppable = true;
bool hides_embedded_title = false;
};
class ToolsMenuServices {
public:
virtual ~ToolsMenuServices() = default;
virtual void show_panels_submenu() = 0;
virtual void show_options_submenu() = 0;
virtual void clear_grid_overlays() = 0;
virtual void reset_camera() = 0;
virtual void show_shortcuts_dialog() = 0;
virtual void start_sonarpen() = 0;
};
[[nodiscard]] constexpr ToolsMenuPlan plan_tools_menu_command(
ToolsMenuCommand command,
bool sonarpen_available = false) noexcept
{
switch (command) {
case ToolsMenuCommand::panels:
return { command, ToolsMenuAction::show_panels_submenu, "Panels", false };
case ToolsMenuCommand::options:
return { command, ToolsMenuAction::show_options_submenu, "Options", false };
case ToolsMenuCommand::clear_grids:
return { command, ToolsMenuAction::clear_grid_overlays, "Clear Grids", true };
case ToolsMenuCommand::reset_camera:
return { command, ToolsMenuAction::reset_camera, "Reset Camera", true };
case ToolsMenuCommand::shortcuts:
return { command, ToolsMenuAction::show_shortcuts_dialog, "Shortcuts", true };
case ToolsMenuCommand::sonarpen:
return {
command,
sonarpen_available ? ToolsMenuAction::start_sonarpen : ToolsMenuAction::no_op_unavailable,
"SonarPen",
sonarpen_available,
};
}
return { command, ToolsMenuAction::no_op_unavailable, "", false };
}
[[nodiscard]] constexpr ToolsPanelPlan plan_tools_panel(
ToolsPanel panel,
bool already_visible) noexcept
{
ToolsPanelPlan plan;
plan.panel = panel;
plan.action = already_visible
? ToolsPanelAction::no_op_already_visible
: ToolsPanelAction::open_floating_panel;
switch (panel) {
case ToolsPanel::presets:
plan.title = "Brushes";
plan.height = 300;
plan.min_height = 300;
plan.min_width = 100;
break;
case ToolsPanel::color:
plan.title = "Color Picker";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::color_advanced:
plan.title = "Color Picker";
plan.width = 300;
plan.height = 300;
break;
case ToolsPanel::layers:
plan.title = "Layers";
plan.height = 300;
plan.min_height = 100;
plan.hides_embedded_title = true;
break;
case ToolsPanel::brush:
plan.title = "Brush Settings";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::grids:
plan.title = "Grid";
plan.height = 300;
plan.hides_embedded_title = true;
break;
case ToolsPanel::animation:
plan.title = "Animation";
plan.width = 500;
plan.height = 300;
plan.droppable = false;
break;
}
return plan;
}
[[nodiscard]] inline pp::foundation::Status execute_tools_menu_plan(
const ToolsMenuPlan& plan,
ToolsMenuServices& services)
{
switch (plan.action) {
case ToolsMenuAction::show_panels_submenu:
services.show_panels_submenu();
return pp::foundation::Status::success();
case ToolsMenuAction::show_options_submenu:
services.show_options_submenu();
return pp::foundation::Status::success();
case ToolsMenuAction::clear_grid_overlays:
services.clear_grid_overlays();
return pp::foundation::Status::success();
case ToolsMenuAction::reset_camera:
services.reset_camera();
return pp::foundation::Status::success();
case ToolsMenuAction::show_shortcuts_dialog:
services.show_shortcuts_dialog();
return pp::foundation::Status::success();
case ToolsMenuAction::start_sonarpen:
services.start_sonarpen();
return pp::foundation::Status::success();
case ToolsMenuAction::no_op_unavailable:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("unknown tools menu action");
}
}

View File

@@ -1,6 +1,10 @@
#include "pch.h"
#include "app.h"
#include "action.h"
#include "app_core/document_layer.h"
#include "app_core/document_resize.h"
#include "app_core/document_export.h"
#include "app_core/document_session.h"
#include "settings.h"
#include "node_dialog_open.h"
#include "node_dialog_browse.h"
@@ -21,11 +25,33 @@
#include "oculus_vr.h"
#elif __WEB__
void webgl_pick_file(std::function<void(std::string)> callback);
void webgl_pick_file_save(const std::string& path,
const std::string& name, std::function<void(bool)> callback);
void webgl_sync();
#endif
namespace {
[[nodiscard]] bool can_start_document_export(App& app, bool requires_license)
{
const auto decision = pp::app::plan_document_export_start(
requires_license,
!requires_license || app.check_license(),
app.canvas != nullptr);
switch (decision) {
case pp::app::DocumentExportStartDecision::start_now:
return true;
case pp::app::DocumentExportStartDecision::show_license_disabled:
app.message_box("License", "This function is disabled in demo mode.");
return false;
case pp::app::DocumentExportStartDecision::unavailable_no_canvas:
return false;
}
return false;
}
}
std::shared_ptr<NodeProgressBar> App::show_progress(const std::string& title, int total /*= 0*/)
{
auto pb = std::make_shared<NodeProgressBar>();
@@ -105,6 +131,41 @@ void App::dialog_about()
layout[main_id]->add_child(dialog);
}
void App::continue_document_workflow_after_optional_save(std::function<void()> action)
{
const bool has_canvas = canvas != nullptr;
const bool has_unsaved_changes = has_canvas && Canvas::I->m_unsaved;
const auto decision = pp::app::plan_document_workflow(has_canvas, has_unsaved_changes);
switch (decision) {
case pp::app::DocumentWorkflowDecision::unavailable:
return;
case pp::app::DocumentWorkflowDecision::continue_now:
action();
return;
case pp::app::DocumentWorkflowDecision::prompt_save_before_continue:
break;
}
auto m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Would you like to save this document before closing?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this, m, action](Node*) {
Canvas::I->project_save([this, m, action](bool success) {
if (success)
action();
else
message_box("Saving Error", "There was a problem saving the document");
});
m->destroy();
};
m->btn_cancel->on_click = [m, action](Node*) {
action();
m->destroy();
};
}
void App::dialog_newdoc()
{
auto show_dialog = [this] {
@@ -122,25 +183,32 @@ void App::dialog_newdoc()
dialog->btn_ok->on_click = [this, dialog](Node*)
{
std::string name = dialog->input->m_text;
std::string path = work_path + "/" + name + ".ppi";
if (name.empty())
const auto plan = pp::app::plan_new_document(
work_path,
name,
dialog->m_resolution->m_current_index,
[](const std::string& path) {
return Asset::exist(path);
});
if (!plan)
{
message_box("Warning", "You need to specify a name to file.");
const bool missing_name =
plan.status().code == pp::foundation::StatusCode::invalid_argument;
message_box(
"Warning",
missing_name ? "You need to specify a name to file." : plan.status().message);
return;
}
auto action = [this, dialog, name, path] {
std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
int res = resolutions[dialog->m_resolution->m_current_index];
doc_name = name;
doc_path = path;
doc_filename = name + ".ppi";
doc_dir = work_path;
auto action = [this, dialog, plan = plan.value()] {
doc_name = plan.target.name;
doc_path = plan.target.path;
doc_filename = plan.target.name + ".ppi";
doc_dir = plan.target.directory;
layers->clear();
canvas->m_canvas->m_layers.clear();
canvas->m_canvas->resize(res, res);
canvas->m_canvas->resize(plan.resolution, plan.resolution);
canvas->reset_camera();
ActionManager::clear();
@@ -154,7 +222,7 @@ void App::dialog_newdoc()
App::I->hideKeyboard();
};
if (Asset::exist(path))
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
{
// ask confirm is file already exist
auto msgbox = new NodeMessageBox();
@@ -181,34 +249,7 @@ void App::dialog_newdoc()
};
};
if (canvas)
{
if (Canvas::I->m_unsaved)
{
auto m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Would you like to save this document before closing?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this, m, show_dialog](Node*) {
Canvas::I->project_save([this, m, show_dialog](bool success){
if (success)
show_dialog();
else
message_box("Saving Error", "There was a problem saving the document");
});
m->destroy();
};
m->btn_cancel->on_click = [this, m, show_dialog](Node*) {
show_dialog();
m->destroy();
};
}
else
{
show_dialog();
}
}
continue_document_workflow_after_optional_save(show_dialog);
}
// DEPRECATED
@@ -242,34 +283,7 @@ void App::dialog_open()
};
};
if (canvas)
{
if (Canvas::I->m_unsaved)
{
auto m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Would you like to save this document before closing?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this,m,show_dialog](Node*){
Canvas::I->project_save([this,m,show_dialog](bool success){
if (success)
show_dialog();
else
message_box("Saving Error", "There was a problem saving the document");
});
m->destroy();
};
m->btn_cancel->on_click = [this,m,show_dialog](Node*) {
show_dialog();
m->destroy();
};
}
else
{
show_dialog();
}
}
continue_document_workflow_after_optional_save(show_dialog);
}
void App::dialog_browse()
@@ -299,34 +313,7 @@ void App::dialog_browse()
};
};
if (canvas)
{
if (Canvas::I->m_unsaved)
{
auto m = layout[main_id]->add_child<NodeMessageBox>();
m->m_title->set_text("Unsaved document");
m->m_message->set_text("Would you like to save this document before closing?");
m->btn_ok->m_text->set_text("Yes");
m->btn_cancel->m_text->set_text("No");
m->btn_ok->on_click = [this, m, show_dialog](Node*) {
Canvas::I->project_save([this, m, show_dialog](bool success){
if (success)
show_dialog();
else
message_box("Saving Error", "There was a problem saving the document");
});
m->destroy();
};
m->btn_cancel->on_click = [this, m, show_dialog](Node*) {
show_dialog();
m->destroy();
};
}
else
{
show_dialog();
}
}
continue_document_workflow_after_optional_save(show_dialog);
}
void App::dialog_save_ver()
@@ -337,35 +324,45 @@ void App::dialog_save_ver()
return;
}
int current = 0;
std::string next = doc_name + ".01";
std::string base = doc_name;
std::regex r(R"((.*)\.(\w{2})$)");
std::smatch m;
if (std::regex_search(doc_name, m, r))
{
base = m[1].str();
current = atoi(m[2].str().c_str());
const auto target = pp::app::find_next_document_version_target(
doc_dir,
doc_name,
[](const std::string& path) {
return Asset::exist(path);
});
if (!target) {
message_box("Saving Error", target.status().message);
return;
}
for (int i = current + 1; i < 99; i++)
{
static char tmp_name[256];
sprintf(tmp_name, "%s.%02d", base.c_str(), i);
next = tmp_name;
if (Asset::exist(doc_dir + "/" + next + ".ppi"))
continue;
break;
}
doc_name = next;
doc_path = doc_dir + "/" + next + ".ppi";
doc_name = target.value().name;
doc_path = target.value().path;
canvas->m_canvas->m_unsaved = true;
title_update();
canvas->m_canvas->project_save(doc_path);
}
void App::save_document(pp::app::DocumentSaveIntent intent)
{
const auto decision = pp::app::plan_document_save(
Canvas::I->m_newdoc,
Canvas::I->m_unsaved,
intent);
switch (decision) {
case pp::app::DocumentSaveDecision::show_save_dialog:
dialog_save();
break;
case pp::app::DocumentSaveDecision::save_existing:
Canvas::I->project_save();
break;
case pp::app::DocumentSaveDecision::save_version:
dialog_save_ver();
break;
case pp::app::DocumentSaveDecision::no_op:
break;
}
}
void App::dialog_save()
{
if (!check_license())
@@ -388,32 +385,36 @@ void App::dialog_save()
dialog->btn_ok->on_click = [this, dialog](Node*)
{
std::string name = dialog->input->m_text;
std::string path = work_path + "/" + name + ".ppi";
if (name.empty())
const auto plan = pp::app::plan_document_file_save(
work_path,
name,
[](const std::string& path) {
return Asset::exist(path);
});
if (!plan)
{
message_box("Warning", "You need to specify a name to file.");
return;
}
auto action = [this, dialog, name, path] {
canvas->m_canvas->project_save(path);
doc_name = name;
doc_path = path;
doc_dir = work_path;
auto action = [this, dialog, plan = plan.value()] {
canvas->m_canvas->project_save(plan.target.path);
doc_name = plan.target.name;
doc_path = plan.target.path;
doc_dir = plan.target.directory;
title_update();
dialog->destroy();
App::I->hideKeyboard();
};
if (Asset::exist(path))
if (plan.value().write_decision == pp::app::DocumentFileWriteDecision::prompt_overwrite)
{
// ask confirm is file already exist
auto msgbox = new NodeMessageBox();
msgbox->set_manager(&layout);
msgbox->init();
msgbox->m_title->set_text("Warning");
msgbox->m_message->set_text(("Are you sure you want to overwrite " + name + "?").c_str());
msgbox->m_message->set_text(("Are you sure you want to overwrite " + plan.value().target.name + "?").c_str());
msgbox->btn_ok->on_click = [this, msgbox, action](Node*) {
action();
msgbox->destroy();
@@ -437,18 +438,17 @@ void App::dialog_save()
void App::dialog_export(std::string ext)
{
if (!check_license())
{
message_box("License", "This function is disabled in demo mode.");
if (!can_start_document_export(*this, true))
return;
// TODO: use picker
const auto target = pp::app::make_document_export_file_target(work_path, doc_name, ext);
if (!target) {
message_box("Export Equirectangular", target.status().message);
return;
}
if (canvas)
{
// TODO: use picker
auto path = work_path + "/" + doc_name + ext;
auto name = doc_name + ext;
canvas->m_canvas->export_equirectangular(path, [this, path, name]{
canvas->m_canvas->export_equirectangular(target.value().path, [this, target = target.value()]{
#if defined(__IOS__)
message_box("Export Equirectangular", "Image exported to Photos");
#elif defined(__OSX__)
@@ -459,83 +459,83 @@ void App::dialog_export(std::string ext)
//auto result = ovr_Media_ShareToFacebook("Sharing from PanoPainter on Oculus Quest", path.c_str(), ovrMediaContentType_Photo);
#elif __WEB__
ui_task([=]{
webgl_pick_file_save(path, name, [](bool success){ });
save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { });
});
#endif
});
}
}
void App::dialog_export_layers()
{
if (!check_license())
{
message_box("License", "This function is disabled in demo mode.");
if (!can_start_document_export(*this, true))
return;
#if defined(__IOS__)
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_layers");
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
if (canvas)
if (Asset::create_dir(target.value().directory))
{
#if defined(__IOS__)
auto dir = work_path + "/" + doc_name + "_layers";
if (Asset::create_dir(dir))
{
auto p = dir + "/" + doc_name;
canvas->m_canvas->export_layers(p, [this, p] {
canvas->m_canvas->export_layers(target.value().stem_path, [this] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
}
#else
pick_dir([this](std::string path) {
auto p = path + "/" + doc_name;
canvas->m_canvas->export_layers(p, [this, p] {
message_box("Export Layers", "Layers exported to: " + p);
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
canvas->m_canvas->export_layers(target.value().stem_path, [this, target = target.value()] {
message_box("Export Layers", "Layers exported to: " + target.stem_path);
});
});
#endif
}
}
void App::dialog_export_anim_frames()
{
if (!check_license())
{
message_box("License", "This function is disabled in demo mode.");
if (!can_start_document_export(*this, true))
return;
#if defined(__IOS__)
const auto target = pp::app::make_document_export_collection_target(work_path, doc_name, "_frames");
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
if (canvas)
if (Asset::create_dir(target.value().directory))
{
#if defined(__IOS__)
auto dir = work_path + "/" + doc_name + "_frames";
if (Asset::create_dir(dir))
{
auto p = dir + "/" + doc_name;
canvas->m_canvas->export_anim_frames(p, [this, p] {
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this] {
message_box("Export Layers", "Image layers exported to Files/PanoPainter");
});
}
#else
pick_dir([this](std::string path) {
auto p = path + "/" + doc_name;
canvas->m_canvas->export_anim_frames(p, [this, p] {
message_box("Export Layers", "Layers exported to: " + p);
const auto target = pp::app::make_document_export_stem_target(path, doc_name);
if (!target) {
message_box("Export Layers", target.status().message);
return;
}
canvas->m_canvas->export_anim_frames(target.value().stem_path, [this, target = target.value()] {
message_box("Export Layers", "Layers exported to: " + target.stem_path);
});
});
#endif
}
}
void App::dialog_export_depth()
{
if (!check_license())
{
message_box("License", "This function is disabled in demo mode.");
if (!can_start_document_export(*this, true))
return;
}
if (canvas)
{
// TODO: use picker
canvas->m_canvas->export_depth(doc_name, [this] {
#if defined(__IOS__)
@@ -546,11 +546,37 @@ void App::dialog_export_depth()
message_box("Export 3D View + Depth", "Image and depth exported to " + work_path);
#endif
});
}
}
void App::dialog_resize()
{
class LegacyDocumentResizeServices final : public pp::app::DocumentResizeServices {
public:
explicit LegacyDocumentResizeServices(App& app) noexcept
: app_(app)
{
}
void resize_document(int width, int height) override
{
if (app_.canvas)
app_.canvas->m_canvas->resize(width, height);
}
void update_title() override
{
app_.title_update();
}
void clear_history() override
{
ActionManager::clear();
}
private:
App& app_;
};
auto dialog = std::make_shared<NodeDialogResize>();
dialog->set_manager(&layout);
dialog->init();
@@ -561,19 +587,26 @@ void App::dialog_resize()
dialog->btn_ok->on_click = [this,dialog](Node*)
{
int res = dialog->get_resolution();
if (canvas)
canvas->m_canvas->resize(res, res);
App::I->title_update();
ActionManager::clear();
const auto plan = pp::app::plan_document_resize(
dialog->combo ? dialog->combo->m_current_index : 0);
if (!plan)
{
dialog->destroy();
return;
}
LegacyDocumentResizeServices services(*this);
const auto status = pp::app::execute_document_resize_plan(plan.value(), services);
if (!status.ok())
LOG("Document resize failed: %s", status.message);
dialog->destroy();
};
}
void App::dialog_export_cube_faces()
{
if (canvas)
{
if (!can_start_document_export(*this, false))
return;
canvas->m_canvas->export_cube_faces(doc_name, [this] {
#if defined(__IOS__)
message_box("Export Cube Faces", "Image and depth exported to Files/PanoPainter");
@@ -583,7 +616,6 @@ void App::dialog_export_cube_faces()
message_box("Export Cube Faces", "Image and depth exported to " + work_path);
#endif
});
}
}
void App::dialog_layer_rename()
@@ -601,6 +633,17 @@ void App::dialog_layer_rename()
dialog->btn_ok->on_click = [this,dialog](Node*)
{
const auto old_name = layers->m_current_layer->m_label_text;
const auto plan = pp::app::plan_document_layer_rename(old_name, dialog->get_name());
if (!plan)
return;
if (plan.value().action == pp::app::DocumentLayerRenameAction::no_op_same_name)
{
dialog->destroy();
App::I->hideKeyboard();
return;
}
struct ActionLayerRename : public Action
{
std::string m_old_name;
@@ -624,9 +667,13 @@ void App::dialog_layer_rename()
};
auto layer_node = std::static_pointer_cast<NodeLayer>(layers->m_current_layer->shared_from_this());
auto* layer = canvas->m_canvas->m_layers[canvas->m_canvas->m_current_layer_idx].get();
ActionManager::add(new ActionLayerRename(layers->m_current_layer->m_label_text, dialog->get_name(), layer_node, layer));
layer_node->set_name(dialog->get_name().c_str());
layer->m_name = dialog->get_name();
ActionManager::add(new ActionLayerRename(
plan.value().old_name,
plan.value().new_name,
layer_node,
layer));
layer_node->set_name(plan.value().new_name.c_str());
layer->m_name = plan.value().new_name;
dialog->destroy();
App::I->hideKeyboard();
};
@@ -681,8 +728,17 @@ void App::dialog_ppbr_export()
void App::dialog_timelapse_export()
{
if (!can_start_document_export(*this, false))
return;
#if __IOS__ || __WEB__
pick_file_save("mp4", doc_name + "-timelapse",
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-timelapse");
if (!target) {
message_box("Export Timelapse", target.status().message);
return;
}
pick_file_save("mp4", target.value().name,
[this](std::string path) {
rec_export(path);
},
@@ -703,8 +759,17 @@ void App::dialog_timelapse_export()
void App::dialog_export_mp4()
{
if (!can_start_document_export(*this, false))
return;
#if __IOS__ || __WEB__
pick_file_save("mp4", doc_name + "-animation",
const auto target = pp::app::make_document_export_suggested_name(doc_name, "-animation");
if (!target) {
message_box("Export Animation", target.status().message);
return;
}
pick_file_save("mp4", target.value().name,
[this](std::string path) {
export_anim_mp4(path);
},

View File

@@ -1,76 +1,90 @@
#include "pch.h"
#include "app.h"
#include "app_core/document_platform_io.h"
#include "app_core/document_sharing.h"
#include "platform_api/platform_services.h"
#include "platform_legacy/legacy_platform_services.h"
#include "renderer_gl/opengl_capabilities.h"
#ifdef __ANDROID__
void displayKeyboard(bool pShow);
void android_pick_file(std::function<void(std::string)> callback);
void android_pick_file_save(std::function<void(std::string)> callback);
std::string android_get_clipboard();
bool android_set_clipboard(const std::string& s);
#elif _WIN32
std::string win32_open_file(const char* filter);
std::string win32_save_file(const char* filter);
std::string win32_open_dir();
void win32_show_cursor(bool visible);
bool win32_clipboard_set_text(const std::string & s);
std::string win32_clipboard_get_text();
#elif __APPLE__
#elif __LINUX__
#include <tinyfiledialogs.h>
#elif __WEB__
void webgl_pick_file(std::function<void(std::string)> callback);
void webgl_pick_file_save(const std::string& path,
const std::string& name, std::function<void(bool)> callback);
void webgl_sync();
#endif
namespace {
[[nodiscard]] GLint rgba8_internal_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}
[[nodiscard]] bool should_dispatch_keyboard_visibility(bool visible) noexcept
{
const auto action = pp::app::plan_virtual_keyboard(visible);
return visible
? action == pp::app::VirtualKeyboardAction::show_keyboard
: action == pp::app::VirtualKeyboardAction::hide_keyboard;
}
[[nodiscard]] bool should_dispatch_cursor_visibility(bool visible) noexcept
{
const auto action = pp::app::plan_cursor_visibility(visible);
return visible
? action == pp::app::CursorVisibilityAction::show_cursor
: action == pp::app::CursorVisibilityAction::hide_cursor;
}
}
namespace {
[[nodiscard]] pp::platform::PlatformServices& active_platform_services()
{
if (App::I)
{
if (auto* services = App::I->platform_services())
return *services;
}
return pp::platform::legacy::platform_services();
}
}
void App::set_platform_services(pp::platform::PlatformServices* services) noexcept
{
platform_services_ = services;
}
pp::platform::PlatformServices* App::platform_services() const noexcept
{
return platform_services_;
}
pp::platform::PlatformStoragePaths App::prepare_storage_paths()
{
return active_platform_services().prepare_storage_paths();
}
std::string App::clipboard_get_text()
{
#if _WIN32
return win32_clipboard_get_text();
#elif __IOS__
return [ios_view clipboard_get_string];
#elif __OSX__
return [osx_view clipboard_get_string];
#elif __ANDROID__
return android_get_clipboard();
#endif
if (pp::app::plan_clipboard_read() != pp::app::ClipboardReadAction::read_text)
return {};
return active_platform_services().clipboard_text();
}
bool App::clipboard_set_text(const std::string& s)
{
#if _WIN32
return win32_clipboard_set_text(s);
#elif __IOS__
return [ios_view clipboard_set_string:s];
#elif __OSX__
return [osx_view clipboard_set_string:s];
#elif __ANDROID__
return android_set_clipboard(s);
#endif
if (pp::app::plan_clipboard_write(s) != pp::app::ClipboardWriteAction::write_text)
return false;
return active_platform_services().set_clipboard_text(s);
}
void App::stacktrace()
{
#if __OSX__
NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
LOG("callstack:\n%s", [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
#endif
active_platform_services().log_stacktrace();
}
void App::crash_test()
{
#ifdef __IOS__
[ios_view crash];
#elif __OSX__
[osx_view hockeyapp_crash];
#elif defined(_WIN32)
__debugbreak();
#elif defined(__ANDROID__)
int *x = nullptr; *x = 42;
LOG("%d", *x);
#endif
active_platform_services().trigger_crash_test();
}
void App::tick(float dt)
@@ -84,7 +98,7 @@ void App::tick(float dt)
void App::resize(float w, float h)
{
LOG("App::resize %d %d", (int)w, (int)h);
uirtt.create(w, h, -1, GL_RGBA8, true);
uirtt.create(static_cast<int>(w), static_cast<int>(h), -1, rgba8_internal_format(), true);
redraw = true;
width = w;
height = h;
@@ -92,123 +106,50 @@ void App::resize(float w, float h)
void App::show_cursor()
{
#ifdef _WIN32
win32_show_cursor(true);
#elif __OSX__
[osx_view show_cursor:true];
#endif
if (!should_dispatch_cursor_visibility(true))
return;
active_platform_services().set_cursor_visible(true);
}
void App::hide_cursor()
{
#ifdef _WIN32
win32_show_cursor(false);
#elif __OSX__
[osx_view show_cursor:false];
#endif
if (!should_dispatch_cursor_visibility(false))
return;
active_platform_services().set_cursor_visible(false);
}
void App::showKeyboard()
{
LOG("show keyboard");
redraw = true;
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view show_keyboard];
});
#elif __ANDROID__
displayKeyboard(true);
#endif
if (!should_dispatch_keyboard_visibility(true))
return;
active_platform_services().set_virtual_keyboard_visible(true);
}
void App::hideKeyboard()
{
LOG("hide keyboard");
redraw = true;
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view hide_keyboard];
});
#elif __ANDROID__
displayKeyboard(false);
#endif
if (!should_dispatch_keyboard_visibility(false))
return;
active_platform_services().set_virtual_keyboard_visible(false);
}
void App::pick_image(std::function<void(std::string path)> callback)
{
redraw = true;
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view pick_photo:callback];
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil];
std::string path = [osx_view pick_file:fileTypes];
if (!path.empty())
callback(path);
});
#elif __ANDROID__
android_pick_file(callback);
#elif _WIN32
std::string path = win32_open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png");
if (!path.empty())
callback(path);
#elif __LINUX__
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
callback(p);
#elif __WEB__
webgl_pick_file(callback);
#endif
active_platform_services().pick_image(std::move(callback));
}
void App::pick_file(std::vector<std::string> types, std::function<void (std::string)> callback)
{
redraw = true;
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
for (const auto& t : types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
[ios_view pick_file:fileTypes then:callback];
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
for (const auto& t : types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
std::string path = [osx_view pick_file:fileTypes];
if (!path.empty())
callback(path);
});
#elif __ANDROID__
android_pick_file(callback);
#elif _WIN32
std::string filter = "Supported Files (";
bool first_type = true;
for (auto& t : types)
{
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
first_type = false;
}
filter.append(")");
filter.push_back(0);
first_type = true;
for (auto& t : types)
{
filter.append(std::string(first_type ? "" : ";") + "*." + t);
first_type = false;
}
filter.push_back(0);
std::string path = win32_open_file(filter.c_str());
if (!path.empty())
callback(path);
#elif __LINUX__
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
callback(p);
#elif __WEB__
webgl_pick_file(callback);
#endif
active_platform_services().pick_file(std::move(types), std::move(callback));
}
#if __IOS__
@@ -220,10 +161,7 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam
std::string path = tmp_path + "/" + default_name + ext;
std::thread([=]{
writer(path);
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view pick_file_save:path];
});
callback(path, true);
save_prepared_file(path, default_name + ext, callback);
}).detach();
}
#elif __WEB__
@@ -234,110 +172,137 @@ void App::pick_file_save(const std::string& type, const std::string& default_nam
auto path = data_path + "/" + default_name + "." + type;
LOG("App::pick_file_save %s", path.c_str());
writer(path);
webgl_pick_file_save(path, default_name + "." + type, callback);
save_prepared_file(path, default_name + "." + type, std::move(callback));
}
#else
void App::pick_file_save(std::vector<std::string> types, std::function<void(std::string)> callback)
{
redraw = true;
#if __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
//NSArray* fileTypes = [NSArray arrayWithObjects:@"ppi", @"PPI", nil];
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:types.size()];
for (const auto& t : types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
std::string path = [osx_view pick_file_save:fileTypes];
if (!path.empty())
callback(path);
});
#elif __ANDROID__
android_pick_file_save(callback);
#elif _WIN32
std::string filter = "Supported Files (";
bool first_type = true;
for (auto& t : types)
{
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
first_type = false;
}
filter.append(")");
filter.push_back(0);
first_type = true;
for (auto& t : types)
{
filter.append(std::string(first_type ? "" : ";") + "*." + t);
first_type = false;
}
filter.push_back(0);
std::string path = win32_save_file(filter.c_str());
if (!path.empty())
callback(path);
#endif
active_platform_services().pick_save_file(std::move(types), std::move(callback));
}
#endif
void App::pick_dir(std::function<void(std::string path)> callback)
{
redraw = true;
#ifdef __IOS__
// NOT IMPLEMENTED
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
std::string path = [osx_view pick_dir];
if (!path.empty())
callback(path);
});
#elif __ANDROID__
// NOT IMPLEMENTED
#elif _WIN32
// TODO: to be implemented
std::string path = win32_open_dir();
if (!path.empty())
callback(path);
#endif
active_platform_services().pick_directory(std::move(callback));
}
void App::display_file(std::string path)
{
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view display_file:path];
});
#elif __OSX__
[[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:path.c_str()]];
// dispatch_async(dispatch_get_main_queue(), ^{
// std::string path = [osx_view pick_file];
// if (!path.empty())
// callback(path);
// });
#elif __ANDROID__
//displayKeyboard(and_app, false);
#elif _WIN32
// std::string path = win32_open_file();
// if (!path.empty())
// callback(path);
#endif
if (pp::app::plan_display_file(path) == pp::app::DisplayFileAction::ignore_empty_path)
return;
active_platform_services().display_file(path);
}
void App::share_file(std::string path)
{
if (path.empty())
const auto plan = pp::app::plan_document_share(path);
if (plan == pp::app::DocumentShareAction::show_save_required_warning)
{
message_box("Sharing failed", "Please save the document before sharing it.");
return;
}
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[ios_view share_file:[NSString stringWithUTF8String:path.c_str()]];
active_platform_services().share_file(path);
}
void App::request_app_close()
{
active_platform_services().request_app_close();
}
void App::attach_ui_thread()
{
active_platform_services().attach_ui_thread();
}
void App::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::bind_default_render_target()
{
active_platform_services().bind_default_render_target();
}
void App::bind_main_render_target()
{
active_platform_services().bind_main_render_target();
}
void App::apply_render_platform_hints()
{
active_platform_services().apply_render_platform_hints();
}
void App::install_render_debug_callback()
{
active_platform_services().install_render_debug_callback();
}
void App::begin_render_capture_frame()
{
active_platform_services().begin_render_capture_frame();
}
void App::end_render_capture_frame()
{
active_platform_services().end_render_capture_frame();
}
bool App::platform_deletes_recorded_files_on_clear()
{
return active_platform_services().deletes_recorded_files_on_clear();
}
void App::clear_platform_recorded_files(std::string path)
{
active_platform_services().clear_recorded_files(path);
}
bool App::platform_enables_live_asset_reloading()
{
return active_platform_services().enables_live_asset_reloading();
}
void App::update_platform_frame(float delta_time_seconds)
{
active_platform_services().update_platform_frame(delta_time_seconds);
}
void App::report_rendered_frames(int frames)
{
active_platform_services().report_rendered_frames(frames);
}
void App::save_prepared_file(
std::string path,
std::string suggested_name,
std::function<void(const std::string& path, bool saved)> callback)
{
active_platform_services().save_prepared_file(
path,
suggested_name,
[callback = std::move(callback)](std::string saved_path, bool saved) {
callback(saved_path, saved);
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
[osx_view share_file:[NSString stringWithUTF8String:path.c_str()]];
});
#elif __ANDROID__
#elif _WIN32
// not implemented
#endif
}
bool App::mouse_down(int button, float x, float y, float pressure, kEventSource source, bool eraser)

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,60 @@
#include "pch.h"
#include "app.h"
#include "renderer_api/shader_catalog.h"
#include "renderer_gl/opengl_capabilities.h"
#include "shader.h"
namespace {
[[nodiscard]] GLenum extension_count_query() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::extension_count_query());
}
[[nodiscard]] GLenum extension_string_name() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::extension_string_name());
}
}
void App::initShaders()
{
#ifdef _DEBUG
if (!check_uniform_uniqueness())
std::logic_error("check_uniform_uniqueness() failed");
LOG("check_uniform_uniqueness() failed");
#endif // _DEBUG
render_task([] {
GLint n_exts;
glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts);
glGetIntegerv(extension_count_query(), &n_exts);
std::vector<std::string> extension_storage;
std::vector<std::string_view> extension_views;
extension_storage.reserve(n_exts);
extension_views.reserve(n_exts);
for (int i = 0; i < n_exts; i++)
{
std::string ext = (const char*)glGetStringi(GL_EXTENSIONS, i);
if (ext.find("shader_framebuffer_fetch") != std::string::npos)
ShaderManager::ext_framebuffer_fetch = true;
if (ext.find("map_buffer_alignment") != std::string::npos)
ShaderManager::ext_map_aligned = true;
#if __GLES__ && !__WEB__
if (ext.find("texture_float") != std::string::npos)
ShaderManager::ext_float32 = true;
if (ext.find("texture_float_linear") != std::string::npos)
ShaderManager::ext_float32_linear = true;
if (ext.find("color_buffer_float") != std::string::npos)
ShaderManager::ext_float32 = true;
if (ext.find("texture_half_float") != std::string::npos)
ShaderManager::ext_float16 = true;
if (ext.find("color_buffer_half_float") != std::string::npos)
ShaderManager::ext_float16 = true;
#endif
LOG("EXT: %s", ext.c_str());
extension_storage.emplace_back((const char*)glGetStringi(extension_string_name(), i));
extension_views.push_back(extension_storage.back());
LOG("EXT: %s", extension_storage.back().c_str());
}
pp::renderer::gl::OpenGlRuntime runtime;
#if __GL__
runtime.desktop_gl = true;
#endif
#if __GLES__
runtime.gles = true;
#endif
#if __WEB__
runtime.web = true;
#endif
const auto capabilities = pp::renderer::gl::detect_opengl_capabilities(extension_views, runtime);
ShaderManager::ext_framebuffer_fetch = capabilities.framebuffer_fetch;
ShaderManager::ext_map_aligned = capabilities.map_buffer_alignment;
ShaderManager::ext_float32 = capabilities.float32_textures;
ShaderManager::ext_float32_linear = capabilities.float32_linear;
ShaderManager::ext_float16 = capabilities.float16_textures;
});
#if __GL__
@@ -45,56 +67,19 @@ void App::initShaders()
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");
LOG("initializing shaders");
if (!ShaderManager::load(kShader::Texture, "data/shaders/texture.glsl"))
LOG("Failed to create shader Texture");
if (!ShaderManager::load(kShader::TextureAlpha, "data/shaders/texture-alpha.glsl"))
LOG("Failed to create shader TextureAlpha");
if (!ShaderManager::load(kShader::TextureMask, "data/shaders/texture-mask.glsl"))
LOG("Failed to create shader TextureMask");
if (!ShaderManager::load(kShader::TextureColorize, "data/shaders/texture-colorize.glsl"))
LOG("Failed to create shader TextureColorize");
if (!ShaderManager::load(kShader::TextureBlend, "data/shaders/texture-blend.glsl"))
LOG("Failed to create shader TextureBlend");
if (!ShaderManager::load(kShader::StrokePreview, "data/shaders/stroke-preview.glsl"))
LOG("Failed to create shader StrokePreview");
if (!ShaderManager::load(kShader::CompErase, "data/shaders/comp-erase.glsl"))
LOG("Failed to create shader CompErase");
if (!ShaderManager::load(kShader::CompDraw, "data/shaders/comp-draw.glsl"))
LOG("Failed to create shader CompDraw");
if (!ShaderManager::load(kShader::Color, "data/shaders/color.glsl"))
LOG("Failed to create shader Color");
if (!ShaderManager::load(kShader::ColorQuad, "data/shaders/color-quad.glsl"))
LOG("Failed to create shader ColorQuad");
if (!ShaderManager::load(kShader::ColorTri, "data/shaders/color-tri.glsl"))
LOG("Failed to create shader ColorTri");
if (!ShaderManager::load(kShader::ColorHue, "data/shaders/color-hue.glsl"))
LOG("Failed to create shader ColorHue");
if (!ShaderManager::load(kShader::UVs, "data/shaders/uvs.glsl"))
LOG("Failed to create shader UVs");
if (!ShaderManager::load(kShader::Font, "data/shaders/font.glsl"))
LOG("Failed to create shader Font");
if (!ShaderManager::load(kShader::Atlas, "data/shaders/atlas.glsl"))
LOG("Failed to create shader Atlas");
if (!ShaderManager::load(kShader::Stroke, "data/shaders/stroke.glsl"))
LOG("Failed to create shader Stroke");
if (!ShaderManager::load(kShader::StrokePad, "data/shaders/stroke-pad.glsl"))
LOG("Failed to create shader StrokePad");
if (!ShaderManager::load(kShader::StrokeDilate, "data/shaders/stroke-dilate.glsl"))
LOG("Failed to create shader StrokeDilate");
if (!ShaderManager::load(kShader::Checkerboard, "data/shaders/checkerboard.glsl"))
LOG("Failed to create shader Checkerboard");
if (!ShaderManager::load(kShader::Equirect, "data/shaders/equirect.glsl"))
LOG("Failed to create shader Equirect");
if (!ShaderManager::load(kShader::BrushStroke, "data/shaders/stroke-instanced.glsl"))
LOG("Failed to create shader BrushStroke");
if (!ShaderManager::load(kShader::VertexColor, "data/shaders/vertex-color.glsl"))
LOG("Failed to create shader VertexColor");
if (!ShaderManager::load(kShader::Lambert, "data/shaders/lambert.glsl"))
LOG("Failed to create shader Lambert");
if (!ShaderManager::load(kShader::LambertLightmap, "data/shaders/lightmap.glsl"))
LOG("Failed to create shader LambertLightmap");
if (!ShaderManager::load(kShader::BakeUV, "data/shaders/bake-uv.glsl"))
LOG("Failed to create shader BakeUV");
const auto shader_catalog = pp::renderer::panopainter_shader_catalog();
const auto catalog_status = pp::renderer::validate_shader_catalog(shader_catalog);
if (!catalog_status.ok())
{
LOG("Shader catalog validation failed: %s", catalog_status.message);
return;
}
for (const auto& shader : shader_catalog)
{
if (!ShaderManager::load(static_cast<kShader>(const_hash(shader.name)), shader.path))
LOG("Failed to create shader %s", shader.name);
}
LOG("shaders initialized");
}

View File

@@ -1,13 +1,98 @@
#include "pch.h"
#include <cstdint>
#include "app.h"
#include "util.h"
#include "shape.h"
#include "renderer_gl/opengl_capabilities.h"
#ifdef _WIN32
bool win32_vr_start();
void win32_vr_stop();
#endif
namespace {
void set_active_texture_unit(std::uint32_t unit_index)
{
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
}
void unbind_texture_2d()
{
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
}
void enable_opengl_state(std::uint32_t state) noexcept
{
glEnable(static_cast<GLenum>(state));
}
void disable_opengl_state(std::uint32_t state) noexcept
{
glDisable(static_cast<GLenum>(state));
}
void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept
{
glViewport(static_cast<GLint>(x), static_cast<GLint>(y), static_cast<GLsizei>(width), static_cast<GLsizei>(height));
}
void clear_opengl_mask(std::uint32_t mask) noexcept
{
glClear(static_cast<GLbitfield>(mask));
}
void apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect viewport)
{
const auto status = pp::renderer::gl::apply_opengl_viewport(
viewport,
pp::renderer::gl::OpenGlViewportDispatch {
.viewport = set_opengl_viewport,
});
if (!status.ok())
LOG("OpenGL VR UI viewport failed: %s", status.message);
}
void apply_vr_ui_scissor_test(bool enabled)
{
const auto status = pp::renderer::gl::apply_opengl_scissor_test(
enabled,
pp::renderer::gl::OpenGlScissorTestDispatch {
.enable = enable_opengl_state,
.disable = disable_opengl_state,
});
if (!status.ok())
LOG("OpenGL VR UI scissor test failed: %s", status.message);
}
void apply_vr_render_capability(std::uint32_t state, bool enabled)
{
const auto status = pp::renderer::gl::apply_opengl_capability(
state,
enabled,
pp::renderer::gl::OpenGlCapabilityDispatch {
.enable = enable_opengl_state,
.disable = disable_opengl_state,
});
if (!status.ok())
LOG("OpenGL VR render state failed: %s", status.message);
}
void clear_vr_depth_buffer()
{
const auto status = pp::renderer::gl::clear_opengl_buffers(
pp::renderer::gl::framebuffer_depth_buffer_mask(),
pp::renderer::gl::OpenGlBufferClearDispatch {
.clear = clear_opengl_mask,
});
if (!status.ok())
LOG("OpenGL VR depth clear failed: %s", status.message);
}
}
bool trigger_down = false;
cbuffer<glm::vec3> controller_points(10);
glm::vec3 controller_last_point;
@@ -37,13 +122,16 @@ void App::vr_draw_ui()
{
uirtt.bindFramebuffer();
uirtt.clear();
glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight());
glEnable(GL_SCISSOR_TEST);
apply_vr_ui_viewport(pp::renderer::gl::OpenGlViewportRect {
.width = static_cast<std::int32_t>(uirtt.getWidth()),
.height = static_cast<std::int32_t>(uirtt.getHeight()),
});
apply_vr_ui_scissor_test(true);
auto observer = std::bind(&App::update_ui_observer, this, std::placeholders::_1);
for (int i = 1; i < layout[main_id]->m_children.size(); i++)
layout[main_id]->m_children[i]->watch(observer);
//msgbox->watch(observer);
glDisable(GL_SCISSOR_TEST);
apply_vr_ui_scissor_test(false);
uirtt.unbindFramebuffer();
}
@@ -185,12 +273,12 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
glm::vec3 origin = glm::vec3(0, 0, -1) * glm::transpose(glm::mat3(pose));
vr_rot = glm::lookAt({ 0, 0, 0 }, origin, { 0, 1, 0 });
auto blend = glIsEnabled(GL_BLEND);
auto depth = glIsEnabled(GL_DEPTH_TEST);
auto blend = glIsEnabled(pp::renderer::gl::blend_state());
auto depth = glIsEnabled(pp::renderer::gl::depth_test_state());
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);
apply_vr_render_capability(pp::renderer::gl::blend_state(), false);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
clear_vr_depth_buffer();
for (int plane_index = 0; plane_index < 6; plane_index++)
{
@@ -205,9 +293,9 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
m_face_plane.draw_fill();
}
glEnable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), true);
clear_vr_depth_buffer();
for (size_t i = 0; i < canvas->m_canvas->m_layers.size(); i++)
{
@@ -241,17 +329,17 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
ShaderManager::u_int(kShaderUniform::Mask, canvas->m_canvas->m_smask_active);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
canvas->m_canvas->m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
m_face_plane.draw_fill();
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
}
else if (canvas->m_canvas->m_show_tmp && canvas->m_canvas->m_current_layer_idx == layer_index)
@@ -292,28 +380,28 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
canvas->m_canvas->m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
canvas->m_canvas->m_smask.rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
canvas->m_canvas->m_tmp_dual[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE4);
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
unbind_texture_2d();
m_face_plane.draw_fill();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
canvas->m_canvas->m_tmp_dual[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
canvas->m_canvas->m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
canvas->m_canvas->m_tmp[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
}
else
@@ -325,7 +413,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
ShaderManager::u_int(kShaderUniform::Highlight, canvas->m_canvas->m_layers[layer_index]->m_hightlight);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).bindTexture();
m_face_plane.draw_fill();
canvas->m_canvas->m_layers[layer_index]->rtt(plane_index).unbindTexture();
@@ -352,7 +440,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
m_face_plane.draw_stroke();
}
glDisable(GL_DEPTH_TEST);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), false);
// draw the brush
/*
auto mode = dynamic_cast<CanvasModePen*>(canvas->m_canvas->modes[(int)canvas->m_canvas->m_current_mode][0]);
@@ -377,8 +465,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size / height)) *
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
set_active_texture_unit(0);
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
tex.bind();
sampler_linear.bind(0);
@@ -399,7 +487,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
uirtt.bindTexture();
m_face_plane.draw_fill();
uirtt.unbindTexture();
@@ -451,8 +539,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
glm::scale(glm::vec3(canvas->m_canvas->m_current_brush->m_tip_size * 100.f / height)) *
glm::eulerAngleZ(canvas->m_canvas->m_current_brush->m_tip_angle * (float)(M_PI * 2.0))
);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
apply_vr_render_capability(pp::renderer::gl::blend_state(), true);
set_active_texture_unit(0);
auto& tex = *canvas->m_canvas->m_current_brush->m_tip_texture;
tex.bind();
sampler_linear.bind(0);
@@ -466,7 +554,7 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
for (auto& mode : *canvas->m_canvas->m_mode)
mode->on_Draw(ortho_proj, proj, camera);
glDisable(GL_DEPTH_TEST);
glDisable(pp::renderer::gl::depth_test_state());
if (canvas->m_canvas->m_smask_active)
{
canvas->m_canvas->modes[(int)kCanvasMode::MaskFree][0]->on_Draw(ortho_proj, proj, camera);
@@ -479,8 +567,8 @@ void App::vr_draw(const glm::mat4& proj, const glm::mat4& camera, const glm::mat
mode->on_Draw(ortho_proj, proj, camera);
*/
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
apply_vr_render_capability(pp::renderer::gl::blend_state(), blend != 0U);
apply_vr_render_capability(pp::renderer::gl::depth_test_state(), depth != 0U);
sampler.unbind();
}

View File

@@ -9,6 +9,10 @@
#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>
#define STB_IMAGE_WRITE_STATIC
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb/stb_image_write.h>
namespace pp::assets {
namespace {
@@ -33,6 +37,17 @@ namespace {
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}
void append_png_bytes(void* context, void* data, int size)
{
if (context == nullptr || data == nullptr || size <= 0) {
return;
}
auto* bytes = static_cast<std::vector<std::byte>*>(context);
const auto* begin = static_cast<const std::byte*>(data);
bytes->insert(bytes->end(), begin, begin + size);
}
}
pp::foundation::Result<Rgba8Image> decode_png_rgba8(std::span<const std::byte> bytes)
@@ -91,4 +106,54 @@ pp::foundation::Result<Rgba8Image> decode_png_rgba8(std::span<const std::byte> b
return pp::foundation::Result<Rgba8Image>::success(std::move(image));
}
pp::foundation::Result<std::vector<std::byte>> encode_png_rgba8(
std::uint32_t width,
std::uint32_t height,
std::span<const std::uint8_t> pixels)
{
if (width == 0 || height == 0) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PNG dimensions must be greater than zero"));
}
if (width > static_cast<std::uint32_t>(std::numeric_limits<int>::max())
|| height > static_cast<std::uint32_t>(std::numeric_limits<int>::max())) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PNG dimensions exceed encoder limits"));
}
const auto byte_count = rgba_byte_size(width, height);
if (!byte_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(byte_count.status());
}
if (pixels.size() != byte_count.value()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("RGBA pixel payload size does not match dimensions"));
}
const auto stride = static_cast<std::uint64_t>(width) * 4ULL;
if (stride > static_cast<std::uint64_t>(std::numeric_limits<int>::max())) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PNG row stride exceeds encoder limits"));
}
std::vector<std::byte> encoded;
const auto result = stbi_write_png_to_func(
append_png_bytes,
&encoded,
static_cast<int>(width),
static_cast<int>(height),
4,
pixels.data(),
static_cast<int>(stride));
if (result == 0 || encoded.empty()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("RGBA pixels could not be encoded as PNG"));
}
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(encoded));
}
}

View File

@@ -18,4 +18,9 @@ struct Rgba8Image {
[[nodiscard]] pp::foundation::Result<Rgba8Image> decode_png_rgba8(
std::span<const std::byte> bytes);
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> encode_png_rgba8(
std::uint32_t width,
std::uint32_t height,
std::span<const std::uint8_t> pixels);
}

View File

@@ -3,8 +3,12 @@
#include "assets/image_metadata.h"
#include "foundation/binary_stream.h"
#include <array>
#include <bit>
#include <cmath>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
namespace pp::assets {
@@ -43,6 +47,26 @@ namespace {
return pp::foundation::Result<float>::success(std::bit_cast<float>(bits.value()));
}
void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
{
bytes.push_back(static_cast<std::byte>(value & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
}
void append_f32(std::vector<std::byte>& bytes, float value)
{
append_u32(bytes, std::bit_cast<std::uint32_t>(value));
}
void append_ascii(std::vector<std::byte>& bytes, std::string_view value)
{
for (const auto ch : value) {
bytes.push_back(static_cast<std::byte>(ch));
}
}
[[nodiscard]] pp::foundation::Status skip_bytes(
pp::foundation::ByteReader& reader,
std::size_t bytes) noexcept
@@ -68,6 +92,18 @@ namespace {
return pp::foundation::Status::success();
}
[[nodiscard]] std::string generated_layer_name(std::string_view base_name, std::uint32_t layer_index, std::uint32_t layer_count)
{
if (layer_count == 1U) {
return std::string(base_name);
}
std::string name(base_name);
name.push_back(' ');
name += std::to_string(layer_index + 1U);
return name;
}
[[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept
{
const auto next = summary.compressed_face_bytes + static_cast<std::uint64_t>(bytes);
@@ -303,6 +339,11 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
seen_orders[order.value()] = true;
}
if (!std::isfinite(opacity.value())) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI layer opacity must be finite"));
}
if (opacity.value() < 0.0F || opacity.value() > 1.0F) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
@@ -342,6 +383,11 @@ pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid"));
}
if (blend_mode.value() > 4U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range"));
}
if (index != nullptr) {
layer_summary.blend_mode = blend_mode.value();
layer_summary.alpha_locked = alpha_locked.value() != 0U;
@@ -616,4 +662,227 @@ pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(std::s
return pp::foundation::Result<PpiDecodedProjectImages>::success(std::move(decoded));
}
pp::foundation::Result<std::vector<std::byte>> create_ppi_project(PpiProjectConfig config)
{
const auto canvas_status = validate_canvas_size(config.width, config.height);
if (!canvas_status.ok()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(canvas_status);
}
if (config.layers.empty() || config.layers.size() > max_ppi_layer_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
}
std::uint32_t total_frame_count = 0;
std::vector<std::size_t> layer_frame_offsets;
layer_frame_offsets.reserve(config.layers.size());
for (const auto& layer : config.layers) {
if (layer.name.empty()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI layer name must not be empty"));
}
if (layer.name.size() > max_ppi_layer_name_length) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
}
if (!std::isfinite(layer.metadata.opacity)) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI layer opacity must be finite"));
}
if (layer.metadata.opacity < 0.0F || layer.metadata.opacity > 1.0F) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
}
if (layer.metadata.blend_mode > 4U) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range"));
}
if (layer.frames.empty() || layer.frames.size() > max_ppi_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
}
if (layer.frames.size() > max_ppi_frame_count - total_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI total layer frame count exceeds the configured range"));
}
layer_frame_offsets.push_back(total_frame_count);
total_frame_count += static_cast<std::uint32_t>(layer.frames.size());
for (const auto& frame : layer.frames) {
if (frame.duration_ms == 0) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
}
}
}
std::vector<std::array<bool, 6>> seen_faces(total_frame_count);
std::uint64_t total_payload_bytes = 0;
for (const auto& face : config.dirty_faces) {
if (face.layer_index >= config.layers.size()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face layer index is outside the layer list"));
}
if (face.frame_index >= config.layers[face.layer_index].frames.size()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face frame index is outside the frame list"));
}
if (face.face_index >= 6U) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face index is outside the cube face list"));
}
const auto slot_index = layer_frame_offsets[face.layer_index] + static_cast<std::size_t>(face.frame_index);
if (seen_faces[slot_index][face.face_index]) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face slot is duplicated"));
}
seen_faces[slot_index][face.face_index] = true;
if (face.width == 0 || face.height == 0) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face dimensions must be greater than zero"));
}
if (face.x > config.width || face.width > config.width - face.x
|| face.y > config.height || face.height > config.height - face.y) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face box is outside the canvas"));
}
if (face.png_rgba8.empty()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face PNG payload must not be empty"));
}
if (face.png_rgba8.size() > static_cast<std::size_t>(std::numeric_limits<std::uint32_t>::max())) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face PNG payload is too large"));
}
const auto next_payload_bytes = total_payload_bytes + face.png_rgba8.size();
if (next_payload_bytes > max_ppi_face_payload_bytes) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face PNG payloads exceed the configured limit"));
}
total_payload_bytes = next_payload_bytes;
const auto metadata = validate_face_png_payload(face.png_rgba8, face.width, face.height);
if (!metadata) {
return pp::foundation::Result<std::vector<std::byte>>::failure(metadata.status());
}
}
std::vector<std::byte> bytes {
std::byte { 'P' },
std::byte { 'P' },
std::byte { 'I' },
std::byte { 0 },
};
append_u32(bytes, 0);
append_u32(bytes, 4);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 128);
append_u32(bytes, 128);
append_u32(bytes, 4);
constexpr std::size_t thumbnail_bytes = 128U * 128U * 4U;
bytes.resize(ppi_header_size + thumbnail_bytes, std::byte { 0 });
append_u32(bytes, config.width);
append_u32(bytes, config.height);
append_u32(bytes, static_cast<std::uint32_t>(config.layers.size()));
append_u32(bytes, total_frame_count);
for (std::uint32_t layer = 0; layer < config.layers.size(); ++layer) {
const auto& layer_config = config.layers[layer];
append_u32(bytes, layer);
append_f32(bytes, layer_config.metadata.opacity);
append_u32(bytes, static_cast<std::uint32_t>(layer_config.name.size()));
append_ascii(bytes, layer_config.name);
append_u32(bytes, layer_config.metadata.blend_mode);
bytes.push_back(layer_config.metadata.alpha_locked ? std::byte { 1 } : std::byte { 0 });
bytes.push_back(layer_config.metadata.visible ? std::byte { 1 } : std::byte { 0 });
append_u32(bytes, static_cast<std::uint32_t>(layer_config.frames.size()));
for (std::uint32_t frame = 0; frame < layer_config.frames.size(); ++frame) {
append_u32(bytes, layer_config.frames[frame].duration_ms);
for (std::uint32_t face = 0; face < 6U; ++face) {
const PpiDirtyFacePayloadConfig* dirty_face = nullptr;
for (const auto& candidate : config.dirty_faces) {
if (candidate.layer_index == layer && candidate.frame_index == frame
&& candidate.face_index == face) {
dirty_face = &candidate;
break;
}
}
if (dirty_face == nullptr) {
append_u32(bytes, 0);
continue;
}
append_u32(bytes, 1);
append_u32(bytes, dirty_face->x);
append_u32(bytes, dirty_face->y);
append_u32(bytes, dirty_face->x + dirty_face->width);
append_u32(bytes, dirty_face->y + dirty_face->height);
append_u32(bytes, static_cast<std::uint32_t>(dirty_face->png_rgba8.size()));
bytes.insert(bytes.end(), dirty_face->png_rgba8.begin(), dirty_face->png_rgba8.end());
}
}
}
append_u32(bytes, 0);
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(bytes));
}
pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(PpiMinimalProjectConfig config)
{
if (config.layer_count == 0 || config.layer_count > max_ppi_layer_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
}
if (config.frame_count == 0 || config.frame_count > max_ppi_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI frame count is outside the configured range"));
}
std::vector<std::string> names;
names.reserve(config.layer_count);
std::vector<std::vector<PpiFrameConfig>> frame_lists;
frame_lists.reserve(config.layer_count);
std::vector<PpiLayerConfig> layers;
layers.reserve(config.layer_count);
for (std::uint32_t layer = 0; layer < config.layer_count; ++layer) {
names.push_back(generated_layer_name(config.layer_name, layer, config.layer_count));
auto& frames = frame_lists.emplace_back();
frames.assign(config.frame_count, PpiFrameConfig { .duration_ms = config.frame_duration_ms });
layers.push_back(PpiLayerConfig {
.name = names.back(),
.metadata = config.layer_metadata,
.frames = std::span<const PpiFrameConfig>(frames.data(), frames.size()),
});
}
return create_ppi_project(PpiProjectConfig {
.width = config.width,
.height = config.height,
.layers = std::span<const PpiLayerConfig>(layers.data(), layers.size()),
.dirty_faces = config.dirty_faces,
});
}
}

View File

@@ -8,6 +8,7 @@
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace pp::assets {
@@ -118,6 +119,52 @@ struct PpiDecodedProjectImages {
std::vector<PpiDecodedFacePayload> faces;
};
struct PpiDirtyFacePayloadConfig {
std::uint32_t layer_index = 0;
std::uint32_t frame_index = 0;
std::uint32_t face_index = 0;
std::uint32_t x = 0;
std::uint32_t y = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::span<const std::byte> png_rgba8;
};
struct PpiLayerMetadataConfig {
float opacity = 1.0F;
std::uint32_t blend_mode = 0;
bool alpha_locked = false;
bool visible = true;
};
struct PpiFrameConfig {
std::uint32_t duration_ms = 100;
};
struct PpiLayerConfig {
std::string_view name;
PpiLayerMetadataConfig metadata;
std::span<const PpiFrameConfig> frames;
};
struct PpiProjectConfig {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::span<const PpiLayerConfig> layers;
std::span<const PpiDirtyFacePayloadConfig> dirty_faces;
};
struct PpiMinimalProjectConfig {
std::uint32_t width = 0;
std::uint32_t height = 0;
std::string layer_name;
PpiLayerMetadataConfig layer_metadata;
std::uint32_t layer_count = 1;
std::uint32_t frame_count = 1;
std::uint32_t frame_duration_ms = 100;
std::span<const PpiDirtyFacePayloadConfig> dirty_faces;
};
[[nodiscard]] pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(
std::span<const std::byte> bytes) noexcept;
@@ -143,4 +190,10 @@ struct PpiDecodedProjectImages {
[[nodiscard]] pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(
std::span<const std::byte> bytes);
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> create_ppi_project(
PpiProjectConfig config);
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(
PpiMinimalProjectConfig config);
}

View File

@@ -4,8 +4,10 @@
#include "app.h"
#include "texture.h"
#include "node_progress_bar.h"
#include "renderer_gl/opengl_capabilities.h"
#include <thread>
#include <algorithm>
#include <cstdint>
#include <numeric>
#ifdef __APPLE__
@@ -15,6 +17,124 @@
void webgl_sync();
#endif
namespace {
GLint current_canvas_stroke_internal_format()
{
if (ShaderManager::ext_float32_linear)
return static_cast<GLint>(pp::renderer::gl::rgba32f_internal_format());
if (ShaderManager::ext_float16)
return static_cast<GLint>(pp::renderer::gl::rgba16f_internal_format());
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}
GLint rgba8_internal_format()
{
return static_cast<GLint>(pp::renderer::gl::rgba8_internal_format());
}
GLenum texture_2d_target()
{
return static_cast<GLenum>(pp::renderer::gl::texture_2d_target());
}
GLenum rgba_pixel_format()
{
return static_cast<GLenum>(pp::renderer::gl::rgba_pixel_format());
}
GLenum unsigned_byte_component_type()
{
return static_cast<GLenum>(pp::renderer::gl::unsigned_byte_component_type());
}
GLenum viewport_query()
{
return static_cast<GLenum>(pp::renderer::gl::viewport_query());
}
GLenum color_clear_value_query()
{
return static_cast<GLenum>(pp::renderer::gl::color_clear_value_query());
}
GLenum depth_test_state()
{
return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
}
GLenum scissor_test_state()
{
return static_cast<GLenum>(pp::renderer::gl::scissor_test_state());
}
GLenum blend_state()
{
return static_cast<GLenum>(pp::renderer::gl::blend_state());
}
GLenum renderbuffer_target()
{
return static_cast<GLenum>(pp::renderer::gl::renderbuffer_target());
}
GLenum depth_component24_format()
{
return static_cast<GLenum>(pp::renderer::gl::depth_component24_format());
}
GLenum framebuffer_target()
{
return static_cast<GLenum>(pp::renderer::gl::framebuffer_target());
}
GLenum framebuffer_depth_attachment()
{
return static_cast<GLenum>(pp::renderer::gl::framebuffer_depth_attachment());
}
GLint texture_filter_linear()
{
return static_cast<GLint>(pp::renderer::gl::linear_texture_filter());
}
GLint texture_filter_linear_mipmap_linear()
{
return static_cast<GLint>(pp::renderer::gl::linear_mipmap_linear_texture_filter());
}
GLint texture_filter_nearest()
{
return static_cast<GLint>(pp::renderer::gl::nearest_texture_filter());
}
GLint texture_wrap_repeat()
{
return static_cast<GLint>(pp::renderer::gl::repeat_texture_wrap());
}
GLint texture_wrap_clamp_to_border()
{
return static_cast<GLint>(pp::renderer::gl::clamp_to_border_texture_wrap());
}
pp::renderer::gl::OpenGlPixelFormat texture_format_for_image_channels(int channel_count)
{
return pp::renderer::gl::texture_format_for_channel_count(static_cast<std::uint32_t>(channel_count));
}
void set_active_texture_unit(std::uint32_t unit_index)
{
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
}
void unbind_texture_2d()
{
glBindTexture(texture_2d_target(), 0);
}
}
Canvas* Canvas::I;
std::vector<CanvasMode*> Canvas::modes[] = {
@@ -86,7 +206,14 @@ void Canvas::pick_update(int plane)
m_layers_merge.rtt(i).bindFramebuffer();
if (!m_pick_data[plane])
m_pick_data[plane] = std::make_unique<glm::u8vec4[]>(m_width * m_height);
glReadPixels(0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, m_pick_data[plane].get());
glReadPixels(
0,
0,
m_width,
m_height,
rgba_pixel_format(),
unsigned_byte_component_type(),
m_pick_data[plane].get());
m_layers_merge.rtt(i).unbindFramebuffer();
});
@@ -160,9 +287,9 @@ void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz)
m_mixer.bindFramebuffer();
glViewport(0, 0, m_mixer.getWidth(), m_mixer.getHeight());
glDisable(GL_DEPTH_TEST);
glEnable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(depth_test_state());
glEnable(scissor_test_state());
glDisable(blend_state());
glScissor(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
@@ -200,17 +327,17 @@ void Canvas::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2& bb_sz)
ShaderManager::u_int(kShaderUniform::UsePattern, false);
ShaderManager::u_int(kShaderUniform::BlendMode, b->m_blend_mode);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_smask.rtt(plane_index).bindTexture();
m_node->m_face_plane.draw_fill();
m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).unbindTexture();
}
m_sampler.unbind();
@@ -298,7 +425,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
{
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tex[i].bind(); // bg, copy of framebuffer (copied before drawing)
}
@@ -316,7 +443,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
glm::ivec2 tex_sz = glm::clamp(glm::ceil(bb_sz) + pad * 2.f, { 0, 0 }, (glm::vec2)(glm::ivec2(m_width, m_height) - tex_pos));
if (!ShaderManager::ext_framebuffer_fetch)
{
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, tex_pos.x, tex_pos.y,
glCopyTexSubImage2D(texture_2d_target(), 0, tex_pos.x, tex_pos.y,
tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
}
@@ -344,7 +471,7 @@ glm::vec4 Canvas::stroke_draw_samples(int i, std::vector<vertex_t>& P)
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tex[i].unbind();
}
@@ -441,8 +568,8 @@ void Canvas::stroke_draw()
GLint vp[4];
GLfloat cc[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
glGetIntegerv(viewport_query(), vp);
glGetFloatv(color_clear_value_query(), cc);
const auto& brush = m_current_stroke->m_brush;
const auto& dual_brush = m_dual_stroke->m_brush;
@@ -460,7 +587,7 @@ void Canvas::stroke_draw()
if (brush->m_pattern_flipx) patt_scale.x *= -1.f;
if (brush->m_pattern_flipy) patt_scale.y *= -1.f;
glDisable(GL_BLEND);
glDisable(blend_state());
ShaderManager::use(kShader::Stroke);
ShaderManager::u_int(kShaderUniform::Tex, 0); // brush
if (!ShaderManager::ext_framebuffer_fetch)
@@ -485,13 +612,13 @@ void Canvas::stroke_draw()
// DRAW MAIN BRUSH
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
brush->m_tip_texture->bind();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
brush->m_pattern_texture ?
brush->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE3);
unbind_texture_2d();
set_active_texture_unit(3);
m_mixer.bindTexture();
auto frames = stroke_draw_compute(*m_current_stroke);
@@ -532,9 +659,9 @@ void Canvas::stroke_draw()
}
}
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
m_mixer.unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
brush->m_tip_texture->unbind();
// pad stroke
@@ -550,7 +677,7 @@ void Canvas::stroke_draw()
ShaderManager::u_vec4(kShaderUniform::Col, pad_color);
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
ShaderManager::u_int(kShaderUniform::TexBG, 1);
}
for (int i = 0; i < 6; i++)
@@ -584,14 +711,14 @@ void Canvas::stroke_draw()
glm::vec2 sz = glm::min(m_size, zw(b) + pad) - o;
m_tex[i].bind();
if (sz.x > 0 && sz.y > 0)
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, o.x, o.y, o.x, o.y, sz.x, sz.y);
glCopyTexSubImage2D(texture_2d_target(), 0, o.x, o.y, o.x, o.y, sz.x, sz.y);
}
m_brush_shape.draw_fill();
m_tmp[i].unbindFramebuffer();
}
if (!ShaderManager::ext_framebuffer_fetch)
{
glBindTexture(GL_TEXTURE_2D, 0);
unbind_texture_2d();
}
// DRAW DUAL BRUSH
@@ -604,10 +731,10 @@ void Canvas::stroke_draw()
ShaderManager::u_float(kShaderUniform::Wet, 0);
ShaderManager::u_float(kShaderUniform::Noise, 0);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
dual_brush->m_tip_texture ?
dual_brush->m_tip_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
unbind_texture_2d();
auto frames_dual = stroke_draw_compute(*m_dual_stroke);
for (auto& f : frames_dual)
{
@@ -747,9 +874,9 @@ void Canvas::stroke_commit()
// save viewport and clear color states
GLint vp[4];
GLfloat cc[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
GLboolean blend = glIsEnabled(GL_BLEND);
glGetIntegerv(viewport_query(), vp);
glGetFloatv(color_clear_value_query(), cc);
auto blend = glIsEnabled(blend_state());
// allocate action to add to history
auto action = new ActionStroke;
@@ -760,7 +887,7 @@ void Canvas::stroke_commit()
// prepare common states
glViewport(0, 0, m_width, m_height);
glDisable(GL_BLEND);
glDisable(blend_state());
const auto& b = m_current_stroke->m_brush;
@@ -775,7 +902,14 @@ void Canvas::stroke_commit()
// save image before commit
glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
glReadPixels(m_dirty_box[i].x, m_dirty_box[i].y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
glReadPixels(
m_dirty_box[i].x,
m_dirty_box[i].y,
box_sz.x,
box_sz.y,
rgba_pixel_format(),
unsigned_byte_component_type(),
action->m_image[i].get());
action->m_box[i] = m_dirty_box[i];
action->m_old_box[i] = m_layers[m_current_layer_idx]->box(i);
@@ -792,13 +926,13 @@ void Canvas::stroke_commit()
m_layers[m_current_layer_idx]->face(i) = true;
// copy to tmp2 for layer blending
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
m_tex2[i].unbind();
m_tmp[i].bindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tex2[i].bind();
m_sampler.bind(0);
m_sampler_nearest.bind(1);
@@ -815,17 +949,17 @@ void Canvas::stroke_commit()
ShaderManager::u_float(kShaderUniform::Alpha, 1);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].bind();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[i].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_smask.rtt(i).bindTexture();
m_plane.draw_fill();
m_smask.rtt(i).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[i].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].unbind();
}
else
@@ -859,28 +993,28 @@ void Canvas::stroke_commit()
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_pattern_offset);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].bind();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[i].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_smask.rtt(i).bindTexture();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_tmp_dual[i].bindTexture();
glActiveTexture(GL_TEXTURE4);
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
unbind_texture_2d();
m_plane.draw_fill();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_tmp_dual[i].unbindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_smask.rtt(i).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[i].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].unbind();
}
// else
@@ -903,19 +1037,19 @@ void Canvas::stroke_commit()
ShaderManager::use(kShader::StrokeDilate);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
ShaderManager::u_int(kShaderUniform::TexBG, 0);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
m_plane.draw_fill();
m_layers[m_current_layer_idx]->rtt(i).unbindFramebuffer();
}
// restore viewport and clear color states
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
blend ? glEnable(blend_state()) : glDisable(blend_state());
glViewport(vp[0], vp[1], vp[2], vp[3]);
glClearColor(cc[0], cc[1], cc[2], cc[3]);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
// save history
action->m_layer_idx = m_current_layer_idx;
@@ -962,7 +1096,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
use_blend |= Canvas::I->m_current_stroke->m_brush->m_blend_mode != 0;
// if not using shader blend, use gl rasterizer blend
glDisable(GL_DEPTH_TEST);
glDisable(depth_test_state());
for (int plane_index = 0; plane_index < 6; plane_index++)
{
@@ -974,7 +1108,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
if (use_blend)
{
glDisable(GL_BLEND);
glDisable(blend_state());
m_layers_merge.rtt(plane_index).clear();
}
else
@@ -986,7 +1120,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
m_plane.draw_fill();
}
glEnable(GL_BLEND);
glEnable(blend_state());
}
for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
@@ -1018,17 +1152,17 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
//ShaderManager::u_int(kShaderUniform::Lock, m_layers[layer_index]->m_alpha_locked);
ShaderManager::u_int(kShaderUniform::Mask, m_smask_active);
ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_smask.rtt(plane_index).bindTexture();
m_plane.draw_fill();
m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).unbindTexture();
}
else if (m_current_stroke && m_show_tmp && m_current_layer_idx == layer_index)
@@ -1068,28 +1202,28 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_smask.rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_tmp_dual[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE4);
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
unbind_texture_2d();
m_plane.draw_fill();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_tmp_dual[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_tmp[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).unbindTexture();
}
else
@@ -1101,7 +1235,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
ShaderManager::u_int(kShaderUniform::Highlight, m_layers[layer_index]->m_hightlight);
ShaderManager::u_mat4(kShaderUniform::MVP, ortho);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).bindTexture();
m_plane.draw_fill();
m_layers[layer_index]->rtt(plane_index).unbindTexture();
@@ -1129,35 +1263,35 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
ShaderManager::u_int(kShaderUniform::TexBG, 2);
}
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_merge_rtt.bindTexture();
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_merge_tex.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
}
m_plane.draw_fill();
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_merge_tex.unbind();
}
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_merge_rtt.unbindTexture();
}
}
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_merge_tex.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
// draw the grid behind the layers using a temporary copy
if (use_blend)
{
glEnable(GL_BLEND);
glEnable(blend_state());
//draw the grid
if (draw_checkerboard)
@@ -1170,7 +1304,7 @@ void Canvas::draw_merge(bool draw_checkerboard, std::array<bool, 6> faces /*= SI
// draw the layers
m_sampler.bind(0);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_merge_tex.bind();
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
@@ -1319,7 +1453,7 @@ void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
{
// prepare common states
glViewport(0, 0, m_width, m_height);
glDisable(GL_BLEND);
glDisable(blend_state());
for (int i = 0; i < 6; i++)
{
@@ -1336,9 +1470,9 @@ void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
m_layers[dest_idx]->face(i) = true;
// copy to tmp2 for layer blending
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, m_width, m_height);
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, m_width, m_height);
m_tex2[i].unbind();
m_sampler.bind(0);
@@ -1356,13 +1490,13 @@ void Canvas::layer_merge(int source_idx, int dest_idx) // m_layer index
ShaderManager::u_int(kShaderUniform::UseDual, false);
ShaderManager::u_int(kShaderUniform::UsePattern, false);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].bind();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_layers[source_idx]->rtt(i).bindTexture();
m_plane.draw_fill();
m_layers[source_idx]->rtt(i).unbindTexture();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex2[i].unbind();
}
@@ -1571,8 +1705,8 @@ void Canvas::FloodData::apply()
App::I->render_task([&]
{
rtt.bindTexture();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, rtt.getWidth(), rtt.getHeight(),
GL_RGBA, GL_UNSIGNED_BYTE, rgb[plane].get());
glTexSubImage2D(texture_2d_target(), 0, 0, 0, rtt.getWidth(), rtt.getHeight(),
rgba_pixel_format(), unsigned_byte_component_type(), rgb[plane].get());
rtt.unbindTexture();
});
layer->face(plane) = true;
@@ -1588,23 +1722,11 @@ void Canvas::resize(int width, int height)
m_size = { width, height };
for (int i = 0; i < 6; i++)
{
if (ShaderManager::ext_float32_linear)
{
m_tmp[i].create(width, height, -1, GL_RGBA32F);
m_tmp_dual[i].create(width, height, -1, GL_RGBA32F);
}
else if (ShaderManager::ext_float16)
{
m_tmp[i].create(width, height, -1, GL_RGBA16F);
m_tmp_dual[i].create(width, height, -1, GL_RGBA16F);
}
else
{
m_tmp[i].create(width, height, -1, GL_RGBA8);
m_tmp_dual[i].create(width, height, -1, GL_RGBA8);
}
m_tex[i].create(width, height, GL_RGBA8);
m_tex2[i].create(width, height, GL_RGBA8);
const auto stroke_format = current_canvas_stroke_internal_format();
m_tmp[i].create(width, height, -1, stroke_format);
m_tmp_dual[i].create(width, height, -1, stroke_format);
m_tex[i].create(width, height, rgba8_internal_format());
m_tex2[i].create(width, height, rgba8_internal_format());
}
for (auto& l : m_layers)
l->resize(width, height);
@@ -1640,35 +1762,23 @@ bool Canvas::create(int width, int height)
m_size = { width, height };
for (int i = 0; i < 6; i++)
{
if (ShaderManager::ext_float32_linear)
{
m_tmp[i].create(width, height, -1, GL_RGBA32F);
m_tmp_dual[i].create(width, height, -1, GL_RGBA32F);
}
else if (ShaderManager::ext_float16)
{
m_tmp[i].create(width, height, -1, GL_RGBA16F);
m_tmp_dual[i].create(width, height, -1, GL_RGBA16F);
}
else
{
m_tmp[i].create(width, height, -1, GL_RGBA8);
m_tmp_dual[i].create(width, height, -1, GL_RGBA8);
}
m_tex[i].create(width, height, GL_RGBA8);
m_tex2[i].create(width, height, GL_RGBA8);
const auto stroke_format = current_canvas_stroke_internal_format();
m_tmp[i].create(width, height, -1, stroke_format);
m_tmp_dual[i].create(width, height, -1, stroke_format);
m_tex[i].create(width, height, rgba8_internal_format());
m_tex2[i].create(width, height, rgba8_internal_format());
}
#if defined(__GLES__)
m_sampler_brush.create();
#else
m_sampler_brush.create(GL_LINEAR, GL_CLAMP_TO_BORDER);
m_sampler_brush.create(texture_filter_linear(), texture_wrap_clamp_to_border());
#endif
m_sampler.create(GL_LINEAR);
m_sampler_nearest.create(GL_NEAREST);
m_sampler_brush.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
m_sampler.create(texture_filter_linear());
m_sampler_nearest.create(texture_filter_nearest());
m_sampler_brush.set_filter(texture_filter_linear_mipmap_linear(), texture_filter_linear());
m_sampler_brush.set_border({ 1, 1, 1, 1 });
m_sampler_stencil.create(GL_LINEAR, GL_REPEAT);
m_sampler_mix.create(GL_NEAREST, GL_REPEAT);
m_sampler_stencil.create(texture_filter_linear(), texture_wrap_repeat());
m_sampler_mix.create(texture_filter_nearest(), texture_wrap_repeat());
m_sampler_linear.create();
m_plane.create<1>(1, 1);
m_plane_brush.create<1>(1, 1);
@@ -1754,17 +1864,20 @@ void Canvas::import_equirectangular_thread(std::string file_path, std::shared_pt
{
Texture2D tex;
static const GLint indices[] = { 5, 0, 4, 1, 2, 3 };
static const GLint formats[] = { GL_RED, GL_RG, GL_RGB, GL_RGBA };
static const GLint iformats[] = { GL_R8, GL_RG8, GL_RGB8, GL_RGBA8 };
tex.create(img.width, img.width, iformats[img.comp - 1], formats[img.comp - 1]);
const auto texture_format = texture_format_for_image_channels(img.comp);
tex.create(
img.width,
img.width,
static_cast<GLint>(texture_format.internal_format),
static_cast<GLint>(texture_format.pixel_format));
int stride = img.width * img.width * img.comp;
Plane plane;
plane.create<1>(2, 2);
draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
glDisable(GL_DEPTH_TEST);
glDisable(depth_test_state());
tex.update(img.m_data.get() + indices[i] * stride);
m_sampler.bind(0);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
tex.bind();
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
@@ -1782,9 +1895,9 @@ void Canvas::import_equirectangular_thread(std::string file_path, std::shared_pt
Sphere sphere;
sphere.create<64, 64>(2.f);
draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
glDisable(GL_DEPTH_TEST);
glDisable(depth_test_state());
m_sampler.bind(0);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
tex.bind();
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
@@ -1920,8 +2033,8 @@ void Canvas::export_depth_thread(std::string file_name)
rtt.bindFramebuffer();
rtt.clear({ 0, 0, 0, 1 });
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glEnable(blend_state());
glDisable(depth_test_state());
glViewport(0, 0, rtt.getWidth(), rtt.getHeight());
for (int plane_index = 0; plane_index < 6; plane_index++)
{
@@ -1937,7 +2050,7 @@ void Canvas::export_depth_thread(std::string file_name)
ShaderManager::u_int(kShaderUniform::Highlight, false);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers_merge.rtt(plane_index).bindTexture();
m_plane.draw_fill();
m_layers_merge.rtt(plane_index).unbindTexture();
@@ -1955,8 +2068,8 @@ void Canvas::export_depth_thread(std::string file_name)
{
rtt.bindFramebuffer();
rtt.clear({ 0, 0, 0, 1 });
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glEnable(blend_state());
glDisable(depth_test_state());
glViewport(0, 0, rtt.getWidth(), rtt.getHeight());
for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
{
@@ -1978,7 +2091,7 @@ void Canvas::export_depth_thread(std::string file_name)
ShaderManager::u_vec4(kShaderUniform::Col, { glm::vec3((float)(layer_index + 1) / (float)(m_layers.size() + 1)), 1.f });
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).bindTexture();
m_plane.draw_fill();
m_layers[layer_index]->rtt(plane_index).unbindTexture();
@@ -2646,9 +2759,9 @@ Image Canvas::thumbnail_generate(int w, int h)
// save viewport and clear color states
GLint vp[4];
GLfloat cc[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
GLboolean blend = glIsEnabled(GL_BLEND);
glGetIntegerv(viewport_query(), vp);
glGetFloatv(color_clear_value_query(), cc);
auto blend = glIsEnabled(blend_state());
// prepare common states
glViewport(0, 0, w, h);
@@ -2667,7 +2780,7 @@ Image Canvas::thumbnail_generate(int w, int h)
fb.clear({ 1, 1, 1, 0 });
for (int i = 0; i < 6; i++)
{
glDisable(GL_BLEND);
glDisable(blend_state());
auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1));
ShaderManager::use(kShader::TextureBlend);
@@ -2676,7 +2789,7 @@ Image Canvas::thumbnail_generate(int w, int h)
if (!ShaderManager::ext_framebuffer_fetch)
{
ShaderManager::u_int(kShaderUniform::TexBG, 2);
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
blendtex.bind();
m_sampler_nearest.bind(2);
}
@@ -2689,12 +2802,12 @@ Image Canvas::thumbnail_generate(int w, int h)
continue;
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE2);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h);
set_active_texture_unit(2);
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, w, h);
}
ShaderManager::u_int(kShaderUniform::BlendMode, m_layers[layer_index]->m_blend_mode);
ShaderManager::u_float(kShaderUniform::Alpha, m_layers[layer_index]->m_opacity);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_layers[layer_index]->rtt(i).bindTexture();
m_face_plane.draw_fill();
m_layers[layer_index]->rtt(i).unbindTexture();
@@ -2702,14 +2815,14 @@ Image Canvas::thumbnail_generate(int w, int h)
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
blendtex.unbind();
}
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
blendtex.bind();
// copy the content of the fb before drawing the grid
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, w, h);
glCopyTexSubImage2D(texture_2d_target(), 0, 0, 0, 0, 0, w, h);
// draw the grid
ShaderManager::use(kShader::Checkerboard);
@@ -2717,7 +2830,7 @@ Image Canvas::thumbnail_generate(int w, int h)
m_face_plane.draw_fill();
// now blend with the background
glEnable(GL_BLEND);
glEnable(blend_state());
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
@@ -2736,10 +2849,10 @@ Image Canvas::thumbnail_generate(int w, int h)
blendtex.destroy();
// restore viewport and clear color states
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
blend ? glEnable(blend_state()) : glDisable(blend_state());
glViewport(vp[0], vp[1], vp[2], vp[3]);
glClearColor(cc[0], cc[1], cc[2], cc[3]);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
});
return image;
@@ -2779,30 +2892,30 @@ void Canvas::draw_objects_direct(std::function<void(const glm::mat4& camera, con
// save viewport and clear color states
GLint vp[4];
GLfloat cc[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
GLboolean blend = glIsEnabled(GL_BLEND);
glGetIntegerv(viewport_query(), vp);
glGetFloatv(color_clear_value_query(), cc);
auto blend = glIsEnabled(blend_state());
// prepare common states
glViewport(0, 0, layer.w, layer.h);
glDisable(GL_BLEND);
glDisable(blend_state());
GLuint rboID;
glGenRenderbuffers(1, &rboID);
glBindRenderbuffer(GL_RENDERBUFFER, rboID);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, layer.w, layer.h);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindRenderbuffer(renderbuffer_target(), rboID);
glRenderbufferStorage(renderbuffer_target(), depth_component24_format(), layer.w, layer.h);
glBindRenderbuffer(renderbuffer_target(), 0);
glm::mat4 proj = glm::perspective(glm::radians(90.f), 1.f, .01f, 1000.f);
for (int i = 0; i < 6; i++)
{
glm::mat4 plane_camera = glm::lookAt(glm::vec3(0), m_plane_origin[i], m_plane_tangent[i]);
layer.rtt(i, frame).bindFramebuffer();
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID);
glFramebufferRenderbuffer(framebuffer_target(), framebuffer_depth_attachment(), renderbuffer_target(), rboID);
observer(plane_camera, proj, i);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(framebuffer_target(), framebuffer_depth_attachment(), renderbuffer_target(), 0);
layer.rtt(i, frame).unbindFramebuffer();
layer.face(i, frame) = true;
@@ -2812,10 +2925,10 @@ void Canvas::draw_objects_direct(std::function<void(const glm::mat4& camera, con
glDeleteRenderbuffers(1, &rboID);
// restore viewport and clear color states
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
blend ? glEnable(blend_state()) : glDisable(blend_state());
glViewport(vp[0], vp[1], vp[2], vp[3]);
glClearColor(cc[0], cc[1], cc[2], cc[3]);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
});
}
@@ -2826,24 +2939,24 @@ void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm:
// save viewport and clear color states
GLint vp[4];
GLfloat cc[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
GLboolean blend = glIsEnabled(GL_BLEND);
glGetIntegerv(viewport_query(), vp);
glGetFloatv(color_clear_value_query(), cc);
auto blend = glIsEnabled(blend_state());
// prepare common states
glViewport(0, 0, layer.w, layer.h);
glDisable(GL_BLEND);
glDisable(blend_state());
GLuint rboID;
glGenRenderbuffers(1, &rboID);
glBindRenderbuffer(GL_RENDERBUFFER, rboID);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, layer.w, layer.h);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindRenderbuffer(renderbuffer_target(), rboID);
glRenderbufferStorage(renderbuffer_target(), depth_component24_format(), layer.w, layer.h);
glBindRenderbuffer(renderbuffer_target(), 0);
RTT rtt;
rtt.create(layer.w, layer.h);
rtt.bindFramebuffer();
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboID);
glFramebufferRenderbuffer(framebuffer_target(), framebuffer_depth_attachment(), renderbuffer_target(), rboID);
rtt.unbindFramebuffer();
// allocate action to add to history
@@ -2877,7 +2990,7 @@ void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm:
if (has_data)
{
action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
glReadPixels(bounds.x, bounds.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
glReadPixels(bounds.x, bounds.y, box_sz.x, box_sz.y, rgba_pixel_format(), unsigned_byte_component_type(), action->m_image[i].get());
action->m_box[i] = bounds;
}
action->m_old_box[i] = layer.box(i, frame);
@@ -2890,7 +3003,7 @@ void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm:
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-0.5f, 0.5f, -0.5f, 0.5f));
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_sampler_nearest.bind(0);
rtt.bindTexture();
m_plane.draw_fill();
@@ -2917,10 +3030,10 @@ void Canvas::draw_objects(std::function<void(const glm::mat4& camera, const glm:
rtt.destroy();
// restore viewport and clear color states
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
blend ? glEnable(blend_state()) : glDisable(blend_state());
glViewport(vp[0], vp[1], vp[2], vp[3]);
glClearColor(cc[0], cc[1], cc[2], cc[3]);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
});
}

View File

@@ -4,6 +4,7 @@
#include "canvas.h"
#include "canvas_actions.h"
#include "node_panel_layer.h"
#include "renderer_gl/opengl_capabilities.h"
void ActionStroke::undo()
{
@@ -36,8 +37,21 @@ void ActionStroke::undo()
{
App::I->render_task([&]
{
const auto texture_target = pp::renderer::gl::texture_2d_target();
const auto pixel_format = pp::renderer::gl::rgba_pixel_format();
const auto component_type = pp::renderer::gl::unsigned_byte_component_type();
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).bindTexture();
glTexSubImage2D(GL_TEXTURE_2D, 0, (int)m_box[i].x, (int)m_box[i].y, (int)box_sz.x, (int)box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, m_image[i].get());
glTexSubImage2D(
texture_target,
0,
(int)m_box[i].x,
(int)m_box[i].y,
(int)box_sz.x,
(int)box_sz.y,
pixel_format,
component_type,
m_image[i].get());
m_canvas->m_layers[m_layer_idx]->rtt(i, m_frame_idx).unbindTexture();
});
}
@@ -76,11 +90,22 @@ Action* ActionStroke::get_redo()
glm::vec2 box_sz = zw(box) - xy(box);
if (box_sz.x > 0 && box_sz.y > 0 && box_sz.x <= layer->w && box_sz.y <= layer->h)
{
action->m_image[i] = std::make_unique<uint8_t[]>(box_sz.x * box_sz.y * 4);
action->m_image[i] = std::make_unique<uint8_t[]>(
static_cast<size_t>((int)box_sz.x) * static_cast<size_t>((int)box_sz.y) * 4U);
App::I->render_task([&]
{
const auto pixel_format = pp::renderer::gl::rgba_pixel_format();
const auto component_type = pp::renderer::gl::unsigned_byte_component_type();
layer->rtt(i, m_frame_idx).bindFramebuffer();
glReadPixels(box_or.x, box_or.y, box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
glReadPixels(
(int)box_or.x,
(int)box_or.y,
(int)box_sz.x,
(int)box_sz.y,
pixel_format,
component_type,
action->m_image[i].get());
layer->rtt(i, m_frame_idx).unbindFramebuffer();
});
}

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "canvas_layer.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"
#include "rtt.h"
uint32_t Layer::s_count = 0;
@@ -44,7 +45,7 @@ TextureCube Layer::gen_cube()
{
ret.bind();
rtt(i).bindFramebuffer();
glCopyTexSubImage2D(TextureCube::m_faces_map[i], 0, 0, 0, 0, 0, w, w);
glCopyTexSubImage2D(pp::renderer::gl::cube_face_texture_target(i), 0, 0, 0, 0, 0, w, w);
rtt(i).unbindFramebuffer();
});
}
@@ -70,7 +71,7 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
latlong.create(size.x * 4, size.y * 2);
ret.create(size.x * 4, size.y * 2);
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
latlong.bindFramebuffer();
@@ -78,8 +79,8 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
glBindTexture(pp::renderer::gl::texture_cube_map_target(), cube.m_cubetex_id);
ShaderManager::use(kShader::Equirect);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
@@ -88,7 +89,7 @@ Texture2D Layer::gen_equirect(glm::ivec2 size /*= { 0, 0 }*/)
Canvas::I->m_plane.draw_fill();
ret.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, latlong.getWidth(), latlong.getHeight());
glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, 0, 0, 0, 0, latlong.getWidth(), latlong.getHeight());
latlong.unbindFramebuffer();
@@ -115,13 +116,13 @@ PBO Layer::gen_equirect_pbo(glm::ivec2 size /*= { 0, 0 }*/)
App::I->render_task([&]
{
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
latlong.bindFramebuffer();
glViewport(0, 0, latlong.getWidth(), latlong.getHeight());
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, cube.m_cubetex_id);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
glBindTexture(pp::renderer::gl::texture_cube_map_target(), cube.m_cubetex_id);
ShaderManager::use(kShader::Equirect);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f));
@@ -458,7 +459,7 @@ void LayerFrame::clear(const glm::vec4& c)
{
// push clear color state
GLfloat cc[4];
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
glClearColor(c.r, c.g, c.b, c.a);
bool erase = (c.a == 0.f);
@@ -466,7 +467,7 @@ void LayerFrame::clear(const glm::vec4& c)
for (int i = 0; i < 6; i++)
{
m_rtt[i].bindFramebuffer();
glClear(GL_COLOR_BUFFER_BIT);
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
m_rtt[i].unbindFramebuffer();
if (erase)
@@ -530,9 +531,11 @@ void LayerFrame::restore(const Snapshot& snap)
m_rtt[i].bindTexture();
glm::vec2 box_sz = zw(m_dirty_box[i]) - xy(m_dirty_box[i]);
glTexSubImage2D(GL_TEXTURE_2D, 0,
m_dirty_box[i].x, m_dirty_box[i].y,
box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE,
glTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0,
static_cast<int>(m_dirty_box[i].x), static_cast<int>(m_dirty_box[i].y),
static_cast<int>(box_sz.x), static_cast<int>(box_sz.y),
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
snap.image[i].get());
m_rtt[i].unbindTexture();
LOG("restore face %d - %d bytes (%dx%d)", i,
@@ -560,8 +563,11 @@ LayerFrame::Snapshot LayerFrame::snapshot(std::array<glm::vec4, 6>* dirty_box /*
m_rtt[i].bindFramebuffer();
glm::vec2 box_sz = zw(snap.m_dirty_box[i]) - xy(snap.m_dirty_box[i]);
glReadPixels(snap.m_dirty_box[i].x, snap.m_dirty_box[i].y,
box_sz.x, box_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, snap.image[i].get());
glReadPixels(static_cast<int>(snap.m_dirty_box[i].x), static_cast<int>(snap.m_dirty_box[i].y),
static_cast<int>(box_sz.x), static_cast<int>(box_sz.y),
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
snap.image[i].get());
m_rtt[i].unbindFramebuffer();
}
});

View File

@@ -1,4 +1,7 @@
#include "pch.h"
#include <cstdint>
#include "log.h"
#include "canvas_modes.h"
#include "layout.h"
@@ -7,9 +10,19 @@
#include "node_canvas.h"
#include "app.h"
#include "util.h"
#include "renderer_gl/opengl_capabilities.h"
NodeCanvas* CanvasMode::node;
namespace {
void set_active_texture_unit(std::uint32_t unit_index)
{
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
}
}
void CanvasModeBasicCamera::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
{
switch (me->m_type)
@@ -292,7 +305,10 @@ void CanvasModePen::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const
}
glReadPixels((pos.x / App::I->width) * fb_width,
((App::I->height - pos.y - 1) / App::I->height) * fb_height,
1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
1, 1,
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
&pixel);
bool outline = glm::min(tip_scale.x, tip_scale.y) < 20 || m_resizing ? false : m_draw_outline;
ShaderManager::u_int(kShaderUniform::DrawOutline, outline);
ShaderManager::u_vec4(kShaderUniform::Col, outline ? glm::vec4(1.f - glm::vec3(pixel) / 255.f, 1.f) : tip_color);
@@ -303,15 +319,15 @@ void CanvasModePen::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const
glm::eulerAngleZ(tip_angle) *
glm::scale(glm::vec3(tip_scale, 1))
);
bool blend = glIsEnabled(GL_BLEND);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
bool blend = glIsEnabled(pp::renderer::gl::blend_state());
glEnable(pp::renderer::gl::blend_state());
set_active_texture_unit(0);
auto& tex = *brush->m_tip_texture;
tex.bind();
Canvas::I->m_sampler_brush.bind(0);
Canvas::I->m_plane.draw_fill();
tex.unbind();
if (!blend) glDisable(GL_BLEND);
if (!blend) glDisable(pp::renderer::gl::blend_state());
}
}
@@ -409,15 +425,15 @@ void CanvasModeLine::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, cons
glm::eulerAngleZ(tip_angle) *
glm::scale(glm::vec3(tip_scale, 1))
);
bool blend = glIsEnabled(GL_BLEND);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
bool blend = glIsEnabled(pp::renderer::gl::blend_state());
glEnable(pp::renderer::gl::blend_state());
set_active_texture_unit(0);
auto& tex = *brush->m_tip_texture;
tex.bind();
Canvas::I->m_sampler_brush.bind(0);
Canvas::I->m_plane.draw_fill();
tex.unbind();
if (!blend) glDisable(GL_BLEND);
if (!blend) glDisable(pp::renderer::gl::blend_state());
}
}
@@ -704,8 +720,8 @@ void CanvasModeMaskFree::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
m_selection_cam = Canvas::I->get_camera();
//m_points2d = poly_intersect(poly_remove_duplicate(m_points2d), Canvas::I->face_to_shape2D(0));
auto drawer = [this](const glm::mat4& camera, const glm::mat4& proj) {
//glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
// blending state intentionally left unchanged here.
glDisable(pp::renderer::gl::depth_test_state());
ShaderManager::use(kShader::Color);
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
ShaderManager::u_vec4(kShaderUniform::Col,
@@ -783,8 +799,8 @@ void CanvasModeMaskFree::on_MouseEvent(MouseEvent* me, glm::vec2& loc)
void CanvasModeMaskFree::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const glm::mat4& camera)
{
bool depth = glIsEnabled(GL_DEPTH_TEST);
glDisable(GL_DEPTH_TEST);
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
glDisable(pp::renderer::gl::depth_test_state());
if (m_points.size() > 3)
{
if (m_dragging)
@@ -803,7 +819,7 @@ void CanvasModeMaskFree::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
// m_shape.draw_stroke();
//}
}
if (depth) glEnable(GL_DEPTH_TEST);
if (depth) glEnable(pp::renderer::gl::depth_test_state());
}
@@ -840,7 +856,7 @@ void CanvasModeMaskLine::leave(kCanvasMode next)
if (!m_points.empty())
{
auto drawer = [this](const glm::mat4& camera, const glm::mat4& proj) {
//glEnable(GL_BLEND);
// blending state intentionally left unchanged here.
ShaderManager::use(kShader::Color);
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera);
ShaderManager::u_vec4(kShaderUniform::Col, {1, 1, 1, 1});
@@ -1248,7 +1264,9 @@ void CanvasModeTransform::enter(kCanvasMode prev)
Canvas::I->m_layers[Canvas::I->m_current_layer_idx]->rtt(plane).bindFramebuffer();
m_tex[plane].create(bb_sz.x, bb_sz.y);
m_tex[plane].bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
glCopyTexSubImage2D(
pp::renderer::gl::texture_2d_target(),
0, 0, 0, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
m_tex[plane].unbind();
Canvas::I->m_layers[Canvas::I->m_current_layer_idx]->rtt(plane).unbindFramebuffer();
});
@@ -1307,15 +1325,22 @@ void CanvasModeTransform::enter(kCanvasMode prev)
App::I->render_task([&]
{
glViewport(0, 0, layer->w, layer->h);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
glDisable(pp::renderer::gl::depth_test_state());
glDisable(pp::renderer::gl::blend_state());
set_active_texture_unit(0);
ShaderManager::use(kShader::Color);
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
ShaderManager::u_vec4(kShaderUniform::Col, { 0, 0, 0, 0 });
layer->rtt(i).bindFramebuffer();
// copy framebuffer to action data
glReadPixels(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
glReadPixels(
bb_min.x,
bb_min.y,
bb_sz.x,
bb_sz.y,
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
action->m_image[i].get());
for (int j = 0; j < 6; j++)
m_shape[j].draw_fill();
layer->rtt(i).unbindFramebuffer();
@@ -1407,19 +1432,28 @@ void CanvasModeTransform::leave(kCanvasMode next)
{
layer->rtt(i).bindFramebuffer();
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
glDisable(pp::renderer::gl::depth_test_state());
glDisable(pp::renderer::gl::blend_state());
set_active_texture_unit(0);
glViewport(0, 0, layer->rtt(i).getWidth(), layer->rtt(i).getHeight());
// save fb content for history
glReadPixels(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y, GL_RGBA, GL_UNSIGNED_BYTE, action->m_image[i].get());
glReadPixels(
bb_min.x,
bb_min.y,
bb_sz.x,
bb_sz.y,
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
action->m_image[i].get());
// copy fb content to texture for blending
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
Canvas::I->m_tex2[i].bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, bb_min.x, bb_min.y, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
glCopyTexSubImage2D(
pp::renderer::gl::texture_2d_target(),
0, bb_min.x, bb_min.y, bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
// slot for m_tex
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
for (int j = 0; j < 6; j++)
{
ShaderManager::use(kShader::CompDraw);
@@ -1456,10 +1490,10 @@ void CanvasModeTransform::leave(kCanvasMode next)
void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj, const glm::mat4& camera)
{
bool depth = glIsEnabled(GL_DEPTH_TEST);
glDisable(GL_DEPTH_TEST);
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
glDisable(pp::renderer::gl::depth_test_state());
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
for (int i = 0; i < 6; i++)
{
ShaderManager::use(kShader::Color);
@@ -1470,7 +1504,7 @@ void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, proj * camera * m_xform * m_xform_local);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_tex[i].bind();
Canvas::I->m_sampler_linear.bind(0);
m_shape[i].draw_fill();
@@ -1499,7 +1533,7 @@ void CanvasModeTransform::on_Draw(const glm::mat4& ortho, const glm::mat4& proj,
m_circle.draw_stroke();
}
if (depth) glEnable(GL_DEPTH_TEST);
if (depth) glEnable(pp::renderer::gl::depth_test_state());
}
void CanvasModeTransform::on_MouseEvent(MouseEvent* me, glm::vec2& loc)

View File

@@ -1,8 +1,10 @@
#include "document/document.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <string>
#include <utility>
namespace pp::document {
@@ -107,37 +109,70 @@ namespace {
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
[[nodiscard]] pp::foundation::Result<std::size_t> byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
std::uint32_t height,
std::uint32_t components,
const char* dimensions_overflow_message,
const char* byte_size_overflow_message,
const char* payload_limit_message,
const char* addressable_memory_message) noexcept
{
const auto width64 = static_cast<std::uint64_t>(width);
const auto height64 = static_cast<std::uint64_t>(height);
if (width64 > std::numeric_limits<std::uint64_t>::max() / height64) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel dimensions overflow"));
pp::foundation::Status::out_of_range(dimensions_overflow_message));
}
const auto pixels = width64 * height64;
if (pixels > std::numeric_limits<std::uint64_t>::max() / rgba8_components) {
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel byte size overflows"));
pp::foundation::Status::out_of_range(byte_size_overflow_message));
}
const auto bytes = pixels * rgba8_components;
const auto bytes = pixels * components;
if (bytes > max_face_pixel_payload_bytes) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel payload exceeds the configured limit"));
pp::foundation::Status::out_of_range(payload_limit_message));
}
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel payload exceeds addressable memory"));
pp::foundation::Status::out_of_range(addressable_memory_message));
}
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
{
return byte_size(
width,
height,
rgba8_components,
"face pixel dimensions overflow",
"face pixel byte size overflows",
"face pixel payload exceeds the configured limit",
"face pixel payload exceeds addressable memory");
}
[[nodiscard]] pp::foundation::Result<std::size_t> alpha8_byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
{
return byte_size(
width,
height,
alpha8_components,
"selection mask dimensions overflow",
"selection mask byte size overflows",
"selection mask payload exceeds the configured limit",
"selection mask payload exceeds addressable memory");
}
[[nodiscard]] pp::foundation::Status validate_face_pixels(
LayerFacePixels pixels,
std::uint32_t document_width,
@@ -168,6 +203,60 @@ namespace {
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_selection_mask(
SelectionMask mask,
std::uint32_t document_width,
std::uint32_t document_height) noexcept
{
if (mask.face_index >= cube_face_count) {
return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
}
if (mask.width == 0 || mask.height == 0) {
return pp::foundation::Status::invalid_argument("selection mask dimensions must be greater than zero");
}
if (mask.x > document_width || mask.width > document_width - mask.x
|| mask.y > document_height || mask.height > document_height - mask.y) {
return pp::foundation::Status::out_of_range("selection mask rectangle is outside the document");
}
const auto expected_bytes = alpha8_byte_size(mask.width, mask.height);
if (!expected_bytes) {
return expected_bytes.status();
}
if (mask.alpha8.size() != expected_bytes.value()) {
return pp::foundation::Status::invalid_argument("selection mask byte size does not match dimensions");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_frame_face_pixels(
std::span<const AnimationFrame> frames,
std::uint32_t document_width,
std::uint32_t document_height) noexcept
{
for (const auto& frame : frames) {
std::array<bool, cube_face_count> seen_faces {};
for (const auto& pixels : frame.face_pixels) {
const auto pixels_status = validate_face_pixels(pixels, document_width, document_height);
if (!pixels_status.ok()) {
return pixels_status;
}
if (seen_faces[pixels.face_index]) {
return pp::foundation::Status::invalid_argument(
"snapshot contains duplicate face pixel payloads for a cube face");
}
seen_faces[pixels.face_index] = true;
}
}
return pp::foundation::Status::success();
}
}
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
@@ -244,6 +333,11 @@ pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(Docu
pp::foundation::Status::out_of_range("document layer frame count exceeds the configured limit"));
}
const auto face_pixels_status = validate_frame_face_pixels(layer_frames, config.width, config.height);
if (!face_pixels_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(face_pixels_status);
}
for (const auto& frame_config : layer_frames) {
const auto duration_status = validate_frame_duration(frame_config.duration_ms);
if (!duration_status.ok()) {
@@ -269,9 +363,33 @@ pp::foundation::Result<CanvasDocument> CanvasDocument::create_from_snapshot(Docu
return pp::foundation::Result<CanvasDocument>::failure(duration_status);
}
const auto face_pixels_status = validate_frame_face_pixels(
std::span<const AnimationFrame>(&frame_config, 1),
config.width,
config.height);
if (!face_pixels_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(face_pixels_status);
}
document.frames_.push_back(frame_config);
}
std::array<bool, cube_face_count> seen_selection_masks {};
for (const auto& mask : config.selection_masks) {
const auto mask_status = validate_selection_mask(mask, document.width_, document.height_);
if (!mask_status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(mask_status);
}
if (seen_selection_masks[mask.face_index]) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::invalid_argument(
"snapshot contains duplicate selection masks for a cube face"));
}
seen_selection_masks[mask.face_index] = true;
document.selection_masks_.push_back(mask);
}
return pp::foundation::Result<CanvasDocument>::success(document);
}
@@ -325,6 +443,11 @@ std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
return count;
}
std::size_t CanvasDocument::selection_mask_payload_count() const noexcept
{
return selection_masks_.size();
}
std::span<const Layer> CanvasDocument::layers() const noexcept
{
return layers_;
@@ -335,6 +458,11 @@ std::span<const AnimationFrame> CanvasDocument::frames() const noexcept
return frames_;
}
std::span<const SelectionMask> CanvasDocument::selection_masks() const noexcept
{
return selection_masks_;
}
pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view name)
{
if (layers_.size() >= max_layer_count) {
@@ -656,6 +784,47 @@ pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels(
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_selection_mask(SelectionMask mask)
{
const auto mask_status = validate_selection_mask(mask, width_, height_);
if (!mask_status.ok()) {
return mask_status;
}
const auto existing = std::find_if(
selection_masks_.begin(),
selection_masks_.end(),
[face_index = mask.face_index](const SelectionMask& candidate) {
return candidate.face_index == face_index;
});
if (existing == selection_masks_.end()) {
selection_masks_.push_back(std::move(mask));
} else {
*existing = std::move(mask);
}
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::clear_selection_mask(std::uint32_t face_index) noexcept
{
if (face_index >= cube_face_count) {
return pp::foundation::Status::out_of_range("selection mask cube face index is outside the document");
}
const auto existing = std::find_if(
selection_masks_.begin(),
selection_masks_.end(),
[face_index](const SelectionMask& candidate) {
return candidate.face_index == face_index;
});
if (existing == selection_masks_.end()) {
return pp::foundation::Status::out_of_range("selection mask face is not present");
}
selection_masks_.erase(existing);
return pp::foundation::Status::success();
}
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
CanvasDocument initial_document,
std::size_t max_entries)

View File

@@ -20,6 +20,7 @@ constexpr std::size_t max_document_history_entries = 10000;
constexpr std::size_t max_layer_name_length = 128;
constexpr std::uint32_t cube_face_count = 6;
constexpr std::uint32_t rgba8_components = 4;
constexpr std::uint32_t alpha8_components = 1;
constexpr std::uint64_t max_face_pixel_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
struct DocumentConfig {
@@ -37,6 +38,15 @@ struct LayerFacePixels {
std::vector<std::uint8_t> rgba8;
};
struct SelectionMask {
std::uint32_t face_index = 0;
std::uint32_t x = 0;
std::uint32_t y = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::vector<std::uint8_t> alpha8;
};
struct AnimationFrame {
std::uint32_t duration_ms = 100;
std::vector<LayerFacePixels> face_pixels;
@@ -65,6 +75,7 @@ struct DocumentSnapshotConfig {
std::uint32_t height = 0;
std::span<const DocumentLayerConfig> layers;
std::span<const AnimationFrame> frames;
std::span<const SelectionMask> selection_masks;
};
class CanvasDocument {
@@ -79,8 +90,10 @@ public:
[[nodiscard]] std::uint64_t animation_duration_ms() const noexcept;
[[nodiscard]] pp::foundation::Result<std::uint64_t> layer_animation_duration_ms(std::size_t index) const noexcept;
[[nodiscard]] std::size_t face_pixel_payload_count() const noexcept;
[[nodiscard]] std::size_t selection_mask_payload_count() const noexcept;
[[nodiscard]] std::span<const Layer> layers() const noexcept;
[[nodiscard]] std::span<const AnimationFrame> frames() const noexcept;
[[nodiscard]] std::span<const SelectionMask> selection_masks() const noexcept;
[[nodiscard]] pp::foundation::Result<std::size_t> add_layer(std::string_view name);
[[nodiscard]] pp::foundation::Status remove_layer(std::size_t index);
@@ -102,6 +115,8 @@ public:
std::size_t layer_index,
std::size_t frame_index,
LayerFacePixels pixels);
[[nodiscard]] pp::foundation::Status set_selection_mask(SelectionMask mask);
[[nodiscard]] pp::foundation::Status clear_selection_mask(std::uint32_t face_index) noexcept;
private:
std::uint32_t width_ = 0;
@@ -110,6 +125,7 @@ private:
std::size_t active_frame_index_ = 0;
std::vector<Layer> layers_;
std::vector<AnimationFrame> frames_;
std::vector<SelectionMask> selection_masks_;
};
class DocumentHistory {

107
src/document/ppi_export.cpp Normal file
View File

@@ -0,0 +1,107 @@
#include "document/ppi_export.h"
#include "assets/image_pixels.h"
#include "assets/ppi_header.h"
#include <cstdint>
#include <span>
#include <utility>
namespace pp::document {
namespace {
[[nodiscard]] pp::foundation::Result<std::uint32_t> ppi_blend_mode(
pp::paint::BlendMode blend_mode) noexcept
{
switch (blend_mode) {
case pp::paint::BlendMode::normal:
return pp::foundation::Result<std::uint32_t>::success(0);
case pp::paint::BlendMode::multiply:
return pp::foundation::Result<std::uint32_t>::success(1);
case pp::paint::BlendMode::screen:
return pp::foundation::Result<std::uint32_t>::success(2);
case pp::paint::BlendMode::color_dodge:
return pp::foundation::Result<std::uint32_t>::success(3);
case pp::paint::BlendMode::overlay:
return pp::foundation::Result<std::uint32_t>::success(4);
}
return pp::foundation::Result<std::uint32_t>::failure(
pp::foundation::Status::invalid_argument("document layer blend mode cannot be exported to PPI"));
}
}
pp::foundation::Result<std::vector<std::byte>> export_ppi_project_document(
const CanvasDocument& document)
{
std::vector<std::vector<pp::assets::PpiFrameConfig>> frame_configs;
frame_configs.reserve(document.layers().size());
std::vector<pp::assets::PpiLayerConfig> layer_configs;
layer_configs.reserve(document.layers().size());
std::vector<std::vector<std::byte>> payloads;
payloads.reserve(document.face_pixel_payload_count());
std::vector<pp::assets::PpiDirtyFacePayloadConfig> dirty_faces;
dirty_faces.reserve(document.face_pixel_payload_count());
for (std::size_t layer_index = 0; layer_index < document.layers().size(); ++layer_index) {
const auto& layer = document.layers()[layer_index];
const auto blend_mode = ppi_blend_mode(layer.blend_mode);
if (!blend_mode) {
return pp::foundation::Result<std::vector<std::byte>>::failure(blend_mode.status());
}
auto& frames = frame_configs.emplace_back();
frames.reserve(layer.frames.size());
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
const auto& frame = layer.frames[frame_index];
frames.push_back(pp::assets::PpiFrameConfig {
.duration_ms = frame.duration_ms,
});
for (const auto& face : frame.face_pixels) {
const auto encoded = pp::assets::encode_png_rgba8(
face.width,
face.height,
face.rgba8);
if (!encoded) {
return pp::foundation::Result<std::vector<std::byte>>::failure(encoded.status());
}
payloads.push_back(encoded.value());
const auto& payload = payloads.back();
dirty_faces.push_back(pp::assets::PpiDirtyFacePayloadConfig {
.layer_index = static_cast<std::uint32_t>(layer_index),
.frame_index = static_cast<std::uint32_t>(frame_index),
.face_index = face.face_index,
.x = face.x,
.y = face.y,
.width = face.width,
.height = face.height,
.png_rgba8 = std::span<const std::byte>(payload.data(), payload.size()),
});
}
}
layer_configs.push_back(pp::assets::PpiLayerConfig {
.name = layer.name,
.metadata = pp::assets::PpiLayerMetadataConfig {
.opacity = layer.opacity,
.blend_mode = blend_mode.value(),
.alpha_locked = layer.alpha_locked,
.visible = layer.visible,
},
.frames = std::span<const pp::assets::PpiFrameConfig>(frames.data(), frames.size()),
});
}
return pp::assets::create_ppi_project(pp::assets::PpiProjectConfig {
.width = document.width(),
.height = document.height(),
.layers = std::span<const pp::assets::PpiLayerConfig>(layer_configs.data(), layer_configs.size()),
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(dirty_faces.data(), dirty_faces.size()),
});
}
}

14
src/document/ppi_export.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include "document/document.h"
#include "foundation/result.h"
#include <cstddef>
#include <vector>
namespace pp::document {
[[nodiscard]] pp::foundation::Result<std::vector<std::byte>> export_ppi_project_document(
const CanvasDocument& document);
}

View File

@@ -79,6 +79,7 @@ namespace {
.height = project.body.summary.height,
.layers = layers,
.frames = frames,
.selection_masks = {},
});
}

View File

@@ -5,6 +5,152 @@
#include "asset.h"
#include "util.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"
#include <array>
#include <cstddef>
#include <cstdint>
namespace {
[[nodiscard]] GLint font_atlas_internal_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).internal_format);
}
[[nodiscard]] GLint font_atlas_pixel_format() noexcept
{
return static_cast<GLint>(pp::renderer::gl::texture_format_for_channel_count(1U).pixel_format);
}
[[nodiscard]] GLenum texture_unit(std::uint32_t unit_index) noexcept
{
return static_cast<GLenum>(pp::renderer::gl::active_texture_unit(unit_index));
}
void gen_buffers_adapter(std::uint32_t count, std::uint32_t* ids) noexcept
{
glGenBuffers(static_cast<GLsizei>(count), ids);
}
void bind_buffer_adapter(std::uint32_t target, std::uint32_t buffer) noexcept
{
glBindBuffer(static_cast<GLenum>(target), static_cast<GLuint>(buffer));
}
void buffer_data_adapter(
std::uint32_t target,
std::intptr_t byte_count,
const void* data,
std::uint32_t usage) noexcept
{
glBufferData(static_cast<GLenum>(target), static_cast<GLsizeiptr>(byte_count), data, static_cast<GLenum>(usage));
}
void gen_vertex_arrays_adapter(std::uint32_t count, std::uint32_t* ids) noexcept
{
glGenVertexArrays(static_cast<GLsizei>(count), ids);
}
void bind_vertex_array_adapter(std::uint32_t vertex_array) noexcept
{
glBindVertexArray(static_cast<GLuint>(vertex_array));
}
void enable_vertex_attrib_array_adapter(std::uint32_t index) noexcept
{
glEnableVertexAttribArray(static_cast<GLuint>(index));
}
void vertex_attrib_pointer_adapter(
std::uint32_t index,
std::int32_t component_count,
std::uint32_t component_type,
std::uint8_t normalized,
std::int32_t stride,
const void* offset) noexcept
{
glVertexAttribPointer(
static_cast<GLuint>(index),
static_cast<GLint>(component_count),
static_cast<GLenum>(component_type),
static_cast<GLboolean>(normalized),
static_cast<GLsizei>(stride),
offset);
}
void draw_elements_adapter(
std::uint32_t mode,
std::int32_t count,
std::uint32_t index_type,
const void* index_offset) noexcept
{
glDrawElements(
static_cast<GLenum>(mode),
static_cast<GLsizei>(count),
static_cast<GLenum>(index_type),
index_offset);
}
void draw_arrays_adapter(std::uint32_t mode, std::int32_t first, std::int32_t count) noexcept
{
glDrawArrays(static_cast<GLenum>(mode), static_cast<GLint>(first), static_cast<GLsizei>(count));
}
[[nodiscard]] std::span<const pp::renderer::gl::OpenGlVertexAttribute> text_mesh_vertex_attributes() noexcept
{
static const std::array<pp::renderer::gl::OpenGlVertexAttribute, 2> attributes {
pp::renderer::gl::OpenGlVertexAttribute {
.index = 0U,
.component_count = 2,
.component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
.normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
.stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
.offset = 0U,
},
pp::renderer::gl::OpenGlVertexAttribute {
.index = 1U,
.component_count = 2,
.component_type = pp::renderer::gl::vertex_attribute_float_component_type(),
.normalized = static_cast<std::uint8_t>(pp::renderer::gl::vertex_attribute_not_normalized()),
.stride = static_cast<std::int32_t>(sizeof(glm::vec4)),
.offset = static_cast<std::uintptr_t>(sizeof(float) * 2),
},
};
return attributes;
}
[[nodiscard]] pp::renderer::gl::OpenGlMeshCreateDispatch text_mesh_create_dispatch() noexcept
{
return pp::renderer::gl::OpenGlMeshCreateDispatch {
.gen_buffers = gen_buffers_adapter,
.bind_buffer = bind_buffer_adapter,
.buffer_data = buffer_data_adapter,
.gen_vertex_arrays = gen_vertex_arrays_adapter,
.bind_vertex_array = bind_vertex_array_adapter,
.enable_vertex_attrib_array = enable_vertex_attrib_array_adapter,
.vertex_attrib_pointer = vertex_attrib_pointer_adapter,
};
}
[[nodiscard]] pp::renderer::gl::OpenGlBufferUploadDispatch text_buffer_upload_dispatch() noexcept
{
return pp::renderer::gl::OpenGlBufferUploadDispatch {
.bind_buffer = bind_buffer_adapter,
.buffer_data = buffer_data_adapter,
};
}
[[nodiscard]] pp::renderer::gl::OpenGlMeshDrawDispatch text_mesh_draw_dispatch() noexcept
{
return pp::renderer::gl::OpenGlMeshDrawDispatch {
.bind_vertex_array = bind_vertex_array_adapter,
.draw_elements = draw_elements_adapter,
.draw_arrays = draw_arrays_adapter,
};
}
}
std::map<std::string, Font> FontManager::m_fonts;
Sampler FontManager::m_sampler;
@@ -52,7 +198,7 @@ bool Font::load(const std::string& ttf, int font_size, float font_scale)
// offset = 0;
stbtt_BakeFontBitmap(file.m_data, 0, (float)font_size*scale, bitmap.get(), w, h, start_char, num_chars, chars.data());
calc_bounds();
font_tex.create(w, h, GL_R8, GL_RED, bitmap.get());
font_tex.create(w, h, font_atlas_internal_format(), font_atlas_pixel_format(), bitmap.get());
file.close();
size = font_size;
return true;
@@ -155,18 +301,22 @@ bool TextMesh::create()
{
App::I->render_task([this]
{
glGenBuffers(2, font_buffers);
#if USE_VBO
glGenVertexArrays(1, &font_array);
glBindVertexArray(font_array);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)(sizeof(float) * 2));
glBindVertexArray(0);
#endif // USE_VBO
const auto mesh = pp::renderer::gl::create_opengl_mesh_objects(
pp::renderer::gl::OpenGlMeshUpload {
.vertex_data = nullptr,
.vertex_byte_count = 0,
.index_data = nullptr,
.index_byte_count = 0,
.indexed = true,
.vertex_array_count = 1U,
.attributes = text_mesh_vertex_attributes(),
},
text_mesh_create_dispatch());
if (mesh.ok()) {
font_buffers[0] = static_cast<GLuint>(mesh.value().vertex_buffer);
font_buffers[1] = static_cast<GLuint>(mesh.value().index_buffer);
font_array = static_cast<GLuint>(mesh.value().vertex_arrays[0]);
}
});
return true;
}
@@ -254,12 +404,24 @@ void TextMesh::update(const std::string& text, const std::string& font, int size
font_array_count = (int)idx.size();
App::I->render_task([&]
{
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size() * sizeof(GLushort), idx.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
glBufferData(GL_ARRAY_BUFFER, v.size() * sizeof(glm::vec4), v.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
(void)pp::renderer::gl::upload_opengl_buffer_data(
pp::renderer::gl::OpenGlBufferUpload {
.target = pp::renderer::gl::element_array_buffer_target(),
.buffer_id = font_buffers[1],
.data = idx.data(),
.byte_count = static_cast<std::intptr_t>(idx.size() * sizeof(GLushort)),
.usage = pp::renderer::gl::static_draw_buffer_usage(),
},
text_buffer_upload_dispatch());
(void)pp::renderer::gl::upload_opengl_buffer_data(
pp::renderer::gl::OpenGlBufferUpload {
.target = pp::renderer::gl::array_buffer_target(),
.buffer_id = font_buffers[0],
.data = v.data(),
.byte_count = static_cast<std::intptr_t>(v.size() * sizeof(glm::vec4)),
.usage = pp::renderer::gl::static_draw_buffer_usage(),
},
text_buffer_upload_dispatch());
});
}
}
@@ -269,27 +431,20 @@ void TextMesh::draw()
auto& f = FontManager::get(font, size, weight, italic);
if (f.font_tex.ready())
{
glActiveTexture(GL_TEXTURE0);
glActiveTexture(texture_unit(0U));
f.font_tex.bind();
FontManager::m_sampler.bind(0);
#if USE_VBO
glBindVertexArray(font_array);
glDrawElements(GL_TRIANGLES, font_array_count, GL_UNSIGNED_SHORT, 0);
glBindVertexArray(0);
#else
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, font_buffers[1]);
glBindBuffer(GL_ARRAY_BUFFER, font_buffers[0]);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec4), (GLvoid*)(sizeof(float) * 2));
glDrawElements(GL_TRIANGLES, font_array_count, GL_UNSIGNED_SHORT, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
#endif // USE_VBO
(void)pp::renderer::gl::draw_opengl_mesh(
pp::renderer::gl::OpenGlMeshDraw {
.vertex_array = font_array,
.mode = pp::renderer::gl::primitive_mode_for_fill_count(3U),
.count = font_array_count,
.indexed = true,
.index_type = pp::renderer::gl::index_type_for_index_size(sizeof(GLushort)),
.index_offset = nullptr,
},
text_mesh_draw_dispatch());
f.font_tex.unbind();
FontManager::m_sampler.unbind();

View File

@@ -1,7 +1,28 @@
#include "foundation/binary_stream.h"
#include <cstdint>
namespace pp::foundation {
namespace {
[[nodiscard]] bool overlaps_backing_storage(
const std::vector<std::byte>& backing,
std::span<const std::byte> bytes) noexcept
{
if (backing.empty() || bytes.empty()) {
return false;
}
const auto backing_begin = reinterpret_cast<std::uintptr_t>(backing.data());
const auto backing_end = backing_begin + backing.size();
const auto bytes_begin = reinterpret_cast<std::uintptr_t>(bytes.data());
const auto bytes_end = bytes_begin + bytes.size();
return bytes_begin < backing_end && backing_begin < bytes_end;
}
}
ByteReader::ByteReader(std::span<const std::byte> bytes) noexcept
: bytes_(bytes)
{
@@ -135,6 +156,12 @@ Status ByteWriter::write_bytes(std::span<const std::byte> bytes)
return Status::invalid_argument("writer has no backing storage");
}
if (overlaps_backing_storage(*bytes_, bytes)) {
const std::vector<std::byte> copy(bytes.begin(), bytes.end());
bytes_->insert(bytes_->end(), copy.begin(), copy.end());
return Status::success();
}
bytes_->insert(bytes_->end(), bytes.begin(), bytes.end());
return Status::success();
}

View File

@@ -1,5 +1,7 @@
#pragma once
#include <utility>
namespace pp::foundation {
enum class StatusCode {
@@ -38,7 +40,7 @@ class Result {
public:
[[nodiscard]] static constexpr Result success(T value) noexcept
{
return Result(value, Status::success());
return Result(std::move(value), Status::success());
}
[[nodiscard]] static constexpr Result failure(Status status) noexcept
@@ -61,6 +63,11 @@ public:
return value_;
}
[[nodiscard]] constexpr T& value() noexcept
{
return value_;
}
[[nodiscard]] constexpr Status status() const noexcept
{
return status_;
@@ -68,7 +75,7 @@ public:
private:
constexpr Result(T value, Status status) noexcept
: value_(value)
: value_(std::move(value))
, status_(status)
{
}

View File

@@ -8,6 +8,8 @@
#include "canvas.h"
#include "keymap.h"
#include "hmd.h"
#include "renderer_gl/opengl_capabilities.h"
#include "platform_windows/windows_platform_services.h"
#include "../resource.h"
#include <shellscalingapi.h>
@@ -242,114 +244,6 @@ void win32_update_fps(int frames)
PostMessage(hWnd, WM_USER_WAKEUP, 0, 0);
}
void win32_show_cursor(bool visible)
{
std::lock_guard<std::mutex> lock(main_task_mutex);
main_tasklist.emplace_back([=] {
if (visible)
while (ShowCursor(true) < 0);
else
while (ShowCursor(false) >= 0);
});
}
std::string win32_clipboard_get_text()
{
std::string ret;
if (OpenClipboard(hWnd))
{
if (HANDLE h = GetClipboardData(CF_TEXT))
{
if (char* s = (char*)GlobalLock(h))
{
ret = s;
GlobalUnlock(h);
}
}
CloseClipboard();
}
return ret;
}
bool win32_clipboard_set_text(const std::string& s)
{
bool success = false;
if (OpenClipboard(hWnd))
{
// owned by SetClipboardData
if (HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, s.size() + 1))
{
if (char* p = (char*)GlobalLock(h))
{
std::copy(s.begin(), s.end(), p);
p[s.size()] = 0; // string null-termination
GlobalUnlock(h);
success = true;
}
EmptyClipboard();
SetClipboardData(CF_TEXT, h);
}
CloseClipboard();
}
return success;
}
std::string win32_open_file(const char* filter)
{
OPENFILENAMEA ofn;
char fileName[MAX_PATH] = "";
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = hWnd;
ofn.lpstrFilter = filter;
ofn.lpstrFile = fileName;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR;
ofn.lpstrDefExt = "";
ofn.lpstrInitialDir = "";
if (GetOpenFileNameA(&ofn) != NULL)
{
return fileName;
}
return "";
}
std::string win32_save_file(const char* filter)
{
OPENFILENAMEA ofn;
char fileName[MAX_PATH] = "";
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = hWnd;
ofn.lpstrFilter = filter;
ofn.lpstrFile = fileName;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR | OFN_OVERWRITEPROMPT;
ofn.lpstrDefExt = "";
ofn.lpstrInitialDir = "";
if (GetSaveFileNameA(&ofn) != NULL)
{
return fileName;
}
return "";
}
std::string win32_open_dir()
{
BROWSEINFOA bi;
char Buffer[MAX_PATH];
ZeroMemory(Buffer, MAX_PATH);
ZeroMemory(&bi, sizeof(bi));
bi.hwndOwner = hWnd;
bi.pszDisplayName = Buffer;
bi.lpszTitle = "Title";
bi.ulFlags = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNONLYFSDIRS | BIF_SHAREABLE;
LPCITEMIDLIST pFolder = SHBrowseForFolderA(&bi);
if (pFolder == NULL) return "";
if (!SHGetPathFromIDListA(pFolder, Buffer)) return "";
return Buffer;
}
int read_WMI_info()
{
// see: http://win32easy.blogspot.co.uk/2011/03/wmi-in-c-query-everyting-from-your-os.html
@@ -828,7 +722,7 @@ void _post_call_callback(const char* name, void* funcptr, int len_args, ...)
GLenum error_code;
error_code = glad_glGetError();
if (error_code != GL_NO_ERROR)
if (error_code != pp::renderer::gl::no_error_code())
{
LOG("ERROR %d in %s\n", error_code, name);
}
@@ -840,6 +734,7 @@ int main(int argc, char** argv)
PIXELFORMATDESCRIPTOR pfd;
App::I = new App();
App::I->set_platform_services(&pp::platform::windows::platform_services());
App::I->initLog();
init_shcore_API();
@@ -958,9 +853,9 @@ int main(int argc, char** argv)
return 0;
}
LOG("GL version: %s", glGetString(GL_VERSION));
LOG("GL vendor: %s", glGetString(GL_VENDOR));
LOG("GL renderer: %s", glGetString(GL_RENDERER));
LOG("GL version: %s", glGetString(static_cast<GLenum>(pp::renderer::gl::version_string_name())));
LOG("GL vendor: %s", glGetString(static_cast<GLenum>(pp::renderer::gl::vendor_string_name())));
LOG("GL renderer: %s", glGetString(static_cast<GLenum>(pp::renderer::gl::renderer_string_name())));
#ifdef USE_RENDERDOC
if (!win32_renderdoc_init())
@@ -968,33 +863,12 @@ int main(int argc, char** argv)
#endif // USE_RENDERDOC
swprintf_s(window_title, L"PanoPainter %s (%s)", g_version_number_w,
str2wstr((char*)glGetString(GL_RENDERER)).c_str());
str2wstr((char*)glGetString(static_cast<GLenum>(pp::renderer::gl::renderer_string_name()))).c_str());
// If supported create a 3.3 context
if (GLAD_WGL_ARB_create_context)
{
int contex_attribs[] =
{
WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
WGL_CONTEXT_MINOR_VERSION_ARB, 3,
WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
0
};
int pixel_attribs[] =
{
WGL_DRAW_TO_WINDOW_ARB, GL_TRUE,
WGL_SUPPORT_OPENGL_ARB, GL_TRUE,
WGL_DOUBLE_BUFFER_ARB, GL_TRUE,
WGL_ACCELERATION_ARB,WGL_FULL_ACCELERATION_ARB,
WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
WGL_COLOR_BITS_ARB, 24,
WGL_DEPTH_BITS_ARB, 16,
//WGL_STENCIL_BITS_ARB, 8,
//WGL_SAMPLE_BUFFERS_ARB, 1, // Number of buffers (must be 1 at time of writing)
//WGL_SAMPLES_ARB, 4, // Number of samples
0
};
const auto wgl_config = pp::renderer::gl::windows_wgl_core_context_3_3_config();
UINT numFormat;
wglMakeCurrent(NULL, NULL);
@@ -1006,9 +880,9 @@ int main(int argc, char** argv)
(float)(clientRect.bottom - clientRect.top), 0, 0, hInst, 0);
hDC = GetDC(hWnd);
wglChoosePixelFormatARB(hDC, pixel_attribs, nullptr, 1, &pxfmt, &numFormat);
wglChoosePixelFormatARB(hDC, wgl_config.pixel_format_attributes.data(), nullptr, 1, &pxfmt, &numFormat);
SetPixelFormat(hDC, pxfmt, &pfd);
hRC = wglCreateContextAttribsARB(hDC, NULL, contex_attribs);
hRC = wglCreateContextAttribsARB(hDC, NULL, wgl_config.context_attributes.data());
wglMakeCurrent(hDC, hRC);
}
else
@@ -1188,7 +1062,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
}
case WM_ACTIVATE:
{
win32_show_cursor(true);
pp::platform::windows::platform_services().set_cursor_visible(true);
App::I->ui_task_async([=] {
int active = GET_WM_ACTIVATE_STATE(wp, lp);
WacomTablet::I.set_focus(active);

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_border.h"
#include "renderer_gl/opengl_capabilities.h"
#include "shader.h"
Plane NodeBorder::m_plane;
@@ -62,21 +63,22 @@ void NodeBorder::draw()
{
ShaderManager::use(kShader::Color);
ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp);
const auto blend_state = pp::renderer::gl::blend_state();
if (m_color.a > 0.f)
{
m_color.a < 1.f ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
m_color.a < 1.f ? glEnable(blend_state) : glDisable(blend_state);
ShaderManager::u_vec4(kShaderUniform::Col, m_color);
m_plane.draw_fill();
glDisable(GL_BLEND);
glDisable(blend_state);
}
if (m_thinkness > 0 && m_border_color.a > 0.f)
{
//glLineWidth(m_thinkness);
ShaderManager::u_vec4(kShaderUniform::Col, m_border_color);
m_border_color.a < 1.f ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
m_border_color.a < 1.f ? glEnable(blend_state) : glDisable(blend_state);
m_plane.draw_stroke();
glDisable(GL_BLEND);
glDisable(blend_state);
}
}

View File

@@ -1,9 +1,85 @@
#include "pch.h"
#include <cstdint>
#include "app_core/canvas_tool_ui.h"
#include "app_core/history_ui.h"
#include "app.h"
#include "log.h"
#include "node_canvas.h"
#include "node_image_texture.h"
#include "settings.h"
#include "renderer_gl/opengl_capabilities.h"
namespace {
void set_active_texture_unit(std::uint32_t unit_index)
{
glActiveTexture(pp::renderer::gl::active_texture_unit(unit_index));
}
void unbind_texture_2d()
{
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
}
void run_history_undo_if_available()
{
const auto plan = pp::app::plan_history_undo(static_cast<int>(ActionManager::I.m_actions.size()));
if (plan && plan.value().invokes_undo)
ActionManager::undo();
}
void run_history_redo_if_available()
{
const auto plan = pp::app::plan_history_redo(static_cast<int>(ActionManager::I.m_redos.size()));
if (plan && plan.value().invokes_redo)
ActionManager::redo();
}
class LegacyNodeCanvasToolServices final : public pp::app::CanvasToolServices {
public:
void select_toolbar_button(pp::app::CanvasToolMode) override
{
}
void set_transform_action(pp::app::CanvasToolTransformAction) override
{
}
void set_canvas_mode(pp::app::CanvasToolMode mode) override
{
switch (mode) {
case pp::app::CanvasToolMode::draw:
Canvas::set_mode(kCanvasMode::Draw);
return;
case pp::app::CanvasToolMode::erase:
Canvas::set_mode(kCanvasMode::Erase);
return;
default:
return;
}
}
void toggle_picking() override
{
}
void toggle_touch_lock() override
{
}
};
void run_canvas_tool_mode(pp::app::CanvasToolMode mode)
{
const auto plan = pp::app::plan_canvas_tool_select(mode);
LegacyNodeCanvasToolServices services;
const auto status = pp::app::execute_canvas_tool_plan(plan, services);
if (!status.ok())
LOG("Canvas input tool action failed: %s", status.message);
}
}
Node* NodeCanvas::clone_instantiate() const
{
@@ -22,12 +98,14 @@ void NodeCanvas::init()
m_canvas->m_node = this;
m_sampler.create();
//m_sampler.set_filter(GL_LINEAR, GL_NEAREST);
//m_sampler.set_filter(pp::renderer::gl::linear_texture_filter(), pp::renderer::gl::nearest_texture_filter());
m_sampler_nearest.create(GL_NEAREST);
m_sampler_nearest.create(pp::renderer::gl::nearest_texture_filter());
m_sampler_linear.create(GL_LINEAR);
m_sampler_stencil.create(GL_LINEAR, GL_REPEAT);
m_sampler_linear.create(pp::renderer::gl::linear_texture_filter());
m_sampler_stencil.create(
pp::renderer::gl::linear_texture_filter(),
pp::renderer::gl::repeat_texture_wrap());
m_face_plane.create<1>(2, 2);
m_line.create();
CanvasMode::node = this;
@@ -45,7 +123,9 @@ void NodeCanvas::restore_context()
m_sampler.create();
m_sampler.set_filter(GL_LINEAR, GL_NEAREST);
m_sampler.set_filter(
pp::renderer::gl::linear_texture_filter(),
pp::renderer::gl::nearest_texture_filter());
m_face_plane.create<1>(2, 2);
m_canvas->snapshot_restore();
CanvasMode::node = this;
@@ -74,13 +154,13 @@ void NodeCanvas::draw()
GLint vp[4];
GLfloat cc[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
auto blend = glIsEnabled(GL_BLEND);
auto depth = glIsEnabled(GL_DEPTH_TEST);
auto scissor = glIsEnabled(GL_SCISSOR_TEST);
glGetIntegerv(pp::renderer::gl::viewport_query(), vp);
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
auto blend = glIsEnabled(pp::renderer::gl::blend_state());
auto depth = glIsEnabled(pp::renderer::gl::depth_test_state());
auto scissor = glIsEnabled(pp::renderer::gl::scissor_test_state());
glDisable(GL_SCISSOR_TEST);
glDisable(pp::renderer::gl::scissor_test_state());
glm::ivec4 c = (glm::ivec4)glm::vec4(box.x, (int)(vp[3] - box.y - box.w), box.z, box.w);
@@ -120,13 +200,13 @@ void NodeCanvas::draw()
{
m_rtt.bindFramebuffer();
glClearColor(1, 1, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
glViewport(0, 0, m_rtt.getWidth(), m_rtt.getHeight());
}
else
{
glClearColor(1, 1, 1, 0);
glClear(GL_COLOR_BUFFER_BIT);
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
glViewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
}
@@ -136,7 +216,7 @@ void NodeCanvas::draw()
if (draw_merged)
{
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
// draw the grid
for (int plane_index = 0; plane_index < 6; plane_index++)
{
@@ -164,7 +244,7 @@ void NodeCanvas::draw()
ShaderManager::u_int(kShaderUniform::Highlight, false);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_canvas->m_layers_merge.rtt(plane_index).bindTexture();
m_face_plane.draw_fill();
m_canvas->m_layers_merge.rtt(plane_index).unbindTexture();
@@ -205,8 +285,8 @@ void NodeCanvas::draw()
}
// if not using shader blend, use gl rasterizer blend
use_blend ? glDisable(GL_BLEND) : glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
use_blend ? glDisable(pp::renderer::gl::blend_state()) : glEnable(pp::renderer::gl::blend_state());
glDisable(pp::renderer::gl::depth_test_state());
const auto& b = m_canvas->m_current_stroke->m_brush;
@@ -254,23 +334,23 @@ void NodeCanvas::draw()
//ShaderManager::u_int(kShaderUniform::Lock, m_canvas->m_layers[layer_index]->m_alpha_locked);
ShaderManager::u_int(kShaderUniform::Mask, m_canvas->m_smask_active);
ShaderManager::u_mat4(kShaderUniform::MVP, plane_mvp_z);
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).bindTexture();
for (int frame = frame_start; frame <= frame_end; frame++)
{
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity* onion_alpha);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
m_face_plane.draw_fill();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
}
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].unbindTexture();
}
else if(m_canvas->m_current_stroke && m_canvas->m_show_tmp && m_canvas->m_current_layer_idx == layer_index)
@@ -309,33 +389,33 @@ void NodeCanvas::draw()
ShaderManager::u_int(kShaderUniform::PatternBlendMode, b->m_pattern_blend_mode);
ShaderManager::u_vec2(kShaderUniform::PatternOffset, Canvas::I->m_pattern_offset);
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).bindTexture();
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_canvas->m_tmp_dual[plane_index].bindTexture();
glActiveTexture(GL_TEXTURE4);
set_active_texture_unit(4);
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
unbind_texture_2d();
for (int frame = frame_start; frame <= frame_end; frame++)
{
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
m_face_plane.draw_fill();
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
}
glActiveTexture(GL_TEXTURE3);
set_active_texture_unit(3);
if (b->m_dual_enabled)
m_canvas->m_tmp_dual[plane_index].unbindTexture();
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_canvas->m_smask.rtt(plane_index).unbindTexture();
glActiveTexture(GL_TEXTURE1);
set_active_texture_unit(1);
m_canvas->m_tmp[plane_index].unbindTexture();
}
else
@@ -350,7 +430,7 @@ void NodeCanvas::draw()
{
float onion_alpha = 1.f - (float)glm::abs(frame - frame_current) / (float)(onion_size + 1);
ShaderManager::u_float(kShaderUniform::Alpha, m_canvas->m_layers[layer_index]->m_opacity * onion_alpha);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).bindTexture();
m_face_plane.draw_fill();
m_canvas->m_layers[layer_index]->rtt(plane_index, frame).unbindTexture();
@@ -376,13 +456,13 @@ void NodeCanvas::draw()
ShaderManager::u_float(kShaderUniform::Alpha, 1.f);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-1, 1, -1, 1));
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_blender_rtt.bindTexture();
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_blender_bg.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0,
glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, 0, 0, 0, 0,
m_blender_bg.size().x, m_blender_bg.size().y);
}
@@ -390,10 +470,10 @@ void NodeCanvas::draw()
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE2);
set_active_texture_unit(2);
m_blender_bg.unbind();
}
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_blender_rtt.unbindTexture();
}
@@ -428,7 +508,7 @@ void NodeCanvas::draw()
// draw the grid behind the layers using a temporary copy
if (use_blend)
{
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
//draw the grid
for (int plane_index = 0; plane_index < 6; plane_index++)
@@ -446,7 +526,7 @@ void NodeCanvas::draw()
// draw the layers
m_sampler.bind(0);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_cache_rtt.bindTexture();
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
@@ -456,7 +536,7 @@ void NodeCanvas::draw()
}
}
glDisable(GL_DEPTH_TEST);
glDisable(pp::renderer::gl::depth_test_state());
if (m_canvas->m_smask_active || m_canvas->m_current_mode == kCanvasMode::Copy || m_canvas->m_current_mode == kCanvasMode::Cut)
{
@@ -471,8 +551,8 @@ void NodeCanvas::draw()
ShaderManager::use(kShader::TextureMask);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_vec2(kShaderUniform::PatternOffset, m_outline_pan);
glActiveTexture(GL_TEXTURE0);
glEnable(GL_BLEND);
set_active_texture_unit(0);
glEnable(pp::renderer::gl::blend_state());
//draw the cube faces
for (int plane_index = 0; plane_index < 6; plane_index++)
@@ -506,12 +586,12 @@ void NodeCanvas::draw()
m_rtt.unbindFramebuffer();
glClearColor(1, 1, 1, 0);
glClear(GL_COLOR_BUFFER_BIT);
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
glViewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
// draw the canvas
m_sampler_nearest.bind(0);
glActiveTexture(GL_TEXTURE0);
set_active_texture_unit(0);
m_rtt.bindTexture();
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
@@ -520,9 +600,9 @@ void NodeCanvas::draw()
m_rtt.unbindTexture();
}
scissor ? glEnable(GL_SCISSOR_TEST) : glDisable(GL_SCISSOR_TEST);
blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND);
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
scissor ? glEnable(pp::renderer::gl::scissor_test_state()) : glDisable(pp::renderer::gl::scissor_test_state());
blend ? glEnable(pp::renderer::gl::blend_state()) : glDisable(pp::renderer::gl::blend_state());
depth ? glEnable(pp::renderer::gl::depth_test_state()) : glDisable(pp::renderer::gl::depth_test_state());
glViewport(vp[0], vp[1], vp[2], vp[3]);
glClearColor(cc[0], cc[1], cc[2], cc[3]);
}
@@ -551,9 +631,9 @@ kEventResult NodeCanvas::handle_event(Event* e)
case kEventType::MouseMove:
if (stylus_eraser != me->m_eraser)
{
Canvas::set_mode(me->m_eraser ?
kCanvasMode::Erase :
kCanvasMode::Draw);
run_canvas_tool_mode(me->m_eraser ?
pp::app::CanvasToolMode::erase :
pp::app::CanvasToolMode::draw);
stylus_eraser = me->m_eraser;
}
case kEventType::MouseScroll:
@@ -576,10 +656,9 @@ kEventResult NodeCanvas::handle_event(Event* e)
break;
case kEventType::KeyDown:
if (ke->m_key == kKey::KeyE)
Canvas::set_mode(kCanvasMode::Erase);
run_canvas_tool_mode(pp::app::CanvasToolMode::erase);
if (ke->m_key == kKey::AndroidBack)
if (!ActionManager::empty())
ActionManager::undo();
run_history_undo_if_available();
if (ke->m_key == kKey::KeyAlt && m_mouse_focus)
App::I->show_cursor();
for (auto& mode : *m_canvas->m_mode)
@@ -588,32 +667,18 @@ kEventResult NodeCanvas::handle_event(Event* e)
case kEventType::KeyUp:
update_cursor();
if (ke->m_key == kKey::KeyE)
Canvas::set_mode(kCanvasMode::Draw);
run_canvas_tool_mode(pp::app::CanvasToolMode::draw);
if (ke->m_key == kKey::KeyTab)
App::I->toggle_ui();
if (ke->m_key == kKey::KeyZ && App::I->keys[(int)kKey::KeyCtrl])
App::I->keys[(int)kKey::KeyShift] ? ActionManager::redo() : ActionManager::undo();
App::I->keys[(int)kKey::KeyShift] ? run_history_redo_if_available() : run_history_undo_if_available();
if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && !App::I->keys[(int)kKey::KeyShift])
{
if (Canvas::I->m_newdoc)
{
App::I->dialog_save();
}
else if (Canvas::I->m_unsaved)
{
Canvas::I->project_save();
}
App::I->save_document(pp::app::DocumentSaveIntent::save);
}
if (ke->m_key == kKey::KeyS && App::I->keys[(int)kKey::KeyCtrl] && App::I->keys[(int)kKey::KeyShift])
{
if (Canvas::I->m_newdoc)
{
App::I->dialog_save();
}
else if (Canvas::I->m_unsaved)
{
App::I->dialog_save_ver();
}
App::I->save_document(pp::app::DocumentSaveIntent::save_dirty_version);
}
if (ke->m_key == kKey::KeyBracketLeft)
{
@@ -646,7 +711,7 @@ kEventResult NodeCanvas::handle_event(Event* e)
break;
case kEventType::TouchTap:
if (te->m_finger_count == 2)
ActionManager::undo();
run_history_undo_if_available();
break;
default:
return kEventResult::Available;
@@ -668,11 +733,11 @@ void NodeCanvas::create_buffers()
auto new_size = GetSize() * m_density;
LOG("NodeCanvas::create_buffers size: %d x %d density %f", (int)new_size.x, (int)new_size.y, m_density);
m_canvas->m_mixer.create((int)new_size.x * m_canvas->m_mixer_scale,
(int)new_size.y * m_canvas->m_mixer_scale, -1, GL_RGBA8);
m_blender_rtt.create((int)new_size.x, (int)new_size.y, -1, GL_RGBA8);
m_cache_rtt.create((int)new_size.x, (int)new_size.y, -1, GL_RGBA8);
m_rtt.create((int)new_size.x, (int)new_size.y, -1, GL_RGBA8, true);
m_blender_bg.create((int)new_size.x, (int)new_size.y, GL_RGBA8);
(int)new_size.y * m_canvas->m_mixer_scale, -1, pp::renderer::gl::rgba8_internal_format());
m_blender_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format());
m_cache_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format());
m_rtt.create((int)new_size.x, (int)new_size.y, -1, pp::renderer::gl::rgba8_internal_format(), true);
m_blender_bg.create((int)new_size.x, (int)new_size.y, pp::renderer::gl::rgba8_internal_format());
}
void NodeCanvas::set_density(float d)

View File

@@ -1,5 +1,6 @@
#include "pch.h"
#include "node_colorwheel.h"
#include "renderer_gl/opengl_capabilities.h"
#include "shader.h"
#include "log.h"
#include "app.h"
@@ -29,43 +30,67 @@ void NodeColorWheel::init_controls()
void NodeColorWheel::loaded()
{
m_circle.create<64>(.5, .4, Circle::kUVMapping::Tube);
m_cur_hue.create<16>(.05, 0.04);
m_cur_quad.create<16>(.04, 0.03, Circle::kUVMapping::Tube);
m_circle.create<64>(.5f, .4f, Circle::kUVMapping::Tube);
m_cur_hue.create<16>(.05f, 0.04f);
m_cur_quad.create<16>(.04f, 0.03f, Circle::kUVMapping::Tube);
float quad_scale = glm::sin(glm::radians(45.f)) * 0.8f;
m_quad.create<1>(quad_scale, quad_scale);
struct vertex_t { glm::vec4 pos; glm::vec2 uvs; glm::vec4 col; };
std::vector<vertex_t> vertices;
float l = 0.4;
float l = 0.4f;
vertices.push_back({{glm::cos(4.f/3.f*glm::pi<float>())*l,glm::sin(4.f/3.f*glm::pi<float>())*l,0,1},{1,-1},{1,1,1,1}});
vertices.push_back({{glm::cos(2.f/3.f*glm::pi<float>())*l,glm::sin(2.f/3.f*glm::pi<float>())*l,0,1},{0,0},{0,0,0,1}});
vertices.push_back({{l,0,0,1},{1,1},{1,0,0,1}});
App::I->render_task([&]
{
const auto buffer_target = pp::renderer::gl::array_buffer_target();
const auto upload_usage = pp::renderer::gl::static_draw_buffer_usage();
const auto attribute_type = pp::renderer::gl::vertex_attribute_float_component_type();
const auto attribute_normalized =
static_cast<GLboolean>(pp::renderer::gl::vertex_attribute_not_normalized());
glGenBuffers(1, &buffers);
glBindBuffer(GL_ARRAY_BUFFER, buffers);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vertex_t), vertices.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(buffer_target, buffers);
glBufferData(buffer_target, vertices.size() * sizeof(vertex_t), vertices.data(), upload_usage);
glBindBuffer(buffer_target, 0);
glGenVertexArrays(1, &arrays);
glBindVertexArray(arrays);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, buffers);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (GLvoid*)0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (GLvoid*)offsetof(vertex_t, uvs));
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (GLvoid*)offsetof(vertex_t, col));
glBindBuffer(buffer_target, buffers);
glVertexAttribPointer(
0,
4,
attribute_type,
attribute_normalized,
sizeof(vertex_t),
(GLvoid*)0);
glVertexAttribPointer(
1,
2,
attribute_type,
attribute_normalized,
sizeof(vertex_t),
(GLvoid*)offsetof(vertex_t, uvs));
glVertexAttribPointer(
2,
4,
attribute_type,
attribute_normalized,
sizeof(vertex_t),
(GLvoid*)offsetof(vertex_t, col));
glBindVertexArray(0);
});
}
void NodeColorWheel::draw()
{
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
ShaderManager::use(kShader::ColorHue);
ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp * glm::eulerAngleZ(glm::radians(-90.f)));
ShaderManager::u_int(kShaderUniform::Direction, 0); // set horizontal
@@ -74,9 +99,8 @@ void NodeColorWheel::draw()
// ShaderManager::use(kShader::ColorTri);
// ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp);
// ShaderManager::u_vec4(kShaderUniform::Col, glm::vec4(m_hsv, 0.f));
// GLenum type = GL_TRIANGLES;
// glBindVertexArray(arrays);
// glDrawArrays(type, 0, 3);
// glDrawArrays(pp::renderer::gl::primitive_mode_for_fill_count(3U), 0, 3);
// glBindVertexArray(0);
ShaderManager::use(kShader::Color);
@@ -144,8 +168,8 @@ kEventResult NodeColorWheel::handle_event(Event* e)
else if (l >= 0.4f && l <= 0.5f)
{
mode = 1;
auto pos = glm::normalize(me->m_pos - m_pos - GetSize() * 0.5f);
m_hsv.x = (glm::atan(pos.y, -pos.x) + glm::pi<float>()) / glm::two_pi<float>();
auto normalized_pos = glm::normalize(me->m_pos - m_pos - GetSize() * 0.5f);
m_hsv.x = (glm::atan(normalized_pos.y, -normalized_pos.x) + glm::pi<float>()) / glm::two_pi<float>();
handle_color_change();
}
else

View File

@@ -1,9 +1,9 @@
#include "pch.h"
#include "log.h"
#include "node_dialog_resize.h"
#include "app_core/document_resize.h"
#include "canvas.h"
#include "node_image_texture.h"
#include "app.h"
#include <array>
Node* NodeDialogResize::clone_instantiate() const
@@ -30,9 +30,12 @@ void NodeDialogResize::init_controls()
combo = find<NodeComboBox>("resolution");
text = find<NodeText>("current-res");
resolution = Canvas::I->m_width;
static char txt[128];
sprintf(txt, "Current: %s", App::I->res_to_string(resolution).c_str());
text->set_text(txt);
const auto state = pp::app::make_document_resize_dialog_state(resolution);
text->set_text(state.current_resolution_text.c_str());
if (combo && state.current_resolution_index >= 0
&& state.current_resolution_index < static_cast<int>(combo->m_items.size())) {
combo->m_current_index = state.current_resolution_index;
}
btn_cancel->on_click = [this](Node*) {
destroy();
};
@@ -47,5 +50,6 @@ void NodeDialogResize::loaded()
int NodeDialogResize::get_resolution()
{
return combo ? App::I->res_from_index(combo->m_current_index) : 512;
const auto plan = pp::app::plan_document_resize(combo ? combo->m_current_index : 0);
return plan ? plan.value().resolution : pp::app::document_resolution_values.front();
}

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_image.h"
#include "renderer_gl/opengl_capabilities.h"
#include "shader.h"
#include "app.h"
@@ -13,7 +14,9 @@ void NodeImage::static_init()
m_plane.create<1>(1, 1);
m_sampler.create();
m_sampler_mips.create();
m_sampler_mips.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
m_sampler_mips.set_filter(
pp::renderer::gl::linear_mipmap_linear_texture_filter(),
pp::renderer::gl::linear_texture_filter());
}
Node* NodeImage::clone_instantiate() const
@@ -99,7 +102,7 @@ void NodeImage::draw()
auto& sampler = m_use_mipmaps ? m_sampler_mips : m_sampler;
sampler.bind(0);
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
if (m_use_atlas)
{
ShaderManager::use(kShader::Atlas);
@@ -114,7 +117,7 @@ void NodeImage::draw()
ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp * glm::scale(glm::vec3(m_scale, 1.f)));
m_plane.draw_fill();
sampler.unbind();
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
}
bool NodeImage::set_image(const std::string& path)
@@ -155,7 +158,12 @@ void NodeImage::load_url(const std::string& url)
int w, h, c;
uint8_t* rgba = stbi_load_from_memory(m_remote_asset->m_data, m_remote_asset->m_len, &w, &h, &c, 4);
m_remote_texture = std::make_shared<Texture2D>();
m_remote_texture->create(w, h, GL_RGBA8, GL_RGBA, rgba);
m_remote_texture->create(
w,
h,
pp::renderer::gl::rgba8_internal_format(),
pp::renderer::gl::rgba_pixel_format(),
rgba);
if (m_use_mipmaps)
m_remote_texture->create_mipmaps();
delete rgba;

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_image_texture.h"
#include "renderer_gl/opengl_capabilities.h"
#include "shader.h"
#include "node_image.h"
@@ -18,14 +19,14 @@ void NodeImageTexture::clone_copy(Node* dest) const
void NodeImageTexture::draw()
{
tex ? tex->bind() : glBindTexture(GL_TEXTURE_2D, 0);
tex ? tex->bind() : glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
auto& sampler = tex && tex->has_mips ? NodeImage::m_sampler_mips : NodeImage::m_sampler;
sampler.bind(0);
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, m_mvp);
NodeImage::m_plane.draw_fill();
sampler.unbind();
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
}

View File

@@ -1,7 +1,9 @@
#include "pch.h"
#include "node_panel_animation.h"
#include "app_core/document_animation.h"
#include "node_button.h"
#include "node_button_custom.h"
#include "renderer_gl/opengl_capabilities.h"
#include "canvas.h"
#include "app.h"
@@ -24,6 +26,152 @@ void NodePanelAnimation::init()
init_controls();
}
void NodePanelAnimation::execute_animation_plan(const pp::app::DocumentAnimationOperationPlan& plan, Layer* layer)
{
class LegacyAnimationServices final : public pp::app::DocumentAnimationServices {
public:
LegacyAnimationServices(NodePanelAnimation& panel, Layer* layer) noexcept
: panel_(panel)
, layer_(layer)
{
}
void add_frame() override
{
Canvas::I->layer().add_frame();
}
void duplicate_frame(int selected_frame) override
{
if (layer_)
layer_->duplicate_frame(selected_frame);
}
void remove_frame(int selected_frame, int target_frame) override
{
if (!layer_)
return;
layer_->remove_frame(selected_frame);
panel_.m_selected_frame_index = target_frame;
}
void set_frame_duration(int selected_frame, int duration) override
{
if (layer_)
layer_->set_frame_duration(selected_frame, duration);
}
int move_frame(int selected_frame, int move_offset) override
{
if (!layer_)
return selected_frame;
panel_.m_selected_frame_index = layer_->move_frame_offset(selected_frame, move_offset);
return panel_.m_selected_frame_index;
}
void select_frame(std::uint32_t layer_id, int layer_index, int selected_frame) override
{
panel_.m_selected_frame_layer_id = layer_id;
panel_.m_selected_frame_index = selected_frame;
panel_.m_timeline->m_frame = selected_frame;
}
void select_layer(int layer_index) override
{
App::I->layers->handle_layer_selected(App::I->layers->get_layer_at(layer_index));
}
void goto_frame(int target_frame) override
{
Canvas::I->anim_goto_frame(target_frame);
}
void set_timeline_frame(int target_frame) override
{
panel_.m_timeline->m_frame = target_frame;
}
void set_onion_size(int onion_size) override
{
panel_.m_timeline->m_onion_size = onion_size;
}
void capture_playback_restore_mode() override
{
playback_restore_mode() = Canvas::I->m_current_mode;
}
void enter_playback_camera_mode() override
{
Canvas::set_mode(kCanvasMode::Camera);
}
void restore_playback_canvas_mode() override
{
Canvas::set_mode(playback_restore_mode());
}
void set_playback_active(bool active) override
{
panel_.btn_play->set_active(active);
}
void reset_playback_timer() override
{
panel_.m_playback_timer = 0;
}
void set_playback_idle_ms(int idle_ms) override
{
App::I->idle_ms = idle_ms;
}
void update_canvas_animation() override
{
Canvas::I->anim_update();
}
void update_frame_status() override
{
panel_.update_frames();
}
void reload_animation_layers() override
{
panel_.load_layers();
}
void mark_unsaved() override
{
Canvas::I->m_unsaved = true;
}
private:
static kCanvasMode& playback_restore_mode()
{
static auto mode = Canvas::I->m_current_mode;
return mode;
}
NodePanelAnimation& panel_;
Layer* layer_ = nullptr;
};
LegacyAnimationServices services(*this, layer);
const auto status = pp::app::execute_animation_operation_plan(plan, services);
if (!status.ok())
LOG("Animation panel action failed: %s", status.message);
}
pp::app::DocumentAnimationPanelState NodePanelAnimation::animation_panel_state() const
{
return pp::app::DocumentAnimationPanelState {
.total_duration = Canvas::I->anim_duration(),
.current_frame = Canvas::I->m_anim_frame,
.playback_active = btn_play->is_active(),
};
}
void NodePanelAnimation::init_controls()
{
m_layers_container = find<NodeScroll>("layers");
@@ -44,80 +192,130 @@ void NodePanelAnimation::init_controls()
m_frame_label = find<NodeText>("frame-index");
btn_add->on_click = [this](Node*) {
Canvas::I->layer().add_frame();
load_layers();
const auto plan = pp::app::plan_animation_add_frame(
Canvas::I->layer().frames_count(),
Canvas::I->m_anim_frame);
if (!plan)
return;
execute_animation_plan(plan.value());
};
btn_duplicate->on_click = [this](Node*) {
Canvas::I->layer().duplicate_frame(m_selected_frame_index);
load_layers();
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
{
const auto plan = pp::app::plan_animation_duplicate_frame(
layer->frames_count(),
m_selected_frame_index);
if (!plan)
return;
execute_animation_plan(plan.value(), layer.get());
}
};
btn_remove->on_click = [this](Node*) {
Canvas::I->layer_with_id(m_selected_frame_layer_id)->remove_frame(m_selected_frame_index);
load_layers();
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
{
const auto plan = pp::app::plan_animation_remove_frame(
layer->frames_count(),
m_selected_frame_index);
if (!plan)
return;
execute_animation_plan(plan.value(), layer.get());
}
};
btn_up->on_click = [this](Node*) {
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
layer->set_frame_duration(m_selected_frame_index, glm::max(layer->frame_duration(m_selected_frame_index) + 1, 1));
load_layers();
{
const auto plan = pp::app::plan_animation_adjust_duration(
layer->frames_count(),
m_selected_frame_index,
layer->frame_duration(m_selected_frame_index),
1);
if (!plan)
return;
execute_animation_plan(plan.value(), layer.get());
}
};
btn_down->on_click = [this](Node*) {
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
layer->set_frame_duration(m_selected_frame_index, glm::max(layer->frame_duration(m_selected_frame_index) - 1, 1));
load_layers();
{
const auto plan = pp::app::plan_animation_adjust_duration(
layer->frames_count(),
m_selected_frame_index,
layer->frame_duration(m_selected_frame_index),
-1);
if (!plan)
return;
execute_animation_plan(plan.value(), layer.get());
}
};
btn_left->on_click = [this](Node*) {
if (!m_selected_frame)
return;
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
m_selected_frame_index = layer->move_frame_offset(m_selected_frame_index, -1);
Canvas::I->anim_goto_frame(m_selected_frame_index);
load_layers();
{
const auto plan = pp::app::plan_animation_move_frame(
layer->frames_count(),
m_selected_frame_index,
-1);
if (!plan)
return;
execute_animation_plan(plan.value(), layer.get());
}
};
btn_right->on_click = [this](Node*) {
if (!m_selected_frame)
return;
if (auto layer = Canvas::I->layer_with_id(m_selected_frame_layer_id))
m_selected_frame_index = layer->move_frame_offset(m_selected_frame_index, +1);
Canvas::I->anim_goto_frame(m_selected_frame_index);
load_layers();
{
const auto plan = pp::app::plan_animation_move_frame(
layer->frames_count(),
m_selected_frame_index,
1);
if (!plan)
return;
execute_animation_plan(plan.value(), layer.get());
}
};
m_onion->on_select = [this] (Node* target, int index) {
m_timeline->m_onion_size = m_onion->get_int();
Canvas::I->anim_update();
const auto plan = pp::app::plan_animation_onion_size(m_onion->get_int());
if (!plan)
return;
execute_animation_plan(plan.value());
};
m_timeline->on_frame_changed = [this] (NodeAnimationTimeline* target, int frame) {
LOG("goto frame %d", frame);
Canvas::I->anim_goto_frame(frame);
load_layers();
const auto plan = pp::app::plan_animation_panel_action(
pp::app::DocumentAnimationPanelAction::goto_frame,
animation_panel_state(),
frame);
if (!plan)
return;
LOG("goto frame %d", plan.value().target_frame);
execute_animation_plan(plan.value());
};
btn_next->on_click = [this] (Node* target) {
Canvas::I->anim_goto_next();
load_layers();
const auto plan = pp::app::plan_animation_panel_action(
pp::app::DocumentAnimationPanelAction::next_frame,
animation_panel_state());
if (!plan)
return;
execute_animation_plan(plan.value());
};
btn_prev->on_click = [this](Node* target) {
Canvas::I->anim_goto_prev();
load_layers();
const auto plan = pp::app::plan_animation_panel_action(
pp::app::DocumentAnimationPanelAction::previous_frame,
animation_panel_state());
if (!plan)
return;
execute_animation_plan(plan.value());
};
btn_play->on_click = [this] (Node* target) {
static auto mode = Canvas::I->m_current_mode;
auto b = static_cast<NodeButtonCustom*>(target);
if (b->is_active())
{
Canvas::set_mode(mode);
b->set_active(false);
App::I->idle_ms = 100;
}
else
{
mode = Canvas::I->m_current_mode;
Canvas::set_mode(kCanvasMode::Camera);
m_playback_timer = 0;
b->set_active(true);
App::I->idle_ms = 10;
}
btn_play->on_click = [this] (Node*) {
const auto plan = pp::app::plan_animation_panel_action(
pp::app::DocumentAnimationPanelAction::toggle_playback,
animation_panel_state());
if (plan)
execute_animation_plan(plan.value());
};
}
@@ -153,11 +351,13 @@ void NodePanelAnimation::load_layers()
m_selected_frame->set_active(false);
frame->set_active(true);
m_selected_frame = frame;
m_selected_frame_layer_id = lid;
m_selected_frame_index = fi;
m_timeline->m_frame = fi;
Canvas::I->anim_goto_frame(fi);
App::I->layers->handle_layer_selected(App::I->layers->get_layer_at(i));
const auto plan = pp::app::plan_animation_select_frame(
Canvas::I->m_layers[i]->frames_count(),
i,
lid,
fi);
if (plan)
execute_animation_plan(plan.value(), Canvas::I->m_layers[i].get());
};
}
}
@@ -182,9 +382,11 @@ void NodePanelAnimation::on_tick(float dt)
if (m_playback_timer > (1.f / m_fps->get_float()))
{
m_playback_timer = 0;
Canvas::I->anim_goto_next();
m_timeline->m_frame = Canvas::I->m_anim_frame;
update_frames();
const auto plan = pp::app::plan_animation_panel_action(
pp::app::DocumentAnimationPanelAction::playback_step,
animation_panel_state());
if (plan)
execute_animation_plan(plan.value());
}
}
}
@@ -236,7 +438,7 @@ void NodeAnimationTimeline::draw()
{
parent::draw();
ShaderManager::use(kShader::Color);
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
float step = 35.f;
glm::vec2 cur_pos = {

View File

@@ -7,6 +7,13 @@
#include "node_button_custom.h"
#include "node_combobox.h"
class Layer;
namespace pp::app {
struct DocumentAnimationPanelState;
struct DocumentAnimationOperationPlan;
}
class NodeAnimationFrame : public NodeButtonCustom
{
public:
@@ -65,6 +72,9 @@ class NodePanelAnimation : public Node
int m_selected_frame_index = -1;
uint32_t m_selected_frame_layer_id = 0;
float m_playback_timer = 0;
void execute_animation_plan(const pp::app::DocumentAnimationOperationPlan& plan, Layer* layer = nullptr);
[[nodiscard]] pp::app::DocumentAnimationPanelState animation_panel_state() const;
public:
using this_class = NodePanelAnimation;
using parent = Node;

View File

@@ -1,9 +1,11 @@
#include "pch.h"
#include "app_core/grid_ui.h"
#include "log.h"
#include "node_panel_grid.h"
#include "canvas.h"
#include "app.h"
#include "image.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"
Node* NodePanelGrid::clone_instantiate() const
@@ -76,28 +78,19 @@ void NodePanelGrid::init_controls()
};
m_hm_load->on_click = [this](Node*) {
const auto plan = pp::app::plan_grid_heightmap_pick();
if (!plan.opens_picker)
return;
App::I->pick_image([this](std::string path) {
Image img;
if (img.load_file(path))
{
m_file_path = path;
m_hm_image = img.resize(128, 128);
m_hm_preview->tex = std::make_shared<Texture2D>();
m_hm_preview->tex->create(m_hm_image);
m_hm_preview->tex->create_mipmaps();
auto sz = m_hm_preview->tex->size();
m_hm_preview->SetAspectRatio(sz.x / sz.y);
m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height());
m_hm_preview->SetHeight(100);
if (m_groud_opacity->get_value() == 0.f)
m_groud_opacity->set_value(1.f);
m_rt_dirty = true;
}
load_heightmap_file(path, true);
});
};
m_hm_clear->on_click = [this](Node*)
{
const auto plan = pp::app::plan_grid_heightmap_clear(static_cast<bool>(m_hm_image.data()));
if (!plan.clears_heightmap)
return;
m_hm_plane.create(1, 1, 100 * get_resolution());
m_hm_image.destroy();
m_hm_preview->tex.reset();
@@ -106,24 +99,26 @@ void NodePanelGrid::init_controls()
m_hm_reload->on_click = [this](Node*)
{
Image img;
if (img.load_file(m_file_path))
{
m_hm_image = img.resize(128, 128);
m_hm_preview->tex = std::make_shared<Texture2D>();
m_hm_preview->tex->create(m_hm_image);
m_hm_preview->tex->create_mipmaps();
auto sz = m_hm_preview->tex->size();
m_hm_preview->SetAspectRatio(sz.x / sz.y);
m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height());
m_hm_preview->SetHeight(100);
m_rt_dirty = true;
}
load_heightmap_file(m_file_path, false);
};
m_render->on_click = [this](Node*)
{
if (ShaderManager::ext_float32 || ShaderManager::ext_float16)
const auto plan = pp::app::plan_grid_lightmap_render(
static_cast<bool>(m_hm_image.data()),
ShaderManager::ext_float32,
ShaderManager::ext_float16,
get_texres(),
get_samples());
if (!plan)
return;
if (plan.value().shows_unsupported_message)
{
App::I->message_box("Rendering failed",
"Your hardware does not support lightmap rendering.");
return;
}
if (plan.value().renders_lightmap)
{
std::thread([this] {
BT_SetTerminate();
@@ -132,17 +127,16 @@ void NodePanelGrid::init_controls()
m_shade_mode = ShadeMode::Textured;
}).detach();
}
else
{
App::I->message_box("Rendering failed",
"Your hardware does not support lightmap rendering.");
}
};
m_commit->on_click = [this](Node*)
{
const auto plan = pp::app::plan_grid_heightmap_commit(Canvas::I != nullptr);
if (!plan.commits_heightmap)
return;
Canvas::I->draw_objects([this](const glm::mat4& camera, const glm::mat4& proj, int i) {
draw_heightmap(proj, camera, true);
}, Canvas::I->layer().m_frame_index, true);
if (plan.updates_ground_opacity)
m_groud_opacity->set_value(0);
};
m_hm_texres->on_select = [this](Node*, int index) {
@@ -160,7 +154,12 @@ void NodePanelGrid::init_controls()
App::I->render_task([&]
{
m_texture.bind();
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.m_data.get());
glGetTexImage(
pp::renderer::gl::texture_2d_target(),
0,
pp::renderer::gl::rgba_pixel_format(),
pp::renderer::gl::unsigned_byte_component_type(),
img.m_data.get());
m_texture.unbind();
});
Image resized = img.resize(texres, texres);
@@ -171,7 +170,9 @@ void NodePanelGrid::init_controls()
int rexres = get_texres();
m_texture.create(rexres, rexres);
m_sampler_mipmap.create();
m_sampler_mipmap.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
m_sampler_mipmap.set_filter(
pp::renderer::gl::linear_mipmap_linear_texture_filter(),
pp::renderer::gl::linear_texture_filter());
m_sampler_linear.create();
m_plane.create<1>(1, 1);
@@ -218,14 +219,41 @@ float NodePanelGrid::get_offset() const
return glm::pow(m_groud_offset->get_value() - 0.5f, 3);
}
bool NodePanelGrid::load_heightmap_file(const std::string& path, bool raise_ground_opacity)
{
const auto plan = raise_ground_opacity
? pp::app::plan_grid_heightmap_load(path)
: pp::app::plan_grid_heightmap_reload(path);
if (!plan)
return false;
Image img;
if (!img.load_file(plan.value().path))
return false;
m_file_path = plan.value().path;
m_hm_image = img.resize(128, 128);
m_hm_preview->tex = std::make_shared<Texture2D>();
m_hm_preview->tex->create(m_hm_image);
m_hm_preview->tex->create_mipmaps();
auto sz = m_hm_preview->tex->size();
m_hm_preview->SetAspectRatio(sz.x / sz.y);
m_hm_plane.create(1, 1, m_hm_image, get_resolution(), get_height());
m_hm_preview->SetHeight(100);
if (plan.value().updates_ground_opacity && m_groud_opacity->get_value() == 0.f)
m_groud_opacity->set_value(1.f);
m_rt_dirty = true;
return true;
}
void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const
{
assert(App::I->is_render_thread());
if (m_groud_opacity->get_value() > 0.f)
{
bool depth = glIsEnabled(GL_DEPTH_TEST);
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);
bool depth = glIsEnabled(pp::renderer::gl::depth_test_state());
glEnable(pp::renderer::gl::depth_test_state());
glClear(pp::renderer::gl::framebuffer_depth_buffer_mask());
auto nav = m_hm_image.m_data ? -(m_hm_preview_nav->m_value - 0.5f) : glm::vec2(0);
auto mvp = proj * camera
@@ -239,14 +267,14 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
auto light_pos = glm::vec3(sinf(light_yaw) + nav.x, light_pitch + get_offset(), cosf(light_yaw) + nav.y);
auto light_dir = glm::normalize(light_pos);
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
// DRAW SOLID
if (m_hm_image.m_data)
{
if (m_shade_mode == ShadeMode::Solid)
{
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
ShaderManager::use(kShader::Lambert);
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
ShaderManager::u_vec3(kShaderUniform::LightDir, light_dir);
@@ -271,7 +299,7 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
ShaderManager::u_float(kShaderUniform::Ambient, get_ambient());
ShaderManager::u_int(kShaderUniform::Tex, 0);
m_sampler_mipmap.bind(0);
glActiveTexture(GL_TEXTURE0);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
m_texture.bind();
m_hm_plane.draw_fill();
m_texture.unbind();
@@ -282,7 +310,7 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
auto wire_alpha = m_hm_image.m_data ? m_hm_wireframe->get_value() : 1.f;
if (wire_alpha > 0.f)
{
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
ShaderManager::use(kShader::Color);
ShaderManager::u_vec4(kShaderUniform::Col, glm::vec4(
glm::vec3(m_groud_value->get_value()),
@@ -292,9 +320,17 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
ShaderManager::u_mat4(kShaderUniform::MVP, mvp);
if (m_hm_image.m_data && m_shade_mode == ShadeMode::Transparent)
{
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glColorMask(
pp::renderer::gl::color_write_disabled(),
pp::renderer::gl::color_write_disabled(),
pp::renderer::gl::color_write_disabled(),
pp::renderer::gl::color_write_disabled());
m_hm_plane.draw_fill();
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glColorMask(
pp::renderer::gl::color_write_enabled(),
pp::renderer::gl::color_write_enabled(),
pp::renderer::gl::color_write_enabled(),
pp::renderer::gl::color_write_enabled());
}
m_hm_plane.draw_stroke();
}
@@ -307,23 +343,23 @@ void NodePanelGrid::draw_heightmap(const glm::mat4& proj, const glm::mat4& camer
{
auto p2d = xy(c) / c.z;
GLint vp[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetIntegerv(pp::renderer::gl::viewport_query(), vp);
auto aspect_ratio = (float)vp[3] / (float)vp[2];
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glEnable(pp::renderer::gl::blend_state());
glDisable(pp::renderer::gl::depth_test_state());
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::ortho(-1.f, 1.f, -1.f, 1.f) *
//glm::scale(glm::vec3(100)) *
glm::translate(glm::vec3(p2d, 0)) * glm::scale(glm::vec3(.1f * aspect_ratio, .1f, 1.f)));
glActiveTexture(GL_TEXTURE0);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
m_sampler_linear.bind(0);
constexpr auto sun_tex = const_hash("data/sun.png");
TextureManager::get(sun_tex).bind();
m_plane.draw_fill();
}
}
depth ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST);
depth ? glEnable(pp::renderer::gl::depth_test_state()) : glDisable(pp::renderer::gl::depth_test_state());
}
}
@@ -355,11 +391,11 @@ void NodePanelGrid::bake_uvs()
RTT fb;
if (ShaderManager::ext_float32)
{
fb.create(m_texture.size().x, m_texture.size().y, -1, GL_RGBA32F);
fb.create(m_texture.size().x, m_texture.size().y, -1, pp::renderer::gl::rgba32f_internal_format());
}
else if (ShaderManager::ext_float16)
{
fb.create(m_texture.size().x, m_texture.size().y, -1, GL_RGBA16F);
fb.create(m_texture.size().x, m_texture.size().y, -1, pp::renderer::gl::rgba16f_internal_format());
}
std::unique_ptr<float[]> data_nor;
@@ -367,8 +403,8 @@ void NodePanelGrid::bake_uvs()
App::I->render_task([&]{
fb.bindFramebuffer();
fb.clear({ 1, 0, 0, 1 });
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glDisable(pp::renderer::gl::blend_state());
glDisable(pp::renderer::gl::depth_test_state());
glViewport(0, 0, fb.getWidth(), fb.getHeight());
ShaderManager::use(kShader::BakeUV);
ShaderManager::u_mat4(kShaderUniform::MVP, glm::mat4(1));

View File

@@ -86,6 +86,7 @@ public:
float get_resolution() const;
float get_height() const;
float get_offset() const;
bool load_heightmap_file(const std::string& path, bool raise_ground_opacity);
void draw_heightmap(const glm::mat4& proj, const glm::mat4& camera, bool commit) const;
void bake_uvs();
};

View File

@@ -1,9 +1,198 @@
#include "pch.h"
#include "app_core/quick_ui.h"
#include "node_panel_quick.h"
#include "node_stroke_preview.h"
#include "node_image.h"
#include "app.h"
namespace {
class LegacyQuickUiServices final : public pp::app::QuickUiServices {
public:
LegacyQuickUiServices(NodePanelQuick& panel, const NodePanelQuick::MiniState* restore_state = nullptr) noexcept
: panel_(panel)
, restore_state_(restore_state)
{
}
void select_slot(pp::app::QuickUiSlotKind slot_kind, int slot_index, bool fire_event) override
{
if (slot_kind == pp::app::QuickUiSlotKind::brush) {
panel_.set_selected_brush_index(slot_index, fire_event);
return;
}
panel_.set_selected_color_index(slot_index, fire_event);
}
void open_slot_popup(pp::app::QuickUiSlotKind slot_kind, int slot_index) override
{
if (slot_kind == pp::app::QuickUiSlotKind::brush) {
open_brush_popup(slot_index);
return;
}
open_color_picker(slot_index);
}
void restore_state(int brush_index, int color_index, bool fire_event) override
{
if (!restore_state_)
return;
for (int i = 0; i < static_cast<int>(panel_.m_button_brushes.size()); i++)
{
auto b = static_cast<NodeStrokePreview*>(panel_.m_button_brushes[i]->m_children[0].get());
b->m_brush = restore_state_->brushes[i];
b->draw_stroke();
auto c = static_cast<NodeBorder*>(panel_.m_button_colors[i]->m_children[0].get());
c->m_color = restore_state_->colors[i];
}
panel_.set_selected_color_index(color_index, fire_event);
panel_.set_selected_brush_index(brush_index, fire_event);
}
void reset_state(bool fire_event) override
{
for (int i = 0; i < static_cast<int>(panel_.m_button_brushes.size()); i++)
{
auto b = static_cast<NodeStrokePreview*>(panel_.m_button_brushes[i]->m_children[0].get());
b->m_brush = std::make_shared<Brush>();
b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png");
b->draw_stroke();
}
static_cast<NodeBorder*>(panel_.m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1);
static_cast<NodeBorder*>(panel_.m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1);
static_cast<NodeBorder*>(panel_.m_button_colors[2]->m_children[0].get())->m_color = glm::vec4(1, 1, 1, 1);
panel_.set_selected_brush_index(0, fire_event);
panel_.set_selected_color_index(0, fire_event);
}
private:
void open_brush_popup(int slot_index)
{
auto button = panel_.m_button_brushes[slot_index];
if (!button)
return;
auto popup = App::I->presets;
auto screen = panel_.root()->m_size;
glm::vec2 tick_sz = { 16, 32 };
glm::vec2 tick_pos = button->m_pos + glm::vec2(button->m_size.x, 0);
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y };
auto tick = panel_.root()->add_child<NodeImage>();
tick->SetPositioning(YGPositionTypeAbsolute);
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
tick->SetSize(tick_sz);
tick->set_image("data/ui/popup-tick.png");
tick->m_scale = { 1, 1 };
float hh = popup->m_container->m_children.size() > 10 ? (screen.y - 90.f) : 400.f;
popup->SetWidth(350);
popup->SetHeight(glm::max(hh, 400.f));
popup->SetPositioning(YGPositionTypeAbsolute);
popup->SetPosition(popup_pos);
panel_.root()->add_child(popup);
panel_.root()->update();
popup->tick(0);
popup->update();
if (tick_pos.x + popup->m_size.x > screen.x)
{
tick_pos = button->m_pos - glm::vec2(tick_sz.x, 0);
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y };
tick->m_scale.x = -1.f;
}
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
popup->SetPosition(popup_pos);
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
popup->update();
popup->m_mouse_ignore = false;
popup->m_flood_events = true;
popup->m_capture_children = false;
popup->mouse_capture();
popup->on_popup_close = [tick](Node*) {
tick->destroy();
};
auto* panel = &panel_;
popup->on_brush_changed = [panel, button](Node*, std::shared_ptr<Brush>& b) {
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
*pr->m_brush = *b;
pr->m_brush->load();
pr->draw_stroke();
if (panel->on_brush_change)
panel->on_brush_change(button, pr->m_brush);
};
}
void open_color_picker(int slot_index)
{
auto target = panel_.m_button_colors[slot_index];
if (!target)
return;
auto popup = panel_.m_picker;
auto screen = panel_.root()->m_size;
glm::vec2 tick_sz = { 16, 32 };
glm::vec2 tick_pos = target->m_pos + glm::vec2(target->m_size.x, 0);
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y - 140.f };
auto tick = panel_.root()->add_child<NodeImage>();
tick->SetPositioning(YGPositionTypeAbsolute);
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
tick->SetSize(tick_sz);
tick->set_image("data/ui/popup-tick.png");
tick->m_scale = { 1, 1 };
popup->SetPositioning(YGPositionTypeAbsolute);
popup->SetPosition(popup_pos);
panel_.root()->add_child(popup);
panel_.root()->update();
popup->tick(0);
popup->update();
if (tick_pos.x + popup->m_size.x > screen.x)
{
tick_pos = target->m_pos - glm::vec2(tick_sz.x, 0);
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y - 140.f };
tick->m_scale.x = -1.f;
}
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
popup->SetPosition(popup_pos);
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
popup->update();
popup->m_mouse_ignore = false;
popup->m_flood_events = true;
popup->m_capture_children = false;
popup->mouse_capture();
auto c = static_cast<NodeBorder*>(target->m_children[0].get());
panel_.m_picker->set_color(c->m_color);
panel_.m_picker->on_popup_close = [tick](Node*) {
tick->destroy();
};
auto* panel = &panel_;
panel_.m_picker->on_color_change = [panel, c](Node*, glm::vec3 rgb) {
c->m_color = glm::vec4(rgb, 1.f);
if (panel->on_color_change)
panel->on_color_change(panel, rgb);
};
}
NodePanelQuick& panel_;
const NodePanelQuick::MiniState* restore_state_ = nullptr;
};
} // namespace
Node* NodePanelQuick::clone_instantiate() const
{
return new this_class;
@@ -31,11 +220,13 @@ void NodePanelQuick::set_color(glm::vec3 color)
int NodePanelQuick::get_selected_brush_index() const
{
auto it = std::find(m_button_brushes.begin(), m_button_brushes.end(), m_button_brush_current);
return std::distance(m_button_brushes.begin(), it);
return static_cast<int>(std::distance(m_button_brushes.begin(), it));
}
void NodePanelQuick::set_selected_brush_index(int idx, bool fire_event /*= false*/)
{
if (!pp::app::validate_quick_slot_index(idx, static_cast<int>(m_button_brushes.size())).ok())
return;
if (m_button_brush_current)
m_button_brush_current->set_active(false);
m_button_brush_current = m_button_brushes[idx];
@@ -48,11 +239,13 @@ void NodePanelQuick::set_selected_brush_index(int idx, bool fire_event /*= false
int NodePanelQuick::get_selected_color_index() const
{
auto it = std::find(m_button_colors.begin(), m_button_colors.end(), m_button_color_current);
return std::distance(m_button_colors.begin(), it);
return static_cast<int>(std::distance(m_button_colors.begin(), it));
}
void NodePanelQuick::set_selected_color_index(int idx, bool fire_event /*= false*/)
{
if (!pp::app::validate_quick_slot_index(idx, static_cast<int>(m_button_colors.size())).ok())
return;
if (m_button_color_current)
m_button_color_current->set_active(false);
m_button_color_current = m_button_colors[idx];
@@ -77,32 +270,30 @@ NodePanelQuick::MiniState NodePanelQuick::get_state() const
void NodePanelQuick::set_state(const MiniState& state, bool fire_event /*= false*/)
{
for (int i = 0; i < 3; i++)
{
auto b = static_cast<NodeStrokePreview*>(m_button_brushes[i]->m_children[0].get());
b->m_brush = state.brushes[i];
b->draw_stroke();
auto c = static_cast<NodeBorder*>(m_button_colors[i]->m_children[0].get());
c->m_color = state.colors[i];
}
set_selected_color_index(state.color_index, fire_event);
set_selected_brush_index(state.brush_index, fire_event);
const auto plan = pp::app::plan_quick_state_restore(
state.brush_index,
state.color_index,
static_cast<int>(m_button_brushes.size()),
fire_event);
if (!plan)
return;
LegacyQuickUiServices services(*this, &state);
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
if (!status.ok())
LOG("Quick restore action failed: %s", status.message);
}
void NodePanelQuick::reset_state(bool fire_event /*= false*/)
{
for (int i = 0; i < 3; i++)
{
auto b = static_cast<NodeStrokePreview*>(m_button_brushes[i]->m_children[0].get());
b->m_brush = std::make_shared<Brush>();
b->m_brush->load_tip("data/brushes/Round-Hard.png", "data/brushes/thumbs/Round-Hard.png");
b->draw_stroke();
}
static_cast<NodeBorder*>(m_button_colors[0]->m_children[0].get())->m_color = glm::vec4(0, 0, 0, 1);
static_cast<NodeBorder*>(m_button_colors[1]->m_children[0].get())->m_color = glm::vec4(.5, .5, .5, 1);
static_cast<NodeBorder*>(m_button_colors[2]->m_children[0].get())->m_color = glm::vec4(1, 1, 1, 1);
set_selected_brush_index(0, fire_event);
set_selected_color_index(0, fire_event);
const auto plan = pp::app::plan_quick_state_reset(static_cast<int>(m_button_brushes.size()), fire_event);
if (!plan)
return;
LegacyQuickUiServices services(*this);
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
if (!status.ok())
LOG("Quick reset action failed: %s", status.message);
}
void NodePanelQuick::init_controls()
@@ -179,10 +370,12 @@ NodeButtonCustom* NodePanelQuick::init_button_brush(const std::string& name, boo
{
LOG("init_button_brush %s", name.c_str());
auto button = find<NodeButtonCustom>(name.c_str());
if (!button)
if (!button) {
LOG("couldn't find button %s", name.c_str());
return nullptr;
}
button->on_click = std::bind(&this_class::handle_button_brush_click, this, std::placeholders::_1);
LOG("button has %d children", button->m_children.size());
LOG("button has %d children", static_cast<int>(button->m_children.size()));
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
pr->m_brush = std::make_shared<Brush>();
pr->m_brush->m_tip_size_pressure = szp;
@@ -197,138 +390,36 @@ NodeButtonCustom* NodePanelQuick::init_button_brush(const std::string& name, boo
void NodePanelQuick::handle_button_brush_click(Node* button)
{
// the first time select the box
if (m_button_brush_current != button)
{
auto b = static_cast<NodeButtonCustom*>(button);
b->set_active(true);
m_button_brush_current->set_active(false);
m_button_brush_current = b;
m_button_brush_current_preview = static_cast<NodeStrokePreview*>(button->m_children[0].get());
if (on_brush_change)
on_brush_change(this, m_button_brush_current_preview->m_brush);
const auto clicked = std::find(m_button_brushes.begin(), m_button_brushes.end(), button);
const int clicked_index = static_cast<int>(std::distance(m_button_brushes.begin(), clicked));
const auto plan = pp::app::plan_quick_slot_click(
pp::app::QuickUiSlotKind::brush,
get_selected_brush_index(),
clicked_index,
static_cast<int>(m_button_brushes.size()));
if (!plan)
return;
}
// if the box is already selected show the popup
auto popup = App::I->presets;
auto screen = root()->m_size;
glm::vec2 tick_sz = { 16, 32 };
glm::vec2 tick_pos = button->m_pos + glm::vec2(button->m_size.x, 0);
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y };
auto tick = root()->add_child<NodeImage>();
tick->SetPositioning(YGPositionTypeAbsolute);
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
tick->SetSize(tick_sz);
tick->set_image("data/ui/popup-tick.png");
tick->m_scale = { 1, 1 };
float hh = popup->m_container->m_children.size() > 10 ? (screen.y - 90.f) : 400.f;
popup->SetWidth(350);
popup->SetHeight(glm::max(hh, 400.f));
popup->SetPositioning(YGPositionTypeAbsolute);
popup->SetPosition(popup_pos);
root()->add_child(popup);
root()->update();
popup->tick(0);
popup->update();
if (tick_pos.x + popup->m_size.x > screen.x)
{
tick_pos = button->m_pos - glm::vec2(tick_sz.x, 0);
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y };
tick->m_scale.x = -1.f;
}
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
popup->SetPosition(popup_pos);
tick->SetPosition(tick_pos.x, tick_pos.y + (button->m_size.y - tick_sz.y) * 0.5f);
popup->update();
popup->m_mouse_ignore = false;
popup->m_flood_events = true;
popup->m_capture_children = false;
popup->mouse_capture();
popup->on_popup_close = [this, tick](Node*) {
tick->destroy();
};
popup->on_brush_changed = [this, button](Node* target, std::shared_ptr<Brush>& b) {
auto pr = static_cast<NodeStrokePreview*>(button->m_children[0].get());
*pr->m_brush = *b;
pr->m_brush->load();
pr->draw_stroke();
if (on_brush_change)
on_brush_change(button, pr->m_brush);
};
LegacyQuickUiServices services(*this);
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
if (!status.ok())
LOG("Quick brush action failed: %s", status.message);
}
void NodePanelQuick::handle_button_color_click(Node* target)
{
// the first time select the box
if (m_button_color_current != target)
{
auto button = static_cast<NodeButtonCustom*>(target);
button->set_active(true);
m_button_color_current->set_active(false);
m_button_color_current = button;
m_button_color_current_inner = static_cast<NodeBorder*>(m_button_color_current->m_children[0].get());
if (on_color_change)
on_color_change(this, m_button_color_current_inner->m_color);
const auto clicked = std::find(m_button_colors.begin(), m_button_colors.end(), target);
const int clicked_index = static_cast<int>(std::distance(m_button_colors.begin(), clicked));
const auto plan = pp::app::plan_quick_slot_click(
pp::app::QuickUiSlotKind::color,
get_selected_color_index(),
clicked_index,
static_cast<int>(m_button_colors.size()));
if (!plan)
return;
}
// if the box is already selected show the popup
auto popup = m_picker;
auto screen = root()->m_size;
glm::vec2 tick_sz = { 16, 32 };
glm::vec2 tick_pos = target->m_pos + glm::vec2(target->m_size.x, 0);
glm::vec2 popup_pos = { tick_pos.x + tick_sz.x, tick_pos.y - 140.f };
auto tick = root()->add_child<NodeImage>();
tick->SetPositioning(YGPositionTypeAbsolute);
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
tick->SetSize(tick_sz);
tick->set_image("data/ui/popup-tick.png");
tick->m_scale = { 1, 1 };
//float hh = popup->m_container->m_children.size() > 10 ? (screen.y / App::I->zoom - 90.f) : 400.f;
//popup->SetHeight(glm::max(hh, 400.f));
popup->SetPositioning(YGPositionTypeAbsolute);
popup->SetPosition(popup_pos);
root()->add_child(popup);
root()->update();
popup->tick(0);
popup->update();
if (tick_pos.x + popup->m_size.x > screen.x)
{
tick_pos = target->m_pos - glm::vec2(tick_sz.x, 0);
popup_pos = { tick_pos.x - popup->GetWidth(), tick_pos.y - 140.f };
tick->m_scale.x = -1.f;
}
popup_pos = glm::clamp(popup_pos, { 0, 0 }, screen - popup->m_size);
popup->SetPosition(popup_pos);
tick->SetPosition(tick_pos.x, tick_pos.y + (target->m_size.y - tick_sz.y) * 0.5f);
popup->update();
popup->m_mouse_ignore = false;
popup->m_flood_events = true;
popup->m_capture_children = false;
popup->mouse_capture();
auto c = static_cast<NodeBorder*>(target->m_children[0].get());
m_picker->set_color(c->m_color);
m_picker->on_popup_close = [this, tick](Node*) {
tick->destroy();
};
m_picker->on_color_change = [this, c](Node*, glm::vec3 rgb) {
c->m_color = glm::vec4(rgb, 1.f);
if (on_color_change)
on_color_change(this, rgb);
};
LegacyQuickUiServices services(*this);
const auto status = pp::app::execute_quick_ui_plan(plan.value(), services);
if (!status.ok())
LOG("Quick color action failed: %s", status.message);
}

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_scroll.h"
#include "renderer_gl/opengl_capabilities.h"
#include "event.h"
#include "shader.h"
@@ -110,7 +111,7 @@ void NodeScroll::draw()
if (rect.w > 0 && rect.z > 0)
{
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
ShaderManager::use(kShader::Color);
if (m_direction == kScrollDirection::Vertical)

View File

@@ -6,6 +6,7 @@
#include "bezier.h"
#include "canvas.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"
std::atomic_int NodeStrokePreview::s_instances{ 0 };
std::atomic_bool NodeStrokePreview::s_running{ false };
@@ -71,7 +72,7 @@ void NodeStrokePreview::restore_context()
NodeBorder::restore_context();
init_controls();
if (m_size.x > 0 && m_size.y > 0)
m_tex_preview.create(m_size.x, m_size.y);
m_tex_preview.create(static_cast<int>(m_size.x), static_cast<int>(m_size.y));
draw_stroke();
}
@@ -89,11 +90,15 @@ void NodeStrokePreview::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2
m_rtt_mixer.bindFramebuffer();
glViewport(0, 0, m_rtt_mixer.getWidth(), m_rtt_mixer.getHeight());
glDisable(GL_DEPTH_TEST);
glEnable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::depth_test_state());
glEnable(pp::renderer::gl::scissor_test_state());
glDisable(pp::renderer::gl::blend_state());
glScissor(bb_min.x, bb_min.y, bb_sz.x, bb_sz.y);
glScissor(
static_cast<int>(bb_min.x),
static_cast<int>(bb_min.y),
static_cast<int>(bb_sz.x),
static_cast<int>(bb_sz.y));
const auto& b = m_brush;
glm::vec2 patt_scale = glm::vec2(b->m_pattern_scale);
@@ -126,16 +131,16 @@ void NodeStrokePreview::stroke_draw_mix(const glm::vec2& bb_min, const glm::vec2
ShaderManager::u_float(kShaderUniform::DualAlpha, b->m_dual_opacity);
m_sampler_linear.bind(0);
glActiveTexture(GL_TEXTURE0);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
m_tex_background.bind();
glActiveTexture(GL_TEXTURE1);
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
m_rtt.bindTexture();
glActiveTexture(GL_TEXTURE3);
glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
m_tex_dual.bind();
glActiveTexture(GL_TEXTURE4);
glActiveTexture(pp::renderer::gl::active_texture_unit(4U));
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
m_plane.draw_fill();
m_rtt_mixer.unbindFramebuffer();
@@ -146,7 +151,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array<vertex_t, 4>& P, Tex
{
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE1);
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
blend_tex.bind(); // bg, copy of framebuffer (copied before drawing)
}
@@ -167,7 +172,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array<vertex_t, 4>& P, Tex
if (!ShaderManager::ext_framebuffer_fetch)
{
// this is also used by the mixer
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, tex_pos.x, tex_pos.y,
glCopyTexSubImage2D(pp::renderer::gl::texture_2d_target(), 0, tex_pos.x, tex_pos.y,
tex_pos.x, tex_pos.y, tex_sz.x, tex_sz.y);
}
@@ -186,7 +191,7 @@ glm::vec4 NodeStrokePreview::stroke_draw_samples(std::array<vertex_t, 4>& P, Tex
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE1);
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
blend_tex.unbind();
}
@@ -259,8 +264,8 @@ void NodeStrokePreview::draw_stroke_immediate()
GLint vp[4];
GLfloat cc[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
glGetIntegerv(pp::renderer::gl::viewport_query(), vp);
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
float zoom = root()->m_zoom;
@@ -345,7 +350,7 @@ void NodeStrokePreview::draw_stroke_immediate()
if (b->m_pattern_flipx) patt_scale.x *= -1.f;
if (b->m_pattern_flipy) patt_scale.y *= -1.f;
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
ShaderManager::use(kShader::Stroke);
ShaderManager::u_int(kShaderUniform::Tex, 0); // brush
if (!ShaderManager::ext_framebuffer_fetch)
@@ -373,10 +378,10 @@ void NodeStrokePreview::draw_stroke_immediate()
ShaderManager::u_float(kShaderUniform::MixAlpha, 0);
ShaderManager::u_float(kShaderUniform::Wet, 0);
ShaderManager::u_float(kShaderUniform::Noise, 0);
glActiveTexture(GL_TEXTURE0);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
dual_brush->m_tip_texture ?
dual_brush->m_tip_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
auto frames_dual = stroke_draw_compute(m_dual_stroke, zoom);
for (auto& f : frames_dual)
{
@@ -387,9 +392,17 @@ void NodeStrokePreview::draw_stroke_immediate()
}
// copy raw stroke to tex
glActiveTexture(GL_TEXTURE1);
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
m_tex_dual.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.x, size.y);
glCopyTexSubImage2D(
pp::renderer::gl::texture_2d_target(),
0,
0,
0,
0,
0,
static_cast<int>(size.x),
static_cast<int>(size.y));
}
// CHEKCERBOARD
@@ -402,7 +415,15 @@ void NodeStrokePreview::draw_stroke_immediate()
m_plane.draw_fill();
//m_rtt.clear({ .3f, .3f, .3f, 1.f });
m_tex_background.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.x, size.y);
glCopyTexSubImage2D(
pp::renderer::gl::texture_2d_target(),
0,
0,
0,
0,
0,
static_cast<int>(size.x),
static_cast<int>(size.y));
// DRAW MAIN BRUSH
@@ -412,19 +433,19 @@ void NodeStrokePreview::draw_stroke_immediate()
ShaderManager::u_float(kShaderUniform::Wet, b->m_tip_wet);
ShaderManager::u_float(kShaderUniform::Noise, b->m_tip_noise);
glActiveTexture(GL_TEXTURE0);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
b->m_tip_texture->bind();
if (!ShaderManager::ext_framebuffer_fetch)
{
glActiveTexture(GL_TEXTURE1);
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
m_tex.bind(); // tmp swap for blending
}
glActiveTexture(GL_TEXTURE2);
glActiveTexture(pp::renderer::gl::active_texture_unit(2U));
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE3);
b->m_tip_mix > 0.f ? m_rtt_mixer.bindTexture() : glBindTexture(GL_TEXTURE_2D, 0);
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
b->m_tip_mix > 0.f ? m_rtt_mixer.bindTexture() : glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
auto frames = stroke_draw_compute(m_stroke, zoom);
m_rtt.clear();
for (auto& f : frames)
@@ -443,13 +464,21 @@ void NodeStrokePreview::draw_stroke_immediate()
ShaderManager::u_float(kShaderUniform::Opacity, f.opacity);
/*auto rect =*/ stroke_draw_samples(f.shapes, m_tex);
}
glActiveTexture(GL_TEXTURE3);
glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
m_rtt_mixer.unbindTexture();
// copy raw stroke to tex
glActiveTexture(GL_TEXTURE1);
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
m_tex.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.x, size.y);
glCopyTexSubImage2D(
pp::renderer::gl::texture_2d_target(),
0,
0,
0,
0,
0,
static_cast<int>(size.x),
static_cast<int>(size.y));
// COMPOSITE
@@ -483,21 +512,29 @@ void NodeStrokePreview::draw_stroke_immediate()
m_sampler_linear.bind(3);
m_sampler_linear_repeat.bind(4);
glActiveTexture(GL_TEXTURE0);
glActiveTexture(pp::renderer::gl::active_texture_unit(0U));
m_tex_background.bind();
glActiveTexture(GL_TEXTURE1);
glActiveTexture(pp::renderer::gl::active_texture_unit(1U));
m_tex.bind();
glActiveTexture(GL_TEXTURE3);
glActiveTexture(pp::renderer::gl::active_texture_unit(3U));
m_tex_dual.bind();
glActiveTexture(GL_TEXTURE4);
glActiveTexture(pp::renderer::gl::active_texture_unit(4U));
b->m_pattern_texture ?
b->m_pattern_texture->bind() :
glBindTexture(GL_TEXTURE_2D, 0);
glBindTexture(pp::renderer::gl::texture_2d_target(), 0);
m_plane.draw_fill();
// copy the result to the actual preview
m_tex_preview.bind();
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, size.x, size.y);
glCopyTexSubImage2D(
pp::renderer::gl::texture_2d_target(),
0,
0,
0,
0,
0,
static_cast<int>(size.x),
static_cast<int>(size.y));
m_rtt.unbindFramebuffer();
@@ -538,9 +575,13 @@ void NodeStrokePreview::draw_stroke()
BT_SetTerminate();
m_sampler_linear.create();
m_sampler_linear_repeat.create(GL_LINEAR, GL_REPEAT);
m_sampler_linear_repeat.create(
pp::renderer::gl::linear_texture_filter(),
pp::renderer::gl::repeat_texture_wrap());
m_sampler_mipmap.create();
m_sampler_mipmap.set_filter(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR);
m_sampler_mipmap.set_filter(
pp::renderer::gl::linear_mipmap_linear_texture_filter(),
pp::renderer::gl::linear_texture_filter());
m_brush_shape.create();
while (s_running)
{

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_text.h"
#include "renderer_gl/opengl_capabilities.h"
#include "shader.h"
Node* NodeText::clone_instantiate() const
@@ -173,9 +174,9 @@ void NodeText::draw()
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, m_proj * pos);
ShaderManager::u_vec4(kShaderUniform::Col, m_color);
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
m_text_mesh.draw();
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
}
void NodeText::handle_resize(glm::vec2 old_size, glm::vec2 new_size, float zoom)

View File

@@ -2,6 +2,7 @@
#include "app.h"
#include "log.h"
#include "node_text_input.h"
#include "renderer_gl/opengl_capabilities.h"
#include "node_border.h"
Node* NodeTextInput::clone_instantiate() const
@@ -219,13 +220,13 @@ void NodeTextInput::draw()
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, m_proj * pos);
ShaderManager::u_vec4(kShaderUniform::Col, m_color);
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
m_text_mesh.draw();
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
if (m_cursor_visible)
{
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
glm::mat4 cur_pos = glm::translate(glm::vec3(m_pos + m_off + xy(m_text_mesh.cur_box), 0));
glm::mat4 cur_scale = glm::scale(glm::vec3(zw(m_text_mesh.cur_box), 1));
glm::mat4 cur_pivot = glm::translate(glm::vec3(0.5, 0.5, 0));

View File

@@ -1,6 +1,7 @@
#include "pch.h"
#include "log.h"
#include "node_viewport.h"
#include "renderer_gl/opengl_capabilities.h"
#include "shader.h"
#include "app.h"
@@ -11,24 +12,24 @@ void NodeViewport::draw()
GLint vp[4];
GLfloat cc[4];
glGetIntegerv(GL_VIEWPORT, vp);
glGetFloatv(GL_COLOR_CLEAR_VALUE, cc);
glGetIntegerv(pp::renderer::gl::viewport_query(), vp);
glGetFloatv(pp::renderer::gl::color_clear_value_query(), cc);
glClearColor(1, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glClear(pp::renderer::gl::framebuffer_color_buffer_mask());
auto box = m_clip * root()->m_zoom;
glm::ivec4 c = (glm::ivec4)glm::vec4(box.x, (int)(vp[3] - box.y - box.w), box.z, box.w);
glViewport(c.x + App::I->off_x, c.y + App::I->off_y, c.z, c.w);
TextureManager::get(m_tex_id).bind();
m_sampler->bind(0);
glEnable(GL_BLEND);
glEnable(pp::renderer::gl::blend_state());
ShaderManager::use(kShader::Texture);
ShaderManager::u_int(kShaderUniform::Tex, 0);
ShaderManager::u_mat4(kShaderUniform::MVP, proj * cam);
m_faces->draw_fill();
m_sampler->unbind();
TextureManager::get(m_tex_id).unbind();
glDisable(GL_BLEND);
glDisable(pp::renderer::gl::blend_state());
glViewport(vp[0], vp[1], vp[2], vp[3]);
glClearColor(cc[0], cc[1], cc[2], cc[3]);

View File

@@ -44,6 +44,67 @@ namespace {
return stroke;
}
[[nodiscard]] float blend_stroke_screen(float base, float stroke) noexcept
{
return base + stroke - (base * stroke);
}
[[nodiscard]] float blend_stroke_hard_light(float base, float stroke) noexcept
{
return stroke < 0.5F
? base * (stroke * 2.0F)
: blend_stroke_screen(base, 2.0F * stroke - 1.0F);
}
[[nodiscard]] float blend_stroke_hard_mix(float base, float stroke) noexcept
{
if (base == 0.0F) {
return 0.0F;
}
return base + stroke < 0.5F ? 0.0F : saturate(base + stroke);
}
[[nodiscard]] float blend_stroke_color_dodge(float base, float stroke) noexcept
{
if (base == 0.0F) {
return 0.0F;
}
if (stroke == 1.0F) {
return 1.0F;
}
return base / (1.0F - stroke);
}
[[nodiscard]] float blend_stroke_color_burn(float base, float stroke) noexcept
{
if (base == 1.0F) {
return 1.0F;
}
if (stroke == 0.0F) {
return 0.0F;
}
return 1.0F - std::min(1.0F, (1.0F - base) / stroke);
}
[[nodiscard]] float blend_stroke_linear_height(float base, float stroke, float depth) noexcept
{
const auto partial = (1.0F - stroke) * std::pow(depth, 0.25F) + (base * depth * 10.0F);
return base * partial;
}
[[nodiscard]] float blend_stroke_height(float base, float stroke, float depth) noexcept
{
const auto a = std::pow(1.0F - stroke, std::max(1.0F, (1.0F - depth) * 10.0F))
* std::pow(depth, 0.25F);
const auto b = base * depth * 5.0F;
return base * (a + b);
}
[[nodiscard]] float blend_rgb(float base, float stroke, float base_alpha, float stroke_alpha, float alpha_total, BlendMode mode) noexcept
{
if (alpha_total <= 0.0F) {
@@ -88,6 +149,44 @@ Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept
};
}
float blend_stroke_alpha(
float base,
float stroke,
float depth,
StrokeBlendMode mode) noexcept
{
base = saturate(base);
stroke = saturate(stroke);
depth = saturate(depth);
switch (mode) {
case StrokeBlendMode::normal:
return saturate(mix(base, stroke, depth));
case StrokeBlendMode::multiply:
return saturate(mix(base, base * stroke, depth));
case StrokeBlendMode::subtract:
return saturate(mix(base, std::max(0.0F, base - stroke), depth));
case StrokeBlendMode::darken:
return saturate(mix(base, std::min(base, stroke), depth));
case StrokeBlendMode::overlay:
return saturate(mix(base, blend_stroke_hard_light(stroke, base), depth));
case StrokeBlendMode::color_dodge:
return saturate(mix(base, blend_stroke_color_dodge(base, stroke), depth));
case StrokeBlendMode::color_burn:
return saturate(mix(base, blend_stroke_color_burn(base, stroke), depth));
case StrokeBlendMode::linear_burn:
return saturate(mix(base, saturate(base + stroke - 1.0F), depth));
case StrokeBlendMode::hard_mix:
return saturate(mix(base, blend_stroke_hard_mix(base, stroke), depth));
case StrokeBlendMode::linear_height:
return saturate(blend_stroke_linear_height(base, stroke, depth));
case StrokeBlendMode::height:
return saturate(blend_stroke_height(base, stroke, depth));
}
return 1.0F;
}
const char* blend_mode_name(BlendMode mode) noexcept
{
switch (mode) {
@@ -106,4 +205,34 @@ const char* blend_mode_name(BlendMode mode) noexcept
return "unknown";
}
const char* stroke_blend_mode_name(StrokeBlendMode mode) noexcept
{
switch (mode) {
case StrokeBlendMode::normal:
return "normal";
case StrokeBlendMode::multiply:
return "multiply";
case StrokeBlendMode::subtract:
return "subtract";
case StrokeBlendMode::darken:
return "darken";
case StrokeBlendMode::overlay:
return "overlay";
case StrokeBlendMode::color_dodge:
return "color_dodge";
case StrokeBlendMode::color_burn:
return "color_burn";
case StrokeBlendMode::linear_burn:
return "linear_burn";
case StrokeBlendMode::hard_mix:
return "hard_mix";
case StrokeBlendMode::linear_height:
return "linear_height";
case StrokeBlendMode::height:
return "height";
}
return "unknown";
}
}

View File

@@ -12,6 +12,20 @@ enum class BlendMode : std::uint8_t {
overlay,
};
enum class StrokeBlendMode : std::uint8_t {
normal,
multiply,
subtract,
darken,
overlay,
color_dodge,
color_burn,
linear_burn,
hard_mix,
linear_height,
height,
};
struct Rgba {
float r = 0.0F;
float g = 0.0F;
@@ -20,6 +34,12 @@ struct Rgba {
};
[[nodiscard]] Rgba blend_pixels(Rgba base, Rgba stroke, BlendMode mode) noexcept;
[[nodiscard]] float blend_stroke_alpha(
float base,
float stroke,
float depth,
StrokeBlendMode mode) noexcept;
[[nodiscard]] const char* blend_mode_name(BlendMode mode) noexcept;
[[nodiscard]] const char* stroke_blend_mode_name(StrokeBlendMode mode) noexcept;
}

View File

@@ -0,0 +1,7 @@
#include "platform_api/platform_services.h"
namespace pp::platform {
namespace {
static_assert(sizeof(PlatformServices*) == sizeof(void*));
}
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include <functional>
#include <string>
#include <string_view>
#include <vector>
namespace pp::platform {
using PickedPathCallback = std::function<void(std::string path)>;
using PreparedFileCallback = std::function<void(std::string path, bool saved)>;
struct PlatformStoragePaths {
std::string data_path;
std::string work_path;
std::string recording_path;
std::string temporary_path;
};
class PlatformServices {
public:
virtual ~PlatformServices() = default;
[[nodiscard]] virtual PlatformStoragePaths prepare_storage_paths() = 0;
virtual void log_stacktrace() = 0;
virtual void trigger_crash_test() = 0;
[[nodiscard]] virtual std::string clipboard_text() = 0;
[[nodiscard]] virtual bool set_clipboard_text(std::string_view text) = 0;
virtual void set_cursor_visible(bool visible) = 0;
virtual void set_virtual_keyboard_visible(bool visible) = 0;
virtual void attach_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 bind_default_render_target() = 0;
virtual void bind_main_render_target() = 0;
virtual void apply_render_platform_hints() = 0;
virtual void install_render_debug_callback() = 0;
virtual void begin_render_capture_frame() = 0;
virtual void end_render_capture_frame() = 0;
[[nodiscard]] virtual bool deletes_recorded_files_on_clear() = 0;
virtual void clear_recorded_files(std::string_view recording_path) = 0;
[[nodiscard]] virtual bool enables_live_asset_reloading() = 0;
virtual void update_platform_frame(float delta_time_seconds) = 0;
virtual void report_rendered_frames(int frames) = 0;
virtual void display_file(std::string_view path) = 0;
virtual void share_file(std::string_view path) = 0;
virtual void request_app_close() = 0;
virtual void pick_image(PickedPathCallback callback) = 0;
virtual void pick_file(std::vector<std::string> file_types, PickedPathCallback callback) = 0;
virtual void pick_save_file(std::vector<std::string> file_types, PickedPathCallback callback) = 0;
virtual void pick_directory(PickedPathCallback callback) = 0;
virtual void save_prepared_file(
std::string_view path,
std::string_view suggested_name,
PreparedFileCallback callback) = 0;
};
}

View File

@@ -0,0 +1,468 @@
#include "pch.h"
#include "platform_legacy/legacy_platform_services.h"
#include "app.h"
#include "app_core/document_platform_io.h"
#include "renderer_gl/opengl_capabilities.h"
#ifdef __ANDROID__
void displayKeyboard(bool pShow);
void android_async_lock();
void android_async_swap();
void android_async_unlock();
void android_attach_jni();
void android_detach_jni();
void android_pick_file(std::function<void(std::string)> callback);
void android_pick_file_save(std::function<void(std::string)> callback);
std::string android_get_clipboard();
bool android_set_clipboard(const std::string& s);
#elif __APPLE__
void delete_all_files_in_path(const std::string& source_path);
#elif __LINUX__
#include <tinyfiledialogs.h>
std::string linux_home_path();
int mkpath(const std::string& dir, mode_t mode = DEFFILEMODE);
void linux_update_fps(int frames);
#elif __WEB__
void webgl_pick_file(std::function<void(std::string)> callback);
void webgl_pick_file_save(
const std::string& path,
const std::string& name,
std::function<void(bool)> callback);
void webgl_sync();
#endif
namespace {
void invoke_picked_path_if_selected(
const std::string& path,
const std::function<void(std::string path)>& callback)
{
if (pp::app::plan_picked_path(path) == pp::app::PickedPathAction::invoke_callback)
callback(path);
}
// DEBT-0017: fallback for platforms that do not inject PlatformServices yet.
class LegacyPlatformServices final : public pp::platform::PlatformServices {
public:
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths() override
{
#if defined(__IOS__)
[App::I->ios_view init_dirs];
return {
App::I->data_path,
App::I->work_path,
App::I->rec_path,
App::I->tmp_path,
};
#elif defined(__OSX__)
[App::I->osx_app init_dirs];
return {
App::I->data_path,
App::I->work_path,
App::I->rec_path,
App::I->tmp_path,
};
#elif __LINUX__
const std::string data_path = linux_home_path() + "/PanoPainter";
mkpath(data_path + "/brushes");
mkpath(data_path + "/brushes/thumbs");
mkpath(data_path + "/patterns");
mkpath(data_path + "/patterns/thumbs");
mkpath(data_path + "/settings");
mkpath(data_path + "/frames");
return {
data_path,
data_path,
data_path + "/frames",
{},
};
#elif __WEB__
const std::string data_path = "/PanoPainter";
mkdir(data_path.c_str(), 0777);
mkdir((data_path + "/brushes").c_str(), 0777);
mkdir((data_path + "/brushes/thumbs").c_str(), 0777);
mkdir((data_path + "/patterns").c_str(), 0777);
mkdir((data_path + "/patterns/thumbs").c_str(), 0777);
mkdir((data_path + "/settings").c_str(), 0777);
mkdir((data_path + "/frames").c_str(), 0777);
return {
data_path,
data_path,
data_path + "/frames",
{},
};
#else
return {
App::I->data_path,
App::I->work_path,
App::I->rec_path,
App::I->tmp_path,
};
#endif
}
void log_stacktrace() override
{
#if defined(__OSX__)
NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
LOG("callstack:\n%s", [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
#endif
}
void trigger_crash_test() override
{
#ifdef __IOS__
[App::I->ios_view crash];
#elif __OSX__
[App::I->osx_view hockeyapp_crash];
#elif defined(__ANDROID__)
int *x = nullptr; *x = 42;
LOG("%d", *x);
#endif
}
[[nodiscard]] std::string clipboard_text() override
{
#if __IOS__
return [App::I->ios_view clipboard_get_string];
#elif __OSX__
return [App::I->osx_view clipboard_get_string];
#elif __ANDROID__
return android_get_clipboard();
#else
return {};
#endif
}
[[nodiscard]] bool set_clipboard_text(std::string_view text) override
{
const std::string value(text);
#if __IOS__
return [App::I->ios_view clipboard_set_string:value];
#elif __OSX__
return [App::I->osx_view clipboard_set_string:value];
#elif __ANDROID__
return android_set_clipboard(value);
#else
return false;
#endif
}
void set_cursor_visible(bool visible) override
{
#ifdef __OSX__
[App::I->osx_view show_cursor:visible];
#else
(void)visible;
#endif
}
void set_virtual_keyboard_visible(bool visible) override
{
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
if (visible)
[App::I->ios_view show_keyboard];
else
[App::I->ios_view hide_keyboard];
});
#elif __ANDROID__
displayKeyboard(visible);
#else
(void)visible;
#endif
}
void attach_ui_thread() override
{
#ifdef __ANDROID__
android_attach_jni();
#endif
}
void detach_ui_thread() override
{
#ifdef __ANDROID__
android_detach_jni();
#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 bind_default_render_target() override
{
glBindFramebuffer(
static_cast<GLenum>(pp::renderer::gl::framebuffer_target()),
pp::renderer::gl::default_framebuffer_id());
}
void bind_main_render_target() override
{
#if __IOS__
[App::I->ios_view->glview bindDrawable];
#else
bind_default_render_target();
#endif
}
void apply_render_platform_hints() override
{
#if defined(__OSX__)
glEnable(static_cast<GLenum>(pp::renderer::gl::program_point_size_state()));
glEnable(static_cast<GLenum>(pp::renderer::gl::line_smooth_state()));
#endif
}
void install_render_debug_callback() override
{
}
void begin_render_capture_frame() override
{
}
void end_render_capture_frame() override
{
}
[[nodiscard]] bool deletes_recorded_files_on_clear() override
{
#if defined(__IOS__) || defined(__OSX__)
return true;
#else
return false;
#endif
}
void clear_recorded_files(std::string_view recording_path) override
{
#if defined(__IOS__) || defined(__OSX__)
delete_all_files_in_path(std::string(recording_path));
#else
(void)recording_path;
#endif
}
[[nodiscard]] bool enables_live_asset_reloading() override
{
#if defined(__OSX__)
return true;
#else
return false;
#endif
}
void update_platform_frame(float delta_time_seconds) override
{
(void)delta_time_seconds;
}
void report_rendered_frames(int frames) override
{
#ifdef __LINUX__
linux_update_fps(frames);
#else
(void)frames;
#endif
}
void pick_image(pp::platform::PickedPathCallback callback) override
{
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->ios_view pick_photo:callback];
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
NSArray* fileTypes = [NSArray arrayWithObjects:@"png", @"PNG", @"jpg", @"JPG", @"jpeg", nil];
std::string path = [App::I->osx_view pick_file:fileTypes];
invoke_picked_path_if_selected(path, callback);
});
#elif __ANDROID__
android_pick_file(callback);
#elif __LINUX__
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
invoke_picked_path_if_selected(p, callback);
#elif __WEB__
webgl_pick_file(callback);
#else
(void)callback;
#endif
}
void pick_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
{
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()];
for (const auto& t : file_types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
[App::I->ios_view pick_file:fileTypes then:callback];
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()];
for (const auto& t : file_types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
std::string path = [App::I->osx_view pick_file:fileTypes];
invoke_picked_path_if_selected(path, callback);
});
#elif __ANDROID__
android_pick_file(callback);
#elif __LINUX__
if (auto p = tinyfd_openFileDialog("Open File", "", 0, nullptr, nullptr, false))
invoke_picked_path_if_selected(p, callback);
#elif __WEB__
webgl_pick_file(callback);
#else
(void)file_types;
(void)callback;
#endif
}
void pick_save_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
{
#if __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableArray<NSString*>* fileTypes = [NSMutableArray arrayWithCapacity:file_types.size()];
for (const auto& t : file_types)
[fileTypes addObject:[NSString stringWithCString:t.c_str() encoding:NSUTF8StringEncoding]];
std::string path = [App::I->osx_view pick_file_save:fileTypes];
invoke_picked_path_if_selected(path, callback);
});
#elif __ANDROID__
android_pick_file_save(callback);
#else
(void)file_types;
(void)callback;
#endif
}
void pick_directory(pp::platform::PickedPathCallback callback) override
{
#ifdef __IOS__
(void)callback;
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
std::string path = [App::I->osx_view pick_dir];
invoke_picked_path_if_selected(path, callback);
});
#elif __ANDROID__
(void)callback;
#else
(void)callback;
#endif
}
void display_file(std::string_view path) override
{
const std::string value(path);
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->ios_view display_file:value];
});
#elif __OSX__
[[NSWorkspace sharedWorkspace] openFile:[NSString stringWithUTF8String:value.c_str()]];
#else
(void)value;
#endif
}
void share_file(std::string_view path) override
{
const std::string value(path);
#ifdef __IOS__
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->ios_view share_file:[NSString stringWithUTF8String:value.c_str()]];
});
#elif __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->osx_view share_file:[NSString stringWithUTF8String:value.c_str()]];
});
#else
(void)value;
#endif
}
void request_app_close() override
{
#ifdef __OSX__
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->osx_view close];
});
#elif __LINUX__
glfwSetWindowShouldClose(App::I->glfw_window, GLFW_TRUE);
#endif
}
void save_prepared_file(
std::string_view path,
std::string_view suggested_name,
pp::platform::PreparedFileCallback callback) override
{
const std::string value(path);
const std::string name(suggested_name);
#ifdef __IOS__
(void)name;
dispatch_async(dispatch_get_main_queue(), ^{
[App::I->ios_view pick_file_save:value];
});
callback(value, true);
#elif __WEB__
webgl_pick_file_save(value, name, [callback = std::move(callback), value](bool success) {
callback(value, success);
});
#else
(void)name;
callback(value, false);
#endif
}
};
}
namespace pp::platform::legacy {
PlatformServices& platform_services()
{
static LegacyPlatformServices services;
return services;
}
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include "platform_api/platform_services.h"
namespace pp::platform::legacy {
[[nodiscard]] PlatformServices& platform_services();
}

View File

@@ -0,0 +1,448 @@
#include "pch.h"
#include "platform_windows/windows_platform_services.h"
#include "log.h"
#include "renderer_gl/opengl_capabilities.h"
#include <deque>
#include <map>
extern HWND hWnd;
extern std::deque<std::packaged_task<void()>> main_tasklist;
extern std::mutex main_task_mutex;
void destroy_window();
void async_lock();
void async_unlock();
void win32_async_swap();
void win32_renderdoc_frame_start();
void win32_renderdoc_frame_end();
void win32_update_fps(int frames);
void win32_update_stylus(float dt);
namespace {
static CONSOLE_SCREEN_BUFFER_INFO render_debug_console_info;
[[nodiscard]] GLenum debug_severity_notification() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::debug_severity_notification());
}
[[nodiscard]] GLenum debug_severity_low() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::debug_severity_low());
}
[[nodiscard]] GLenum debug_severity_medium() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::debug_severity_medium());
}
[[nodiscard]] GLenum debug_severity_high() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::debug_severity_high());
}
[[nodiscard]] GLenum debug_output_state() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::debug_output_state());
}
[[nodiscard]] GLenum debug_output_synchronous_state() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::debug_output_synchronous_state());
}
void handle_gl_callback(
GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
const void* userParam)
{
(void)source;
(void)type;
(void)id;
(void)userParam;
static std::map<GLenum, WORD> colors = {
{ debug_severity_notification(), static_cast<WORD>(8) },
{ debug_severity_low(), static_cast<WORD>(8) },
{ debug_severity_medium(), static_cast<WORD>(FOREGROUND_GREEN | FOREGROUND_INTENSITY) },
{ debug_severity_high(), static_cast<WORD>(FOREGROUND_RED | FOREGROUND_INTENSITY) },
};
if (severity == debug_severity_high()
|| severity == debug_severity_medium()
|| severity == debug_severity_low())
{
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colors[severity]);
LOG("OPENGL: %.*s", length, message);
FlushConsoleInputBuffer(GetStdHandle(STD_OUTPUT_HANDLE));
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), render_debug_console_info.wAttributes);
#ifdef _DEBUG
if (severity == debug_severity_high())
__debugbreak();
#endif
}
}
void show_cursor(bool visible)
{
std::lock_guard<std::mutex> lock(main_task_mutex);
main_tasklist.emplace_back([=] {
if (visible)
while (ShowCursor(true) < 0);
else
while (ShowCursor(false) >= 0);
});
}
std::string clipboard_text()
{
std::string ret;
if (OpenClipboard(hWnd))
{
if (HANDLE h = GetClipboardData(CF_TEXT))
{
if (char* s = static_cast<char*>(GlobalLock(h)))
{
ret = s;
GlobalUnlock(h);
}
}
CloseClipboard();
}
return ret;
}
bool set_clipboard_text(const std::string& s)
{
bool success = false;
if (OpenClipboard(hWnd))
{
// owned by SetClipboardData
if (HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, s.size() + 1))
{
if (char* p = static_cast<char*>(GlobalLock(h)))
{
std::copy(s.begin(), s.end(), p);
p[s.size()] = 0;
GlobalUnlock(h);
success = true;
}
EmptyClipboard();
SetClipboardData(CF_TEXT, h);
}
CloseClipboard();
}
return success;
}
std::string open_file(const char* filter)
{
OPENFILENAMEA ofn;
char fileName[MAX_PATH] = "";
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = hWnd;
ofn.lpstrFilter = filter;
ofn.lpstrFile = fileName;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR;
ofn.lpstrDefExt = "";
ofn.lpstrInitialDir = "";
if (GetOpenFileNameA(&ofn) != NULL)
return fileName;
return "";
}
std::string save_file(const char* filter)
{
OPENFILENAMEA ofn;
char fileName[MAX_PATH] = "";
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = hWnd;
ofn.lpstrFilter = filter;
ofn.lpstrFile = fileName;
ofn.nMaxFile = MAX_PATH;
ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY | OFN_NOCHANGEDIR | OFN_OVERWRITEPROMPT;
ofn.lpstrDefExt = "";
ofn.lpstrInitialDir = "";
if (GetSaveFileNameA(&ofn) != NULL)
return fileName;
return "";
}
std::string open_directory()
{
BROWSEINFOA bi;
char Buffer[MAX_PATH];
ZeroMemory(Buffer, MAX_PATH);
ZeroMemory(&bi, sizeof(bi));
bi.hwndOwner = hWnd;
bi.pszDisplayName = Buffer;
bi.lpszTitle = "Title";
bi.ulFlags = BIF_EDITBOX | BIF_NEWDIALOGSTYLE | BIF_RETURNONLYFSDIRS | BIF_SHAREABLE;
LPCITEMIDLIST pFolder = SHBrowseForFolderA(&bi);
if (pFolder == NULL)
return "";
if (!SHGetPathFromIDListA(pFolder, Buffer))
return "";
return Buffer;
}
void invoke_selected_path(
const std::string& path,
const pp::platform::PickedPathCallback& callback)
{
if (!path.empty())
callback(path);
}
void ensure_directory(const std::string& path)
{
if (!PathFileExistsA(path.c_str()))
CreateDirectoryA(path.c_str(), NULL);
}
std::string build_supported_files_filter(const std::vector<std::string>& types)
{
std::string filter = "Supported Files (";
bool first_type = true;
for (const auto& t : types)
{
filter.append(std::string(first_type ? "" : " ,") + "*." + t);
first_type = false;
}
filter.append(")");
filter.push_back(0);
first_type = true;
for (const auto& t : types)
{
filter.append(std::string(first_type ? "" : ";") + "*." + t);
first_type = false;
}
filter.push_back(0);
return filter;
}
class WindowsPlatformServices final : public pp::platform::PlatformServices {
public:
[[nodiscard]] pp::platform::PlatformStoragePaths prepare_storage_paths() override
{
std::string data_path;
CHAR my_documents[MAX_PATH];
HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents);
if (SUCCEEDED(result))
{
data_path = std::string(my_documents) + "\\PanoPainter";
ensure_directory(data_path);
}
else
{
CHAR path[MAX_PATH];
GetCurrentDirectoryA(sizeof(path), path);
data_path = path;
}
ensure_directory(data_path + "\\frames");
ensure_directory(data_path + "\\brushes");
ensure_directory(data_path + "\\brushes\\thumbs");
ensure_directory(data_path + "\\patterns");
ensure_directory(data_path + "\\patterns\\thumbs");
ensure_directory(data_path + "\\settings");
return {
data_path,
data_path,
data_path + "\\frames",
{},
};
}
void log_stacktrace() override
{
}
void trigger_crash_test() override
{
__debugbreak();
}
[[nodiscard]] std::string clipboard_text() override
{
return ::clipboard_text();
}
[[nodiscard]] bool set_clipboard_text(std::string_view text) override
{
return ::set_clipboard_text(std::string(text));
}
void set_cursor_visible(bool visible) override
{
show_cursor(visible);
}
void set_virtual_keyboard_visible(bool visible) override
{
(void)visible;
}
void attach_ui_thread() override
{
}
void detach_ui_thread() override
{
}
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 bind_default_render_target() override
{
glBindFramebuffer(
static_cast<GLenum>(pp::renderer::gl::framebuffer_target()),
pp::renderer::gl::default_framebuffer_id());
}
void bind_main_render_target() override
{
bind_default_render_target();
}
void apply_render_platform_hints() override
{
glEnable(static_cast<GLenum>(pp::renderer::gl::program_point_size_state()));
glEnable(static_cast<GLenum>(pp::renderer::gl::line_smooth_state()));
}
void install_render_debug_callback() override
{
if (!glDebugMessageCallback)
return;
// colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &render_debug_console_info);
glDebugMessageCallback(handle_gl_callback, nullptr);
glEnable(debug_output_state());
glEnable(debug_output_synchronous_state());
}
void begin_render_capture_frame() override
{
win32_renderdoc_frame_start();
}
void end_render_capture_frame() override
{
win32_renderdoc_frame_end();
}
[[nodiscard]] bool deletes_recorded_files_on_clear() override
{
return false;
}
void clear_recorded_files(std::string_view recording_path) override
{
(void)recording_path;
}
[[nodiscard]] bool enables_live_asset_reloading() override
{
return true;
}
void update_platform_frame(float delta_time_seconds) override
{
win32_update_stylus(delta_time_seconds);
}
void report_rendered_frames(int frames) override
{
win32_update_fps(frames);
}
void display_file(std::string_view path) override
{
(void)path;
}
void share_file(std::string_view path) override
{
(void)path;
}
void request_app_close() override
{
destroy_window();
}
void pick_image(pp::platform::PickedPathCallback callback) override
{
const std::string path = open_file("Image Files (*.jpg, *.png)\0*.jpg;*.png");
invoke_selected_path(path, callback);
}
void pick_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
{
const std::string filter = build_supported_files_filter(file_types);
const std::string path = open_file(filter.c_str());
invoke_selected_path(path, callback);
}
void pick_save_file(std::vector<std::string> file_types, pp::platform::PickedPathCallback callback) override
{
const std::string filter = build_supported_files_filter(file_types);
const std::string path = save_file(filter.c_str());
invoke_selected_path(path, callback);
}
void pick_directory(pp::platform::PickedPathCallback callback) override
{
const std::string path = open_directory();
invoke_selected_path(path, callback);
}
void save_prepared_file(
std::string_view path,
std::string_view suggested_name,
pp::platform::PreparedFileCallback callback) override
{
(void)suggested_name;
callback(std::string(path), false);
}
};
}
namespace pp::platform::windows {
PlatformServices& platform_services()
{
static WindowsPlatformServices services;
return services;
}
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include "platform_api/platform_services.h"
namespace pp::platform::windows {
[[nodiscard]] PlatformServices& platform_services();
}

View File

@@ -0,0 +1,830 @@
#include "renderer_api/recording_renderer.h"
#include <new>
#include <utility>
namespace pp::renderer {
namespace {
[[nodiscard]] const char* non_null_name(const char* name) noexcept
{
return name == nullptr ? "" : name;
}
void push_command(
std::vector<RecordedRenderCommand>* commands,
RecordedRenderCommand command)
{
if (commands != nullptr) {
commands->push_back(command);
}
}
template <typename Resource, typename Interface, typename... Args>
[[nodiscard]] pp::foundation::Result<std::unique_ptr<Interface>> make_recording_resource(
Args&&... args) noexcept
{
auto resource = std::unique_ptr<Resource>(new (std::nothrow) Resource(std::forward<Args>(args)...));
if (!resource) {
return pp::foundation::Result<std::unique_ptr<Interface>>::failure(
pp::foundation::Status::out_of_range("renderer resource allocation failed"));
}
std::unique_ptr<Interface> erased = std::move(resource);
return pp::foundation::Result<std::unique_ptr<Interface>>::success(std::move(erased));
}
}
RecordingTexture2D::RecordingTexture2D(TextureDesc desc) noexcept
: desc_(desc)
{
}
TextureDesc RecordingTexture2D::desc() const noexcept
{
return desc_;
}
RecordingRenderTarget::RecordingRenderTarget(TextureDesc color_desc) noexcept
: color_desc_(color_desc)
{
}
TextureDesc RecordingRenderTarget::color_desc() const noexcept
{
return color_desc_;
}
RecordingShaderProgram::RecordingShaderProgram(const char* debug_name) noexcept
: debug_name_(non_null_name(debug_name))
{
}
const char* RecordingShaderProgram::debug_name() const noexcept
{
return debug_name_;
}
RecordingMesh::RecordingMesh(MeshDesc desc) noexcept
: desc_(desc)
{
}
MeshDesc RecordingMesh::desc() const noexcept
{
return desc_;
}
RecordingReadbackBuffer::RecordingReadbackBuffer(std::uint64_t size_bytes) noexcept
: size_bytes_(size_bytes)
{
}
std::uint64_t RecordingReadbackBuffer::size_bytes() const noexcept
{
return size_bytes_;
}
RecordingCommandContext::RecordingCommandContext(std::vector<RecordedRenderCommand>& commands) noexcept
: commands_(&commands)
{
}
pp::foundation::Status RecordingCommandContext::begin_render_pass(
IRenderTarget& target,
RenderPassDesc desc) noexcept
{
if (in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass is already active");
}
active_target_ = target.color_desc();
if (!has_texture_usage(active_target_.usage, TextureUsage::render_target)) {
return pp::foundation::Status::invalid_argument("render target texture must allow render_target usage");
}
const auto size_status = texture_byte_size(active_target_);
if (!size_status.ok()) {
return size_status.status();
}
const auto render_pass_status = validate_render_pass_desc(desc);
if (!render_pass_status.ok()) {
return render_pass_status;
}
in_render_pass_ = true;
shader_bound_ = false;
mesh_bound_ = false;
active_mesh_ = MeshDesc {};
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::begin_render_pass,
.target_desc = active_target_,
.clear_color_enabled = desc.clear_color_enabled,
.clear_color = desc.clear_color,
.clear_depth_enabled = desc.clear_depth_enabled,
.clear_depth = desc.clear_depth,
.clear_stencil_enabled = desc.clear_stencil_enabled,
.clear_stencil = desc.clear_stencil,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::set_viewport(Viewport viewport) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto status = validate_viewport(viewport, active_target_.extent);
if (!status.ok()) {
return status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::set_viewport,
.viewport = viewport,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::set_scissor(ScissorRect scissor) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto status = validate_scissor(scissor, active_target_.extent);
if (!status.ok()) {
return status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::set_scissor,
.scissor = scissor,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::set_blend_state(BlendState state) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto status = validate_blend_state(state);
if (!status.ok()) {
return status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::set_blend_state,
.blend_state = state,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::set_depth_state(DepthState state) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto status = validate_depth_state(state);
if (!status.ok()) {
return status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::set_depth_state,
.depth_state = state,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::bind_shader(IShaderProgram& shader) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
shader_bound_ = true;
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::bind_shader,
.name = shader.debug_name(),
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::set_shader_uniform(
const char* name,
std::span<const std::byte> bytes) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
if (!shader_bound_) {
return pp::foundation::Status::invalid_argument("shader must be bound before setting uniforms");
}
const auto status = validate_shader_uniform_write(name, bytes);
if (!status.ok()) {
return status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::set_shader_uniform,
.uniform_bytes = static_cast<std::uint64_t>(bytes.size()),
.name = name,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::bind_texture(
std::uint32_t slot,
ITexture2D& texture) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto slot_status = validate_texture_slot(slot);
if (!slot_status.ok()) {
return slot_status;
}
const auto desc = texture.desc();
if (!has_texture_usage(desc.usage, TextureUsage::sampled)) {
return pp::foundation::Status::invalid_argument("bound texture must allow sampled usage");
}
const auto size_status = texture_byte_size(desc);
if (!size_status.ok()) {
return size_status.status();
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::bind_texture,
.texture_desc = desc,
.texture_slot = slot,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::bind_sampler(
std::uint32_t slot,
SamplerDesc sampler) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto slot_status = validate_texture_slot(slot);
if (!slot_status.ok()) {
return slot_status;
}
const auto sampler_status = validate_sampler_desc(sampler);
if (!sampler_status.ok()) {
return sampler_status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::bind_sampler,
.sampler_desc = sampler,
.sampler_slot = slot,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::bind_mesh(IMesh& mesh) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
const auto desc = mesh.desc();
const auto status = validate_mesh_desc(desc);
if (!status.ok()) {
return status;
}
mesh_bound_ = true;
active_mesh_ = desc;
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::bind_mesh,
.mesh_desc = desc,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::draw(DrawDesc desc) noexcept
{
if (!in_render_pass_) {
return pp::foundation::Status::invalid_argument("render pass has not begun");
}
if (!shader_bound_) {
return pp::foundation::Status::invalid_argument("shader must be bound before draw");
}
if (!mesh_bound_) {
return pp::foundation::Status::invalid_argument("mesh must be bound before draw");
}
const auto draw_status = validate_draw_desc(active_mesh_, desc);
if (!draw_status.ok()) {
return draw_status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::draw,
.mesh_desc = active_mesh_,
.draw_desc = desc,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::read_texture(
ITexture2D& texture,
ReadbackRegion region,
IReadbackBuffer& destination) noexcept
{
if (in_render_pass_) {
return pp::foundation::Status::invalid_argument("readback must be outside a render pass");
}
const auto desc = texture.desc();
if (!has_texture_usage(desc.usage, TextureUsage::readback_source)) {
return pp::foundation::Status::invalid_argument("readback texture must allow readback_source usage");
}
const auto bytes = readback_byte_size(desc, region);
if (!bytes) {
return bytes.status();
}
if (destination.size_bytes() < bytes.value()) {
return pp::foundation::Status::out_of_range("readback buffer is too small");
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::read_texture,
.texture_desc = desc,
.readback_region = region,
.readback_bytes = bytes.value(),
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::upload_texture(
ITexture2D& texture,
ReadbackRegion region,
std::span<const std::byte> rgba_or_channel_bytes) noexcept
{
if (in_render_pass_) {
return pp::foundation::Status::invalid_argument("texture upload must be outside a render pass");
}
const auto desc = texture.desc();
if (!has_texture_usage(desc.usage, TextureUsage::upload_destination)) {
return pp::foundation::Status::invalid_argument("texture upload destination must allow upload_destination usage");
}
const auto bytes = readback_byte_size(desc, region);
if (!bytes) {
return bytes.status();
}
if (rgba_or_channel_bytes.size() != bytes.value()) {
return pp::foundation::Status::invalid_argument("texture upload byte size does not match the region");
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::upload_texture,
.texture_desc = desc,
.readback_region = region,
.upload_bytes = bytes.value(),
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::generate_mipmaps(ITexture2D& texture) noexcept
{
if (in_render_pass_) {
return pp::foundation::Status::invalid_argument("mipmap generation must be outside a render pass");
}
const auto desc = texture.desc();
const auto desc_status = validate_mipmap_generation_desc(desc);
if (!desc_status.ok()) {
return desc_status;
}
const auto bytes = texture_byte_size(desc);
if (!bytes.ok()) {
return bytes.status();
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::generate_mipmaps,
.texture_desc = desc,
.generated_mip_levels = desc.mip_levels,
.generated_mip_bytes = bytes.value(),
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::transition_texture(
ITexture2D& texture,
TextureState before,
TextureState after) noexcept
{
if (in_render_pass_) {
return pp::foundation::Status::invalid_argument("texture transition must be outside a render pass");
}
const auto desc = texture.desc();
const auto desc_status = validate_texture_transition_desc(desc, before, after);
if (!desc_status.ok()) {
return desc_status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::transition_texture,
.texture_desc = desc,
.before_state = before,
.after_state = after,
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::copy_texture(
ITexture2D& source,
ReadbackRegion source_region,
ITexture2D& destination,
ReadbackRegion destination_region) noexcept
{
if (in_render_pass_) {
return pp::foundation::Status::invalid_argument("texture copy must be outside a render pass");
}
const auto source_desc = source.desc();
const auto destination_desc = destination.desc();
const auto desc_status = validate_texture_copy_descs(
source_desc,
source_region,
destination_desc,
destination_region);
if (!desc_status.ok()) {
return desc_status;
}
const auto source_bytes = readback_byte_size(source_desc, source_region);
if (!source_bytes.ok()) {
return source_bytes.status();
}
const auto destination_bytes = readback_byte_size(destination_desc, destination_region);
if (!destination_bytes.ok()) {
return destination_bytes.status();
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::copy_texture,
.source_desc = source_desc,
.destination_desc = destination_desc,
.source_region = source_region,
.destination_region = destination_region,
.copy_source_bytes = source_bytes.value(),
.copy_destination_bytes = destination_bytes.value(),
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::capture_frame(
IRenderTarget& target,
IReadbackBuffer& destination) noexcept
{
if (in_render_pass_) {
return pp::foundation::Status::invalid_argument("frame capture must be outside a render pass");
}
const auto desc = target.color_desc();
const auto bytes = frame_capture_byte_size(desc);
if (!bytes) {
return bytes.status();
}
if (destination.size_bytes() < bytes.value()) {
return pp::foundation::Status::out_of_range("frame capture buffer is too small");
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::capture_frame,
.target_desc = desc,
.capture_bytes = bytes.value(),
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingCommandContext::blit_render_target(
IRenderTarget& source,
ReadbackRegion source_region,
IRenderTarget& destination,
ReadbackRegion destination_region,
BlitFilter filter) noexcept
{
if (in_render_pass_) {
return pp::foundation::Status::invalid_argument("render target blit must be outside a render pass");
}
const auto source_desc = source.color_desc();
const auto destination_desc = destination.color_desc();
const auto desc_status = validate_blit_descs(source_desc, destination_desc);
if (!desc_status.ok()) {
return desc_status;
}
const auto filter_status = validate_blit_filter(filter);
if (!filter_status.ok()) {
return filter_status;
}
const auto source_bytes = readback_byte_size(source_desc, source_region);
if (!source_bytes) {
return source_bytes.status();
}
const auto destination_bytes = readback_byte_size(destination_desc, destination_region);
if (!destination_bytes) {
return destination_bytes.status();
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::blit_render_target,
.source_desc = source_desc,
.destination_desc = destination_desc,
.source_region = source_region,
.destination_region = destination_region,
.blit_filter = filter,
.blit_source_bytes = source_bytes.value(),
.blit_destination_bytes = destination_bytes.value(),
});
return pp::foundation::Status::success();
}
void RecordingCommandContext::end_render_pass() noexcept
{
if (!in_render_pass_) {
return;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::end_render_pass,
});
in_render_pass_ = false;
shader_bound_ = false;
mesh_bound_ = false;
active_mesh_ = MeshDesc {};
}
bool RecordingCommandContext::in_render_pass() const noexcept
{
return in_render_pass_;
}
void RecordingCommandContext::reset() noexcept
{
active_target_ = TextureDesc {};
active_mesh_ = MeshDesc {};
in_render_pass_ = false;
shader_bound_ = false;
mesh_bound_ = false;
}
RecordingRenderTrace::RecordingRenderTrace(std::vector<RecordedRenderCommand>& commands) noexcept
: commands_(&commands)
{
}
pp::foundation::Status RecordingRenderTrace::marker(const char* component, const char* name) noexcept
{
const auto status = validate_trace_label(component, name);
if (!status.ok()) {
return status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::trace_marker,
.component = non_null_name(component),
.name = non_null_name(name),
});
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingRenderTrace::begin_scope(const char* component, const char* name) noexcept
{
const auto status = validate_trace_label(component, name);
if (!status.ok()) {
return status;
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::trace_begin_scope,
.component = non_null_name(component),
.name = non_null_name(name),
});
++scope_depth_;
return pp::foundation::Status::success();
}
pp::foundation::Status RecordingRenderTrace::end_scope() noexcept
{
if (scope_depth_ == 0U) {
return pp::foundation::Status::invalid_argument("trace scope has not begun");
}
push_command(commands_, RecordedRenderCommand {
.kind = RecordedRenderCommandKind::trace_end_scope,
});
--scope_depth_;
return pp::foundation::Status::success();
}
void RecordingRenderTrace::reset() noexcept
{
scope_depth_ = 0U;
}
RecordingRenderDevice::RecordingRenderDevice() noexcept
: context_(commands_)
, trace_(commands_)
{
}
const char* RecordingRenderDevice::backend_name() const noexcept
{
return "recording";
}
RenderDeviceFeatures RecordingRenderDevice::features() const noexcept
{
return RenderDeviceFeatures {
.explicit_texture_transitions = true,
.texture_copy = true,
.render_target_blit = true,
.frame_capture = true,
};
}
pp::foundation::Result<std::unique_ptr<ITexture2D>> RecordingRenderDevice::create_texture(
TextureDesc desc) noexcept
{
const auto desc_status = validate_texture_desc(desc);
if (!desc_status.ok()) {
return pp::foundation::Result<std::unique_ptr<ITexture2D>>::failure(desc_status);
}
const auto bytes = texture_byte_size(desc);
if (!bytes.ok()) {
return pp::foundation::Result<std::unique_ptr<ITexture2D>>::failure(bytes.status());
}
return make_recording_resource<RecordingTexture2D, ITexture2D>(desc);
}
pp::foundation::Result<std::unique_ptr<IRenderTarget>> RecordingRenderDevice::create_render_target(
TextureDesc color_desc) noexcept
{
if (!has_texture_usage(color_desc.usage, TextureUsage::render_target)) {
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(
pp::foundation::Status::invalid_argument("render target texture must allow render_target usage"));
}
const auto desc_status = validate_texture_desc(color_desc);
if (!desc_status.ok()) {
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(desc_status);
}
const auto bytes = texture_byte_size(color_desc);
if (!bytes.ok()) {
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(bytes.status());
}
return make_recording_resource<RecordingRenderTarget, IRenderTarget>(color_desc);
}
pp::foundation::Result<std::unique_ptr<IShaderProgram>> RecordingRenderDevice::create_shader_program(
ShaderProgramDesc desc) noexcept
{
const auto status = validate_shader_program_desc(desc);
if (!status.ok()) {
return pp::foundation::Result<std::unique_ptr<IShaderProgram>>::failure(status);
}
return make_recording_resource<RecordingShaderProgram, IShaderProgram>(desc.debug_name);
}
pp::foundation::Result<std::unique_ptr<IMesh>> RecordingRenderDevice::create_mesh(
MeshDesc desc) noexcept
{
const auto status = validate_mesh_desc(desc);
if (!status.ok()) {
return pp::foundation::Result<std::unique_ptr<IMesh>>::failure(status);
}
return make_recording_resource<RecordingMesh, IMesh>(desc);
}
pp::foundation::Result<std::unique_ptr<IReadbackBuffer>> RecordingRenderDevice::create_readback_buffer(
std::uint64_t size_bytes) noexcept
{
if (size_bytes == 0U) {
return pp::foundation::Result<std::unique_ptr<IReadbackBuffer>>::failure(
pp::foundation::Status::invalid_argument("readback buffer size must be greater than zero"));
}
if (size_bytes > max_texture_bytes) {
return pp::foundation::Result<std::unique_ptr<IReadbackBuffer>>::failure(
pp::foundation::Status::out_of_range("readback buffer size exceeds the configured limit"));
}
return make_recording_resource<RecordingReadbackBuffer, IReadbackBuffer>(size_bytes);
}
ICommandContext& RecordingRenderDevice::immediate_context() noexcept
{
return context_;
}
IRenderTrace* RecordingRenderDevice::trace() noexcept
{
return &trace_;
}
std::span<const RecordedRenderCommand> RecordingRenderDevice::commands() const noexcept
{
return commands_;
}
void RecordingRenderDevice::clear() noexcept
{
commands_.clear();
context_.reset();
trace_.reset();
}
const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) noexcept
{
switch (kind) {
case RecordedRenderCommandKind::begin_render_pass:
return "begin_render_pass";
case RecordedRenderCommandKind::set_viewport:
return "set_viewport";
case RecordedRenderCommandKind::set_scissor:
return "set_scissor";
case RecordedRenderCommandKind::set_blend_state:
return "set_blend_state";
case RecordedRenderCommandKind::set_depth_state:
return "set_depth_state";
case RecordedRenderCommandKind::bind_shader:
return "bind_shader";
case RecordedRenderCommandKind::set_shader_uniform:
return "set_shader_uniform";
case RecordedRenderCommandKind::bind_texture:
return "bind_texture";
case RecordedRenderCommandKind::bind_sampler:
return "bind_sampler";
case RecordedRenderCommandKind::bind_mesh:
return "bind_mesh";
case RecordedRenderCommandKind::draw:
return "draw";
case RecordedRenderCommandKind::upload_texture:
return "upload_texture";
case RecordedRenderCommandKind::generate_mipmaps:
return "generate_mipmaps";
case RecordedRenderCommandKind::transition_texture:
return "transition_texture";
case RecordedRenderCommandKind::copy_texture:
return "copy_texture";
case RecordedRenderCommandKind::read_texture:
return "read_texture";
case RecordedRenderCommandKind::capture_frame:
return "capture_frame";
case RecordedRenderCommandKind::blit_render_target:
return "blit_render_target";
case RecordedRenderCommandKind::end_render_pass:
return "end_render_pass";
case RecordedRenderCommandKind::trace_marker:
return "trace_marker";
case RecordedRenderCommandKind::trace_begin_scope:
return "trace_begin_scope";
case RecordedRenderCommandKind::trace_end_scope:
return "trace_end_scope";
}
return "unknown";
}
}

View File

@@ -0,0 +1,229 @@
#pragma once
#include "renderer_api/renderer_api.h"
#include <span>
#include <vector>
namespace pp::renderer {
enum class RecordedRenderCommandKind : std::uint8_t {
begin_render_pass,
set_viewport,
set_scissor,
set_blend_state,
set_depth_state,
bind_shader,
set_shader_uniform,
bind_texture,
bind_sampler,
bind_mesh,
draw,
upload_texture,
generate_mipmaps,
transition_texture,
copy_texture,
read_texture,
capture_frame,
blit_render_target,
end_render_pass,
trace_marker,
trace_begin_scope,
trace_end_scope,
};
struct RecordedRenderCommand {
RecordedRenderCommandKind kind = RecordedRenderCommandKind::draw;
TextureDesc target_desc {};
bool clear_color_enabled = false;
ClearColor clear_color {};
bool clear_depth_enabled = false;
float clear_depth = 1.0F;
bool clear_stencil_enabled = false;
std::uint8_t clear_stencil = 0;
Viewport viewport {};
ScissorRect scissor {};
BlendState blend_state {};
DepthState depth_state {};
MeshDesc mesh_desc {};
DrawDesc draw_desc {};
TextureDesc texture_desc {};
std::uint32_t texture_slot = 0;
SamplerDesc sampler_desc {};
std::uint32_t sampler_slot = 0;
TextureDesc source_desc {};
TextureDesc destination_desc {};
TextureState before_state = TextureState::undefined;
TextureState after_state = TextureState::undefined;
ReadbackRegion readback_region {};
ReadbackRegion source_region {};
ReadbackRegion destination_region {};
BlitFilter blit_filter = BlitFilter::nearest;
std::uint64_t upload_bytes = 0;
std::uint32_t generated_mip_levels = 0;
std::uint64_t generated_mip_bytes = 0;
std::uint64_t copy_source_bytes = 0;
std::uint64_t copy_destination_bytes = 0;
std::uint64_t readback_bytes = 0;
std::uint64_t capture_bytes = 0;
std::uint64_t blit_source_bytes = 0;
std::uint64_t blit_destination_bytes = 0;
std::uint64_t uniform_bytes = 0;
const char* component = "";
const char* name = "";
};
class RecordingTexture2D final : public ITexture2D {
public:
explicit RecordingTexture2D(TextureDesc desc) noexcept;
[[nodiscard]] TextureDesc desc() const noexcept override;
private:
TextureDesc desc_ {};
};
class RecordingRenderTarget final : public IRenderTarget {
public:
explicit RecordingRenderTarget(TextureDesc color_desc) noexcept;
[[nodiscard]] TextureDesc color_desc() const noexcept override;
private:
TextureDesc color_desc_ {};
};
class RecordingShaderProgram final : public IShaderProgram {
public:
explicit RecordingShaderProgram(const char* debug_name) noexcept;
[[nodiscard]] const char* debug_name() const noexcept override;
private:
const char* debug_name_ = "";
};
class RecordingMesh final : public IMesh {
public:
explicit RecordingMesh(MeshDesc desc) noexcept;
[[nodiscard]] MeshDesc desc() const noexcept override;
private:
MeshDesc desc_ {};
};
class RecordingReadbackBuffer final : public IReadbackBuffer {
public:
explicit RecordingReadbackBuffer(std::uint64_t size_bytes) noexcept;
[[nodiscard]] std::uint64_t size_bytes() const noexcept override;
private:
std::uint64_t size_bytes_ = 0;
};
class RecordingCommandContext final : public ICommandContext {
public:
explicit RecordingCommandContext(std::vector<RecordedRenderCommand>& commands) noexcept;
[[nodiscard]] pp::foundation::Status begin_render_pass(
IRenderTarget& target,
RenderPassDesc desc) noexcept override;
[[nodiscard]] pp::foundation::Status set_viewport(Viewport viewport) noexcept override;
[[nodiscard]] pp::foundation::Status set_scissor(ScissorRect scissor) noexcept override;
[[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override;
[[nodiscard]] pp::foundation::Status set_depth_state(DepthState state) noexcept override;
[[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override;
[[nodiscard]] pp::foundation::Status set_shader_uniform(
const char* name,
std::span<const std::byte> bytes) noexcept override;
[[nodiscard]] pp::foundation::Status bind_texture(
std::uint32_t slot,
ITexture2D& texture) noexcept override;
[[nodiscard]] pp::foundation::Status bind_sampler(
std::uint32_t slot,
SamplerDesc sampler) noexcept override;
[[nodiscard]] pp::foundation::Status bind_mesh(IMesh& mesh) noexcept override;
[[nodiscard]] pp::foundation::Status draw(DrawDesc desc) noexcept override;
[[nodiscard]] pp::foundation::Status read_texture(
ITexture2D& texture,
ReadbackRegion region,
IReadbackBuffer& destination) noexcept override;
[[nodiscard]] pp::foundation::Status upload_texture(
ITexture2D& texture,
ReadbackRegion region,
std::span<const std::byte> rgba_or_channel_bytes) noexcept override;
[[nodiscard]] pp::foundation::Status generate_mipmaps(
ITexture2D& texture) noexcept override;
[[nodiscard]] pp::foundation::Status transition_texture(
ITexture2D& texture,
TextureState before,
TextureState after) noexcept override;
[[nodiscard]] pp::foundation::Status copy_texture(
ITexture2D& source,
ReadbackRegion source_region,
ITexture2D& destination,
ReadbackRegion destination_region) noexcept override;
[[nodiscard]] pp::foundation::Status capture_frame(
IRenderTarget& target,
IReadbackBuffer& destination) noexcept override;
[[nodiscard]] pp::foundation::Status blit_render_target(
IRenderTarget& source,
ReadbackRegion source_region,
IRenderTarget& destination,
ReadbackRegion destination_region,
BlitFilter filter) noexcept override;
void end_render_pass() noexcept override;
[[nodiscard]] bool in_render_pass() const noexcept;
void reset() noexcept;
private:
std::vector<RecordedRenderCommand>* commands_ = nullptr;
TextureDesc active_target_ {};
MeshDesc active_mesh_ {};
bool in_render_pass_ = false;
bool shader_bound_ = false;
bool mesh_bound_ = false;
};
class RecordingRenderTrace final : public IRenderTrace {
public:
explicit RecordingRenderTrace(std::vector<RecordedRenderCommand>& commands) noexcept;
[[nodiscard]] pp::foundation::Status marker(const char* component, const char* name) noexcept override;
[[nodiscard]] pp::foundation::Status begin_scope(const char* component, const char* name) noexcept override;
[[nodiscard]] pp::foundation::Status end_scope() noexcept override;
void reset() noexcept;
private:
std::vector<RecordedRenderCommand>* commands_ = nullptr;
std::uint32_t scope_depth_ = 0;
};
class RecordingRenderDevice final : public IRenderDevice {
public:
RecordingRenderDevice() noexcept;
[[nodiscard]] const char* backend_name() const noexcept override;
[[nodiscard]] RenderDeviceFeatures features() const noexcept override;
[[nodiscard]] pp::foundation::Result<std::unique_ptr<ITexture2D>> create_texture(
TextureDesc desc) noexcept override;
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IRenderTarget>> create_render_target(
TextureDesc color_desc) noexcept override;
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IShaderProgram>> create_shader_program(
ShaderProgramDesc desc) noexcept override;
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IMesh>> create_mesh(
MeshDesc desc) noexcept override;
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IReadbackBuffer>> create_readback_buffer(
std::uint64_t size_bytes) noexcept override;
[[nodiscard]] ICommandContext& immediate_context() noexcept override;
[[nodiscard]] IRenderTrace* trace() noexcept override;
[[nodiscard]] std::span<const RecordedRenderCommand> commands() const noexcept;
void clear() noexcept;
private:
std::vector<RecordedRenderCommand> commands_;
RecordingCommandContext context_;
RecordingRenderTrace trace_;
};
[[nodiscard]] const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) noexcept;
}

View File

@@ -12,6 +12,20 @@ namespace {
return text == nullptr || text[0] == '\0';
}
[[nodiscard]] std::size_t bounded_c_string_length(const char* text, std::size_t limit) noexcept
{
if (text == nullptr) {
return 0;
}
std::size_t length = 0;
while (length <= limit && text[length] != '\0') {
++length;
}
return length;
}
[[nodiscard]] pp::foundation::Status validate_shader_stage_source(
ShaderStageSource source,
const char* stage_name) noexcept
@@ -31,6 +45,18 @@ namespace {
return pp::foundation::Status::success();
}
[[nodiscard]] Extent2D mip_level_extent(Extent2D extent, std::uint32_t level) noexcept
{
auto width = extent.width;
auto height = extent.height;
for (std::uint32_t index = 0; index < level; ++index) {
width = width > 1U ? width / 2U : 1U;
height = height > 1U ? height / 2U : 1U;
}
return Extent2D { .width = width, .height = height };
}
}
std::uint32_t bytes_per_pixel(TextureFormat format) noexcept
@@ -47,6 +73,29 @@ std::uint32_t bytes_per_pixel(TextureFormat format) noexcept
return 0;
}
std::uint32_t max_mip_levels_for_extent(Extent2D extent) noexcept
{
if (extent.width == 0U || extent.height == 0U) {
return 0;
}
auto dimension = extent.width > extent.height ? extent.width : extent.height;
std::uint32_t levels = 1;
while (dimension > 1U && levels < max_texture_mip_levels) {
dimension /= 2U;
++levels;
}
return levels;
}
bool has_texture_usage(TextureUsage usage, TextureUsage required) noexcept
{
const auto usage_bits = static_cast<std::uint32_t>(usage);
const auto required_bits = static_cast<std::uint32_t>(required);
return required_bits != 0U && (usage_bits & required_bits) == required_bits;
}
pp::foundation::Status validate_extent(Extent2D extent) noexcept
{
if (extent.width == 0 || extent.height == 0) {
@@ -60,21 +109,81 @@ pp::foundation::Status validate_extent(Extent2D extent) noexcept
return pp::foundation::Status::success();
}
pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexcept
pp::foundation::Status validate_texture_usage(TextureUsage usage) noexcept
{
constexpr auto allowed_usage = TextureUsage::sampled
| TextureUsage::render_target
| TextureUsage::upload_destination
| TextureUsage::readback_source
| TextureUsage::copy_source
| TextureUsage::copy_destination;
const auto usage_bits = static_cast<std::uint32_t>(usage);
const auto allowed_bits = static_cast<std::uint32_t>(allowed_usage);
if (usage_bits == 0U) {
return pp::foundation::Status::invalid_argument("texture usage must not be empty");
}
if ((usage_bits & ~allowed_bits) != 0U) {
return pp::foundation::Status::invalid_argument("texture usage contains unsupported flags");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_resource_label(const char* label) noexcept
{
if (label == nullptr) {
return pp::foundation::Status::invalid_argument("resource label must not be null");
}
if (bounded_c_string_length(label, max_resource_label_bytes) > max_resource_label_bytes) {
return pp::foundation::Status::out_of_range("resource label exceeds the configured limit");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_texture_desc(TextureDesc desc) noexcept
{
const auto extent_status = validate_extent(desc.extent);
if (!extent_status.ok()) {
return pp::foundation::Result<std::uint64_t>::failure(extent_status);
return extent_status;
}
if (bytes_per_pixel(desc.format) == 0U) {
return pp::foundation::Status::invalid_argument("texture format is not supported");
}
if (desc.mip_levels == 0U) {
return pp::foundation::Status::invalid_argument("texture mip level count must be greater than zero");
}
if (desc.mip_levels > max_mip_levels_for_extent(desc.extent)) {
return pp::foundation::Status::out_of_range("texture mip level count exceeds the texture extent");
}
const auto usage_status = validate_texture_usage(desc.usage);
if (!usage_status.ok()) {
return usage_status;
}
return validate_resource_label(desc.debug_name);
}
pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexcept
{
const auto desc_status = validate_texture_desc(desc);
if (!desc_status.ok()) {
return pp::foundation::Result<std::uint64_t>::failure(desc_status);
}
const auto bpp = static_cast<std::uint64_t>(bytes_per_pixel(desc.format));
if (bpp == 0) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::invalid_argument("texture format is not supported"));
}
const auto width = static_cast<std::uint64_t>(desc.extent.width);
const auto height = static_cast<std::uint64_t>(desc.extent.height);
std::uint64_t bytes = 0;
for (std::uint32_t level = 0; level < desc.mip_levels; ++level) {
const auto level_extent = mip_level_extent(desc.extent, level);
const auto width = static_cast<std::uint64_t>(level_extent.width);
const auto height = static_cast<std::uint64_t>(level_extent.height);
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::out_of_range("texture size overflows uint64"));
@@ -86,7 +195,14 @@ pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexce
pp::foundation::Status::out_of_range("texture byte size overflows uint64"));
}
const auto bytes = pixels * bpp;
const auto level_bytes = pixels * bpp;
if (bytes > std::numeric_limits<std::uint64_t>::max() - level_bytes) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::out_of_range("texture byte size overflows uint64"));
}
bytes += level_bytes;
}
if (bytes > max_texture_bytes) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::out_of_range("texture byte size exceeds the configured limit"));
@@ -131,8 +247,199 @@ pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_exte
return pp::foundation::Status::success();
}
pp::foundation::Status validate_scissor(ScissorRect scissor, Extent2D target_extent) noexcept
{
const auto extent_status = validate_extent(target_extent);
if (!extent_status.ok()) {
return extent_status;
}
if (!scissor.enabled) {
return pp::foundation::Status::success();
}
if (scissor.x < 0 || scissor.y < 0) {
return pp::foundation::Status::invalid_argument("scissor origin must be non-negative");
}
if (scissor.width == 0 || scissor.height == 0) {
return pp::foundation::Status::invalid_argument("scissor size must be greater than zero");
}
const auto x = static_cast<std::uint32_t>(scissor.x);
const auto y = static_cast<std::uint32_t>(scissor.y);
if (x > target_extent.width || y > target_extent.height) {
return pp::foundation::Status::out_of_range("scissor origin is outside the render target");
}
if (scissor.width > target_extent.width - x || scissor.height > target_extent.height - y) {
return pp::foundation::Status::out_of_range("scissor exceeds render target bounds");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_render_pass_desc(RenderPassDesc desc) noexcept
{
if (desc.clear_color_enabled
&& (!std::isfinite(desc.clear_color.r)
|| !std::isfinite(desc.clear_color.g)
|| !std::isfinite(desc.clear_color.b)
|| !std::isfinite(desc.clear_color.a))) {
return pp::foundation::Status::invalid_argument("render pass clear color must be finite");
}
if (desc.clear_depth_enabled && !std::isfinite(desc.clear_depth)) {
return pp::foundation::Status::invalid_argument("render pass clear depth must be finite");
}
if (desc.clear_depth_enabled && (desc.clear_depth < 0.0F || desc.clear_depth > 1.0F)) {
return pp::foundation::Status::out_of_range("render pass clear depth must be within 0..1");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept
{
switch (factor) {
case BlendFactor::zero:
case BlendFactor::one:
case BlendFactor::source_alpha:
case BlendFactor::one_minus_source_alpha:
case BlendFactor::destination_alpha:
case BlendFactor::one_minus_destination_alpha:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("blend factor is not supported");
}
pp::foundation::Status validate_blend_op(BlendOp op) noexcept
{
switch (op) {
case BlendOp::add:
case BlendOp::subtract:
case BlendOp::reverse_subtract:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("blend operation is not supported");
}
pp::foundation::Status validate_blend_state(BlendState state) noexcept
{
const auto source_color = validate_blend_factor(state.source_color);
if (!source_color.ok()) {
return source_color;
}
const auto destination_color = validate_blend_factor(state.destination_color);
if (!destination_color.ok()) {
return destination_color;
}
const auto color_op = validate_blend_op(state.color_op);
if (!color_op.ok()) {
return color_op;
}
const auto source_alpha = validate_blend_factor(state.source_alpha);
if (!source_alpha.ok()) {
return source_alpha;
}
const auto destination_alpha = validate_blend_factor(state.destination_alpha);
if (!destination_alpha.ok()) {
return destination_alpha;
}
return validate_blend_op(state.alpha_op);
}
pp::foundation::Status validate_compare_op(CompareOp op) noexcept
{
switch (op) {
case CompareOp::never:
case CompareOp::less:
case CompareOp::equal:
case CompareOp::less_or_equal:
case CompareOp::greater:
case CompareOp::not_equal:
case CompareOp::greater_or_equal:
case CompareOp::always:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("depth compare operation is not supported");
}
pp::foundation::Status validate_depth_state(DepthState state) noexcept
{
return validate_compare_op(state.compare);
}
pp::foundation::Status validate_sampler_filter(SamplerFilter filter) noexcept
{
switch (filter) {
case SamplerFilter::nearest:
case SamplerFilter::linear:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("sampler filter is not supported");
}
pp::foundation::Status validate_sampler_address_mode(SamplerAddressMode mode) noexcept
{
switch (mode) {
case SamplerAddressMode::clamp_to_edge:
case SamplerAddressMode::repeat:
case SamplerAddressMode::mirrored_repeat:
case SamplerAddressMode::clamp_to_border:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("sampler address mode is not supported");
}
pp::foundation::Status validate_sampler_desc(SamplerDesc desc) noexcept
{
const auto min_filter = validate_sampler_filter(desc.min_filter);
if (!min_filter.ok()) {
return min_filter;
}
const auto mag_filter = validate_sampler_filter(desc.mag_filter);
if (!mag_filter.ok()) {
return mag_filter;
}
const auto mip_filter = validate_sampler_filter(desc.mip_filter);
if (!mip_filter.ok()) {
return mip_filter;
}
const auto address_u = validate_sampler_address_mode(desc.address_u);
if (!address_u.ok()) {
return address_u;
}
const auto address_v = validate_sampler_address_mode(desc.address_v);
if (!address_v.ok()) {
return address_v;
}
return validate_sampler_address_mode(desc.address_w);
}
pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept
{
const auto label_status = validate_resource_label(desc.debug_name);
if (!label_status.ok()) {
return label_status;
}
if (desc.vertex_count == 0) {
return pp::foundation::Status::invalid_argument("mesh must contain at least one vertex");
}
@@ -151,10 +458,54 @@ pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept
return pp::foundation::Status::invalid_argument("mesh topology is not supported");
}
pp::foundation::Status validate_draw_desc(MeshDesc mesh, DrawDesc draw) noexcept
{
const auto mesh_status = validate_mesh_desc(mesh);
if (!mesh_status.ok()) {
return mesh_status;
}
if (draw.instance_count == 0) {
return pp::foundation::Status::invalid_argument("draw instance count must be greater than zero");
}
if (draw.vertex_count == 0 && draw.index_count == 0) {
return pp::foundation::Status::invalid_argument("draw must include vertices or indices");
}
if (draw.index_count > 0) {
if (mesh.index_count == 0) {
return pp::foundation::Status::invalid_argument("indexed draw requires an indexed mesh");
}
if (draw.first_index > mesh.index_count || draw.index_count > mesh.index_count - draw.first_index) {
return pp::foundation::Status::out_of_range("draw index range exceeds the bound mesh");
}
return pp::foundation::Status::success();
}
if (draw.first_vertex > mesh.vertex_count || draw.vertex_count > mesh.vertex_count - draw.first_vertex) {
return pp::foundation::Status::out_of_range("draw vertex range exceeds the bound mesh");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_texture_slot(std::uint32_t slot) noexcept
{
if (slot >= max_texture_slots) {
return pp::foundation::Status::out_of_range("texture slot exceeds the configured limit");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept
{
if (desc.debug_name == nullptr) {
return pp::foundation::Status::invalid_argument("shader debug name must not be null");
const auto label_status = validate_resource_label(desc.debug_name);
if (!label_status.ok()) {
return label_status;
}
const auto vertex_status = validate_shader_stage_source(
@@ -174,11 +525,51 @@ pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noex
return pp::foundation::Status::success();
}
pp::foundation::Status validate_shader_uniform_write(
const char* name,
std::span<const std::byte> bytes) noexcept
{
if (is_empty_c_string(name)) {
return pp::foundation::Status::invalid_argument("shader uniform name must not be empty");
}
if (bytes.empty()) {
return pp::foundation::Status::invalid_argument("shader uniform bytes must not be empty");
}
if (bytes.size() > max_shader_uniform_bytes) {
return pp::foundation::Status::out_of_range("shader uniform bytes exceed the configured limit");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_trace_label(const char* component, const char* name) noexcept
{
if (is_empty_c_string(component)) {
return pp::foundation::Status::invalid_argument("trace component must not be empty");
}
if (is_empty_c_string(name)) {
return pp::foundation::Status::invalid_argument("trace name must not be empty");
}
if (bounded_c_string_length(component, max_trace_label_bytes) > max_trace_label_bytes) {
return pp::foundation::Status::out_of_range("trace component exceeds the configured limit");
}
if (bounded_c_string_length(name, max_trace_label_bytes) > max_trace_label_bytes) {
return pp::foundation::Status::out_of_range("trace name exceeds the configured limit");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept
{
const auto extent_status = validate_extent(desc.extent);
if (!extent_status.ok()) {
return extent_status;
const auto desc_status = validate_texture_desc(desc);
if (!desc_status.ok()) {
return desc_status;
}
if (region.width == 0 || region.height == 0) {
@@ -196,6 +587,242 @@ pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion
return pp::foundation::Status::success();
}
pp::foundation::Result<std::uint64_t> readback_byte_size(TextureDesc desc, ReadbackRegion region) noexcept
{
const auto region_status = validate_readback_region(desc, region);
if (!region_status.ok()) {
return pp::foundation::Result<std::uint64_t>::failure(region_status);
}
const auto bpp = static_cast<std::uint64_t>(bytes_per_pixel(desc.format));
if (bpp == 0) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::invalid_argument("texture format is not supported"));
}
const auto width = static_cast<std::uint64_t>(region.width);
const auto height = static_cast<std::uint64_t>(region.height);
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::out_of_range("readback pixel count overflows uint64"));
}
const auto pixels = width * height;
if (pixels > std::numeric_limits<std::uint64_t>::max() / bpp) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::out_of_range("readback byte size overflows uint64"));
}
const auto bytes = pixels * bpp;
if (bytes > max_texture_bytes) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::out_of_range("readback byte size exceeds the configured limit"));
}
return pp::foundation::Result<std::uint64_t>::success(bytes);
}
pp::foundation::Result<std::uint64_t> frame_capture_byte_size(TextureDesc desc) noexcept
{
const auto desc_status = validate_texture_desc(desc);
if (!desc_status.ok()) {
return pp::foundation::Result<std::uint64_t>::failure(desc_status);
}
if (!has_texture_usage(desc.usage, TextureUsage::render_target)) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::invalid_argument("frame capture source must be a render target"));
}
if (!has_texture_usage(desc.usage, TextureUsage::readback_source)) {
return pp::foundation::Result<std::uint64_t>::failure(
pp::foundation::Status::invalid_argument("frame capture source must allow readback"));
}
return texture_byte_size(desc);
}
pp::foundation::Status validate_texture_copy_descs(
TextureDesc source,
ReadbackRegion source_region,
TextureDesc destination,
ReadbackRegion destination_region) noexcept
{
if (!has_texture_usage(source.usage, TextureUsage::copy_source)) {
return pp::foundation::Status::invalid_argument("texture copy source must allow copy_source usage");
}
if (!has_texture_usage(destination.usage, TextureUsage::copy_destination)) {
return pp::foundation::Status::invalid_argument("texture copy destination must allow copy_destination usage");
}
if (source.format != destination.format) {
return pp::foundation::Status::invalid_argument("texture copy endpoints must use matching formats");
}
if (source_region.width != destination_region.width || source_region.height != destination_region.height) {
return pp::foundation::Status::invalid_argument("texture copy regions must have matching dimensions");
}
const auto source_status = validate_readback_region(source, source_region);
if (!source_status.ok()) {
return source_status;
}
return validate_readback_region(destination, destination_region);
}
pp::foundation::Status validate_mipmap_generation_desc(TextureDesc desc) noexcept
{
const auto desc_status = validate_texture_desc(desc);
if (!desc_status.ok()) {
return desc_status;
}
if (desc.mip_levels <= 1U) {
return pp::foundation::Status::invalid_argument("mipmap generation requires more than one mip level");
}
if (!has_texture_usage(desc.usage, TextureUsage::sampled)) {
return pp::foundation::Status::invalid_argument("mipmap texture must allow sampled usage");
}
if (!has_texture_usage(desc.usage, TextureUsage::copy_source)) {
return pp::foundation::Status::invalid_argument("mipmap texture must allow copy_source usage");
}
if (!has_texture_usage(desc.usage, TextureUsage::copy_destination)) {
return pp::foundation::Status::invalid_argument("mipmap texture must allow copy_destination usage");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_texture_state(TextureState state) noexcept
{
switch (state) {
case TextureState::undefined:
case TextureState::shader_read:
case TextureState::render_target:
case TextureState::upload_destination:
case TextureState::copy_source:
case TextureState::copy_destination:
case TextureState::readback_source:
case TextureState::present:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("texture state is not supported");
}
pp::foundation::Status validate_texture_transition_desc(
TextureDesc desc,
TextureState before,
TextureState after) noexcept
{
const auto desc_status = validate_texture_desc(desc);
if (!desc_status.ok()) {
return desc_status;
}
const auto before_status = validate_texture_state(before);
if (!before_status.ok()) {
return before_status;
}
const auto after_status = validate_texture_state(after);
if (!after_status.ok()) {
return after_status;
}
if (before == after) {
return pp::foundation::Status::invalid_argument("texture transition must change state");
}
if (after == TextureState::undefined) {
return pp::foundation::Status::invalid_argument("texture transition destination must not be undefined");
}
if (after == TextureState::shader_read && !has_texture_usage(desc.usage, TextureUsage::sampled)) {
return pp::foundation::Status::invalid_argument("shader-read transition requires sampled usage");
}
if (after == TextureState::render_target && !has_texture_usage(desc.usage, TextureUsage::render_target)) {
return pp::foundation::Status::invalid_argument("render-target transition requires render_target usage");
}
if (after == TextureState::upload_destination && !has_texture_usage(desc.usage, TextureUsage::upload_destination)) {
return pp::foundation::Status::invalid_argument("upload transition requires upload_destination usage");
}
if (after == TextureState::copy_source && !has_texture_usage(desc.usage, TextureUsage::copy_source)) {
return pp::foundation::Status::invalid_argument("copy-source transition requires copy_source usage");
}
if (after == TextureState::copy_destination && !has_texture_usage(desc.usage, TextureUsage::copy_destination)) {
return pp::foundation::Status::invalid_argument("copy-destination transition requires copy_destination usage");
}
if (after == TextureState::readback_source && !has_texture_usage(desc.usage, TextureUsage::readback_source)) {
return pp::foundation::Status::invalid_argument("readback transition requires readback_source usage");
}
return pp::foundation::Status::success();
}
pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept
{
switch (filter) {
case BlitFilter::nearest:
case BlitFilter::linear:
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("blit filter is not supported");
}
pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc destination) noexcept
{
const auto source_status = validate_texture_desc(source);
if (!source_status.ok()) {
return source_status;
}
const auto destination_status = validate_texture_desc(destination);
if (!destination_status.ok()) {
return destination_status;
}
if (!has_texture_usage(source.usage, TextureUsage::render_target)
|| !has_texture_usage(destination.usage, TextureUsage::render_target)) {
return pp::foundation::Status::invalid_argument("blit endpoints must be render targets");
}
if (!has_texture_usage(source.usage, TextureUsage::copy_source)) {
return pp::foundation::Status::invalid_argument("blit source must allow copy_source usage");
}
if (!has_texture_usage(destination.usage, TextureUsage::copy_destination)) {
return pp::foundation::Status::invalid_argument("blit destination must allow copy_destination usage");
}
if (source.format != destination.format) {
return pp::foundation::Status::invalid_argument("blit endpoints must use matching texture formats");
}
const auto source_bytes = texture_byte_size(source);
if (!source_bytes.ok()) {
return source_bytes.status();
}
const auto destination_bytes = texture_byte_size(destination);
if (!destination_bytes.ok()) {
return destination_bytes.status();
}
return pp::foundation::Status::success();
}
const char* texture_format_name(TextureFormat format) noexcept
{
switch (format) {
@@ -210,6 +837,30 @@ const char* texture_format_name(TextureFormat format) noexcept
return "unknown";
}
const char* texture_state_name(TextureState state) noexcept
{
switch (state) {
case TextureState::undefined:
return "undefined";
case TextureState::shader_read:
return "shader_read";
case TextureState::render_target:
return "render_target";
case TextureState::upload_destination:
return "upload_destination";
case TextureState::copy_source:
return "copy_source";
case TextureState::copy_destination:
return "copy_destination";
case TextureState::readback_source:
return "readback_source";
case TextureState::present:
return "present";
}
return "unknown";
}
const char* primitive_topology_name(PrimitiveTopology topology) noexcept
{
switch (topology) {
@@ -224,4 +875,102 @@ const char* primitive_topology_name(PrimitiveTopology topology) noexcept
return "unknown";
}
const char* blit_filter_name(BlitFilter filter) noexcept
{
switch (filter) {
case BlitFilter::nearest:
return "nearest";
case BlitFilter::linear:
return "linear";
}
return "unknown";
}
const char* blend_factor_name(BlendFactor factor) noexcept
{
switch (factor) {
case BlendFactor::zero:
return "zero";
case BlendFactor::one:
return "one";
case BlendFactor::source_alpha:
return "source_alpha";
case BlendFactor::one_minus_source_alpha:
return "one_minus_source_alpha";
case BlendFactor::destination_alpha:
return "destination_alpha";
case BlendFactor::one_minus_destination_alpha:
return "one_minus_destination_alpha";
}
return "unknown";
}
const char* blend_op_name(BlendOp op) noexcept
{
switch (op) {
case BlendOp::add:
return "add";
case BlendOp::subtract:
return "subtract";
case BlendOp::reverse_subtract:
return "reverse_subtract";
}
return "unknown";
}
const char* compare_op_name(CompareOp op) noexcept
{
switch (op) {
case CompareOp::never:
return "never";
case CompareOp::less:
return "less";
case CompareOp::equal:
return "equal";
case CompareOp::less_or_equal:
return "less_or_equal";
case CompareOp::greater:
return "greater";
case CompareOp::not_equal:
return "not_equal";
case CompareOp::greater_or_equal:
return "greater_or_equal";
case CompareOp::always:
return "always";
}
return "unknown";
}
const char* sampler_filter_name(SamplerFilter filter) noexcept
{
switch (filter) {
case SamplerFilter::nearest:
return "nearest";
case SamplerFilter::linear:
return "linear";
}
return "unknown";
}
const char* sampler_address_mode_name(SamplerAddressMode mode) noexcept
{
switch (mode) {
case SamplerAddressMode::clamp_to_edge:
return "clamp_to_edge";
case SamplerAddressMode::repeat:
return "repeat";
case SamplerAddressMode::mirrored_repeat:
return "mirrored_repeat";
case SamplerAddressMode::clamp_to_border:
return "clamp_to_border";
}
return "unknown";
}
}

View File

@@ -4,13 +4,20 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#include <span>
namespace pp::renderer {
constexpr std::uint32_t max_texture_dimension = 32768;
constexpr std::uint32_t max_texture_mip_levels = 16;
constexpr std::uint32_t max_mesh_vertices = 16777216;
constexpr std::uint32_t max_texture_slots = 32;
constexpr std::uint64_t max_texture_bytes = 1024ULL * 1024ULL * 1024ULL;
constexpr std::size_t max_shader_source_bytes = 4ULL * 1024ULL * 1024ULL;
constexpr std::size_t max_shader_uniform_bytes = 64ULL * 1024ULL;
constexpr std::size_t max_trace_label_bytes = 256;
constexpr std::size_t max_resource_label_bytes = 256;
enum class TextureFormat : std::uint8_t {
rgba8,
@@ -18,6 +25,45 @@ enum class TextureFormat : std::uint8_t {
depth24_stencil8,
};
enum class TextureUsage : std::uint32_t {
none = 0,
sampled = 1U << 0U,
render_target = 1U << 1U,
upload_destination = 1U << 2U,
readback_source = 1U << 3U,
copy_source = 1U << 4U,
copy_destination = 1U << 5U,
};
enum class TextureState : std::uint8_t {
undefined,
shader_read,
render_target,
upload_destination,
copy_source,
copy_destination,
readback_source,
present,
};
[[nodiscard]] constexpr TextureUsage operator|(TextureUsage lhs, TextureUsage rhs) noexcept
{
return static_cast<TextureUsage>(
static_cast<std::uint32_t>(lhs) | static_cast<std::uint32_t>(rhs));
}
[[nodiscard]] constexpr TextureUsage operator&(TextureUsage lhs, TextureUsage rhs) noexcept
{
return static_cast<TextureUsage>(
static_cast<std::uint32_t>(lhs) & static_cast<std::uint32_t>(rhs));
}
constexpr TextureUsage& operator|=(TextureUsage& lhs, TextureUsage rhs) noexcept
{
lhs = lhs | rhs;
return lhs;
}
struct Extent2D {
std::uint32_t width = 0;
std::uint32_t height = 0;
@@ -26,7 +72,13 @@ struct Extent2D {
struct TextureDesc {
Extent2D extent;
TextureFormat format = TextureFormat::rgba8;
bool render_target = false;
std::uint32_t mip_levels = 1;
TextureUsage usage = TextureUsage::sampled
| TextureUsage::upload_destination
| TextureUsage::readback_source
| TextureUsage::copy_source
| TextureUsage::copy_destination;
const char* debug_name = "";
};
struct ReadbackRegion {
@@ -45,6 +97,14 @@ struct Viewport {
float max_depth = 1.0F;
};
struct ScissorRect {
bool enabled = false;
std::int32_t x = 0;
std::int32_t y = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
};
struct ClearColor {
float r = 0.0F;
float g = 0.0F;
@@ -52,16 +112,106 @@ struct ClearColor {
float a = 0.0F;
};
struct RenderPassDesc {
bool clear_color_enabled = true;
ClearColor clear_color;
bool clear_depth_enabled = false;
float clear_depth = 1.0F;
bool clear_stencil_enabled = false;
std::uint8_t clear_stencil = 0;
};
enum class PrimitiveTopology : std::uint8_t {
triangles,
triangle_strip,
lines,
};
enum class BlitFilter : std::uint8_t {
nearest,
linear,
};
enum class BlendFactor : std::uint8_t {
zero,
one,
source_alpha,
one_minus_source_alpha,
destination_alpha,
one_minus_destination_alpha,
};
enum class BlendOp : std::uint8_t {
add,
subtract,
reverse_subtract,
};
enum class CompareOp : std::uint8_t {
never,
less,
equal,
less_or_equal,
greater,
not_equal,
greater_or_equal,
always,
};
enum class SamplerFilter : std::uint8_t {
nearest,
linear,
};
enum class SamplerAddressMode : std::uint8_t {
clamp_to_edge,
repeat,
mirrored_repeat,
clamp_to_border,
};
struct BlendState {
bool enabled = false;
BlendFactor source_color = BlendFactor::one;
BlendFactor destination_color = BlendFactor::zero;
BlendOp color_op = BlendOp::add;
BlendFactor source_alpha = BlendFactor::one;
BlendFactor destination_alpha = BlendFactor::zero;
BlendOp alpha_op = BlendOp::add;
bool write_r = true;
bool write_g = true;
bool write_b = true;
bool write_a = true;
};
struct DepthState {
bool test_enabled = false;
bool write_enabled = false;
CompareOp compare = CompareOp::less_or_equal;
};
struct SamplerDesc {
SamplerFilter min_filter = SamplerFilter::linear;
SamplerFilter mag_filter = SamplerFilter::linear;
SamplerFilter mip_filter = SamplerFilter::linear;
SamplerAddressMode address_u = SamplerAddressMode::clamp_to_edge;
SamplerAddressMode address_v = SamplerAddressMode::clamp_to_edge;
SamplerAddressMode address_w = SamplerAddressMode::clamp_to_edge;
};
struct MeshDesc {
std::uint32_t vertex_count = 0;
std::uint32_t index_count = 0;
PrimitiveTopology topology = PrimitiveTopology::triangles;
const char* debug_name = "";
};
struct DrawDesc {
std::uint32_t first_vertex = 0;
std::uint32_t vertex_count = 0;
std::uint32_t first_index = 0;
std::uint32_t index_count = 0;
std::uint32_t instance_count = 1;
};
struct ShaderStageSource {
@@ -76,6 +226,16 @@ struct ShaderProgramDesc {
ShaderStageSource fragment;
};
struct RenderDeviceFeatures {
bool framebuffer_fetch = false;
bool explicit_texture_transitions = false;
bool texture_copy = false;
bool render_target_blit = false;
bool frame_capture = false;
bool float16_render_targets = false;
bool float32_render_targets = false;
};
class ITexture2D {
public:
virtual ~ITexture2D() = default;
@@ -109,7 +269,9 @@ public:
class IRenderTrace {
public:
virtual ~IRenderTrace() = default;
virtual void marker(const char* component, const char* name) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status marker(const char* component, const char* name) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status begin_scope(const char* component, const char* name) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status end_scope() noexcept = 0;
};
class ICommandContext {
@@ -117,11 +279,51 @@ public:
virtual ~ICommandContext() = default;
[[nodiscard]] virtual pp::foundation::Status begin_render_pass(
IRenderTarget& target,
ClearColor clear_color) noexcept = 0;
RenderPassDesc desc) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status set_viewport(Viewport viewport) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status set_scissor(ScissorRect scissor) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status set_blend_state(BlendState state) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status set_depth_state(DepthState state) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status set_shader_uniform(
const char* name,
std::span<const std::byte> bytes) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status bind_texture(
std::uint32_t slot,
ITexture2D& texture) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status bind_sampler(
std::uint32_t slot,
SamplerDesc sampler) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status bind_mesh(IMesh& mesh) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status draw() noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status draw(DrawDesc desc) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status read_texture(
ITexture2D& texture,
ReadbackRegion region,
IReadbackBuffer& destination) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status upload_texture(
ITexture2D& texture,
ReadbackRegion region,
std::span<const std::byte> rgba_or_channel_bytes) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status generate_mipmaps(
ITexture2D& texture) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status transition_texture(
ITexture2D& texture,
TextureState before,
TextureState after) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status copy_texture(
ITexture2D& source,
ReadbackRegion source_region,
ITexture2D& destination,
ReadbackRegion destination_region) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status capture_frame(
IRenderTarget& target,
IReadbackBuffer& destination) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Status blit_render_target(
IRenderTarget& source,
ReadbackRegion source_region,
IRenderTarget& destination,
ReadbackRegion destination_region,
BlitFilter filter) noexcept = 0;
virtual void end_render_pass() noexcept = 0;
};
@@ -129,18 +331,76 @@ class IRenderDevice {
public:
virtual ~IRenderDevice() = default;
[[nodiscard]] virtual const char* backend_name() const noexcept = 0;
[[nodiscard]] virtual RenderDeviceFeatures features() const noexcept = 0;
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<ITexture2D>> create_texture(
TextureDesc desc) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<IRenderTarget>> create_render_target(
TextureDesc color_desc) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<IShaderProgram>> create_shader_program(
ShaderProgramDesc desc) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<IMesh>> create_mesh(
MeshDesc desc) noexcept = 0;
[[nodiscard]] virtual pp::foundation::Result<std::unique_ptr<IReadbackBuffer>> create_readback_buffer(
std::uint64_t size_bytes) noexcept = 0;
[[nodiscard]] virtual ICommandContext& immediate_context() noexcept = 0;
[[nodiscard]] virtual IRenderTrace* trace() noexcept = 0;
};
[[nodiscard]] std::uint32_t bytes_per_pixel(TextureFormat format) noexcept;
[[nodiscard]] std::uint32_t max_mip_levels_for_extent(Extent2D extent) noexcept;
[[nodiscard]] bool has_texture_usage(TextureUsage usage, TextureUsage required) noexcept;
[[nodiscard]] pp::foundation::Status validate_extent(Extent2D extent) noexcept;
[[nodiscard]] pp::foundation::Status validate_texture_usage(TextureUsage usage) noexcept;
[[nodiscard]] pp::foundation::Status validate_resource_label(const char* label) noexcept;
[[nodiscard]] pp::foundation::Status validate_texture_desc(TextureDesc desc) noexcept;
[[nodiscard]] pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept;
[[nodiscard]] pp::foundation::Status validate_scissor(ScissorRect scissor, Extent2D target_extent) noexcept;
[[nodiscard]] pp::foundation::Status validate_render_pass_desc(RenderPassDesc desc) noexcept;
[[nodiscard]] pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept;
[[nodiscard]] pp::foundation::Status validate_blend_op(BlendOp op) noexcept;
[[nodiscard]] pp::foundation::Status validate_blend_state(BlendState state) noexcept;
[[nodiscard]] pp::foundation::Status validate_compare_op(CompareOp op) noexcept;
[[nodiscard]] pp::foundation::Status validate_depth_state(DepthState state) noexcept;
[[nodiscard]] pp::foundation::Status validate_sampler_filter(SamplerFilter filter) noexcept;
[[nodiscard]] pp::foundation::Status validate_sampler_address_mode(SamplerAddressMode mode) noexcept;
[[nodiscard]] pp::foundation::Status validate_sampler_desc(SamplerDesc desc) noexcept;
[[nodiscard]] pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept;
[[nodiscard]] pp::foundation::Status validate_draw_desc(MeshDesc mesh, DrawDesc draw) noexcept;
[[nodiscard]] pp::foundation::Status validate_texture_slot(std::uint32_t slot) noexcept;
[[nodiscard]] pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept;
[[nodiscard]] pp::foundation::Status validate_shader_uniform_write(
const char* name,
std::span<const std::byte> bytes) noexcept;
[[nodiscard]] pp::foundation::Status validate_trace_label(const char* component, const char* name) noexcept;
[[nodiscard]] pp::foundation::Result<std::uint64_t> texture_byte_size(TextureDesc desc) noexcept;
[[nodiscard]] pp::foundation::Result<std::uint64_t> readback_byte_size(
TextureDesc desc,
ReadbackRegion region) noexcept;
[[nodiscard]] pp::foundation::Result<std::uint64_t> frame_capture_byte_size(TextureDesc desc) noexcept;
[[nodiscard]] pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept;
[[nodiscard]] pp::foundation::Status validate_texture_copy_descs(
TextureDesc source,
ReadbackRegion source_region,
TextureDesc destination,
ReadbackRegion destination_region) noexcept;
[[nodiscard]] pp::foundation::Status validate_mipmap_generation_desc(TextureDesc desc) noexcept;
[[nodiscard]] pp::foundation::Status validate_texture_state(TextureState state) noexcept;
[[nodiscard]] pp::foundation::Status validate_texture_transition_desc(
TextureDesc desc,
TextureState before,
TextureState after) noexcept;
[[nodiscard]] pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept;
[[nodiscard]] pp::foundation::Status validate_blit_descs(
TextureDesc source,
TextureDesc destination) noexcept;
[[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept;
[[nodiscard]] const char* texture_state_name(TextureState state) noexcept;
[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept;
[[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept;
[[nodiscard]] const char* blend_factor_name(BlendFactor factor) noexcept;
[[nodiscard]] const char* blend_op_name(BlendOp op) noexcept;
[[nodiscard]] const char* compare_op_name(CompareOp op) noexcept;
[[nodiscard]] const char* sampler_filter_name(SamplerFilter filter) noexcept;
[[nodiscard]] const char* sampler_address_mode_name(SamplerAddressMode mode) noexcept;
}

View File

@@ -0,0 +1,91 @@
#include "renderer_api/shader_catalog.h"
#include <array>
#include <string_view>
namespace pp::renderer {
namespace {
constexpr std::array<ShaderCatalogEntry, 25> pano_catalog {
ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.glsl" },
ShaderCatalogEntry { .name = "texture-alpha", .path = "data/shaders/texture-alpha.glsl" },
ShaderCatalogEntry { .name = "texture-mask", .path = "data/shaders/texture-mask.glsl" },
ShaderCatalogEntry { .name = "texture-colorize", .path = "data/shaders/texture-colorize.glsl" },
ShaderCatalogEntry { .name = "texture-blend", .path = "data/shaders/texture-blend.glsl" },
ShaderCatalogEntry { .name = "stroke-preview", .path = "data/shaders/stroke-preview.glsl" },
ShaderCatalogEntry { .name = "comp-erase", .path = "data/shaders/comp-erase.glsl" },
ShaderCatalogEntry { .name = "comp-draw", .path = "data/shaders/comp-draw.glsl" },
ShaderCatalogEntry { .name = "color", .path = "data/shaders/color.glsl" },
ShaderCatalogEntry { .name = "color-quad", .path = "data/shaders/color-quad.glsl" },
ShaderCatalogEntry { .name = "color-tri", .path = "data/shaders/color-tri.glsl" },
ShaderCatalogEntry { .name = "color-hue", .path = "data/shaders/color-hue.glsl" },
ShaderCatalogEntry { .name = "uvs", .path = "data/shaders/uvs.glsl" },
ShaderCatalogEntry { .name = "font", .path = "data/shaders/font.glsl" },
ShaderCatalogEntry { .name = "atlas", .path = "data/shaders/atlas.glsl" },
ShaderCatalogEntry { .name = "stroke", .path = "data/shaders/stroke.glsl" },
ShaderCatalogEntry { .name = "stroke-pad", .path = "data/shaders/stroke-pad.glsl" },
ShaderCatalogEntry { .name = "stroke-dilate", .path = "data/shaders/stroke-dilate.glsl" },
ShaderCatalogEntry { .name = "checkerboard", .path = "data/shaders/checkerboard.glsl" },
ShaderCatalogEntry { .name = "equirect", .path = "data/shaders/equirect.glsl" },
ShaderCatalogEntry { .name = "brush-stroke", .path = "data/shaders/stroke-instanced.glsl" },
ShaderCatalogEntry { .name = "vertex-color", .path = "data/shaders/vertex-color.glsl" },
ShaderCatalogEntry { .name = "lambert", .path = "data/shaders/lambert.glsl" },
ShaderCatalogEntry { .name = "lambert-lightmap", .path = "data/shaders/lightmap.glsl" },
ShaderCatalogEntry { .name = "bakeuv", .path = "data/shaders/bake-uv.glsl" },
};
[[nodiscard]] bool is_empty_c_string(const char* text) noexcept
{
return text == nullptr || text[0] == '\0';
}
[[nodiscard]] bool has_shader_extension(std::string_view path) noexcept
{
constexpr std::string_view extension = ".glsl";
return path.size() >= extension.size()
&& path.substr(path.size() - extension.size()) == extension;
}
}
std::span<const ShaderCatalogEntry> panopainter_shader_catalog() noexcept
{
return pano_catalog;
}
pp::foundation::Status validate_shader_catalog(std::span<const ShaderCatalogEntry> catalog) noexcept
{
if (catalog.empty()) {
return pp::foundation::Status::invalid_argument("shader catalog must not be empty");
}
if (catalog.size() > max_shader_catalog_entries) {
return pp::foundation::Status::out_of_range("shader catalog exceeds the configured limit");
}
for (std::size_t i = 0; i < catalog.size(); ++i) {
const auto& entry = catalog[i];
if (is_empty_c_string(entry.name)) {
return pp::foundation::Status::invalid_argument("shader catalog entry name must not be empty");
}
if (is_empty_c_string(entry.path)) {
return pp::foundation::Status::invalid_argument("shader catalog entry path must not be empty");
}
if (!has_shader_extension(entry.path)) {
return pp::foundation::Status::invalid_argument("shader catalog path must end with .glsl");
}
for (std::size_t j = i + 1U; j < catalog.size(); ++j) {
if (std::string_view(entry.name) == std::string_view(catalog[j].name)) {
return pp::foundation::Status::invalid_argument("shader catalog entry name is duplicated");
}
}
}
return pp::foundation::Status::success();
}
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <span>
namespace pp::renderer {
constexpr std::size_t max_shader_catalog_entries = 256;
struct ShaderCatalogEntry {
const char* name = "";
const char* path = "";
};
[[nodiscard]] std::span<const ShaderCatalogEntry> panopainter_shader_catalog() noexcept;
[[nodiscard]] pp::foundation::Status validate_shader_catalog(
std::span<const ShaderCatalogEntry> catalog) noexcept;
}

View File

@@ -0,0 +1,442 @@
#include "renderer_gl/command_plan.h"
namespace pp::renderer::gl {
namespace {
[[nodiscard]] bool texture_format_supported(OpenGlRendererTextureFormat format) noexcept
{
return format.internal_format != 0U
&& format.pixel_format != 0U
&& format.component_type != 0U
&& format.bytes_per_pixel != 0U;
}
[[nodiscard]] bool texture_state_supported(pp::renderer::TextureState state) noexcept
{
switch (state) {
case pp::renderer::TextureState::undefined:
case pp::renderer::TextureState::shader_read:
case pp::renderer::TextureState::render_target:
case pp::renderer::TextureState::upload_destination:
case pp::renderer::TextureState::copy_source:
case pp::renderer::TextureState::copy_destination:
case pp::renderer::TextureState::readback_source:
case pp::renderer::TextureState::present:
return true;
default:
return false;
}
}
[[nodiscard]] bool non_empty_name(const char* name) noexcept
{
return name != nullptr && name[0] != '\0';
}
[[nodiscard]] bool requires_render_pass(pp::renderer::RecordedRenderCommandKind kind) noexcept
{
switch (kind) {
case pp::renderer::RecordedRenderCommandKind::set_viewport:
case pp::renderer::RecordedRenderCommandKind::set_scissor:
case pp::renderer::RecordedRenderCommandKind::set_blend_state:
case pp::renderer::RecordedRenderCommandKind::set_depth_state:
case pp::renderer::RecordedRenderCommandKind::bind_shader:
case pp::renderer::RecordedRenderCommandKind::set_shader_uniform:
case pp::renderer::RecordedRenderCommandKind::bind_texture:
case pp::renderer::RecordedRenderCommandKind::bind_sampler:
case pp::renderer::RecordedRenderCommandKind::bind_mesh:
case pp::renderer::RecordedRenderCommandKind::draw:
return true;
default:
return false;
}
}
void record_unsupported_command(OpenGlCommandPlan& plan, std::size_t index) noexcept
{
++plan.unsupported_command_count;
if (plan.first_unsupported_command == OpenGlCommandPlan::npos) {
plan.first_unsupported_command = index;
}
}
void record_render_pass_order_error(OpenGlCommandPlan& plan, std::size_t index) noexcept
{
++plan.render_pass_order_error_count;
if (plan.first_render_pass_order_error == OpenGlCommandPlan::npos) {
plan.first_render_pass_order_error = index;
}
}
void record_dependency_error(OpenGlCommandPlan& plan, std::size_t index) noexcept
{
++plan.dependency_error_count;
if (plan.first_dependency_error == OpenGlCommandPlan::npos) {
plan.first_dependency_error = index;
}
}
}
const char* planned_command_kind_name(OpenGlPlannedCommandKind kind) noexcept
{
switch (kind) {
case OpenGlPlannedCommandKind::unknown:
return "unknown";
case OpenGlPlannedCommandKind::begin_render_pass:
return "begin_render_pass";
case OpenGlPlannedCommandKind::set_viewport:
return "set_viewport";
case OpenGlPlannedCommandKind::set_scissor:
return "set_scissor";
case OpenGlPlannedCommandKind::set_blend_state:
return "set_blend_state";
case OpenGlPlannedCommandKind::set_depth_state:
return "set_depth_state";
case OpenGlPlannedCommandKind::bind_shader:
return "bind_shader";
case OpenGlPlannedCommandKind::set_shader_uniform:
return "set_shader_uniform";
case OpenGlPlannedCommandKind::bind_texture:
return "bind_texture";
case OpenGlPlannedCommandKind::bind_sampler:
return "bind_sampler";
case OpenGlPlannedCommandKind::bind_mesh:
return "bind_mesh";
case OpenGlPlannedCommandKind::draw:
return "draw";
case OpenGlPlannedCommandKind::upload_texture:
return "upload_texture";
case OpenGlPlannedCommandKind::generate_mipmaps:
return "generate_mipmaps";
case OpenGlPlannedCommandKind::transition_texture:
return "transition_texture";
case OpenGlPlannedCommandKind::copy_texture:
return "copy_texture";
case OpenGlPlannedCommandKind::read_texture:
return "read_texture";
case OpenGlPlannedCommandKind::capture_frame:
return "capture_frame";
case OpenGlPlannedCommandKind::blit_render_target:
return "blit_render_target";
case OpenGlPlannedCommandKind::end_render_pass:
return "end_render_pass";
case OpenGlPlannedCommandKind::trace:
return "trace";
case OpenGlPlannedCommandKind::passthrough:
return "passthrough";
}
return "unknown";
}
OpenGlPlannedCommand plan_recorded_render_command(pp::renderer::RecordedRenderCommand command) noexcept
{
OpenGlPlannedCommand planned {};
planned.requires_render_pass = requires_render_pass(command.kind);
planned.supported = true;
switch (command.kind) {
case pp::renderer::RecordedRenderCommandKind::begin_render_pass:
planned.kind = OpenGlPlannedCommandKind::begin_render_pass;
planned.clear_mask = clear_mask_for_render_pass(pp::renderer::RenderPassDesc {
.clear_color_enabled = command.clear_color_enabled,
.clear_color = command.clear_color,
.clear_depth_enabled = command.clear_depth_enabled,
.clear_depth = command.clear_depth,
.clear_stencil_enabled = command.clear_stencil_enabled,
.clear_stencil = command.clear_stencil,
});
planned.clear_values = clear_values_for_render_pass(pp::renderer::RenderPassDesc {
.clear_color_enabled = command.clear_color_enabled,
.clear_color = command.clear_color,
.clear_depth_enabled = command.clear_depth_enabled,
.clear_depth = command.clear_depth,
.clear_stencil_enabled = command.clear_stencil_enabled,
.clear_stencil = command.clear_stencil,
});
planned.texture_format = texture_format_for_renderer_format(command.target_desc.format);
planned.supported = texture_format_supported(planned.texture_format);
break;
case pp::renderer::RecordedRenderCommandKind::set_viewport:
planned.kind = OpenGlPlannedCommandKind::set_viewport;
planned.viewport = viewport_for_renderer_viewport(command.viewport);
break;
case pp::renderer::RecordedRenderCommandKind::set_scissor:
planned.kind = OpenGlPlannedCommandKind::set_scissor;
planned.scissor = scissor_rect_for_renderer_scissor(command.scissor);
break;
case pp::renderer::RecordedRenderCommandKind::set_blend_state:
planned.kind = OpenGlPlannedCommandKind::set_blend_state;
planned.blend = blend_state_for_renderer_blend_state(command.blend_state);
planned.supported = planned.blend.supported;
break;
case pp::renderer::RecordedRenderCommandKind::set_depth_state:
planned.kind = OpenGlPlannedCommandKind::set_depth_state;
planned.depth = depth_state_for_renderer_depth_state(command.depth_state);
planned.supported = planned.depth.supported;
break;
case pp::renderer::RecordedRenderCommandKind::bind_shader:
planned.kind = OpenGlPlannedCommandKind::bind_shader;
planned.name = command.name;
planned.supported = non_empty_name(planned.name);
break;
case pp::renderer::RecordedRenderCommandKind::set_shader_uniform:
planned.kind = OpenGlPlannedCommandKind::set_shader_uniform;
planned.name = command.name;
planned.uniform_bytes = command.uniform_bytes;
planned.supported = non_empty_name(planned.name) && planned.uniform_bytes > 0U;
break;
case pp::renderer::RecordedRenderCommandKind::bind_texture:
planned.kind = OpenGlPlannedCommandKind::bind_texture;
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
planned.texture_slot = command.texture_slot;
planned.supported = texture_format_supported(planned.texture_format)
&& planned.texture_slot < pp::renderer::max_texture_slots;
break;
case pp::renderer::RecordedRenderCommandKind::bind_sampler:
planned.kind = OpenGlPlannedCommandKind::bind_sampler;
planned.sampler = sampler_state_for_renderer_sampler_desc(command.sampler_desc);
planned.sampler_slot = command.sampler_slot;
planned.supported = planned.sampler.supported
&& planned.sampler_slot < pp::renderer::max_texture_slots;
break;
case pp::renderer::RecordedRenderCommandKind::bind_mesh:
planned.kind = OpenGlPlannedCommandKind::bind_mesh;
planned.primitive_mode = primitive_mode_for_renderer_topology(command.mesh_desc.topology);
planned.supported = planned.primitive_mode != 0U;
break;
case pp::renderer::RecordedRenderCommandKind::draw:
planned.kind = OpenGlPlannedCommandKind::draw;
planned.primitive_mode = primitive_mode_for_renderer_topology(command.mesh_desc.topology);
planned.draw_vertex_count = command.draw_desc.vertex_count;
planned.draw_index_count = command.draw_desc.index_count;
planned.supported = planned.primitive_mode != 0U;
break;
case pp::renderer::RecordedRenderCommandKind::upload_texture:
planned.kind = OpenGlPlannedCommandKind::upload_texture;
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
planned.readback_region = command.readback_region;
planned.upload_bytes = command.upload_bytes;
planned.supported = texture_format_supported(planned.texture_format);
break;
case pp::renderer::RecordedRenderCommandKind::generate_mipmaps:
planned.kind = OpenGlPlannedCommandKind::generate_mipmaps;
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
planned.generated_mip_levels = command.generated_mip_levels;
planned.generated_mip_bytes = command.generated_mip_bytes;
planned.supported = texture_format_supported(planned.texture_format);
break;
case pp::renderer::RecordedRenderCommandKind::transition_texture:
planned.kind = OpenGlPlannedCommandKind::transition_texture;
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
planned.before_state = command.before_state;
planned.after_state = command.after_state;
planned.supported = texture_format_supported(planned.texture_format)
&& texture_state_supported(planned.before_state)
&& texture_state_supported(planned.after_state);
break;
case pp::renderer::RecordedRenderCommandKind::copy_texture:
planned.kind = OpenGlPlannedCommandKind::copy_texture;
planned.source_texture_format = texture_format_for_renderer_format(command.source_desc.format);
planned.destination_texture_format = texture_format_for_renderer_format(command.destination_desc.format);
planned.source_region = command.source_region;
planned.destination_region = command.destination_region;
planned.copy_source_bytes = command.copy_source_bytes;
planned.copy_destination_bytes = command.copy_destination_bytes;
planned.supported = texture_format_supported(planned.source_texture_format)
&& texture_format_supported(planned.destination_texture_format);
break;
case pp::renderer::RecordedRenderCommandKind::read_texture:
planned.kind = OpenGlPlannedCommandKind::read_texture;
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
planned.readback_region = command.readback_region;
planned.readback_bytes = command.readback_bytes;
planned.supported = texture_format_supported(planned.texture_format);
break;
case pp::renderer::RecordedRenderCommandKind::capture_frame:
planned.kind = OpenGlPlannedCommandKind::capture_frame;
planned.texture_format = texture_format_for_renderer_format(command.target_desc.format);
planned.capture_bytes = command.capture_bytes;
planned.supported = texture_format_supported(planned.texture_format);
break;
case pp::renderer::RecordedRenderCommandKind::blit_render_target:
planned.kind = OpenGlPlannedCommandKind::blit_render_target;
planned.blit_filter = blit_filter_for_renderer_filter(command.blit_filter);
planned.source_texture_format = texture_format_for_renderer_format(command.source_desc.format);
planned.destination_texture_format = texture_format_for_renderer_format(command.destination_desc.format);
planned.source_region = command.source_region;
planned.destination_region = command.destination_region;
planned.blit_source_bytes = command.blit_source_bytes;
planned.blit_destination_bytes = command.blit_destination_bytes;
planned.supported = planned.blit_filter.supported
&& texture_format_supported(planned.source_texture_format)
&& texture_format_supported(planned.destination_texture_format);
break;
case pp::renderer::RecordedRenderCommandKind::end_render_pass:
planned.kind = OpenGlPlannedCommandKind::end_render_pass;
break;
case pp::renderer::RecordedRenderCommandKind::trace_marker:
case pp::renderer::RecordedRenderCommandKind::trace_begin_scope:
case pp::renderer::RecordedRenderCommandKind::trace_end_scope:
planned.kind = OpenGlPlannedCommandKind::trace;
break;
default:
planned.kind = OpenGlPlannedCommandKind::unknown;
planned.supported = false;
break;
}
return planned;
}
OpenGlCommandPlan plan_recorded_render_commands(
std::span<const pp::renderer::RecordedRenderCommand> commands)
{
OpenGlCommandPlan plan;
plan.commands.reserve(commands.size());
bool in_render_pass = false;
bool shader_bound_in_pass = false;
bool mesh_bound_in_pass = false;
for (std::size_t index = 0; index < commands.size(); ++index) {
OpenGlPlannedCommand planned = plan_recorded_render_command(commands[index]);
if (!planned.supported) {
record_unsupported_command(plan, index);
}
switch (planned.kind) {
case OpenGlPlannedCommandKind::begin_render_pass:
++plan.render_pass_count;
if (in_render_pass) {
record_render_pass_order_error(plan, index);
}
in_render_pass = true;
shader_bound_in_pass = false;
mesh_bound_in_pass = false;
break;
case OpenGlPlannedCommandKind::end_render_pass:
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
in_render_pass = false;
shader_bound_in_pass = false;
mesh_bound_in_pass = false;
break;
case OpenGlPlannedCommandKind::draw:
++plan.draw_command_count;
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
if (!shader_bound_in_pass || !mesh_bound_in_pass) {
record_dependency_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::bind_shader:
++plan.shader_bind_command_count;
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
shader_bound_in_pass = true;
break;
case OpenGlPlannedCommandKind::set_shader_uniform:
++plan.uniform_command_count;
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
if (!shader_bound_in_pass) {
record_dependency_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::bind_texture:
++plan.texture_bind_command_count;
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::bind_sampler:
++plan.sampler_bind_command_count;
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::bind_mesh:
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
mesh_bound_in_pass = true;
break;
case OpenGlPlannedCommandKind::upload_texture:
++plan.upload_command_count;
if (in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::generate_mipmaps:
++plan.mipmap_command_count;
if (in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::transition_texture:
++plan.transition_command_count;
if (in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::copy_texture:
++plan.copy_command_count;
if (in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::read_texture:
++plan.readback_command_count;
if (in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::capture_frame:
++plan.capture_command_count;
if (in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::passthrough:
++plan.passthrough_command_count;
if (planned.requires_render_pass && !in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::trace:
++plan.trace_command_count;
break;
case OpenGlPlannedCommandKind::blit_render_target:
if (in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
default:
if (planned.requires_render_pass && !in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
}
plan.commands.push_back(planned);
}
plan.ended_in_render_pass = in_render_pass;
if (in_render_pass) {
record_render_pass_order_error(plan, commands.size());
}
plan.supported = plan.unsupported_command_count == 0U
&& plan.render_pass_order_error_count == 0U
&& plan.dependency_error_count == 0U;
return plan;
}
}

Some files were not shown because too many files have changed in this diff Show More