-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMainWindow.xaml.h
More file actions
665 lines (598 loc) · 34.9 KB
/
MainWindow.xaml.h
File metadata and controls
665 lines (598 loc) · 34.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
#pragma once
#include "MainWindow.g.h"
#include "Rendering/RenderEngine.h"
#include "Rendering/DisplayMonitor.h"
#include "Rendering/GraphEvaluator.h"
#include "Graph/EffectGraph.h"
#include "Graph/GraphUiSnapshot.h"
#include "Effects/EffectRegistry.h"
#include "Effects/SourceNodeFactory.h"
#include "Effects/CustomPixelShaderEffect.h"
#include "Effects/CustomComputeShaderEffect.h"
#include "Controls/ShaderEditorController.h"
#include "Controls/NodeGraphController.h"
#include "Controls/PixelInspectorController.h"
#include "Controls/PixelTraceController.h"
#include "Controls/OutputWindow.h"
#include "Controls/LogWindow.h"
#include "Controls/NodeLog.h"
#include "EffectDesignerWindow.xaml.h"
#include "Engine/Mcp/McpHttpServer.h"
#include "Engine/Mcp/EngineMcpRoutes.h"
#include "Rendering/RenderThreadDispatcher.h"
namespace winrt::ShaderLab::implementation
{
struct MainWindow : MainWindowT<MainWindow>
{
MainWindow();
~MainWindow();
// Auto-start MCP server (set from --mcp command-line flag).
void SetAutoStartMcp(bool autoStart) { m_autoStartMcp = autoStart; }
// Device preference (set from --gpu / --warp command-line flags).
void SetDevicePreference(::ShaderLab::Rendering::DevicePreference pref) { m_devicePref = pref; }
// File path passed on the command line via Explorer FTA.
// Loaded after rendering is initialized.
void SetPendingOpenPath(std::wstring path) { m_pendingOpenPath = std::move(path); }
// XAML-bound event handlers (must be public for generated code).
void OnColumnSplitterPointerPressed(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnColumnSplitterPointerMoved(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnColumnSplitterPointerReleased(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnGpuInfoTapped(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::TappedRoutedEventArgs const& args);
// Status-bar broom button (Phase 8 p8-status-bar-button).
winrt::fire_and_forget OnReaperBroomClicked(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);
void OnNodeGraphDragOver(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::DragEventArgs const& args);
winrt::fire_and_forget OnNodeGraphDrop(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::DragEventArgs const& args);
void OnSaveAccelerator(
winrt::Microsoft::UI::Xaml::Input::KeyboardAccelerator const& sender,
winrt::Microsoft::UI::Xaml::Input::KeyboardAcceleratorInvokedEventArgs const& args);
void OnSaveAsAccelerator(
winrt::Microsoft::UI::Xaml::Input::KeyboardAccelerator const& sender,
winrt::Microsoft::UI::Xaml::Input::KeyboardAcceleratorInvokedEventArgs const& args);
private:
HWND GetWindowHandle();
void InitializeRendering();
void SwitchAdapter(::ShaderLab::Rendering::DevicePreference pref, LUID adapterLuid);
void OnPreviewPanelLoaded();
void RegisterCustomEffects();
void UpdateStatusBar();
void PopulatePreviewNodeSelector();
void UpdateOutdatedEffectsButton();
void UpdatePreviewOverlay();
void PopulateDisplayProfileSelector();
ID2D1Image* GetPreviewImage();
ID2D1Image* ResolveDisplayImage(uint32_t nodeId);
// Returns the preview viewport size in DIPs (not physical pixels).
D2D1_SIZE_F PreviewViewportDips() const;
void ApplyDisplayProfile(const ::ShaderLab::Rendering::DisplayProfile& profile);
void RevertToLiveDisplay();
// Mirror the active display profile (live or simulated) into the
// properties of every "Working Space" parameter node in the graph.
// Called from the few hot paths where the profile changes
// (ApplyDisplayProfile, RevertToLiveDisplay, display-change
// callback) and once per render tick so newly-added Working Space
// nodes pick up live values immediately. Cheap no-op when the graph
// contains no Working Space nodes.
void UpdateWorkingSpaceNodes();
void ResetAfterGraphLoad(bool reopenOutputWindows = true);
// Pixel trace helpers.
D2D1_RECT_F GetPreviewImageBounds();
bool PointerToImageCoords(
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args,
float& outNormX, float& outNormY);
void PopulatePixelTraceTree();
void UpdatePixelTraceValues();
winrt::Microsoft::UI::Xaml::Controls::Grid CreateTraceRow(
const ::ShaderLab::Controls::PixelTraceNode& traceNode);
// Event handlers.
void OnPreviewSizeChanged(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::SizeChangedEventArgs const& args);
void OnPreviewPointerMoved(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnPreviewKeyDown(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::KeyRoutedEventArgs const& args);
void OnDisplayProfileSelectionChanged(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const& args);
winrt::fire_and_forget LoadIccProfileAsync();
void OnPreviewPointerPressed(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnTraceUnitSelectionChanged(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const& args);
void OnSaveGraphClicked(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);
void OnLoadGraphClicked(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);
// Save the current graph; if no file path is known, prompts via picker.
winrt::fire_and_forget SaveGraphAsync();
// Always prompts via picker, even if a path is known.
winrt::Windows::Foundation::IAsyncAction SaveGraphAsAsync();
winrt::fire_and_forget LoadGraphAsync();
// Quietly write the graph to disk at the previously-picked path.
// Returns true if the write succeeded; false if no path or the
// write failed. Public-ish so the close-confirmation dialog can
// call it on the "Save" branch.
bool SaveGraphToCurrentPath();
// Async wrapper used by the close-confirmation flow so the
// dialog can co_await the embed-progress UI.
winrt::Windows::Foundation::IAsyncAction SaveGraphToCurrentPathAsync();
// Run a save with a modal progress dialog. Reuses
// m_currentFilePath / m_embedMedia.
winrt::Windows::Foundation::IAsyncAction RunSaveWithProgressAsync();
// Mark the current graph as having unsaved edits. Cheap; call it
// from any user-driven mutation site. Resets on save / load / new.
void MarkUnsaved();
// Async confirmation dialog used by AppWindow Closing handler.
// Returns 0 = save (then proceed), 1 = discard, 2 = cancel.
winrt::Windows::Foundation::IAsyncOperation<int32_t> PromptUnsavedChangesAsync();
void RefreshTitleBar();
// Load a .effectgraph (or legacy .json) from a known path. Used by
// file activation (double-click in Explorer) and by the picker.
winrt::Windows::Foundation::IAsyncAction LoadGraphFromPathAsync(winrt::hstring path);
// Path of the most recently saved or loaded graph (empty until
// the user picks a destination via Save As / open).
std::wstring m_currentFilePath;
bool m_unsavedChanges{ false };
// User preference: embed referenced media inside the .effectgraph
// zip. Default true; reset by checkbox in the save flow. Sticky
// across saves of the same window.
bool m_embedMedia{ true };
// Directories holding files extracted from the most recently
// loaded .effectgraph archives. Cleaned up at shutdown so the
// user's %TEMP% doesn't accumulate stale graph media.
std::vector<std::wstring> m_extractedMediaDirs;
// Heartbeat: every HeartbeatIntervalSec we touch a sentinel
// file inside each extracted dir so a future instance can
// tell our dirs from orphans left behind by a crash.
winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_heartbeatTimer{ nullptr };
static constexpr uint32_t HeartbeatIntervalSec = 60;
static constexpr uint32_t HeartbeatStaleSec = 150; // > 2x interval
void StartHeartbeatTimer();
void TouchHeartbeats();
// Scan %TEMP% for ShaderLab-* dirs whose heartbeat is older
// than HeartbeatStaleSec. If any are found, prompt the user
// once at startup to clean them up.
winrt::fire_and_forget ReapStaleMediaDirsAsync();
void PopulateAddNodeFlyout();
void OnAddEffectNode(const ::ShaderLab::Effects::EffectDescriptor& desc);
void OnAddImageSourceClicked(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);
winrt::fire_and_forget AddImageSourceAsync();
void OnAddFloodSourceClicked(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);
winrt::fire_and_forget AddWindowsGraphicsCaptureSourceAsync();
void OnNodeAdded(uint32_t nodeId);
void OnPreviewPointerDragged(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnPreviewPointerReleased(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
// Render loop. UI-only work runs on the m_renderTimer DispatcherQueueTimer
// (XAML reads/writes, FPS panel updates, RenderNodeGraph against the UI
// D2D context). RenderTickBody runs on the dedicated render-worker
// thread (or, in synchronous mode, inline from OnRenderTick).
void OnRenderTick(
winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer const& sender,
winrt::Windows::Foundation::IInspectable const& args);
void RenderTickBody(double deltaSec);
void RenderFrame(double deltaSeconds = 0.0);
// Render-thread frame body. Walks the graph, runs eval + deferred
// compute, draws the preview image into one of the offscreen
// targets, and publishes via m_offscreenPublishedIdx. NO swap-chain
// Present (UI thread does that in BlitOffscreenToSwapChain).
void RenderFrameToOffscreen(double deltaSec);
// UI-thread blit step. Reads m_offscreenPublishedIdx, copies the
// corresponding bitmap into the SwapChainPanel-bound swap chain,
// calls Present1. Idempotent when no new frame has been published.
void BlitOffscreenToSwapChain();
// Recreates the UI-side D2D wrapper bitmaps when the render engine's
// offscreen size changes. Must run on UI thread (m_uiD2dContext is
// UI-owned).
bool EnsureOffscreenUiWrappers();
// Render-worker thread loop. Owns the render-engine D2D context. Drives
// its own cadence (16ms wakeup) and drains m_renderDispatcher commands
// each iteration. Spawned by InitializeRendering, joined in Shutdown.
void RenderWorkerLoop(std::stop_token stop);
// Device stack.
::ShaderLab::Rendering::RenderEngine m_renderEngine;
::ShaderLab::Rendering::DisplayMonitor m_displayMonitor;
::ShaderLab::Rendering::GraphEvaluator m_graphEvaluator;
// Render-thread plumbing. The dispatcher carries closures from UI /
// MCP / NodeGraphController producers to whichever thread owns
// rendering. Until the actual worker thread spawns (Phase 7), the
// dispatcher runs in synchronous mode -- closures execute inline on
// the calling thread, preserving today's single-thread behavior.
::ShaderLab::Rendering::RenderThreadDispatcher m_renderDispatcher{ /*synchronous=*/false };
// Render-worker thread. Spawned in InitializeRendering once the
// device is up; joined in Shutdown after m_isShuttingDown is set
// and the dispatcher is signalled. Runs RenderWorkerLoop.
std::jthread m_renderWorker;
// Latest published immutable snapshot of the graph + per-node runtime
// state. The render path republishes after each frame; UI reads pull
// via std::atomic_load(&m_uiGraphSnapshot). Initially nullptr until
// the first render tick publishes.
std::atomic<std::shared_ptr<const ::ShaderLab::Graph::GraphUiSnapshot>>
m_uiGraphSnapshot{ nullptr };
// Offscreen-render publish protocol (Phase 7). Render thread renders
// into m_renderEngine.OffscreenRenderBitmap(writeIdx); when done, it
// stores writeIdx into m_offscreenPublishedIdx (release). UI thread
// loads m_offscreenPublishedIdx (acquire) and blits the corresponding
// bitmap. Index -1 means no frame published yet.
//
// m_offscreenSourceBitmapUi[idx] are UI-side D2D bitmap wrappers of
// the same D3D11 textures as the render-side bitmaps -- created on
// m_uiD2dContext, used as DrawImage source. They get rebuilt in lock-
// step with EnsureOffscreenTargets when the offscreen size changes.
winrt::com_ptr<ID2D1Bitmap1> m_offscreenSourceBitmapUi[2];
std::atomic<int32_t> m_offscreenPublishedIdx{ -1 };
std::atomic<uint64_t> m_offscreenPublishedVersion{ 0 };
// Tracks the size we last allocated UI-side wrappers for. When this
// doesn't match RenderEngine's offscreen size, UI thread rebuilds.
uint32_t m_offscreenWrapperWidth{ 0 };
uint32_t m_offscreenWrapperHeight{ 0 };
// Generation counters. graphGeneration bumps every time the render
// path observes a graph mutation (HasDirtyNodes etc.); frameGeneration
// bumps once per render tick. Both are written only from the render
// path so a non-atomic uint64 is fine.
uint64_t m_graphGeneration{ 0 };
uint64_t m_frameGeneration{ 0 };
// UI-thread cached value of the last snapshot frameGeneration we
// observed in OnRenderTick. When the worker thread bumps
// m_frameGeneration (in RenderWorkerLoop, every iteration), this
// stays behind until the UI tick catches up. We use the difference
// to decide whether to redraw the editor canvas -- without it the
// canvas only redraws on UI-side interaction, so live values like
// clock progress, video position, and analysis-output fields
// appear frozen even though the snapshot has fresh data.
uint64_t m_lastSeenFrameGeneration{ 0 };
// UI-thread cached value of the last offscreen-publish version we
// blitted. The render worker bumps m_offscreenPublishedVersion
// every successful EndDraw. If it hasn't changed since our last
// tick, we skip the blit + Present1 -- otherwise the UI thread
// vsync-waits at 60 Hz even when the worker is only producing
// frames at 10 Hz, starving input event delivery during heavy
// graph eval (causing dropdown / hover input lag).
uint64_t m_lastBlittedVersion{ 0 };
// UI-side D2D stack -- separate D2D factory + device + immediate
// context for drawing the node-graph editor canvas and pixel-trace
// swatch panel. Shares the D3D11 device with the render engine but
// never touches its D2D resources. Once the render thread (Phase 7)
// owns the engine D2D context exclusively, the editor canvas keeps
// working on the UI thread without crossing the multithread fence.
winrt::com_ptr<ID2D1Factory7> m_uiD2dFactory;
winrt::com_ptr<ID2D1Device6> m_uiD2dDevice;
winrt::com_ptr<ID2D1DeviceContext5> m_uiD2dContext;
// Effect graph.
::ShaderLab::Graph::EffectGraph m_graph;
::ShaderLab::Effects::SourceNodeFactory m_sourceFactory;
// Controllers.
::ShaderLab::Controls::ShaderEditorController m_shaderEditor;
::ShaderLab::Controls::NodeGraphController m_nodeGraphController;
::ShaderLab::Controls::PixelInspectorController m_pixelInspector;
::ShaderLab::Controls::PixelTraceController m_pixelTrace;
// Output windows (one per additional Output node).
std::vector<std::unique_ptr<::ShaderLab::Controls::OutputWindow>> m_outputWindows;
// P7: parallel list of cross-thread sinks (one per OutputWindow). The
// render worker iterates this snapshot WITHOUT touching the UI-owned
// OutputWindow itself. Each entry's shared_ptr keeps the cross-thread
// state alive even if the UI closes the window mid-render. The list
// mutex protects vector mutations (open/close); the per-sink mutex
// inside OutputSinkRenderState protects view-state updates.
std::vector<std::shared_ptr<::ShaderLab::Controls::OutputSinkRenderState>> m_outputSinks;
std::mutex m_outputSinksMutex;
void OpenOutputWindow(uint32_t nodeId);
void CloseOutputWindow(uint32_t nodeId);
void PresentOutputWindows();
// P7 render-thread entry point: iterate a snapshot of m_outputSinks
// and render each non-closed sink's offscreen pair from its node's
// current cachedOutput. Called from RenderFrameToOffscreen after
// the main preview offscreen draw completes (BeginDraw scope still
// open is fine; we do our own SetTarget+BeginDraw on each sink's
// render-side bitmap).
void RenderOutputSinks();
// Per-node log system.
std::unordered_map<uint32_t, ::ShaderLab::Controls::NodeLog> m_nodeLogs;
std::vector<std::unique_ptr<::ShaderLab::Controls::LogWindow>> m_logWindows;
void OpenLogWindow(uint32_t nodeId);
void UpdateLogWindows();
::ShaderLab::Controls::NodeLog& GetNodeLog(uint32_t nodeId) { return m_nodeLogs[nodeId]; }
// Render loop timer.
winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_renderTimer{ nullptr };
std::atomic<uint64_t> m_frameCount{ 0 };
uint64_t m_lastVideoUploadCount{ 0 };
float m_lastFps{ 0.0f };
float m_lastVideoFps{ 0.0f };
std::chrono::steady_clock::time_point m_fpsTimePoint;
std::chrono::steady_clock::time_point m_lastRenderTick;
// Render cadence: target 1 / monitor refresh, clamped to [60, 240] Hz.
// Refreshed on init and on every display change so a 120/144/240 Hz
// panel actually drives the render loop at its native rate. Going
// higher than display refresh wastes work; going lower than 60 Hz
// makes interactions feel laggy on unusual modes (e.g. 30 Hz TV out).
uint32_t m_targetRefreshHz{ 60 };
uint32_t QueryDisplayRefreshHz() const noexcept;
void UpdateRenderTimerInterval();
// Per-frame performance timings (microseconds, rolling averages).
struct FrameTimings {
double totalUs{}; // wall-clock between consecutive timer ticks (true frame interval)
// Graph evaluation phases (render thread, RenderFrameToOffscreen).
// These add up to totalUs and represent the actual cost of one
// graph eval -- which is the meaningful number for HDR shader /
// tonemap perf evaluation, the primary focus of this app.
double sourcesPrepUs{}; // PrepareSourceNode loop (image/video upload)
double evaluateUs{}; // GraphEvaluator::Evaluate (passes 1 + 2)
double deferredComputeUs{}; // ProcessDeferredCompute (D3D11 compute dispatches) + post-PDC eval
double drawUs{}; // DrawImage(preview) into the offscreen target
double endDrawFlushUs{}; // dc->EndDraw() flush; renamed from presentUs --
// actual SwapChain Present1 happens UI-side and
// is reported under uiTickUs.
// UI thread overhead (OnRenderTick total). Mostly Present1 vsync
// wait and Direct3D pipeline drain. NOT counted in totalUs.
double uiTickUs{}; // OnRenderTick wall time: drain + blit + Present1 + canvas redraw
// Misc per-frame work, both on the render side.
double videoTickUs{}; // (currently unused on P7 path; kept for compat)
double outputWindowsUs{}; // PresentOutputWindows (peeled-off output panes; P7-pending)
double traceUs{}; // PopulatePixelTraceTree + RenderTraceSwatches (UI thread)
uint32_t computeDispatches{};
uint32_t framesSampled{};
uint32_t endDrawFailed{}; // diagnostic: count of EndDraw failures (D2DERR_RECREATE_TARGET, etc.)
};
FrameTimings m_frameTiming;
FrameTimings m_lastFrameTiming; // snapshot for MCP read
HWND m_hwnd{ nullptr };
bool m_customEffectsRegistered{ false };
bool m_isShuttingDown{ false };
std::atomic<bool> m_renderShouldStop{ false };
// Video seek slider / position label (updated per-tick while playing).
winrt::Microsoft::UI::Xaml::Controls::Slider m_videoSeekSlider{ nullptr };
winrt::Microsoft::UI::Xaml::Controls::TextBlock m_videoPositionLabel{ nullptr };
uint32_t m_videoSeekNodeId{ 0 };
bool m_videoSeekSuppressEvents{ false };
// Per-node preview.
uint32_t m_previewNodeId{ 0 }; // Tracks selected node for inline viewport
std::vector<uint32_t> m_topoOrder; // cached for [ ] navigation
// Node clipboard for copy/paste.
struct ClipboardEntry {
::ShaderLab::Graph::EffectNode node;
uint32_t originalId;
};
std::vector<ClipboardEntry> m_nodeClipboard;
std::vector<::ShaderLab::Graph::EffectEdge> m_edgeClipboard;
// Display profile selection.
std::vector<::ShaderLab::Rendering::DisplayProfile> m_displayPresets;
std::optional<::ShaderLab::Rendering::DisplayProfile> m_loadedIccProfile;
int32_t m_committedProfileIndex{ 0 };
bool m_suppressProfileEvent{ false };
// Pixel trace.
bool m_traceActive{ false };
uint32_t m_traceUnit{ 0 }; // 0=scRGB, 1=sRGB, 2=Nits, 3=PQ
uint32_t m_lastTraceTopologyHash{ 0 };
std::vector<winrt::Microsoft::UI::Xaml::Controls::Grid> m_traceRowCache;
// Node graph editor rendering.
winrt::com_ptr<IDXGISwapChain1> m_graphSwapChain;
winrt::com_ptr<ID2D1Bitmap1> m_graphRenderTarget;
winrt::com_ptr<ID2D1SolidColorBrush> m_graphGridBrush;
uint32_t m_graphPanelWidth{ 0 };
uint32_t m_graphPanelHeight{ 0 };
float m_graphPanelDipsWidth{ 0.0f };
float m_graphPanelDipsHeight{ 0.0f };
void InitializeGraphPanel();
void EnsureUiD2dContext();
void ReleaseUiD2dContext();
void ResizeGraphPanel(float widthDips, float heightDips);
void UpdateGraphPanelScale();
void RenderNodeGraph();
D2D1_POINT_2F GraphPanelPointerToCanvas(
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnGraphPanelPointerPressed(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnGraphPanelPointerMoved(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnGraphPanelPointerReleased(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
void OnGraphPanelPointerWheel(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs const& args);
bool m_isGraphPanning{ false };
D2D1_POINT_2F m_graphPanStart{};
D2D1_POINT_2F m_graphPanOrigin{};
void UpdatePropertiesPanel();
// Look up a node either in the latest published GraphUiSnapshot (if
// one has been published) or in the live graph (fallback during
// pre-first-render startup). Used by UI code paths that need to read
// runtime state -- runtimeError, analysisOutput -- which are written
// by the evaluator and would race on direct m_graph reads once the
// render thread spawns. Note: this returns a pointer into the
// snapshot which is owned by a shared_ptr; the caller should keep
// the snapshot alive (e.g., via a local copy) for the duration of
// their reads.
std::shared_ptr<const ::ShaderLab::Graph::GraphUiSnapshot>
CurrentGraphSnapshot() const
{
return std::atomic_load(&m_uiGraphSnapshot);
}
// True when any descendant of PropertiesPanel currently has keyboard
// focus (TextBox cursor, NumberBox edit, dropdown open). Used by the
// 4 Hz binding-value refresh path to avoid clobbering an in-progress
// edit by Clear() + recreate on the panel.
bool IsPropertiesPanelInteracting();
void ShowCurveEditorDialog(uint32_t nodeId, const std::wstring& propertyKey, std::function<void()> markDirty);
void AddMathExpressionInput(uint32_t nodeId);
void RemoveMathExpressionInput(uint32_t nodeId, const std::wstring& paramName);
winrt::fire_and_forget BrowseImageForSourceNode(uint32_t nodeId);
winrt::fire_and_forget BrowseVideoForSourceNode();
winrt::fire_and_forget BrowseVideoForExistingNode(uint32_t nodeId);
void OnSaveImageClicked(
winrt::Windows::Foundation::IInspectable const& sender,
winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);
winrt::fire_and_forget SaveImageAsync();
// Capture the current preview as a PNG byte buffer.
// Returns empty vector on failure.
std::vector<uint8_t> CapturePreviewAsPng();
// Encode a D2D image as a PNG byte buffer (BGRA8 via WIC). Used by
// CapturePreviewAsPng and CaptureNodeAsPng to share the encoder path.
// Caps each axis at maxDim pixels to keep responses bounded.
std::vector<uint8_t> CaptureImageAsPng(ID2D1Image* image, uint32_t maxDim = 2048);
// Capture an arbitrary node's resolved output as a PNG byte buffer.
// Forces a render frame first so dirty downstream nodes evaluate.
// Returns:
// - empty + outNotFound=true when the node ID doesn't exist.
// - empty + outNotReady=true when the node exists but isn't yet ready.
// - empty + neither flag set on encode failure.
std::vector<uint8_t> CaptureNodeAsPng(uint32_t nodeId,
bool& outNotFound,
bool& outNotReady);
// Read a w x h pixel region from a node's resolved output as scRGB
// FP32 RGBA values (one float4 per pixel, row-major from top-left).
// Forces a render frame first. Region is clipped to image bounds.
// Returns true on success and populates `outPixels` with w*h*4 floats.
// outActualW/H reflect the (clipped) region actually read.
bool ReadPixelRegion(uint32_t nodeId,
int32_t x, int32_t y, uint32_t w, uint32_t h,
std::vector<float>& outPixels,
uint32_t& outActualW, uint32_t& outActualH,
bool& outNotFound, bool& outNotReady);
// Capture the live node-graph view (current pan/zoom, sized to the
// graph swap-chain panel) as a PNG byte buffer. Renders into an
// off-screen bitmap so it doesn't disturb the live render tick.
// Returns empty vector on failure.
std::vector<uint8_t> CaptureGraphAsPng();
// Pan/zoom the node-graph view to fit all nodes on screen, with the
// given viewport-space padding (DIPs). No-op when the graph is empty.
void FitGraphView(float padding = 40.0f);
// Shared draw routine for the node-graph scene (clear + dot grid +
// controller render). Used by both the live render tick and the
// off-screen snapshot capture so the two never drift.
void RenderGraphScene(ID2D1DeviceContext5* dc, D2D1_SIZE_F viewSize);
void OpenEffectDesigner();
void EnforceCustomEffectNameUniqueness(uint32_t modifiedNodeId);
uint32_t m_selectedNodeId{ 0 };
bool m_isDraggingNode{ false };
bool m_isDraggingConnection{ false };
bool m_isDraggingSlider{ false };
uint32_t m_sliderDragNodeId{ 0 };
// Effect Designer window.
winrt::ShaderLab::EffectDesignerWindow m_designerWindow{ nullptr };
// MCP HTTP server for AI agent integration.
std::unique_ptr<::ShaderLab::McpHttpServer> m_mcpServer;
// TEMP (Phase 8 perf debugging): default ON so the MCP-driven
// graph-building loop doesn't require a manual toggle every
// restart. Revert to false once the crash repro is sorted.
bool m_autoStartMcp{ true };
::ShaderLab::Rendering::DevicePreference m_devicePref{ ::ShaderLab::Rendering::DevicePreference::Default };
std::wstring m_pendingOpenPath; // file path from Explorer FTA, loaded after init
void SetupMcpRoutes();
template<typename F> auto DispatchSync(F&& fn) -> decltype(fn());
// Phase 7: MainWindow as IEngineCommandSink, hands engine-side
// routes a closure that runs on the UI thread (via DispatchSync).
// Engine-pure routes that don't need UI thread coordination call
// sink.Dispatch with a closure that just reads engine state.
// Mutating routes use it to ensure m_graph mutations don't race
// with the render tick on the UI thread.
struct GuiEngineCommandSink : public ::ShaderLab::Mcp::IEngineCommandSink
{
MainWindow* window{ nullptr };
explicit GuiEngineCommandSink(MainWindow* w) : window(w) {}
::ShaderLab::McpHttpServer::Response Dispatch(
std::function<::ShaderLab::McpHttpServer::Response(
::ShaderLab::Mcp::EngineContext&)> closure) override;
// ---- Event hooks ---------------------------------------------
// Wired to the same UI methods that fire on native user
// interactions (toolbar add-node, drag-edge, etc) so MCP-driven
// mutations and user-driven mutations take the same code path
// through the GUI.
void OnNodeAdded(uint32_t /*nodeId*/) override;
void OnNodeRemoved(uint32_t nodeId) override;
void OnNodeChanged(uint32_t /*nodeId*/) override;
void OnGraphCleared() override;
void OnGraphLoaded() override;
void OnGraphStructureChanged() override;
void OnCustomEffectRecompiled(uint32_t nodeId) override;
void OnDisplayProfileChanged() override;
};
std::unique_ptr<GuiEngineCommandSink> m_engineSink;
// MCP activity indicator state.
// Updated from the MCP listener thread via the activity callback;
// polled from the UI render tick to drive the dot color + tooltip.
std::atomic<int64_t> m_mcpLastActivityMs{ 0 };
std::atomic<uint64_t> m_mcpRequestCount{ 0 };
std::atomic<uint64_t> m_mcpUiUpdateSeq{ 0 }; // bumped every callback; UI compares
uint64_t m_mcpLastUiUpdateSeq{ 0 };
std::mutex m_mcpLastReqMutex;
std::string m_mcpLastReqMethod;
std::string m_mcpLastReqPath;
std::string m_mcpLastReqPeer;
uint16_t m_mcpLastReqStatus{ 0 };
std::set<std::string> m_mcpKnownPeers; // distinct peer addresses seen since server start
void UpdateMcpActivityIndicator();
void ResetMcpActivityState();
void UpdateFpsTooltip();
// Canonical FPS / timing strings shared by the main status bar and
// every output window. Single source of truth.
std::wstring BuildFpsStatusText() const;
std::wstring BuildFpsTooltipText() const;
// Column splitter drag state.
bool m_isDraggingSplitter{ false };
double m_splitterDragStartX{ 0 };
double m_splitterStartCol0Width{ 0 };
double m_splitterStartCol2Width{ 0 };
// Preview pan/zoom.
float m_previewPanX{ 0.0f };
float m_previewPanY{ 0.0f };
float m_previewZoom{ 1.0f };
bool m_needsFitPreview{ false };
bool m_forceRender{ true }; // Force first render + after pan/zoom changes
bool m_isPreviewPanning{ false };
float m_previewPanStartX{ 0.0f };
float m_previewPanStartY{ 0.0f };
float m_previewPanOriginX{ 0.0f };
float m_previewPanOriginY{ 0.0f };
bool m_previewDragMoved{ false };
float m_traceClickDipX{ 0.0f };
float m_traceClickDipY{ 0.0f };
float m_traceClickPanX{ 0.0f };
float m_traceClickPanY{ 0.0f };
float m_traceClickZoom{ 1.0f };
bool m_traceOutOfBounds{ false };
void UpdateCrosshairOverlay();
void FitPreviewToView();
// Trace swatch HDR swap chain.
winrt::com_ptr<IDXGISwapChain1> m_traceSwapChain;
winrt::com_ptr<ID2D1Bitmap1> m_traceSwatchTarget;
uint32_t m_traceSwatchHeight{ 0 };
void InitializeTraceSwatchPanel();
void RenderTraceSwatches();
};
}
namespace winrt::ShaderLab::factory_implementation
{
struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
{
};
}