The codebase is divided between a host-agnostic engine DLL and one or more host applications:
-
ShaderLabEngine.dllowns everything that doesn't need a UI thread or a swap chain: theEffectGraphmodel + JSON serialization, theGraphEvaluator(per-node D2D effect cache, dirty propagation, two-pass evaluate),SourceNodeFactory(image / video / DXGI / WGC sources),EffectRegistry(40+ wrapped D2D effects + 20+ ShaderLab effects with embedded HLSL),DisplayMonitor+ ICC parsing, theEffects/CustomPixelShaderEffect/CustomComputeShaderEffectCOM classes, the generic D3D11 compute dispatch helper (Rendering/D3D11ComputeRunner.{h,cpp}), theShaderCompiler(D3DCompile + D3DReflect), and the MCP HTTP server itself plus all 20 engine-pure routes (Engine/Mcp/McpHttpServer.{h,cpp}+Engine/Mcp/EngineMcpRoutes.{h,cpp}). Engine-pure helpers extracted for reuse:Rendering/PixelReadback.{h,cpp}(FP32 RGBA region readback),Rendering/CaptureNode.{h,cpp}(D2D + WIC PNG encode),Rendering/WorkingSpaceSync.{h,cpp}(Working Space parameter node refresh). -
ShaderLab.exe(the WinUI 3 host) keeps everything that genuinely needs WinUI:MainWindow.xaml.{h,cpp}(which itself is split into sibling partial TUsMainWindow.WorkingSpace.cpp,MainWindow.GraphFileIo.cpp,MainWindow.RenderTick.cpp,MainWindow.McpRoutes.cppfor the 16 UI-coupled routes),Controls/NodeGraphController(canvas rendering),Controls/OutputWindow(per-Output OS window),Controls/ShaderEditorController, the Effect Designer modal window, andRenderEngine(D3D11 + D2D1 device stack,SwapChainPanelbinding). -
ShaderLabHeadless.exe(see below) reuses everything from the engine DLL with no WinUI dependency.
When MCP routes mutate engine state, they fire through an IEngineCommandSink (Engine/Mcp/EngineMcpRoutes.h) so that:
- The host marshals the mutation closure to whatever thread is appropriate. The GUI host routes to the render worker thread via
RenderThreadDispatcher::DispatchSync(P7+) som_graphstays single-writer; the headless host runs the closure inline since there is no separate consumer. - The host runs event hooks afterwards. Hooks may need to touch XAML (which is STA-only), so the GUI sink re-marshals UI work back to the UI thread via
DispatcherQueue().TryEnqueuefrom inside the render-thread closure.
The eight hooks are: OnNodeAdded, OnNodeRemoved, OnNodeChanged, OnGraphCleared, OnGraphLoaded, OnGraphStructureChanged, OnCustomEffectRecompiled, OnDisplayProfileChanged. The GUI's MainWindow::GuiEngineCommandSink overrides each one to call the same UI methods that handle native user interactions (AutoLayout, RebuildLayout, PopulatePreviewNodeSelector, PopulateAddNodeFlyout, UpdateStatusBar, MarkAllDirty, CloseOutputWindow, ResetAfterGraphLoad). The headless host leaves each hook as the default no-op. Result: an MCP client calling /graph/add-node triggers exactly the same downstream UI code path as the user clicking the toolbar.
See Threading Model for the full UI / render-worker split and the offscreen-blit composition path.
The 16 routes that remain in MainWindow.McpRoutes.cpp are intentionally app-side because they are either UI-coupled (/graph/snapshot, /graph/view*, /preview/view*, /render/preview-node, /render/capture, /render/pixel-trace) or host-specific (/, POST / JSON-RPC dispatcher, /context, /perf, /node/<id>/logs, /render/pixel/<x>/<y>).