Clean up
This commit is contained in:
442
src/README.md
Normal file
442
src/README.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# RenderCadenceCompositor
|
||||
|
||||
This app is the modular version of the working DeckLink render-cadence probe.
|
||||
|
||||
Its job is to prove the production-facing foundation before the current compositor's shader/runtime/control features are ported over.
|
||||
|
||||
Before adding features here, read the guardrails in [Render Cadence Golden Rules](../../docs/RENDER_CADENCE_GOLDEN_RULES.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
RenderThread
|
||||
owns a hidden OpenGL context
|
||||
polls the oldest ready input frame without waiting
|
||||
uploads input frames into a render-owned GL texture
|
||||
renders simple BGRA8 motion at selected cadence
|
||||
queues async PBO readback
|
||||
publishes completed frames into SystemFrameExchange
|
||||
|
||||
InputFrameMailbox
|
||||
owns bounded FIFO CPU input slots
|
||||
keeps a bounded three-ready-frame input buffer for render
|
||||
trims frames beyond that bound to avoid runaway input latency
|
||||
protects the one frame currently being uploaded by render
|
||||
uses a single contiguous copy when capture row stride matches mailbox row stride
|
||||
|
||||
SystemFrameExchange
|
||||
owns Free / Rendering / Completed / Scheduled slots
|
||||
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
consumes completed system-memory frames
|
||||
schedules them into DeckLink up to target depth
|
||||
never renders
|
||||
```
|
||||
|
||||
Startup builds a small output preroll reserve before DeckLink scheduled playback starts. When DeckLink input is available, startup also waits briefly for three ready input frames before the render thread starts so the first render ticks are deliberate rather than lucky.
|
||||
|
||||
## Current Scope
|
||||
|
||||
Included now:
|
||||
|
||||
- output-only DeckLink
|
||||
- optional DeckLink input edge with BGRA8 capture or raw UYVY8 capture decoded on the render thread
|
||||
- non-blocking startup when DeckLink output is unavailable
|
||||
- hidden render-thread-owned OpenGL context
|
||||
- simple smooth-motion renderer
|
||||
- BGRA8-only output
|
||||
- non-blocking three-frame FIFO input mailbox for render
|
||||
- fast contiguous mailbox copy path for matching input row strides
|
||||
- bounded three-frame input warmup before render cadence starts
|
||||
- render-thread-owned input texture upload
|
||||
- async PBO readback
|
||||
- bounded FIFO system-memory frame exchange
|
||||
- bounded completed-frame output preroll reserve before DeckLink playback, with DeckLink scheduled depth still targeted at four
|
||||
- conservative DeckLink schedule-lead telemetry and recovery
|
||||
- background Slang compile of `shaders/happy-accident`
|
||||
- app-owned display/render layer model for shader build readiness
|
||||
- app-owned submission of a completed shader artifact
|
||||
- render-thread-owned runtime render scene for ready shader layers
|
||||
- shared-context GL prepare worker for runtime shader program compile/link
|
||||
- render-thread-only GL program swap once a prepared program is ready
|
||||
- manifest-driven stateless single-pass shader packages
|
||||
- manifest-driven stateless named-pass shader packages
|
||||
- atomic render-plan swap after every pass program is prepared
|
||||
- HTTP shader list populated from supported stateless full-frame shader packages
|
||||
- default float, vec2, color, boolean, enum, and trigger parameters
|
||||
- small JSON writer for future HTTP/WebSocket payloads
|
||||
- JSON serialization for cadence telemetry snapshots
|
||||
- background logging with `log`, `warning`, and `error` levels
|
||||
- local HTTP control server matching the OpenAPI route surface
|
||||
- HTTP layer controls for add, remove, reorder, bypass, shader change, parameter update, and parameter reset
|
||||
- trigger parameters as latest-pulse controls with shader-visible count/time
|
||||
- startup config provider for `config/runtime-host.json`
|
||||
- quiet telemetry health monitor
|
||||
- non-GL frame-exchange tests
|
||||
- non-GL input-mailbox tests
|
||||
|
||||
Intentionally not included yet:
|
||||
|
||||
- additional input format conversion/scaling
|
||||
- temporal/history/feedback shader storage
|
||||
- texture/LUT asset upload
|
||||
- text-parameter rasterization
|
||||
- runtime state
|
||||
- OSC control
|
||||
- persistent control/state writes
|
||||
- trigger event history for stacked repeated pulses
|
||||
- preview
|
||||
- screenshots
|
||||
- persistence
|
||||
|
||||
Those features should be ported only after the cadence spine is stable.
|
||||
|
||||
## V1 Feature Parity Checklist
|
||||
|
||||
This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
||||
|
||||
- [x] Stable DeckLink output cadence
|
||||
- [x] BGRA8 system-memory output path
|
||||
- [x] Render thread owns its primary GL context
|
||||
- [x] Output startup warmup before scheduled playback
|
||||
- [x] Non-blocking startup when DeckLink output is unavailable
|
||||
- [x] Runtime shader package discovery
|
||||
- [x] Background Slang shader compile
|
||||
- [x] Shared-context GL shader/program preparation
|
||||
- [x] Render-thread program swap at a frame boundary
|
||||
- [x] Stateless single-pass shader rendering
|
||||
- [x] Stateless named-pass shader rendering
|
||||
- [x] Atomic multipass render-plan commit
|
||||
- [x] Shader add/remove control path
|
||||
- [x] Previous-layer texture handoff for stacked shaders
|
||||
- [x] Supported shader list in HTTP/UI state
|
||||
- [x] Local HTTP server
|
||||
- [x] WebSocket state updates for the UI
|
||||
- [x] OpenAPI document serving
|
||||
- [x] Static control UI serving
|
||||
- [x] Startup config loading from `config/runtime-host.json`
|
||||
- [x] Cadence telemetry JSON
|
||||
- [x] Health logging for schedule/drop/starvation events
|
||||
- [x] Runtime parameter updates from HTTP controls
|
||||
- [x] Layer reorder/bypass/set-shader/update-parameter/reset-parameter HTTP controls
|
||||
- [x] Trigger parameter pulse count/time for latest trigger events
|
||||
- [x] Optional DeckLink input capture
|
||||
- [x] UYVY8 input capture with render-thread GPU decode to shader input texture
|
||||
- [x] Three-frame FIFO CPU input mailbox for render
|
||||
- [x] Fast contiguous input mailbox copy when source/destination stride matches
|
||||
- [x] Bounded three-frame input warmup before render cadence starts
|
||||
- [x] Render-owned input texture upload
|
||||
- [x] Runtime shaders receive input through `gVideoInput`
|
||||
- [x] Live DeckLink input bound to `gVideoInput`
|
||||
- [ ] Input format conversion/scaling
|
||||
- [ ] Temporal history buffers
|
||||
- [ ] Feedback buffers
|
||||
- [ ] Texture asset loading and upload
|
||||
- [ ] LUT asset loading and upload
|
||||
- [ ] Text parameter rasterization
|
||||
- [ ] Trigger history/event buffers for overlapping repeated trigger effects
|
||||
- [ ] Full runtime state store/read model
|
||||
- [ ] Persistent layer stack/config writes
|
||||
- [ ] OSC ingress
|
||||
- [ ] Preview output
|
||||
- [ ] Screenshot capture
|
||||
- [ ] External keying support
|
||||
- [ ] Full V1 health/runtime presentation model
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RenderCadenceCompositor -- /m:1
|
||||
```
|
||||
|
||||
The executable is:
|
||||
|
||||
```text
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Run from VS Code with:
|
||||
|
||||
```text
|
||||
Debug RenderCadenceCompositor
|
||||
```
|
||||
|
||||
Or from a terminal:
|
||||
|
||||
```powershell
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
|
||||
```
|
||||
|
||||
Press Enter to stop.
|
||||
|
||||
To test a different compatible shader package:
|
||||
|
||||
```powershell
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe --shader solid-color
|
||||
```
|
||||
|
||||
Use `--no-shader` to keep the simple motion fallback only.
|
||||
|
||||
## Startup Config
|
||||
|
||||
On startup the app loads `config/runtime-host.json` through `AppConfigProvider`, then applies explicit CLI overrides.
|
||||
|
||||
Currently consumed fields:
|
||||
|
||||
- `serverPort`
|
||||
- `shaderLibrary`
|
||||
- `oscBindAddress`
|
||||
- `oscPort`
|
||||
- `oscSmoothing`
|
||||
- `inputVideoFormat`
|
||||
- `inputFrameRate`
|
||||
- `outputVideoFormat`
|
||||
- `outputFrameRate`
|
||||
- `autoReload`
|
||||
- `maxTemporalHistoryFrames`
|
||||
- `previewFps`
|
||||
- `enableExternalKeying`
|
||||
|
||||
The loaded config is treated as a read-only startup snapshot. Subsystems that need config should receive this snapshot or a narrowed config struct from app orchestration; they should not reload files independently.
|
||||
|
||||
Supported CLI overrides:
|
||||
|
||||
- `--shader <shader-id>`
|
||||
- `--no-shader`
|
||||
- `--port <port>`
|
||||
|
||||
## Expected Telemetry
|
||||
|
||||
Startup, shutdown, shader-build, and render-thread event messages are written through the app logger. Telemetry is intentionally separate and remains a compact once-per-second cadence line.
|
||||
|
||||
The logger writes to the console, `OutputDebugStringA`, and `logs/render-cadence-compositor.log` by default. Render-thread log calls use the non-blocking path so diagnostics do not become cadence blockers.
|
||||
|
||||
## HTTP Control Server
|
||||
|
||||
The app starts a local HTTP control server on `http://127.0.0.1:8080` by default, searching nearby ports if that one is busy.
|
||||
|
||||
Current endpoints:
|
||||
|
||||
- `GET /` and UI asset paths: serve the bundled control UI from `ui/dist`
|
||||
- `GET /api/state`: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer
|
||||
- `GET /ws`: upgrades to a WebSocket and streams state snapshots when they change
|
||||
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
|
||||
- `GET /docs`: serves Swagger UI
|
||||
- `POST /api/layers/add`, `/remove`, `/reorder`, `/set-bypass`, `/set-shader`, `/update-parameter`, and `/reset-parameters` use the shared runtime control-command path
|
||||
- other OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }`
|
||||
|
||||
The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and translates POST bodies into runtime control commands. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly.
|
||||
|
||||
## Optional DeckLink Output
|
||||
|
||||
DeckLink output is an optional edge service in this app.
|
||||
|
||||
Startup order is:
|
||||
|
||||
1. start render thread
|
||||
2. build a bounded completed-frame output preroll reserve at normal render cadence
|
||||
3. try to attach DeckLink output
|
||||
4. start telemetry and HTTP either way
|
||||
|
||||
If DeckLink discovery or output setup fails, the app logs a warning and continues running without starting the output scheduler or scheduled playback. This keeps render cadence, runtime shader testing, HTTP state, and logging available on machines without DeckLink hardware or drivers.
|
||||
|
||||
`/api/state` reports the output status in `videoIO.statusMessage`.
|
||||
|
||||
## Optional DeckLink Input
|
||||
|
||||
DeckLink input is an optional edge service in this app.
|
||||
|
||||
Startup order is:
|
||||
|
||||
1. create `InputFrameMailbox`
|
||||
2. try to attach DeckLink input for the configured input mode
|
||||
3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes
|
||||
4. start `DeckLinkInputThread`
|
||||
5. wait briefly for three ready input warmup frames before starting render cadence
|
||||
6. leave input absent if discovery, setup, format support, or stream startup fails
|
||||
|
||||
`DeckLinkInput` and `DeckLinkInputThread` are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into `InputFrameMailbox`; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The render upload path consumes the oldest ready input frame from a bounded three-ready-frame queue, so the input behaves like a small jitter buffer instead of a latest-frame preview mailbox. The mailbox trims older frames beyond that bound to avoid runaway latency, uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
|
||||
|
||||
Input warmup is startup-only and bounded. It may delay render-thread startup for a short window, but it does not add waits to the steady-state render cadence loop.
|
||||
|
||||
The app samples telemetry once per second.
|
||||
|
||||
Normal cadence samples are available through `GET /api/state` and are not printed to the console. The telemetry monitor only logs health events:
|
||||
|
||||
- warning when DeckLink late/dropped-frame counters increase, including schedule lead and recovery count
|
||||
- warning when schedule failures increase
|
||||
- error when the app/DeckLink output buffer is starved
|
||||
|
||||
Render cadence telemetry:
|
||||
|
||||
- `clockOverruns`: render cadence overruns where missed time was detected
|
||||
- `clockSkippedFrames`: selected-cadence frame intervals skipped instead of catch-up rendering
|
||||
- `clockOveruns` / `clockSkipped`: compatibility aliases for quick polling scripts
|
||||
|
||||
Input telemetry:
|
||||
|
||||
- `inputFramesReceived`: frames accepted into `InputFrameMailbox`
|
||||
- `inputFramesDropped`: ready input frames dropped or missed because the mailbox was full
|
||||
- `renderFrameMs`: most recent render-thread draw duration, excluding completed-readback copy and readback queue work
|
||||
- `renderFrameBudgetUsedPercent`: most recent render draw time as a percentage of the selected frame budget
|
||||
- `renderFrameMaxMs`: maximum observed render-thread draw duration for this process
|
||||
- `readbackQueueMs`: time spent queueing the most recent async BGRA8 PBO readback
|
||||
- `completedReadbackCopyMs`: time spent mapping/copying the most recent completed readback into system-memory frame storage
|
||||
- `completedDrops`: oldest completed unscheduled system-memory frames dropped because the bounded completed reserve overflowed; this is an app-side reserve drop, not a DeckLink dropped-frame report
|
||||
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
|
||||
- `deckLinkScheduleLeadAvailable`: whether DeckLink schedule-lead telemetry was available for the latest sample
|
||||
- `deckLinkScheduleLeadFrames`: estimated distance between the DeckLink playback cursor and the next scheduled stream time
|
||||
- `deckLinkPlaybackFrameIndex`: latest sampled DeckLink playback frame index
|
||||
- `deckLinkNextScheduleFrameIndex`: next stream frame index the app intends to schedule
|
||||
- `deckLinkPlaybackStreamTime`: latest sampled DeckLink playback stream time in DeckLink time units
|
||||
- `deckLinkScheduleRealignments`: conservative schedule-cursor recoveries triggered by late/drop pressure or dangerously low lead
|
||||
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
||||
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
||||
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
||||
- `inputReadingFrames`: input frames currently protected while render uploads them
|
||||
- `inputLatestAgeMs`: age of the newest submitted input frame
|
||||
- `inputUploadMs`: render-thread GL upload/decode submission time for the latest uploaded input frame
|
||||
- `inputFormatSupported`: whether the latest frame reaching the render upload path was BGRA8 or UYVY8 compatible
|
||||
- `inputSignalPresent`: whether any input frame has reached the mailbox
|
||||
- `inputCaptureFps`: DeckLink input callback capture rate
|
||||
- `inputConvertMs`: input-edge CPU conversion time; expected to remain `0` for BGRA8 and raw UYVY8 capture because UYVY8 decode is render-thread GPU work
|
||||
- `inputSubmitMs`: time spent copying/submitting the latest captured input frame to `InputFrameMailbox`
|
||||
- `inputCaptureFormat`: selected DeckLink input format (`BGRA8`, `UYVY8`, or `none`)
|
||||
- `inputNoSignalFrames`: DeckLink callbacks reporting no input source
|
||||
- `inputUnsupportedFrames`: input frames rejected before mailbox submission
|
||||
- `inputSubmitMisses`: input frames that could not be submitted to the mailbox
|
||||
|
||||
Runtime shaders continue rendering when input is missing. If no mailbox frame has been uploaded yet, shader samplers use the runtime fallback source texture; once DeckLink input is flowing, shaders such as CRT and trigger-ripple sample the current input through `gVideoInput`.
|
||||
|
||||
Healthy first-run signs:
|
||||
|
||||
- visible DeckLink output is smooth
|
||||
- `renderFps` is close to the selected cadence
|
||||
- `scheduleFps` is close to the selected cadence after warmup
|
||||
- `scheduled` stays near 4
|
||||
- `decklinkBuffered` stays near 4 when available
|
||||
- `deckLinkScheduleLeadFrames` remains positive and stable when available
|
||||
- `deckLinkScheduleRealignments` does not increase continuously
|
||||
- `late` and `dropped` do not increase continuously
|
||||
- `scheduleFailures` does not increase
|
||||
- `shaderCommitted` becomes `1` after the background Happy Accident compile completes
|
||||
- `shaderFailures` remains `0`
|
||||
|
||||
`completedPollMisses` means the DeckLink scheduling thread woke up before a completed frame was available. It is not a DeckLink playout underrun by itself. Treat it as healthy polling noise when `scheduled`, `decklinkBuffered`, `late`, `dropped`, and `scheduleFailures` remain stable.
|
||||
|
||||
## Runtime Slang Shader Test
|
||||
|
||||
On startup the app begins compiling the selected shader package on a background thread owned by the app orchestration layer. The default is `shaders/happy-accident`.
|
||||
|
||||
The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. Once a completed shader artifact is published, the render-thread-owned runtime scene queues changed layers to a shared-context GL prepare worker. That worker compiles/links runtime shader programs off the cadence thread. The render thread only swaps in an already-prepared GL program at a frame boundary. If either the Slang build or GL preparation fails, the app keeps rendering the current renderer or simple motion fallback.
|
||||
|
||||
Current runtime shader support is deliberately limited to stateless full-frame packages:
|
||||
|
||||
- one or more named passes
|
||||
- one sampled source input per pass
|
||||
- named intermediate outputs routed by the pass manifest
|
||||
- final visible output must be named `layerOutput`
|
||||
- no temporal history
|
||||
- no feedback storage
|
||||
- no texture/LUT assets yet
|
||||
- no text parameters yet
|
||||
- manifest defaults initialize parameters
|
||||
- HTTP controls can update runtime parameter values without rebuilding GL programs when the shader program is unchanged
|
||||
- trigger parameters are treated as latest-pulse controls: each press increments the trigger count and records the current runtime time
|
||||
- repeated trigger history is not stored yet, so effects such as `trigger-ripple` restart from the latest trigger rather than accumulating overlapping ripples
|
||||
- the first layer receives a small fallback source texture until DeckLink input is available
|
||||
- the first layer receives the latest DeckLink input texture through both `gVideoInput` and `gLayerInput` when input frames are available
|
||||
- stacked layers receive the original input through `gVideoInput` and the previous ready layer output through `gLayerInput`
|
||||
|
||||
Shader source semantics:
|
||||
|
||||
- `gVideoInput` means the latest decoded shader-visible video input for every layer.
|
||||
- `gLayerInput` means the previous layer output.
|
||||
- the first layer may receive `gLayerInput = gVideoInput`.
|
||||
- later layers receive `gVideoInput = original input` and `gLayerInput = previous layer`.
|
||||
- named intermediate pass inputs inside a multipass layer are still routed through the selected pass-source slot; layer stacking should use `gLayerInput`.
|
||||
|
||||
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
|
||||
|
||||
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter definitions, current parameter values, build status, and render-ready artifacts. POST controls mutate this app-owned model and may start background shader builds when the selected shader changes.
|
||||
|
||||
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed pass programs to the shared-context prepare worker, swaps in a full prepared render plan only after every pass is ready, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; multipass shaders route named intermediate outputs through their manifest-declared pass inputs, and the final ready layer renders to the output target.
|
||||
|
||||
Successful handoff signs:
|
||||
|
||||
- telemetry shows `shaderCommitted=1`
|
||||
- output changes from the simple motion pattern to the Happy Accident shader
|
||||
- render/schedule cadence remains near 60 fps during and after the handoff
|
||||
- DeckLink buffer remains stable
|
||||
|
||||
## Baseline Result
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
User-visible result:
|
||||
|
||||
- output was smooth
|
||||
- DeckLink held a 4-frame buffer
|
||||
|
||||
Representative telemetry:
|
||||
|
||||
```text
|
||||
renderFps=59.9 scheduleFps=59.9 free=8 completed=0 scheduled=4 completedPollMisses=30 scheduleFailures=0 completions=720 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=1.2
|
||||
renderFps=59.8 scheduleFps=59.8 free=7 completed=1 scheduled=4 completedPollMisses=36 scheduleFailures=0 completions=1080 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=4.7
|
||||
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 completedPollMisses=86 scheduleFailures=0 completions=1381 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=2.1
|
||||
```
|
||||
|
||||
Read:
|
||||
|
||||
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
||||
- app scheduled depth stayed at 4
|
||||
- actual DeckLink buffered depth stayed at 4
|
||||
- DeckLink schedule lead remained positive during healthy playback
|
||||
- no late frames, dropped frames, or schedule failures were observed
|
||||
- completed poll misses were benign because playout remained fully fed
|
||||
|
||||
## Tests
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RenderCadenceCompositorFrameExchangeTests -- /m:1
|
||||
ctest --test-dir build\vs2022-x64-debug -C Debug -R RenderCadenceCompositorFrameExchangeTests --output-on-failure
|
||||
```
|
||||
|
||||
## Relationship To The Probe
|
||||
|
||||
`apps/DeckLinkRenderCadenceProbe` proved the timing model in one compact file.
|
||||
|
||||
This app keeps the same core behavior but splits it into modules that can grow:
|
||||
|
||||
- `frames/`: system-memory handoff
|
||||
- `platform/`: COM/Win32/hidden GL context support
|
||||
- `render/`: cadence thread, clock, and simple renderer
|
||||
- `frames/InputFrameMailbox`: non-blocking bounded FIFO CPU input handoff with contiguous-copy fast path for matching row strides
|
||||
- `render/InputFrameTexture`: render-thread-owned upload of the currently acquired CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
|
||||
- `render/readback/`: PBO-backed BGRA8 readback and completed-frame publication
|
||||
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
||||
- `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
||||
- `runtime/`: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoff
|
||||
- `control/`: control action results and runtime-state JSON presentation
|
||||
- `control/http/`: local HTTP API, static UI serving, OpenAPI serving, and WebSocket updates
|
||||
- `json/`: compact JSON serialization helpers
|
||||
- `video/`: DeckLink output wrapper and scheduling thread
|
||||
- `telemetry/`: cadence telemetry
|
||||
- `telemetry/TelemetryHealthMonitor`: quiet health event logging from telemetry samples
|
||||
- `app/`: startup/shutdown orchestration
|
||||
- `app/AppConfigProvider`: startup config loading and CLI overrides
|
||||
|
||||
## Next Porting Steps
|
||||
|
||||
Only after this app matches the probe's smooth output:
|
||||
|
||||
1. replace `SimpleMotionRenderer` with a render-scene interface
|
||||
2. port shader package rendering
|
||||
3. port runtime snapshots/live state
|
||||
4. add control services
|
||||
5. add preview/screenshot from system-memory frames
|
||||
6. add scaling and additional input format support after the BGRA8/raw-UYVY8 input edge is stable
|
||||
228
src/RenderCadenceCompositor.cpp
Normal file
228
src/RenderCadenceCompositor.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
#include "app/AppConfig.h"
|
||||
#include "app/AppConfigProvider.h"
|
||||
#include "app/RenderCadenceApp.h"
|
||||
#include "frames/InputFrameMailbox.h"
|
||||
#include "frames/SystemFrameExchange.h"
|
||||
#include "logging/Logger.h"
|
||||
#include "render/RenderThread.h"
|
||||
#include "video/DeckLinkInput.h"
|
||||
#include "video/DeckLinkInputThread.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
|
||||
constexpr std::size_t kReadbackDepth = 6;
|
||||
constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2;
|
||||
|
||||
class ComInitGuard
|
||||
{
|
||||
public:
|
||||
~ComInitGuard()
|
||||
{
|
||||
if (mInitialized)
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
bool Initialize()
|
||||
{
|
||||
const HRESULT result = CoInitialize(nullptr);
|
||||
mInitialized = SUCCEEDED(result);
|
||||
mResult = result;
|
||||
return mInitialized;
|
||||
}
|
||||
|
||||
HRESULT Result() const { return mResult; }
|
||||
|
||||
private:
|
||||
bool mInitialized = false;
|
||||
HRESULT mResult = S_OK;
|
||||
};
|
||||
|
||||
bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout)
|
||||
{
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (std::chrono::steady_clock::now() - start < timeout)
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
if (metrics.readyCount >= targetReadyFrames)
|
||||
return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
RenderCadenceCompositor::AppConfigProvider configProvider;
|
||||
std::string configError;
|
||||
if (!configProvider.LoadDefault(configError))
|
||||
{
|
||||
RenderCadenceCompositor::Logger::Instance().Start(RenderCadenceCompositor::DefaultAppConfig().logging);
|
||||
RenderCadenceCompositor::LogError("app", "Config load failed: " + configError);
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 1;
|
||||
}
|
||||
configProvider.ApplyCommandLine(argc, argv);
|
||||
|
||||
RenderCadenceCompositor::AppConfig appConfig = configProvider.Config();
|
||||
RenderCadenceCompositor::Logger::Instance().Start(appConfig.logging);
|
||||
RenderCadenceCompositor::Log(
|
||||
"app",
|
||||
"RenderCadenceCompositor starting. Starts render cadence, system-memory exchange, DeckLink scheduled output, and telemetry. Press Enter to stop.");
|
||||
RenderCadenceCompositor::Log("app", "Loaded config from " + configProvider.SourcePath().string());
|
||||
|
||||
ComInitGuard com;
|
||||
if (!com.Initialize())
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "COM initialization failed: 0x" << std::hex << com.Result();
|
||||
RenderCadenceCompositor::LogError("app", message.str());
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 1;
|
||||
}
|
||||
|
||||
SystemFrameExchangeConfig frameExchangeConfig;
|
||||
RenderCadenceCompositor::VideoFormatDimensions(
|
||||
appConfig.outputVideoFormat,
|
||||
frameExchangeConfig.width,
|
||||
frameExchangeConfig.height);
|
||||
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
|
||||
frameExchangeConfig.capacity =
|
||||
appConfig.warmupCompletedFrames +
|
||||
kDeckLinkTargetBufferedFrames +
|
||||
kWritableOutputReserveFrames;
|
||||
frameExchangeConfig.maxCompletedFrames = appConfig.warmupCompletedFrames;
|
||||
|
||||
SystemFrameExchange frameExchange(frameExchangeConfig);
|
||||
|
||||
InputFrameMailboxConfig inputMailboxConfig;
|
||||
RenderCadenceCompositor::VideoFormatDimensions(
|
||||
appConfig.inputVideoFormat,
|
||||
inputMailboxConfig.width,
|
||||
inputMailboxConfig.height);
|
||||
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||
inputMailboxConfig.capacity = 4;
|
||||
inputMailboxConfig.maxReadyFrames = 3;
|
||||
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||
|
||||
VideoFormat inputVideoMode;
|
||||
VideoFormat outputVideoMode;
|
||||
std::string inputVideoModeError;
|
||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||
if (!inputVideoModeResolved)
|
||||
{
|
||||
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||
}
|
||||
if (!outputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
appConfig.deckLink.outputVideoMode = outputVideoMode;
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
||||
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||
bool deckLinkInputStarted = false;
|
||||
if (inputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::DeckLinkInputConfig deckLinkInputConfig;
|
||||
deckLinkInputConfig.videoFormat = inputVideoMode;
|
||||
std::string deckLinkInputError;
|
||||
if (deckLinkInput.Initialize(deckLinkInputConfig, deckLinkInputError))
|
||||
{
|
||||
inputMailboxConfig.pixelFormat = deckLinkInput.CapturePixelFormat();
|
||||
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||
inputMailbox.Configure(inputMailboxConfig);
|
||||
}
|
||||
|
||||
if (deckLinkInput.IsInitialized() && deckLinkInputThread.Start(deckLinkInputError))
|
||||
{
|
||||
deckLinkInputStarted = true;
|
||||
RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + ".");
|
||||
RenderCadenceCompositor::Log("app", "Waiting for DeckLink input warmup frames.");
|
||||
constexpr std::size_t kInputStartupBufferedFrames = 3;
|
||||
constexpr std::chrono::milliseconds kInputWarmupTimeout(1000);
|
||||
if (WaitForInputWarmup(inputMailbox, kInputStartupBufferedFrames, kInputWarmupTimeout))
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||
RenderCadenceCompositor::Log(
|
||||
"app",
|
||||
"DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) +
|
||||
" submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||
}
|
||||
else
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" +
|
||||
std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning("app", "DeckLink input edge unavailable; runtime shaders will use fallback input until a real input edge is available. " + deckLinkInputError);
|
||||
deckLinkInput.ReleaseResources();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning("app", "DeckLink input mode was not resolved; runtime shaders will use fallback input until a real input edge is available.");
|
||||
}
|
||||
|
||||
RenderThread::Config renderConfig;
|
||||
renderConfig.width = frameExchangeConfig.width;
|
||||
renderConfig.height = frameExchangeConfig.height;
|
||||
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||
: fallbackFrameDurationMilliseconds;
|
||||
renderConfig.pboDepth = kReadbackDepth;
|
||||
|
||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||
|
||||
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
||||
app.SetDeckLinkInputMetricsProvider([&deckLinkInput]() {
|
||||
return deckLinkInput.Metrics();
|
||||
});
|
||||
|
||||
std::string error;
|
||||
if (!app.Start(error))
|
||||
{
|
||||
RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error);
|
||||
if (deckLinkInputStarted)
|
||||
deckLinkInputThread.Stop();
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::getline(std::cin, line);
|
||||
app.Stop();
|
||||
if (deckLinkInputStarted)
|
||||
deckLinkInputThread.Stop();
|
||||
RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped.");
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 0;
|
||||
}
|
||||
39
src/app/AppConfig.cpp
Normal file
39
src/app/AppConfig.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "AppConfig.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
AppConfig DefaultAppConfig()
|
||||
{
|
||||
AppConfig config;
|
||||
config.deckLink.externalKeyingEnabled = false;
|
||||
config.deckLink.outputAlphaRequired = false;
|
||||
config.outputThread.targetBufferedFrames = 4;
|
||||
config.telemetry.interval = std::chrono::seconds(1);
|
||||
config.logging.minimumLevel = LogLevel::Log;
|
||||
config.logging.writeToConsole = true;
|
||||
config.logging.writeToDebugOutput = true;
|
||||
config.logging.writeToFile = true;
|
||||
config.logging.filePath = "logs/render-cadence-compositor.log";
|
||||
config.logging.maxQueuedMessages = 1024;
|
||||
config.http.preferredPort = 8080;
|
||||
config.http.portSearchCount = 20;
|
||||
config.http.idleSleep = std::chrono::milliseconds(10);
|
||||
config.shaderLibrary = "shaders";
|
||||
config.oscBindAddress = "0.0.0.0";
|
||||
config.oscPort = 9000;
|
||||
config.oscSmoothing = 0.18;
|
||||
config.inputVideoFormat = "1080p";
|
||||
config.inputFrameRate = "59.94";
|
||||
config.outputVideoFormat = "1080p";
|
||||
config.outputFrameRate = "59.94";
|
||||
config.autoReload = true;
|
||||
config.maxTemporalHistoryFrames = 12;
|
||||
config.previewFps = 30.0;
|
||||
config.warmupCompletedFrames = 4;
|
||||
config.warmupTimeout = std::chrono::seconds(3);
|
||||
config.prerollTimeout = std::chrono::seconds(3);
|
||||
config.prerollPoll = std::chrono::milliseconds(2);
|
||||
config.runtimeShaderId = "happy-accident";
|
||||
return config;
|
||||
}
|
||||
}
|
||||
41
src/app/AppConfig.h
Normal file
41
src/app/AppConfig.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "../control/http/HttpControlServer.h"
|
||||
#include "../logging/Logger.h"
|
||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct AppConfig
|
||||
{
|
||||
DeckLinkOutputConfig deckLink;
|
||||
DeckLinkOutputThreadConfig outputThread;
|
||||
TelemetryHealthMonitorConfig telemetry;
|
||||
LoggerConfig logging;
|
||||
HttpControlServerConfig http;
|
||||
std::string shaderLibrary = "shaders";
|
||||
std::string oscBindAddress = "0.0.0.0";
|
||||
unsigned short oscPort = 9000;
|
||||
double oscSmoothing = 0.18;
|
||||
std::string inputVideoFormat = "1080p";
|
||||
std::string inputFrameRate = "59.94";
|
||||
std::string outputVideoFormat = "1080p";
|
||||
std::string outputFrameRate = "59.94";
|
||||
bool autoReload = true;
|
||||
std::size_t maxTemporalHistoryFrames = 12;
|
||||
double previewFps = 30.0;
|
||||
std::size_t warmupCompletedFrames = 4;
|
||||
std::chrono::milliseconds warmupTimeout = std::chrono::seconds(3);
|
||||
std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3);
|
||||
std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2);
|
||||
std::string runtimeShaderId = "happy-accident";
|
||||
};
|
||||
|
||||
AppConfig DefaultAppConfig();
|
||||
}
|
||||
282
src/app/AppConfigProvider.cpp
Normal file
282
src/app/AppConfigProvider.cpp
Normal file
@@ -0,0 +1,282 @@
|
||||
#include "AppConfigProvider.h"
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::filesystem::path ExecutableDirectory()
|
||||
{
|
||||
char path[MAX_PATH] = {};
|
||||
const DWORD length = GetModuleFileNameA(nullptr, path, static_cast<DWORD>(sizeof(path)));
|
||||
if (length == 0 || length >= sizeof(path))
|
||||
return std::filesystem::current_path();
|
||||
return std::filesystem::path(path).parent_path();
|
||||
}
|
||||
|
||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
{
|
||||
error = "Could not open config file: " + path.string();
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
const JsonValue* Find(const JsonValue& root, const char* key)
|
||||
{
|
||||
return root.find(key);
|
||||
}
|
||||
|
||||
void ApplyString(const JsonValue& root, const char* key, std::string& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (value && value->isString())
|
||||
target = value->asString();
|
||||
}
|
||||
|
||||
void ApplyBool(const JsonValue& root, const char* key, bool& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (value && value->isBoolean())
|
||||
target = value->asBoolean();
|
||||
}
|
||||
|
||||
void ApplyDouble(const JsonValue& root, const char* key, double& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (value && value->isNumber())
|
||||
target = value->asNumber();
|
||||
}
|
||||
|
||||
void ApplySize(const JsonValue& root, const char* key, std::size_t& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (value && value->isNumber() && value->asNumber() >= 0.0)
|
||||
target = static_cast<std::size_t>(value->asNumber());
|
||||
}
|
||||
|
||||
void ApplyPort(const JsonValue& root, const char* key, unsigned short& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (!value || !value->isNumber())
|
||||
return;
|
||||
|
||||
const double port = value->asNumber();
|
||||
if (port >= 1.0 && port <= 65535.0)
|
||||
target = static_cast<unsigned short>(port);
|
||||
}
|
||||
}
|
||||
|
||||
AppConfigProvider::AppConfigProvider() :
|
||||
mConfig(DefaultAppConfig())
|
||||
{
|
||||
}
|
||||
|
||||
bool AppConfigProvider::LoadDefault(std::string& error)
|
||||
{
|
||||
const std::filesystem::path path = FindConfigFile();
|
||||
if (path.empty())
|
||||
{
|
||||
error = "Could not locate config/runtime-host.json from current directory or executable directory.";
|
||||
return false;
|
||||
}
|
||||
return Load(path, error);
|
||||
}
|
||||
|
||||
bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& error)
|
||||
{
|
||||
mConfig = DefaultAppConfig();
|
||||
mSourcePath = path;
|
||||
mLoadedFromFile = false;
|
||||
|
||||
std::string fileError;
|
||||
const std::string text = ReadTextFile(path, fileError);
|
||||
if (!fileError.empty())
|
||||
{
|
||||
error = fileError;
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(text, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Config root must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
ApplyString(root, "shaderLibrary", mConfig.shaderLibrary);
|
||||
ApplyPort(root, "serverPort", mConfig.http.preferredPort);
|
||||
ApplyString(root, "oscBindAddress", mConfig.oscBindAddress);
|
||||
ApplyPort(root, "oscPort", mConfig.oscPort);
|
||||
ApplyDouble(root, "oscSmoothing", mConfig.oscSmoothing);
|
||||
ApplyString(root, "inputVideoFormat", mConfig.inputVideoFormat);
|
||||
ApplyString(root, "inputFrameRate", mConfig.inputFrameRate);
|
||||
ApplyString(root, "outputVideoFormat", mConfig.outputVideoFormat);
|
||||
ApplyString(root, "outputFrameRate", mConfig.outputFrameRate);
|
||||
ApplyBool(root, "autoReload", mConfig.autoReload);
|
||||
ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames);
|
||||
ApplyDouble(root, "previewFps", mConfig.previewFps);
|
||||
ApplyBool(root, "enableExternalKeying", mConfig.deckLink.externalKeyingEnabled);
|
||||
|
||||
mLoadedFromFile = true;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppConfigProvider::ApplyCommandLine(int argc, char** argv)
|
||||
{
|
||||
for (int index = 1; index < argc; ++index)
|
||||
{
|
||||
const std::string argument = argv[index];
|
||||
if (argument == "--shader" && index + 1 < argc)
|
||||
{
|
||||
mConfig.runtimeShaderId = argv[++index];
|
||||
continue;
|
||||
}
|
||||
if (argument == "--no-shader")
|
||||
{
|
||||
mConfig.runtimeShaderId.clear();
|
||||
continue;
|
||||
}
|
||||
if (argument == "--port" && index + 1 < argc)
|
||||
{
|
||||
const int port = std::atoi(argv[++index]);
|
||||
if (port >= 1 && port <= 65535)
|
||||
mConfig.http.preferredPort = static_cast<unsigned short>(port);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate)
|
||||
{
|
||||
double rate = fallbackRate;
|
||||
try
|
||||
{
|
||||
rate = std::stod(rateText);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
rate = fallbackRate;
|
||||
}
|
||||
|
||||
if (rate <= 0.0)
|
||||
rate = fallbackRate;
|
||||
return 1000.0 / rate;
|
||||
}
|
||||
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
|
||||
{
|
||||
struct ModeRate
|
||||
{
|
||||
BMDDisplayMode mode;
|
||||
int64_t frameDuration;
|
||||
int64_t timeScale;
|
||||
};
|
||||
|
||||
static const ModeRate rates[] =
|
||||
{
|
||||
{ bmdModeHD720p50, 1, 50 },
|
||||
{ bmdModeHD720p5994, 1001, 60000 },
|
||||
{ bmdModeHD720p60, 1, 60 },
|
||||
{ bmdModeHD1080i50, 1, 25 },
|
||||
{ bmdModeHD1080i5994, 1001, 30000 },
|
||||
{ bmdModeHD1080i6000, 1, 30 },
|
||||
{ bmdModeHD1080p2398, 1001, 24000 },
|
||||
{ bmdModeHD1080p24, 1, 24 },
|
||||
{ bmdModeHD1080p25, 1, 25 },
|
||||
{ bmdModeHD1080p2997, 1001, 30000 },
|
||||
{ bmdModeHD1080p30, 1, 30 },
|
||||
{ bmdModeHD1080p50, 1, 50 },
|
||||
{ bmdModeHD1080p5994, 1001, 60000 },
|
||||
{ bmdModeHD1080p6000, 1, 60 },
|
||||
{ bmdMode4K2160p2398, 1001, 24000 },
|
||||
{ bmdMode4K2160p24, 1, 24 },
|
||||
{ bmdMode4K2160p25, 1, 25 },
|
||||
{ bmdMode4K2160p2997, 1001, 30000 },
|
||||
{ bmdMode4K2160p30, 1, 30 },
|
||||
{ bmdMode4K2160p50, 1, 50 },
|
||||
{ bmdMode4K2160p5994, 1001, 60000 },
|
||||
{ bmdMode4K2160p60, 1, 60 }
|
||||
};
|
||||
|
||||
for (const ModeRate& rate : rates)
|
||||
{
|
||||
if (rate.mode == displayMode && rate.timeScale > 0)
|
||||
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
|
||||
}
|
||||
|
||||
return fallbackMilliseconds;
|
||||
}
|
||||
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
||||
{
|
||||
std::string normalized = formatName;
|
||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char character) {
|
||||
return static_cast<char>(std::tolower(character));
|
||||
});
|
||||
|
||||
if (normalized == "720p")
|
||||
{
|
||||
width = 1280;
|
||||
height = 720;
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalized == "2160p" || normalized == "4k" || normalized == "uhd")
|
||||
{
|
||||
width = 3840;
|
||||
height = 2160;
|
||||
return;
|
||||
}
|
||||
|
||||
width = 1920;
|
||||
height = 1080;
|
||||
}
|
||||
|
||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath)
|
||||
{
|
||||
return FindRepoPath(relativePath);
|
||||
}
|
||||
|
||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath)
|
||||
{
|
||||
std::vector<std::filesystem::path> starts;
|
||||
starts.push_back(std::filesystem::current_path());
|
||||
starts.push_back(ExecutableDirectory());
|
||||
|
||||
for (std::filesystem::path start : starts)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
const std::filesystem::path candidate = start / relativePath;
|
||||
if (std::filesystem::exists(candidate))
|
||||
return candidate;
|
||||
|
||||
const std::filesystem::path parent = start.parent_path();
|
||||
if (parent.empty() || parent == start)
|
||||
break;
|
||||
start = parent;
|
||||
}
|
||||
}
|
||||
|
||||
return std::filesystem::path();
|
||||
}
|
||||
}
|
||||
35
src/app/AppConfigProvider.h
Normal file
35
src/app/AppConfigProvider.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
class AppConfigProvider
|
||||
{
|
||||
public:
|
||||
AppConfigProvider();
|
||||
|
||||
bool Load(const std::filesystem::path& path, std::string& error);
|
||||
bool LoadDefault(std::string& error);
|
||||
void ApplyCommandLine(int argc, char** argv);
|
||||
|
||||
const AppConfig& Config() const { return mConfig; }
|
||||
const std::filesystem::path& SourcePath() const { return mSourcePath; }
|
||||
bool LoadedFromFile() const { return mLoadedFromFile; }
|
||||
|
||||
private:
|
||||
AppConfig mConfig;
|
||||
std::filesystem::path mSourcePath;
|
||||
bool mLoadedFromFile = false;
|
||||
};
|
||||
|
||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||
}
|
||||
291
src/app/RenderCadenceApp.h
Normal file
291
src/app/RenderCadenceApp.h
Normal file
@@ -0,0 +1,291 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "AppConfigProvider.h"
|
||||
#include "RuntimeLayerController.h"
|
||||
#include "../logging/Logger.h"
|
||||
#include "../control/RuntimeStateJson.h"
|
||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||
#include "../video/DeckLinkInput.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
template <typename RenderThread>
|
||||
auto StartRenderThread(RenderThread& renderThread, std::string& error, int) -> decltype(renderThread.Start(error), bool())
|
||||
{
|
||||
return renderThread.Start(error);
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::true_type)
|
||||
{
|
||||
return renderThread.Start();
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::false_type)
|
||||
{
|
||||
renderThread.Start();
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
auto StartRenderThread(RenderThread& renderThread, std::string&, long) -> decltype(renderThread.Start(), bool())
|
||||
{
|
||||
return StartRenderThreadWithoutError(renderThread, std::is_same<decltype(renderThread.Start()), bool>());
|
||||
}
|
||||
}
|
||||
|
||||
template <typename RenderThread, typename SystemFrameExchange>
|
||||
class RenderCadenceApp
|
||||
{
|
||||
public:
|
||||
RenderCadenceApp(RenderThread& renderThread, SystemFrameExchange& frameExchange, AppConfig config = DefaultAppConfig()) :
|
||||
mRenderThread(renderThread),
|
||||
mFrameExchange(frameExchange),
|
||||
mConfig(config),
|
||||
mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
|
||||
mTelemetryHealth(mConfig.telemetry),
|
||||
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
|
||||
mRenderThread.SubmitRuntimeRenderLayers(layers);
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
RenderCadenceApp(const RenderCadenceApp&) = delete;
|
||||
RenderCadenceApp& operator=(const RenderCadenceApp&) = delete;
|
||||
|
||||
~RenderCadenceApp()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool Start(std::string& error)
|
||||
{
|
||||
mRuntimeLayers.Initialize(
|
||||
mConfig.shaderLibrary,
|
||||
static_cast<unsigned>(mConfig.maxTemporalHistoryFrames),
|
||||
mConfig.runtimeShaderId);
|
||||
|
||||
Log("app", "Starting render thread.");
|
||||
if (!detail::StartRenderThread(mRenderThread, error, 0))
|
||||
{
|
||||
LogError("app", "Render thread start failed: " + error);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId);
|
||||
|
||||
if (!BuildSettledOutputReserve(error))
|
||||
{
|
||||
LogError("app", error);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
StartOptionalVideoOutput();
|
||||
mTelemetryHealth.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||
StartHttpServer();
|
||||
Log("app", "RenderCadenceCompositor started.");
|
||||
mStarted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mHttpServer.Stop();
|
||||
mTelemetryHealth.Stop();
|
||||
mOutputThread.Stop();
|
||||
mOutput.Stop();
|
||||
mRuntimeLayers.Stop();
|
||||
mRenderThread.Stop();
|
||||
mOutput.ReleaseResources();
|
||||
if (mStarted)
|
||||
Log("app", "RenderCadenceCompositor shutdown complete.");
|
||||
mStarted = false;
|
||||
}
|
||||
|
||||
bool Started() const { return mStarted; }
|
||||
const DeckLinkOutput& Output() const { return mOutput; }
|
||||
void SetDeckLinkInputMetricsProvider(std::function<DeckLinkInputMetrics()> provider)
|
||||
{
|
||||
mDeckLinkInputMetricsProvider = std::move(provider);
|
||||
}
|
||||
|
||||
private:
|
||||
void StartOptionalVideoOutput()
|
||||
{
|
||||
std::string outputError;
|
||||
Log("app", "Initializing optional DeckLink output.");
|
||||
if (!mOutput.Initialize(
|
||||
mConfig.deckLink,
|
||||
[this](const VideoIOCompletion& completion) {
|
||||
mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer);
|
||||
},
|
||||
outputError))
|
||||
{
|
||||
DisableVideoOutput("DeckLink output unavailable: " + outputError);
|
||||
return;
|
||||
}
|
||||
|
||||
Log("app", "Starting DeckLink output thread.");
|
||||
if (!mOutputThread.Start())
|
||||
{
|
||||
DisableVideoOutput("DeckLink output thread failed to start.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("app", "Waiting for DeckLink preroll frames.");
|
||||
if (!WaitForPreroll())
|
||||
{
|
||||
DisableVideoOutput("Timed out waiting for DeckLink preroll frames.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("app", "Starting DeckLink scheduled playback.");
|
||||
if (!mOutput.StartScheduledPlayback(outputError))
|
||||
{
|
||||
DisableVideoOutput("DeckLink scheduled playback failed: " + outputError);
|
||||
return;
|
||||
}
|
||||
|
||||
mVideoOutputEnabled = true;
|
||||
mVideoOutputStatus = "DeckLink scheduled output running.";
|
||||
Log("app", mVideoOutputStatus);
|
||||
Log(
|
||||
"app",
|
||||
"DeckLink output mode: " + mOutput.State().outputDisplayModeName +
|
||||
", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms.");
|
||||
}
|
||||
|
||||
bool BuildSettledOutputReserve(std::string& error)
|
||||
{
|
||||
const auto reserveTimeout = mConfig.warmupTimeout;
|
||||
Log("app",
|
||||
"Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
|
||||
" completed frame(s).");
|
||||
if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "Timed out waiting for output preroll reserve.";
|
||||
return false;
|
||||
}
|
||||
|
||||
void DisableVideoOutput(const std::string& reason)
|
||||
{
|
||||
mOutputThread.Stop();
|
||||
mOutput.Stop();
|
||||
mOutput.ReleaseResources();
|
||||
mFrameExchange.Clear();
|
||||
mVideoOutputEnabled = false;
|
||||
mVideoOutputStatus = reason;
|
||||
LogWarning("app", reason + " Continuing without video output.");
|
||||
}
|
||||
|
||||
void StartHttpServer()
|
||||
{
|
||||
HttpControlServerCallbacks callbacks;
|
||||
callbacks.getStateJson = [this]() {
|
||||
return BuildStateJson();
|
||||
};
|
||||
callbacks.addLayer = [this](const std::string& body) {
|
||||
return mRuntimeLayers.HandleAddLayer(body);
|
||||
};
|
||||
callbacks.removeLayer = [this](const std::string& body) {
|
||||
return mRuntimeLayers.HandleRemoveLayer(body);
|
||||
};
|
||||
callbacks.executePost = [this](const std::string& path, const std::string& body) {
|
||||
RuntimeControlCommand command;
|
||||
std::string error;
|
||||
if (!ParseRuntimeControlCommand(path, body, command, error))
|
||||
return ControlActionResult{ false, error };
|
||||
return mRuntimeLayers.HandleControlCommand(command);
|
||||
};
|
||||
|
||||
std::string error;
|
||||
if (!mHttpServer.Start(
|
||||
FindRepoPath("ui/dist"),
|
||||
FindRepoPath("docs"),
|
||||
mConfig.http,
|
||||
callbacks,
|
||||
error))
|
||||
{
|
||||
LogWarning("http", "HTTP control server did not start: " + error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::string BuildStateJson()
|
||||
{
|
||||
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||
ApplyDeckLinkInputMetrics(telemetry);
|
||||
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
|
||||
return RuntimeStateToJson(RuntimeStateJsonInput{
|
||||
mConfig,
|
||||
telemetry,
|
||||
mHttpServer.Port(),
|
||||
mVideoOutputEnabled,
|
||||
mVideoOutputStatus,
|
||||
mRuntimeLayers.ShaderCatalog(),
|
||||
layerSnapshot
|
||||
});
|
||||
}
|
||||
|
||||
void ApplyDeckLinkInputMetrics(CadenceTelemetrySnapshot& telemetry)
|
||||
{
|
||||
if (!mDeckLinkInputMetricsProvider)
|
||||
return;
|
||||
|
||||
const DeckLinkInputMetrics inputMetrics = mDeckLinkInputMetricsProvider();
|
||||
telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
||||
telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
||||
telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
||||
telemetry.inputUnsupportedFrames = inputMetrics.unsupportedFrames;
|
||||
telemetry.inputSubmitMisses = inputMetrics.submitMisses;
|
||||
telemetry.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none";
|
||||
if (telemetry.sampleSeconds > 0.0)
|
||||
telemetry.inputCaptureFps = static_cast<double>(inputMetrics.capturedFrames - mLastInputCapturedFrames) / telemetry.sampleSeconds;
|
||||
mLastInputCapturedFrames = inputMetrics.capturedFrames;
|
||||
}
|
||||
|
||||
bool WaitForPreroll() const
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout;
|
||||
while (std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
if (mFrameExchange.Metrics().scheduledCount >= mConfig.outputThread.targetBufferedFrames)
|
||||
return true;
|
||||
std::this_thread::sleep_for(mConfig.prerollPoll);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RenderThread& mRenderThread;
|
||||
SystemFrameExchange& mFrameExchange;
|
||||
AppConfig mConfig;
|
||||
DeckLinkOutput mOutput;
|
||||
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
|
||||
TelemetryHealthMonitor mTelemetryHealth;
|
||||
CadenceTelemetry mHttpTelemetry;
|
||||
HttpControlServer mHttpServer;
|
||||
RuntimeLayerController mRuntimeLayers;
|
||||
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
|
||||
uint64_t mLastInputCapturedFrames = 0;
|
||||
bool mStarted = false;
|
||||
bool mVideoOutputEnabled = false;
|
||||
std::string mVideoOutputStatus = "DeckLink output not started.";
|
||||
};
|
||||
}
|
||||
92
src/app/RuntimeLayerController.cpp
Normal file
92
src/app/RuntimeLayerController.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
||||
mPublisher(std::move(publisher))
|
||||
{
|
||||
}
|
||||
|
||||
RuntimeLayerController::~RuntimeLayerController()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::SetPublisher(RenderLayerPublisher publisher)
|
||||
{
|
||||
mPublisher = std::move(publisher);
|
||||
}
|
||||
|
||||
void RuntimeLayerController::Initialize(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames, std::string& runtimeShaderId)
|
||||
{
|
||||
LoadSupportedShaderCatalog(shaderLibrary, maxTemporalHistoryFrames);
|
||||
InitializeLayerModel(runtimeShaderId);
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StartStartupBuild(const std::string& runtimeShaderId)
|
||||
{
|
||||
if (runtimeShaderId.empty())
|
||||
{
|
||||
Log("runtime-shader", "Runtime shader build disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Starting background Slang build for shader '" + runtimeShaderId + "'.");
|
||||
const std::string layerId = FirstRuntimeLayerId();
|
||||
if (!layerId.empty())
|
||||
StartLayerShaderBuild(layerId, runtimeShaderId);
|
||||
}
|
||||
|
||||
void RuntimeLayerController::Stop()
|
||||
{
|
||||
StopAllRuntimeShaderBuilds();
|
||||
}
|
||||
|
||||
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
RuntimeLayerModelSnapshot snapshot = mRuntimeLayerModel.Snapshot();
|
||||
if (telemetry.shaderBuildFailures > 0)
|
||||
{
|
||||
snapshot.compileSucceeded = false;
|
||||
snapshot.compileMessage = "Runtime shader GL commit failed; see logs for details.";
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
void RuntimeLayerController::PublishRuntimeRenderLayers()
|
||||
{
|
||||
if (!mPublisher)
|
||||
return;
|
||||
|
||||
std::vector<RuntimeRenderLayerModel> renderLayers;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
renderLayers = mRuntimeLayerModel.Snapshot().renderLayers;
|
||||
}
|
||||
mPublisher(renderLayers);
|
||||
}
|
||||
|
||||
bool RuntimeLayerController::MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.MarkBuildReady(artifact, error))
|
||||
{
|
||||
LogWarning("runtime-shader", error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.MarkBuildFailed(layerId, message, error))
|
||||
LogWarning("runtime-shader", error);
|
||||
}
|
||||
|
||||
}
|
||||
63
src/app/RuntimeLayerController.h
Normal file
63
src/app/RuntimeLayerController.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "../control/ControlActionResult.h"
|
||||
#include "../control/RuntimeControlCommand.h"
|
||||
#include "../runtime/RuntimeLayerModel.h"
|
||||
#include "../runtime/RuntimeShaderBridge.h"
|
||||
#include "../runtime/SupportedShaderCatalog.h"
|
||||
#include "../telemetry/CadenceTelemetry.h"
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
class RuntimeLayerController
|
||||
{
|
||||
public:
|
||||
using RenderLayerPublisher = std::function<void(const std::vector<RuntimeRenderLayerModel>&)>;
|
||||
|
||||
explicit RuntimeLayerController(RenderLayerPublisher publisher = RenderLayerPublisher());
|
||||
RuntimeLayerController(const RuntimeLayerController&) = delete;
|
||||
RuntimeLayerController& operator=(const RuntimeLayerController&) = delete;
|
||||
~RuntimeLayerController();
|
||||
|
||||
void SetPublisher(RenderLayerPublisher publisher);
|
||||
void Initialize(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames, std::string& runtimeShaderId);
|
||||
void StartStartupBuild(const std::string& runtimeShaderId);
|
||||
void Stop();
|
||||
|
||||
ControlActionResult HandleAddLayer(const std::string& body);
|
||||
ControlActionResult HandleRemoveLayer(const std::string& body);
|
||||
ControlActionResult HandleControlCommand(const RuntimeControlCommand& command);
|
||||
|
||||
RuntimeLayerModelSnapshot Snapshot(const CadenceTelemetrySnapshot& telemetry) const;
|
||||
const SupportedShaderCatalog& ShaderCatalog() const { return mShaderCatalog; }
|
||||
|
||||
private:
|
||||
void LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames);
|
||||
void InitializeLayerModel(std::string& runtimeShaderId);
|
||||
void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId);
|
||||
void RetireLayerShaderBuild(const std::string& layerId);
|
||||
void CleanupRetiredShaderBuilds();
|
||||
void StopAllRuntimeShaderBuilds();
|
||||
void PublishRuntimeRenderLayers();
|
||||
bool MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact);
|
||||
void MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message);
|
||||
|
||||
std::string FirstRuntimeLayerId() const;
|
||||
static bool ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error);
|
||||
|
||||
RenderLayerPublisher mPublisher;
|
||||
SupportedShaderCatalog mShaderCatalog;
|
||||
mutable std::mutex mRuntimeLayerMutex;
|
||||
RuntimeLayerModel mRuntimeLayerModel;
|
||||
std::mutex mShaderBuildMutex;
|
||||
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> mShaderBuilds;
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> mRetiredShaderBuilds;
|
||||
};
|
||||
}
|
||||
122
src/app/RuntimeLayerControllerBuild.cpp
Normal file
122
src/app/RuntimeLayerControllerBuild.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "AppConfigProvider.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames)
|
||||
{
|
||||
const std::filesystem::path shaderRoot = FindRepoPath(shaderLibrary);
|
||||
std::string error;
|
||||
if (!mShaderCatalog.Load(shaderRoot, maxTemporalHistoryFrames, error))
|
||||
{
|
||||
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
|
||||
return;
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
|
||||
}
|
||||
|
||||
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error))
|
||||
{
|
||||
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
|
||||
runtimeShaderId.clear();
|
||||
mRuntimeLayerModel.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
RetireLayerShaderBuild(layerId);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
|
||||
}
|
||||
|
||||
auto bridge = std::make_unique<RuntimeShaderBridge>();
|
||||
RuntimeShaderBridge* bridgePtr = bridge.get();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
mShaderBuilds[layerId] = std::move(bridge);
|
||||
}
|
||||
|
||||
bridgePtr->Start(
|
||||
layerId,
|
||||
shaderId,
|
||||
[this](const RuntimeShaderArtifact& artifact) {
|
||||
if (MarkRuntimeBuildReady(artifact))
|
||||
PublishRuntimeRenderLayers();
|
||||
},
|
||||
[this, layerId](const std::string& message) {
|
||||
MarkRuntimeBuildFailedForLayer(layerId, message);
|
||||
LogError("runtime-shader", "Runtime Slang build failed: " + message);
|
||||
});
|
||||
}
|
||||
|
||||
void RuntimeLayerController::RetireLayerShaderBuild(const std::string& layerId)
|
||||
{
|
||||
std::unique_ptr<RuntimeShaderBridge> bridge;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
auto bridgeIt = mShaderBuilds.find(layerId);
|
||||
if (bridgeIt == mShaderBuilds.end())
|
||||
return;
|
||||
bridge = std::move(bridgeIt->second);
|
||||
mShaderBuilds.erase(bridgeIt);
|
||||
bridge->RequestStop();
|
||||
mRetiredShaderBuilds.push_back(std::move(bridge));
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::CleanupRetiredShaderBuilds()
|
||||
{
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> readyToStop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
for (auto it = mRetiredShaderBuilds.begin(); it != mRetiredShaderBuilds.end();)
|
||||
{
|
||||
if ((*it)->CanStopWithoutWaiting())
|
||||
{
|
||||
readyToStop.push_back(std::move(*it));
|
||||
it = mRetiredShaderBuilds.erase(it);
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::unique_ptr<RuntimeShaderBridge>& bridge : readyToStop)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StopAllRuntimeShaderBuilds()
|
||||
{
|
||||
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> retiredBuilds;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
builds.swap(mShaderBuilds);
|
||||
retiredBuilds.swap(mRetiredShaderBuilds);
|
||||
}
|
||||
for (auto& entry : builds)
|
||||
entry.second->Stop();
|
||||
for (auto& bridge : retiredBuilds)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
std::string RuntimeLayerController::FirstRuntimeLayerId() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
return mRuntimeLayerModel.FirstLayerId();
|
||||
}
|
||||
}
|
||||
160
src/app/RuntimeLayerControllerControls.cpp
Normal file
160
src/app/RuntimeLayerControllerControls.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string shaderId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "shaderId", shaderId, error))
|
||||
return { false, error };
|
||||
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
|
||||
StartLayerShaderBuild(layerId, shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string layerId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "layerId", layerId, error))
|
||||
return { false, error };
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer removed: " + layerId);
|
||||
RetireLayerShaderBuild(layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string error;
|
||||
switch (command.type)
|
||||
{
|
||||
case RuntimeControlCommandType::AddLayer:
|
||||
{
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId);
|
||||
StartLayerShaderBuild(layerId, command.shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::RemoveLayer:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer removed: " + command.layerId);
|
||||
RetireLayerShaderBuild(command.layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::ReorderLayer:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::SetLayerBypass:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::SetLayerShader:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId);
|
||||
StartLayerShaderBuild(command.layerId, command.shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::UpdateLayerParameter:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::ResetLayerParameters:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.ResetParameters(command.layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::Unsupported:
|
||||
break;
|
||||
}
|
||||
|
||||
return { false, "Unsupported runtime control command." };
|
||||
}
|
||||
|
||||
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
|
||||
{
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
const JsonValue* field = root.find(fieldName);
|
||||
if (!field || !field->isString() || field->asString().empty())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
value = field->asString();
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
12
src/control/ControlActionResult.h
Normal file
12
src/control/ControlActionResult.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct ControlActionResult
|
||||
{
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
};
|
||||
}
|
||||
127
src/control/RuntimeControlCommand.cpp
Normal file
127
src/control/RuntimeControlCommand.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "RuntimeControlCommand.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const JsonValue* RequireObjectField(const JsonValue& root, const char* fieldName, std::string& error)
|
||||
{
|
||||
const JsonValue* field = root.find(fieldName);
|
||||
if (!field)
|
||||
error = std::string("Request field '") + fieldName + "' is required.";
|
||||
return field;
|
||||
}
|
||||
|
||||
bool RequireStringField(const JsonValue& root, const char* fieldName, std::string& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isString() || field->asString().empty())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
value = field->asString();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RequireBoolField(const JsonValue& root, const char* fieldName, bool& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isBoolean())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a boolean.";
|
||||
return false;
|
||||
}
|
||||
value = field->asBoolean();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RequireIntegerField(const JsonValue& root, const char* fieldName, int& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isNumber())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a number.";
|
||||
return false;
|
||||
}
|
||||
value = static_cast<int>(field->asNumber());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseRuntimeControlCommand(
|
||||
const std::string& path,
|
||||
const std::string& body,
|
||||
RuntimeControlCommand& command,
|
||||
std::string& error)
|
||||
{
|
||||
command = RuntimeControlCommand();
|
||||
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path == "/api/layers/add")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::AddLayer;
|
||||
return RequireStringField(root, "shaderId", command.shaderId, error);
|
||||
}
|
||||
if (path == "/api/layers/remove")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::RemoveLayer;
|
||||
return RequireStringField(root, "layerId", command.layerId, error);
|
||||
}
|
||||
if (path == "/api/layers/reorder")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::ReorderLayer;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireIntegerField(root, "targetIndex", command.targetIndex, error);
|
||||
}
|
||||
if (path == "/api/layers/set-bypass")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::SetLayerBypass;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireBoolField(root, "bypass", command.bypass, error);
|
||||
}
|
||||
if (path == "/api/layers/set-shader")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::SetLayerShader;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireStringField(root, "shaderId", command.shaderId, error);
|
||||
}
|
||||
if (path == "/api/layers/update-parameter")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::UpdateLayerParameter;
|
||||
const JsonValue* value = nullptr;
|
||||
if (!RequireStringField(root, "layerId", command.layerId, error)
|
||||
|| !RequireStringField(root, "parameterId", command.parameterId, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
value = RequireObjectField(root, "value", error);
|
||||
if (!value)
|
||||
return false;
|
||||
command.value = *value;
|
||||
return true;
|
||||
}
|
||||
if (path == "/api/layers/reset-parameters")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::ResetLayerParameters;
|
||||
return RequireStringField(root, "layerId", command.layerId, error);
|
||||
}
|
||||
|
||||
command.type = RuntimeControlCommandType::Unsupported;
|
||||
error = "Endpoint is not implemented in RenderCadenceCompositor yet.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
37
src/control/RuntimeControlCommand.h
Normal file
37
src/control/RuntimeControlCommand.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
enum class RuntimeControlCommandType
|
||||
{
|
||||
AddLayer,
|
||||
RemoveLayer,
|
||||
ReorderLayer,
|
||||
SetLayerBypass,
|
||||
SetLayerShader,
|
||||
UpdateLayerParameter,
|
||||
ResetLayerParameters,
|
||||
Unsupported
|
||||
};
|
||||
|
||||
struct RuntimeControlCommand
|
||||
{
|
||||
RuntimeControlCommandType type = RuntimeControlCommandType::Unsupported;
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string parameterId;
|
||||
int targetIndex = 0;
|
||||
bool bypass = false;
|
||||
JsonValue value;
|
||||
};
|
||||
|
||||
bool ParseRuntimeControlCommand(
|
||||
const std::string& path,
|
||||
const std::string& body,
|
||||
RuntimeControlCommand& command,
|
||||
std::string& error);
|
||||
}
|
||||
324
src/control/RuntimeStateJson.h
Normal file
324
src/control/RuntimeStateJson.h
Normal file
@@ -0,0 +1,324 @@
|
||||
#pragma once
|
||||
|
||||
#include "../app/AppConfig.h"
|
||||
#include "../app/AppConfigProvider.h"
|
||||
#include "../json/JsonWriter.h"
|
||||
#include "../runtime/RuntimeLayerModel.h"
|
||||
#include "../runtime/SupportedShaderCatalog.h"
|
||||
#include "../telemetry/CadenceTelemetryJson.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct RuntimeStateJsonInput
|
||||
{
|
||||
const AppConfig& config;
|
||||
const CadenceTelemetrySnapshot& telemetry;
|
||||
unsigned short serverPort = 0;
|
||||
bool videoOutputEnabled = false;
|
||||
std::string videoOutputStatus;
|
||||
const SupportedShaderCatalog& shaderCatalog;
|
||||
const RuntimeLayerModelSnapshot& runtimeLayers;
|
||||
};
|
||||
|
||||
inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyString("backend", "decklink");
|
||||
writer.KeyNull("modelName");
|
||||
writer.KeyBool("supportsInternalKeying", false);
|
||||
writer.KeyBool("supportsExternalKeying", false);
|
||||
writer.KeyBool("keyerInterfaceAvailable", false);
|
||||
writer.KeyBool("externalKeyingRequested", input.config.deckLink.externalKeyingEnabled);
|
||||
writer.KeyBool("externalKeyingActive", input.videoOutputEnabled && input.config.deckLink.externalKeyingEnabled);
|
||||
writer.KeyString("statusMessage", input.videoOutputStatus);
|
||||
writer.EndObject();
|
||||
}
|
||||
|
||||
inline void OutputDimensions(const RuntimeStateJsonInput& input, unsigned& width, unsigned& height)
|
||||
{
|
||||
VideoFormatDimensions(input.config.outputVideoFormat, width, height);
|
||||
}
|
||||
|
||||
inline const char* ShaderParameterTypeName(ShaderParameterType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ShaderParameterType::Float: return "float";
|
||||
case ShaderParameterType::Vec2: return "vec2";
|
||||
case ShaderParameterType::Color: return "color";
|
||||
case ShaderParameterType::Boolean: return "bool";
|
||||
case ShaderParameterType::Enum: return "enum";
|
||||
case ShaderParameterType::Text: return "text";
|
||||
case ShaderParameterType::Trigger: return "trigger";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
inline void WriteNumberArray(JsonWriter& writer, const std::vector<double>& values)
|
||||
{
|
||||
writer.BeginArray();
|
||||
for (double value : values)
|
||||
writer.Double(value);
|
||||
writer.EndArray();
|
||||
}
|
||||
|
||||
inline void WriteDefaultParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter)
|
||||
{
|
||||
switch (parameter.type)
|
||||
{
|
||||
case ShaderParameterType::Boolean:
|
||||
writer.Bool(parameter.defaultBoolean);
|
||||
return;
|
||||
case ShaderParameterType::Enum:
|
||||
writer.String(parameter.defaultEnumValue);
|
||||
return;
|
||||
case ShaderParameterType::Text:
|
||||
writer.String(parameter.defaultTextValue);
|
||||
return;
|
||||
case ShaderParameterType::Trigger:
|
||||
writer.Double(0.0);
|
||||
return;
|
||||
case ShaderParameterType::Float:
|
||||
writer.Double(parameter.defaultNumbers.empty() ? 0.0 : parameter.defaultNumbers.front());
|
||||
return;
|
||||
case ShaderParameterType::Vec2:
|
||||
case ShaderParameterType::Color:
|
||||
WriteNumberArray(writer, parameter.defaultNumbers);
|
||||
return;
|
||||
}
|
||||
writer.Null();
|
||||
}
|
||||
|
||||
inline void WriteParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue& value)
|
||||
{
|
||||
switch (parameter.type)
|
||||
{
|
||||
case ShaderParameterType::Boolean:
|
||||
writer.Bool(value.booleanValue);
|
||||
return;
|
||||
case ShaderParameterType::Enum:
|
||||
writer.String(value.enumValue);
|
||||
return;
|
||||
case ShaderParameterType::Text:
|
||||
writer.String(value.textValue);
|
||||
return;
|
||||
case ShaderParameterType::Trigger:
|
||||
case ShaderParameterType::Float:
|
||||
writer.Double(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
return;
|
||||
case ShaderParameterType::Vec2:
|
||||
case ShaderParameterType::Color:
|
||||
WriteNumberArray(writer, value.numberValues);
|
||||
return;
|
||||
}
|
||||
writer.Null();
|
||||
}
|
||||
|
||||
inline void WriteTemporalJson(JsonWriter& writer, const TemporalSettings& temporal)
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("enabled", temporal.enabled);
|
||||
writer.KeyString("historySource", "none");
|
||||
writer.KeyUInt("requestedHistoryLength", temporal.requestedHistoryLength);
|
||||
writer.KeyUInt("effectiveHistoryLength", temporal.effectiveHistoryLength);
|
||||
writer.EndObject();
|
||||
}
|
||||
|
||||
inline void WriteFeedbackJson(JsonWriter& writer, const FeedbackSettings& feedback)
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("enabled", feedback.enabled);
|
||||
writer.KeyString("writePass", feedback.writePassId);
|
||||
writer.EndObject();
|
||||
}
|
||||
|
||||
inline const char* RuntimeLayerBuildStateName(RuntimeLayerBuildState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case RuntimeLayerBuildState::Pending: return "pending";
|
||||
case RuntimeLayerBuildState::Ready: return "ready";
|
||||
case RuntimeLayerBuildState::Failed: return "failed";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue* value)
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyString("id", parameter.id);
|
||||
writer.KeyString("label", parameter.label.empty() ? parameter.id : parameter.label);
|
||||
writer.KeyString("description", parameter.description);
|
||||
writer.KeyString("type", ShaderParameterTypeName(parameter.type));
|
||||
writer.Key("defaultValue");
|
||||
WriteDefaultParameterValue(writer, parameter);
|
||||
writer.Key("value");
|
||||
if (value)
|
||||
WriteParameterValue(writer, parameter, *value);
|
||||
else
|
||||
WriteDefaultParameterValue(writer, parameter);
|
||||
|
||||
if (!parameter.minNumbers.empty())
|
||||
{
|
||||
writer.Key("min");
|
||||
WriteNumberArray(writer, parameter.minNumbers);
|
||||
}
|
||||
if (!parameter.maxNumbers.empty())
|
||||
{
|
||||
writer.Key("max");
|
||||
WriteNumberArray(writer, parameter.maxNumbers);
|
||||
}
|
||||
if (!parameter.stepNumbers.empty())
|
||||
{
|
||||
writer.Key("step");
|
||||
WriteNumberArray(writer, parameter.stepNumbers);
|
||||
}
|
||||
if (parameter.type == ShaderParameterType::Enum)
|
||||
{
|
||||
writer.Key("options");
|
||||
writer.BeginArray();
|
||||
for (const ShaderParameterOption& option : parameter.enumOptions)
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyString("value", option.value);
|
||||
writer.KeyString("label", option.label.empty() ? option.value : option.label);
|
||||
writer.EndObject();
|
||||
}
|
||||
writer.EndArray();
|
||||
}
|
||||
if (parameter.type == ShaderParameterType::Text)
|
||||
{
|
||||
writer.KeyUInt("maxLength", parameter.maxLength);
|
||||
if (!parameter.fontId.empty())
|
||||
writer.KeyString("font", parameter.fontId);
|
||||
}
|
||||
writer.EndObject();
|
||||
}
|
||||
|
||||
inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
||||
{
|
||||
writer.BeginArray();
|
||||
for (const RuntimeLayerReadModel& layer : input.runtimeLayers.displayLayers)
|
||||
{
|
||||
const ShaderPackage* shaderPackage = input.shaderCatalog.FindPackage(layer.shaderId);
|
||||
writer.BeginObject();
|
||||
writer.KeyString("id", layer.id);
|
||||
writer.KeyString("shaderId", layer.shaderId);
|
||||
writer.KeyString("shaderName", layer.shaderName);
|
||||
writer.KeyBool("bypass", layer.bypass);
|
||||
writer.KeyString("buildState", RuntimeLayerBuildStateName(layer.buildState));
|
||||
writer.KeyBool("renderReady", layer.renderReady);
|
||||
writer.KeyString("message", layer.message);
|
||||
writer.Key("temporal");
|
||||
if (shaderPackage)
|
||||
WriteTemporalJson(writer, shaderPackage->temporal);
|
||||
else
|
||||
WriteTemporalJson(writer, TemporalSettings());
|
||||
writer.Key("feedback");
|
||||
if (shaderPackage)
|
||||
WriteFeedbackJson(writer, shaderPackage->feedback);
|
||||
else
|
||||
WriteFeedbackJson(writer, FeedbackSettings());
|
||||
writer.Key("parameters");
|
||||
writer.BeginArray();
|
||||
for (const ShaderParameterDefinition& parameter : layer.parameterDefinitions)
|
||||
{
|
||||
const auto valueIt = layer.parameterValues.find(parameter.id);
|
||||
WriteParameterDefinitionJson(writer, parameter, valueIt == layer.parameterValues.end() ? nullptr : &valueIt->second);
|
||||
}
|
||||
writer.EndArray();
|
||||
writer.EndObject();
|
||||
}
|
||||
writer.EndArray();
|
||||
}
|
||||
|
||||
inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
||||
{
|
||||
JsonWriter writer;
|
||||
writer.BeginObject();
|
||||
|
||||
writer.Key("app");
|
||||
writer.BeginObject();
|
||||
writer.KeyUInt("serverPort", input.serverPort);
|
||||
writer.KeyUInt("oscPort", input.config.oscPort);
|
||||
writer.KeyString("oscBindAddress", input.config.oscBindAddress);
|
||||
writer.KeyDouble("oscSmoothing", input.config.oscSmoothing);
|
||||
writer.KeyBool("autoReload", input.config.autoReload);
|
||||
writer.KeyUInt("maxTemporalHistoryFrames", static_cast<uint64_t>(input.config.maxTemporalHistoryFrames));
|
||||
writer.KeyDouble("previewFps", input.config.previewFps);
|
||||
writer.KeyBool("enableExternalKeying", input.config.deckLink.externalKeyingEnabled);
|
||||
writer.KeyString("inputVideoFormat", input.config.inputVideoFormat);
|
||||
writer.KeyString("inputFrameRate", input.config.inputFrameRate);
|
||||
writer.KeyString("outputVideoFormat", input.config.outputVideoFormat);
|
||||
writer.KeyString("outputFrameRate", input.config.outputFrameRate);
|
||||
writer.EndObject();
|
||||
|
||||
writer.Key("runtime");
|
||||
writer.BeginObject();
|
||||
writer.KeyUInt("layerCount", static_cast<uint64_t>(input.runtimeLayers.displayLayers.size()));
|
||||
writer.KeyBool("compileSucceeded", input.runtimeLayers.compileSucceeded);
|
||||
writer.KeyString("compileMessage", input.runtimeLayers.compileMessage);
|
||||
writer.EndObject();
|
||||
|
||||
writer.Key("video");
|
||||
writer.BeginObject();
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
OutputDimensions(input, outputWidth, outputHeight);
|
||||
writer.KeyBool("hasSignal", input.videoOutputEnabled);
|
||||
writer.KeyUInt("width", outputWidth);
|
||||
writer.KeyUInt("height", outputHeight);
|
||||
writer.KeyString("modeName", input.config.outputVideoFormat + " output-only");
|
||||
writer.EndObject();
|
||||
|
||||
writer.Key("decklink");
|
||||
WriteVideoIoStatusJson(writer, input);
|
||||
writer.Key("videoIO");
|
||||
WriteVideoIoStatusJson(writer, input);
|
||||
|
||||
writer.Key("performance");
|
||||
writer.BeginObject();
|
||||
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
||||
writer.KeyDouble("renderMs", input.telemetry.renderFrameMilliseconds);
|
||||
writer.KeyNull("smoothedRenderMs");
|
||||
writer.KeyDouble("budgetUsedPercent", input.telemetry.renderFrameBudgetUsedPercent);
|
||||
writer.KeyNull("completionIntervalMs");
|
||||
writer.KeyNull("smoothedCompletionIntervalMs");
|
||||
writer.KeyNull("maxCompletionIntervalMs");
|
||||
writer.KeyUInt("lateFrameCount", input.telemetry.displayedLate);
|
||||
writer.KeyUInt("droppedFrameCount", input.telemetry.dropped);
|
||||
writer.KeyNull("flushedFrameCount");
|
||||
writer.Key("cadence");
|
||||
WriteCadenceTelemetryJson(writer, input.telemetry);
|
||||
writer.EndObject();
|
||||
|
||||
writer.KeyNull("backendPlayout");
|
||||
writer.KeyNull("runtimeEvents");
|
||||
writer.Key("shaders");
|
||||
writer.BeginArray();
|
||||
for (const SupportedShaderSummary& shader : input.shaderCatalog.Shaders())
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyString("id", shader.id);
|
||||
writer.KeyString("name", shader.name);
|
||||
writer.KeyString("description", shader.description);
|
||||
writer.KeyString("category", shader.category);
|
||||
writer.KeyBool("available", true);
|
||||
writer.KeyNull("error");
|
||||
writer.EndObject();
|
||||
}
|
||||
writer.EndArray();
|
||||
writer.Key("stackPresets");
|
||||
writer.BeginArray();
|
||||
writer.EndArray();
|
||||
writer.Key("layers");
|
||||
WriteLayersJson(writer, input);
|
||||
|
||||
writer.EndObject();
|
||||
return writer.StringValue();
|
||||
}
|
||||
}
|
||||
293
src/control/http/HttpControlServer.cpp
Normal file
293
src/control/http/HttpControlServer.cpp
Normal file
@@ -0,0 +1,293 @@
|
||||
#include "HttpControlServer.h"
|
||||
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool InitializeWinsock(std::string& error)
|
||||
{
|
||||
WSADATA wsaData = {};
|
||||
const int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
|
||||
if (result != 0)
|
||||
{
|
||||
error = "WSAStartup failed.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UniqueSocket::UniqueSocket(SOCKET socket) :
|
||||
mSocket(socket)
|
||||
{
|
||||
}
|
||||
|
||||
UniqueSocket::~UniqueSocket()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
UniqueSocket::UniqueSocket(UniqueSocket&& other) noexcept :
|
||||
mSocket(other.release())
|
||||
{
|
||||
}
|
||||
|
||||
UniqueSocket& UniqueSocket::operator=(UniqueSocket&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
reset(other.release());
|
||||
return *this;
|
||||
}
|
||||
|
||||
SOCKET UniqueSocket::release()
|
||||
{
|
||||
const SOCKET socket = mSocket;
|
||||
mSocket = INVALID_SOCKET;
|
||||
return socket;
|
||||
}
|
||||
|
||||
void UniqueSocket::reset(SOCKET socket)
|
||||
{
|
||||
if (valid())
|
||||
closesocket(mSocket);
|
||||
mSocket = socket;
|
||||
}
|
||||
|
||||
HttpControlServer::~HttpControlServer()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool HttpControlServer::Start(
|
||||
const std::filesystem::path& uiRoot,
|
||||
const std::filesystem::path& docsRoot,
|
||||
HttpControlServerConfig config,
|
||||
HttpControlServerCallbacks callbacks,
|
||||
std::string& error)
|
||||
{
|
||||
Stop();
|
||||
|
||||
if (!InitializeWinsock(error))
|
||||
return false;
|
||||
mWinsockStarted = true;
|
||||
|
||||
mUiRoot = uiRoot;
|
||||
mDocsRoot = docsRoot;
|
||||
mConfig = config;
|
||||
mCallbacks = std::move(callbacks);
|
||||
|
||||
mListenSocket.reset(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
|
||||
if (!mListenSocket.valid())
|
||||
{
|
||||
error = "Could not create HTTP control server socket.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
u_long nonBlocking = 1;
|
||||
ioctlsocket(mListenSocket.get(), FIONBIO, &nonBlocking);
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
bool bound = false;
|
||||
for (unsigned short offset = 0; offset < mConfig.portSearchCount; ++offset)
|
||||
{
|
||||
address.sin_port = htons(static_cast<u_short>(mConfig.preferredPort + offset));
|
||||
if (bind(mListenSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) == 0)
|
||||
{
|
||||
mPort = static_cast<unsigned short>(mConfig.preferredPort + offset);
|
||||
bound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bound)
|
||||
{
|
||||
error = "Could not bind HTTP control server to loopback.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(mListenSocket.get(), SOMAXCONN) != 0)
|
||||
{
|
||||
error = "Could not listen on HTTP control server socket.";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
mRunning.store(true, std::memory_order_release);
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
Log("http", "HTTP control server listening on http://127.0.0.1:" + std::to_string(mPort));
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpControlServer::Stop()
|
||||
{
|
||||
mRunning.store(false, std::memory_order_release);
|
||||
mListenSocket.reset();
|
||||
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
|
||||
std::vector<std::thread> clientThreads;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||
clientThreads.swap(mClientThreads);
|
||||
for (std::thread& thread : mFinishedClientThreads)
|
||||
clientThreads.push_back(std::move(thread));
|
||||
mFinishedClientThreads.clear();
|
||||
}
|
||||
for (std::thread& thread : clientThreads)
|
||||
{
|
||||
if (thread.joinable())
|
||||
thread.join();
|
||||
}
|
||||
|
||||
if (mWinsockStarted)
|
||||
{
|
||||
WSACleanup();
|
||||
mWinsockStarted = false;
|
||||
}
|
||||
mPort = 0;
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::RouteRequestForTest(const HttpRequest& request) const
|
||||
{
|
||||
return RouteRequest(request);
|
||||
}
|
||||
|
||||
void HttpControlServer::SetCallbacksForTest(HttpControlServerCallbacks callbacks)
|
||||
{
|
||||
mCallbacks = std::move(callbacks);
|
||||
}
|
||||
|
||||
void HttpControlServer::SetRootsForTest(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot)
|
||||
{
|
||||
mUiRoot = uiRoot;
|
||||
mDocsRoot = docsRoot;
|
||||
}
|
||||
|
||||
void HttpControlServer::ThreadMain()
|
||||
{
|
||||
while (mRunning.load(std::memory_order_acquire))
|
||||
{
|
||||
JoinFinishedClientThreads();
|
||||
TryAcceptClient();
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpControlServer::TryAcceptClient()
|
||||
{
|
||||
sockaddr_in clientAddress = {};
|
||||
int addressSize = sizeof(clientAddress);
|
||||
UniqueSocket clientSocket(accept(mListenSocket.get(), reinterpret_cast<sockaddr*>(&clientAddress), &addressSize));
|
||||
if (!clientSocket.valid())
|
||||
return false;
|
||||
|
||||
return HandleClient(std::move(clientSocket));
|
||||
}
|
||||
|
||||
bool HttpControlServer::HandleClient(UniqueSocket clientSocket)
|
||||
{
|
||||
char buffer[16384];
|
||||
const int received = recv(clientSocket.get(), buffer, sizeof(buffer), 0);
|
||||
if (received <= 0)
|
||||
return false;
|
||||
|
||||
HttpRequest request;
|
||||
if (!ParseHttpRequest(std::string(buffer, buffer + received), request))
|
||||
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Bad Request"));
|
||||
|
||||
if (request.path == "/ws")
|
||||
return HandleWebSocketClient(std::move(clientSocket), request);
|
||||
|
||||
return SendResponse(clientSocket.get(), RouteRequest(request));
|
||||
}
|
||||
|
||||
bool HttpControlServer::SendResponse(SOCKET clientSocket, const HttpResponse& response) const
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << "HTTP/1.1 " << response.status << "\r\n"
|
||||
<< "Content-Type: " << response.contentType << "\r\n"
|
||||
<< "Content-Length: " << response.body.size() << "\r\n"
|
||||
<< "Access-Control-Allow-Origin: *\r\n"
|
||||
<< "Connection: close\r\n\r\n"
|
||||
<< response.body;
|
||||
|
||||
const std::string payload = stream.str();
|
||||
return send(clientSocket, payload.c_str(), static_cast<int>(payload.size()), 0) == static_cast<int>(payload.size());
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpRequest& request) const
|
||||
{
|
||||
if (request.method == "GET")
|
||||
return ServeGet(request);
|
||||
if (request.method == "POST")
|
||||
return ServePost(request);
|
||||
if (request.method == "OPTIONS")
|
||||
return TextResponse("204 No Content", std::string());
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
}
|
||||
|
||||
bool HttpControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
|
||||
{
|
||||
const std::size_t requestLineEnd = rawRequest.find("\r\n");
|
||||
if (requestLineEnd == std::string::npos)
|
||||
return false;
|
||||
|
||||
const std::string requestLine = rawRequest.substr(0, requestLineEnd);
|
||||
const std::size_t methodEnd = requestLine.find(' ');
|
||||
if (methodEnd == std::string::npos)
|
||||
return false;
|
||||
|
||||
const std::size_t pathEnd = requestLine.find(' ', methodEnd + 1);
|
||||
if (pathEnd == std::string::npos)
|
||||
return false;
|
||||
|
||||
request.method = requestLine.substr(0, methodEnd);
|
||||
request.path = requestLine.substr(methodEnd + 1, pathEnd - methodEnd - 1);
|
||||
request.headers.clear();
|
||||
|
||||
const std::size_t queryStart = request.path.find('?');
|
||||
if (queryStart != std::string::npos)
|
||||
request.path = request.path.substr(0, queryStart);
|
||||
|
||||
const std::size_t headersStart = requestLineEnd + 2;
|
||||
const std::size_t bodySeparator = rawRequest.find("\r\n\r\n", headersStart);
|
||||
const std::size_t headersEnd = bodySeparator == std::string::npos ? rawRequest.size() : bodySeparator;
|
||||
|
||||
for (std::size_t lineStart = headersStart; lineStart < headersEnd;)
|
||||
{
|
||||
const std::size_t lineEnd = rawRequest.find("\r\n", lineStart);
|
||||
const std::size_t currentLineEnd = lineEnd == std::string::npos ? headersEnd : (std::min)(lineEnd, headersEnd);
|
||||
const std::string line = rawRequest.substr(lineStart, currentLineEnd - lineStart);
|
||||
const std::size_t separator = line.find(':');
|
||||
if (separator != std::string::npos)
|
||||
{
|
||||
const std::string key = ToLower(line.substr(0, separator));
|
||||
std::string value = line.substr(separator + 1);
|
||||
const std::size_t first = value.find_first_not_of(" \t");
|
||||
const std::size_t last = value.find_last_not_of(" \t");
|
||||
request.headers[key] = first == std::string::npos ? std::string() : value.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
if (lineEnd == std::string::npos || lineEnd >= headersEnd)
|
||||
break;
|
||||
lineStart = lineEnd + 2;
|
||||
}
|
||||
|
||||
request.body = bodySeparator == std::string::npos ? std::string() : rawRequest.substr(bodySeparator + 4);
|
||||
return !request.method.empty() && !request.path.empty();
|
||||
}
|
||||
}
|
||||
135
src/control/http/HttpControlServer.h
Normal file
135
src/control/http/HttpControlServer.h
Normal file
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include "ControlActionResult.h"
|
||||
|
||||
#include <winsock2.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct HttpControlServerConfig
|
||||
{
|
||||
unsigned short preferredPort = 8080;
|
||||
unsigned short portSearchCount = 20;
|
||||
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10);
|
||||
};
|
||||
|
||||
struct HttpControlServerCallbacks
|
||||
{
|
||||
std::function<std::string()> getStateJson;
|
||||
std::function<ControlActionResult(const std::string&)> addLayer;
|
||||
std::function<ControlActionResult(const std::string&)> removeLayer;
|
||||
std::function<ControlActionResult(const std::string&, const std::string&)> executePost;
|
||||
};
|
||||
|
||||
class UniqueSocket
|
||||
{
|
||||
public:
|
||||
explicit UniqueSocket(SOCKET socket = INVALID_SOCKET);
|
||||
~UniqueSocket();
|
||||
|
||||
UniqueSocket(const UniqueSocket&) = delete;
|
||||
UniqueSocket& operator=(const UniqueSocket&) = delete;
|
||||
|
||||
UniqueSocket(UniqueSocket&& other) noexcept;
|
||||
UniqueSocket& operator=(UniqueSocket&& other) noexcept;
|
||||
|
||||
SOCKET get() const { return mSocket; }
|
||||
bool valid() const { return mSocket != INVALID_SOCKET; }
|
||||
SOCKET release();
|
||||
void reset(SOCKET socket = INVALID_SOCKET);
|
||||
|
||||
private:
|
||||
SOCKET mSocket = INVALID_SOCKET;
|
||||
};
|
||||
|
||||
class HttpControlServer
|
||||
{
|
||||
public:
|
||||
struct HttpRequest
|
||||
{
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpResponse
|
||||
{
|
||||
std::string status;
|
||||
std::string contentType;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
HttpControlServer() = default;
|
||||
~HttpControlServer();
|
||||
|
||||
HttpControlServer(const HttpControlServer&) = delete;
|
||||
HttpControlServer& operator=(const HttpControlServer&) = delete;
|
||||
|
||||
bool Start(
|
||||
const std::filesystem::path& uiRoot,
|
||||
const std::filesystem::path& docsRoot,
|
||||
HttpControlServerConfig config,
|
||||
HttpControlServerCallbacks callbacks,
|
||||
std::string& error);
|
||||
void Stop();
|
||||
|
||||
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||
unsigned short Port() const { return mPort; }
|
||||
|
||||
void SetCallbacksForTest(HttpControlServerCallbacks callbacks);
|
||||
void SetRootsForTest(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot);
|
||||
HttpResponse RouteRequestForTest(const HttpRequest& request) const;
|
||||
|
||||
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
||||
static std::string WebSocketAcceptKey(const std::string& clientKey);
|
||||
|
||||
private:
|
||||
void ThreadMain();
|
||||
bool TryAcceptClient();
|
||||
bool HandleClient(UniqueSocket clientSocket);
|
||||
bool HandleWebSocketClient(UniqueSocket clientSocket, const HttpRequest& request);
|
||||
void WebSocketClientMain(UniqueSocket clientSocket);
|
||||
void JoinFinishedClientThreads();
|
||||
bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const;
|
||||
HttpResponse RouteRequest(const HttpRequest& request) const;
|
||||
HttpResponse ServeGet(const HttpRequest& request) const;
|
||||
HttpResponse ServePost(const HttpRequest& request) const;
|
||||
HttpResponse ServeOpenApiSpec() const;
|
||||
HttpResponse ServeSwaggerDocs() const;
|
||||
HttpResponse ServeUiAsset(const std::string& relativePath) const;
|
||||
std::string LoadTextFile(const std::filesystem::path& path) const;
|
||||
|
||||
static HttpResponse JsonResponse(const std::string& status, const std::string& body);
|
||||
static HttpResponse TextResponse(const std::string& status, const std::string& body);
|
||||
static HttpResponse HtmlResponse(const std::string& status, const std::string& body);
|
||||
static std::string ActionResponse(bool ok, const std::string& error = std::string());
|
||||
static bool SendWebSocketText(SOCKET clientSocket, const std::string& text);
|
||||
static std::string GuessContentType(const std::filesystem::path& path);
|
||||
static bool IsSafeRelativePath(const std::filesystem::path& path);
|
||||
static std::string ToLower(std::string text);
|
||||
|
||||
std::filesystem::path mUiRoot;
|
||||
std::filesystem::path mDocsRoot;
|
||||
HttpControlServerConfig mConfig;
|
||||
HttpControlServerCallbacks mCallbacks;
|
||||
UniqueSocket mListenSocket;
|
||||
std::thread mThread;
|
||||
std::mutex mClientThreadsMutex;
|
||||
std::vector<std::thread> mClientThreads;
|
||||
std::vector<std::thread> mFinishedClientThreads;
|
||||
std::atomic<bool> mRunning{ false };
|
||||
unsigned short mPort = 0;
|
||||
bool mWinsockStarted = false;
|
||||
};
|
||||
}
|
||||
203
src/control/http/HttpControlServerRoutes.cpp
Normal file
203
src/control/http/HttpControlServerRoutes.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
#include "HttpControlServer.h"
|
||||
|
||||
#include "../json/JsonWriter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool IsKnownPostEndpoint(const std::string& path)
|
||||
{
|
||||
return path == "/api/layers/add"
|
||||
|| path == "/api/layers/remove"
|
||||
|| path == "/api/layers/move"
|
||||
|| path == "/api/layers/reorder"
|
||||
|| path == "/api/layers/set-bypass"
|
||||
|| path == "/api/layers/set-shader"
|
||||
|| path == "/api/layers/update-parameter"
|
||||
|| path == "/api/layers/reset-parameters"
|
||||
|| path == "/api/stack-presets/save"
|
||||
|| path == "/api/stack-presets/load"
|
||||
|| path == "/api/reload"
|
||||
|| path == "/api/screenshot";
|
||||
}
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
|
||||
{
|
||||
if (request.path == "/api/state")
|
||||
return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
|
||||
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
||||
return ServeOpenApiSpec();
|
||||
if (request.path == "/docs" || request.path == "/docs/")
|
||||
return ServeSwaggerDocs();
|
||||
if (request.path == "/" || request.path == "/index.html")
|
||||
return ServeUiAsset("index.html");
|
||||
if (request.path.rfind("/assets/", 0) == 0)
|
||||
return ServeUiAsset(request.path.substr(1));
|
||||
if (request.path.size() > 1)
|
||||
{
|
||||
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
|
||||
if (asset.status != "404 Not Found")
|
||||
return asset;
|
||||
}
|
||||
return ServeUiAsset("index.html");
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
|
||||
{
|
||||
if (!IsKnownPostEndpoint(request.path))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
if (mCallbacks.executePost)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.executePost(request.path, request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.addLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.removeLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
return {
|
||||
"400 Bad Request",
|
||||
"application/json",
|
||||
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
|
||||
};
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
|
||||
{
|
||||
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
|
||||
const std::string body = LoadTextFile(path);
|
||||
return body.empty()
|
||||
? TextResponse("404 Not Found", "OpenAPI spec not found")
|
||||
: HttpResponse{ "200 OK", GuessContentType(path), body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
|
||||
{
|
||||
std::ostringstream html;
|
||||
html << "<!doctype html>\n"
|
||||
<< "<html><head><meta charset=\"utf-8\"><title>Video Shader Toys API Docs</title>\n"
|
||||
<< "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\"></head>\n"
|
||||
<< "<body><div id=\"swagger-ui\"></div>\n"
|
||||
<< "<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
|
||||
<< "<script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
|
||||
<< "</body></html>\n";
|
||||
return { "200 OK", "text/html", html.str() };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const
|
||||
{
|
||||
if (mUiRoot.empty())
|
||||
return TextResponse("404 Not Found", "UI root is not configured");
|
||||
|
||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||
if (!IsSafeRelativePath(sanitizedPath))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
const std::filesystem::path path = mUiRoot / sanitizedPath;
|
||||
const std::string body = LoadTextFile(path);
|
||||
if (body.empty())
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
return { "200 OK", GuessContentType(path), body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
return std::string();
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "application/json", body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "text/plain", body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "text/html", body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
||||
{
|
||||
JsonWriter writer;
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("ok", ok);
|
||||
if (!error.empty())
|
||||
writer.KeyString("error", error);
|
||||
writer.EndObject();
|
||||
return writer.StringValue();
|
||||
}
|
||||
|
||||
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
||||
{
|
||||
const std::string extension = ToLower(path.extension().string());
|
||||
if (extension == ".yaml" || extension == ".yml")
|
||||
return "application/yaml";
|
||||
if (extension == ".json")
|
||||
return "application/json";
|
||||
if (extension == ".js" || extension == ".mjs")
|
||||
return "text/javascript";
|
||||
if (extension == ".css")
|
||||
return "text/css";
|
||||
if (extension == ".html" || extension == ".htm")
|
||||
return "text/html";
|
||||
if (extension == ".svg")
|
||||
return "image/svg+xml";
|
||||
if (extension == ".png")
|
||||
return "image/png";
|
||||
if (extension == ".jpg" || extension == ".jpeg")
|
||||
return "image/jpeg";
|
||||
if (extension == ".ico")
|
||||
return "image/x-icon";
|
||||
if (extension == ".map")
|
||||
return "application/json";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path)
|
||||
{
|
||||
if (path.empty() || path.is_absolute())
|
||||
return false;
|
||||
|
||||
for (const std::filesystem::path& part : path)
|
||||
{
|
||||
if (part == "..")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ToLower(std::string text)
|
||||
{
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
|
||||
return static_cast<char>(std::tolower(character));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
}
|
||||
248
src/control/http/HttpControlServerWebSocket.cpp
Normal file
248
src/control/http/HttpControlServerWebSocket.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
#include "HttpControlServer.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::array<uint8_t, 20> Sha1(const std::string& input)
|
||||
{
|
||||
auto leftRotate = [](uint32_t value, uint32_t bits) {
|
||||
return (value << bits) | (value >> (32U - bits));
|
||||
};
|
||||
|
||||
std::vector<uint8_t> data(input.begin(), input.end());
|
||||
const uint64_t bitLength = static_cast<uint64_t>(data.size()) * 8ULL;
|
||||
data.push_back(0x80);
|
||||
while ((data.size() % 64) != 56)
|
||||
data.push_back(0);
|
||||
for (int shift = 56; shift >= 0; shift -= 8)
|
||||
data.push_back(static_cast<uint8_t>((bitLength >> shift) & 0xff));
|
||||
|
||||
uint32_t h0 = 0x67452301;
|
||||
uint32_t h1 = 0xefcdab89;
|
||||
uint32_t h2 = 0x98badcfe;
|
||||
uint32_t h3 = 0x10325476;
|
||||
uint32_t h4 = 0xc3d2e1f0;
|
||||
|
||||
for (std::size_t offset = 0; offset < data.size(); offset += 64)
|
||||
{
|
||||
uint32_t words[80] = {};
|
||||
for (std::size_t i = 0; i < 16; ++i)
|
||||
{
|
||||
const std::size_t index = offset + i * 4;
|
||||
words[i] = (static_cast<uint32_t>(data[index]) << 24)
|
||||
| (static_cast<uint32_t>(data[index + 1]) << 16)
|
||||
| (static_cast<uint32_t>(data[index + 2]) << 8)
|
||||
| static_cast<uint32_t>(data[index + 3]);
|
||||
}
|
||||
for (std::size_t i = 16; i < 80; ++i)
|
||||
words[i] = leftRotate(words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16], 1);
|
||||
|
||||
uint32_t a = h0;
|
||||
uint32_t b = h1;
|
||||
uint32_t c = h2;
|
||||
uint32_t d = h3;
|
||||
uint32_t e = h4;
|
||||
|
||||
for (std::size_t i = 0; i < 80; ++i)
|
||||
{
|
||||
uint32_t f = 0;
|
||||
uint32_t k = 0;
|
||||
if (i < 20)
|
||||
{
|
||||
f = (b & c) | ((~b) & d);
|
||||
k = 0x5a827999;
|
||||
}
|
||||
else if (i < 40)
|
||||
{
|
||||
f = b ^ c ^ d;
|
||||
k = 0x6ed9eba1;
|
||||
}
|
||||
else if (i < 60)
|
||||
{
|
||||
f = (b & c) | (b & d) | (c & d);
|
||||
k = 0x8f1bbcdc;
|
||||
}
|
||||
else
|
||||
{
|
||||
f = b ^ c ^ d;
|
||||
k = 0xca62c1d6;
|
||||
}
|
||||
|
||||
const uint32_t temp = leftRotate(a, 5) + f + e + k + words[i];
|
||||
e = d;
|
||||
d = c;
|
||||
c = leftRotate(b, 30);
|
||||
b = a;
|
||||
a = temp;
|
||||
}
|
||||
|
||||
h0 += a;
|
||||
h1 += b;
|
||||
h2 += c;
|
||||
h3 += d;
|
||||
h4 += e;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 20> digest = {};
|
||||
const uint32_t parts[] = { h0, h1, h2, h3, h4 };
|
||||
for (std::size_t i = 0; i < 5; ++i)
|
||||
{
|
||||
digest[i * 4] = static_cast<uint8_t>((parts[i] >> 24) & 0xff);
|
||||
digest[i * 4 + 1] = static_cast<uint8_t>((parts[i] >> 16) & 0xff);
|
||||
digest[i * 4 + 2] = static_cast<uint8_t>((parts[i] >> 8) & 0xff);
|
||||
digest[i * 4 + 3] = static_cast<uint8_t>(parts[i] & 0xff);
|
||||
}
|
||||
return digest;
|
||||
}
|
||||
|
||||
std::string Base64Encode(const uint8_t* data, std::size_t size)
|
||||
{
|
||||
static constexpr char kAlphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string output;
|
||||
output.reserve(((size + 2) / 3) * 4);
|
||||
for (std::size_t i = 0; i < size; i += 3)
|
||||
{
|
||||
const uint32_t a = data[i];
|
||||
const uint32_t b = i + 1 < size ? data[i + 1] : 0;
|
||||
const uint32_t c = i + 2 < size ? data[i + 2] : 0;
|
||||
const uint32_t triple = (a << 16) | (b << 8) | c;
|
||||
output.push_back(kAlphabet[(triple >> 18) & 0x3f]);
|
||||
output.push_back(kAlphabet[(triple >> 12) & 0x3f]);
|
||||
output.push_back(i + 1 < size ? kAlphabet[(triple >> 6) & 0x3f] : '=');
|
||||
output.push_back(i + 2 < size ? kAlphabet[triple & 0x3f] : '=');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpControlServer::HandleWebSocketClient(UniqueSocket clientSocket, const HttpRequest& request)
|
||||
{
|
||||
const auto keyIt = request.headers.find("sec-websocket-key");
|
||||
if (keyIt == request.headers.end() || keyIt->second.empty())
|
||||
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Missing WebSocket key"));
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << "HTTP/1.1 101 Switching Protocols\r\n"
|
||||
<< "Upgrade: websocket\r\n"
|
||||
<< "Connection: Upgrade\r\n"
|
||||
<< "Sec-WebSocket-Accept: " << WebSocketAcceptKey(keyIt->second) << "\r\n\r\n";
|
||||
const std::string response = stream.str();
|
||||
if (send(clientSocket.get(), response.c_str(), static_cast<int>(response.size()), 0) != static_cast<int>(response.size()))
|
||||
return false;
|
||||
|
||||
u_long nonBlocking = 1;
|
||||
ioctlsocket(clientSocket.get(), FIONBIO, &nonBlocking);
|
||||
|
||||
std::thread thread([this, socket = std::move(clientSocket)]() mutable {
|
||||
WebSocketClientMain(std::move(socket));
|
||||
});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||
mClientThreads.push_back(std::move(thread));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpControlServer::WebSocketClientMain(UniqueSocket clientSocket)
|
||||
{
|
||||
std::string previousState;
|
||||
while (mRunning.load(std::memory_order_acquire))
|
||||
{
|
||||
const std::string state = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
|
||||
if (state != previousState)
|
||||
{
|
||||
if (!SendWebSocketText(clientSocket.get(), state))
|
||||
break;
|
||||
previousState = state;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||
const std::thread::id currentId = std::this_thread::get_id();
|
||||
for (auto it = mClientThreads.begin(); it != mClientThreads.end(); ++it)
|
||||
{
|
||||
if (it->get_id() != currentId)
|
||||
continue;
|
||||
mFinishedClientThreads.push_back(std::move(*it));
|
||||
mClientThreads.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HttpControlServer::JoinFinishedClientThreads()
|
||||
{
|
||||
std::vector<std::thread> finished;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||
finished.swap(mFinishedClientThreads);
|
||||
}
|
||||
for (std::thread& thread : finished)
|
||||
{
|
||||
if (thread.joinable())
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& text)
|
||||
{
|
||||
if (clientSocket == INVALID_SOCKET)
|
||||
return false;
|
||||
|
||||
std::vector<unsigned char> frame;
|
||||
frame.reserve(text.size() + 16);
|
||||
frame.push_back(0x81);
|
||||
if (text.size() <= 125)
|
||||
{
|
||||
frame.push_back(static_cast<unsigned char>(text.size()));
|
||||
}
|
||||
else if (text.size() <= 0xffff)
|
||||
{
|
||||
frame.push_back(126);
|
||||
frame.push_back(static_cast<unsigned char>((text.size() >> 8) & 0xff));
|
||||
frame.push_back(static_cast<unsigned char>(text.size() & 0xff));
|
||||
}
|
||||
else
|
||||
{
|
||||
frame.push_back(127);
|
||||
const uint64_t length = static_cast<uint64_t>(text.size());
|
||||
for (int shift = 56; shift >= 0; shift -= 8)
|
||||
frame.push_back(static_cast<unsigned char>((length >> shift) & 0xff));
|
||||
}
|
||||
frame.insert(frame.end(), text.begin(), text.end());
|
||||
|
||||
const char* data = reinterpret_cast<const char*>(frame.data());
|
||||
int remaining = static_cast<int>(frame.size());
|
||||
while (remaining > 0)
|
||||
{
|
||||
const int sent = send(clientSocket, data, remaining, 0);
|
||||
if (sent <= 0)
|
||||
{
|
||||
const int error = WSAGetLastError();
|
||||
if (error == WSAEWOULDBLOCK)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
data += sent;
|
||||
remaining -= sent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string HttpControlServer::WebSocketAcceptKey(const std::string& clientKey)
|
||||
{
|
||||
static constexpr const char* kWebSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
const std::array<uint8_t, 20> digest = Sha1(clientKey + kWebSocketGuid);
|
||||
return Base64Encode(digest.data(), digest.size());
|
||||
}
|
||||
}
|
||||
250
src/frames/InputFrameMailbox.cpp
Normal file
250
src/frames/InputFrameMailbox.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
#include "InputFrameMailbox.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
InputFrameMailboxConfig NormalizeConfig(InputFrameMailboxConfig config)
|
||||
{
|
||||
if (config.rowBytes == 0)
|
||||
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
InputFrameMailbox::InputFrameMailbox(const InputFrameMailboxConfig& config)
|
||||
{
|
||||
Configure(config);
|
||||
}
|
||||
|
||||
void InputFrameMailbox::Configure(const InputFrameMailboxConfig& config)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mConfig = NormalizeConfig(config);
|
||||
mReadyIndices.clear();
|
||||
mSlots.clear();
|
||||
mSlots.resize(mConfig.capacity);
|
||||
|
||||
const std::size_t byteCount = FrameByteCount();
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.bytes.resize(byteCount);
|
||||
slot.state = InputFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
}
|
||||
|
||||
mCounters = InputFrameMailboxMetrics();
|
||||
}
|
||||
|
||||
InputFrameMailboxConfig InputFrameMailbox::Config() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mConfig;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex)
|
||||
{
|
||||
if (bytes == nullptr || rowBytes == 0)
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mSlots.empty() || mConfig.width == 0 || mConfig.height == 0)
|
||||
return false;
|
||||
|
||||
std::size_t slotIndex = mSlots.size();
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
if (mSlots[index].state == InputFrameSlotState::Free)
|
||||
{
|
||||
slotIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIndex == mSlots.size())
|
||||
{
|
||||
if (!DropOldestReadyLocked())
|
||||
{
|
||||
++mCounters.submitMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
if (mSlots[index].state == InputFrameSlotState::Free)
|
||||
{
|
||||
slotIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIndex == mSlots.size())
|
||||
{
|
||||
++mCounters.submitMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
Slot& slot = mSlots[slotIndex];
|
||||
const std::size_t destinationRowBytes = mConfig.rowBytes;
|
||||
const std::size_t sourceRowBytes = static_cast<std::size_t>(rowBytes);
|
||||
const unsigned char* source = static_cast<const unsigned char*>(bytes);
|
||||
if (sourceRowBytes == destinationRowBytes)
|
||||
{
|
||||
std::memcpy(slot.bytes.data(), source, destinationRowBytes * static_cast<std::size_t>(mConfig.height));
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::size_t copyRowBytes = (std::min)(sourceRowBytes, destinationRowBytes);
|
||||
for (unsigned y = 0; y < mConfig.height; ++y)
|
||||
{
|
||||
std::memcpy(
|
||||
slot.bytes.data() + static_cast<std::size_t>(y) * destinationRowBytes,
|
||||
source + static_cast<std::size_t>(y) * sourceRowBytes,
|
||||
copyRowBytes);
|
||||
}
|
||||
}
|
||||
|
||||
slot.state = InputFrameSlotState::Ready;
|
||||
slot.frameIndex = frameIndex;
|
||||
++slot.generation;
|
||||
mReadyIndices.push_back(slotIndex);
|
||||
TrimReadyFramesLocked();
|
||||
++mCounters.submittedFrames;
|
||||
mCounters.latestFrameIndex = frameIndex;
|
||||
mCounters.hasSubmittedFrame = true;
|
||||
mLatestSubmitTime = std::chrono::steady_clock::now();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
while (!mReadyIndices.empty())
|
||||
{
|
||||
const std::size_t index = mReadyIndices.front();
|
||||
mReadyIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||
continue;
|
||||
|
||||
mSlots[index].state = InputFrameSlotState::Reading;
|
||||
FillFrameLocked(index, frame);
|
||||
++mCounters.consumedFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
frame = InputFrame();
|
||||
++mCounters.consumeMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::Release(const InputFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!IsValidLocked(frame))
|
||||
return false;
|
||||
|
||||
Slot& slot = mSlots[frame.index];
|
||||
if (slot.state != InputFrameSlotState::Reading)
|
||||
return false;
|
||||
|
||||
slot.state = InputFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
return true;
|
||||
}
|
||||
|
||||
void InputFrameMailbox::Clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mReadyIndices.clear();
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.state = InputFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
}
|
||||
}
|
||||
|
||||
InputFrameMailboxMetrics InputFrameMailbox::Metrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
InputFrameMailboxMetrics metrics = mCounters;
|
||||
metrics.capacity = mSlots.size();
|
||||
if (metrics.hasSubmittedFrame)
|
||||
{
|
||||
metrics.latestFrameAgeMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||
std::chrono::steady_clock::now() - mLatestSubmitTime).count();
|
||||
}
|
||||
|
||||
for (const Slot& slot : mSlots)
|
||||
{
|
||||
switch (slot.state)
|
||||
{
|
||||
case InputFrameSlotState::Free:
|
||||
++metrics.freeCount;
|
||||
break;
|
||||
case InputFrameSlotState::Ready:
|
||||
++metrics.readyCount;
|
||||
break;
|
||||
case InputFrameSlotState::Reading:
|
||||
++metrics.readingCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::IsValidLocked(const InputFrame& frame) const
|
||||
{
|
||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||
}
|
||||
|
||||
void InputFrameMailbox::FillFrameLocked(std::size_t index, InputFrame& frame) const
|
||||
{
|
||||
const Slot& slot = mSlots[index];
|
||||
frame.bytes = slot.bytes.empty() ? nullptr : slot.bytes.data();
|
||||
frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
||||
frame.width = mConfig.width;
|
||||
frame.height = mConfig.height;
|
||||
frame.pixelFormat = mConfig.pixelFormat;
|
||||
frame.index = index;
|
||||
frame.generation = slot.generation;
|
||||
frame.frameIndex = slot.frameIndex;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::DropOldestReadyLocked()
|
||||
{
|
||||
while (!mReadyIndices.empty())
|
||||
{
|
||||
const std::size_t index = mReadyIndices.front();
|
||||
mReadyIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||
continue;
|
||||
|
||||
mSlots[index].state = InputFrameSlotState::Free;
|
||||
mSlots[index].frameIndex = 0;
|
||||
++mSlots[index].generation;
|
||||
++mCounters.droppedReadyFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void InputFrameMailbox::TrimReadyFramesLocked()
|
||||
{
|
||||
if (mConfig.maxReadyFrames == 0)
|
||||
return;
|
||||
while (mReadyIndices.size() > mConfig.maxReadyFrames)
|
||||
DropOldestReadyLocked();
|
||||
}
|
||||
|
||||
std::size_t InputFrameMailbox::FrameByteCount() const
|
||||
{
|
||||
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||
}
|
||||
93
src/frames/InputFrameMailbox.h
Normal file
93
src/frames/InputFrameMailbox.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
enum class InputFrameSlotState
|
||||
{
|
||||
Free,
|
||||
Ready,
|
||||
Reading
|
||||
};
|
||||
|
||||
struct InputFrameMailboxConfig
|
||||
{
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
unsigned rowBytes = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t maxReadyFrames = 0;
|
||||
};
|
||||
|
||||
struct InputFrame
|
||||
{
|
||||
const void* bytes = nullptr;
|
||||
long rowBytes = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
std::size_t index = 0;
|
||||
uint64_t generation = 0;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
struct InputFrameMailboxMetrics
|
||||
{
|
||||
std::size_t capacity = 0;
|
||||
std::size_t freeCount = 0;
|
||||
std::size_t readyCount = 0;
|
||||
std::size_t readingCount = 0;
|
||||
uint64_t submittedFrames = 0;
|
||||
uint64_t consumedFrames = 0;
|
||||
uint64_t droppedReadyFrames = 0;
|
||||
uint64_t submitMisses = 0;
|
||||
uint64_t consumeMisses = 0;
|
||||
uint64_t latestFrameIndex = 0;
|
||||
bool hasSubmittedFrame = false;
|
||||
double latestFrameAgeMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
class InputFrameMailbox
|
||||
{
|
||||
public:
|
||||
InputFrameMailbox() = default;
|
||||
explicit InputFrameMailbox(const InputFrameMailboxConfig& config);
|
||||
|
||||
void Configure(const InputFrameMailboxConfig& config);
|
||||
InputFrameMailboxConfig Config() const;
|
||||
|
||||
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
||||
bool TryAcquireOldest(InputFrame& frame);
|
||||
bool Release(const InputFrame& frame);
|
||||
void Clear();
|
||||
InputFrameMailboxMetrics Metrics() const;
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
std::vector<unsigned char> bytes;
|
||||
InputFrameSlotState state = InputFrameSlotState::Free;
|
||||
uint64_t frameIndex = 0;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
bool IsValidLocked(const InputFrame& frame) const;
|
||||
void FillFrameLocked(std::size_t index, InputFrame& frame) const;
|
||||
bool DropOldestReadyLocked();
|
||||
void TrimReadyFramesLocked();
|
||||
std::size_t FrameByteCount() const;
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
InputFrameMailboxConfig mConfig;
|
||||
std::vector<Slot> mSlots;
|
||||
std::deque<std::size_t> mReadyIndices;
|
||||
InputFrameMailboxMetrics mCounters;
|
||||
std::chrono::steady_clock::time_point mLatestSubmitTime;
|
||||
};
|
||||
299
src/frames/SystemFrameExchange.cpp
Normal file
299
src/frames/SystemFrameExchange.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
#include "SystemFrameExchange.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
SystemFrameExchangeConfig NormalizeConfig(SystemFrameExchangeConfig config)
|
||||
{
|
||||
if (config.rowBytes == 0)
|
||||
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
SystemFrameExchange::SystemFrameExchange(const SystemFrameExchangeConfig& config)
|
||||
{
|
||||
Configure(config);
|
||||
}
|
||||
|
||||
void SystemFrameExchange::Configure(const SystemFrameExchangeConfig& config)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mConfig = NormalizeConfig(config);
|
||||
mCompletedIndices.clear();
|
||||
mSlots.clear();
|
||||
mSlots.resize(mConfig.capacity);
|
||||
|
||||
const std::size_t byteCount = FrameByteCount();
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.bytes.resize(byteCount);
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
}
|
||||
|
||||
mCounters = SystemFrameExchangeMetrics();
|
||||
mCondition.notify_all();
|
||||
}
|
||||
|
||||
SystemFrameExchangeConfig SystemFrameExchange::Config() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mConfig;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::AcquireForRender(SystemFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!AcquireFreeLocked(frame))
|
||||
{
|
||||
frame = SystemFrame();
|
||||
++mCounters.acquireMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
++mCounters.acquiredFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::PublishCompleted(const SystemFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!IsValidLocked(frame))
|
||||
return false;
|
||||
|
||||
Slot& slot = mSlots[frame.index];
|
||||
if (slot.state != SystemFrameSlotState::Rendering)
|
||||
return false;
|
||||
|
||||
slot.state = SystemFrameSlotState::Completed;
|
||||
slot.frameIndex = frame.frameIndex;
|
||||
mCompletedIndices.push_back(frame.index);
|
||||
TrimCompletedLocked();
|
||||
++mCounters.completedFrames;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::ConsumeCompletedForSchedule(SystemFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
while (!mCompletedIndices.empty())
|
||||
{
|
||||
const std::size_t index = mCompletedIndices.front();
|
||||
mCompletedIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != SystemFrameSlotState::Completed)
|
||||
continue;
|
||||
|
||||
mSlots[index].state = SystemFrameSlotState::Scheduled;
|
||||
FillFrameLocked(index, frame);
|
||||
++mCounters.scheduledFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
frame = SystemFrame();
|
||||
++mCounters.completedPollMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::ReleaseScheduledByBytes(void* bytes)
|
||||
{
|
||||
if (bytes == nullptr)
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
Slot& slot = mSlots[index];
|
||||
if (slot.bytes.empty() || slot.bytes.data() != bytes)
|
||||
continue;
|
||||
if (slot.state != SystemFrameSlotState::Scheduled)
|
||||
return false;
|
||||
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
return mCondition.wait_for(lock, timeout, [&]() {
|
||||
return CompletedCountLocked() >= targetDepth;
|
||||
});
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::WaitForStableCompletedDepth(
|
||||
std::size_t targetDepth,
|
||||
std::chrono::milliseconds stableDuration,
|
||||
std::chrono::milliseconds timeout)
|
||||
{
|
||||
if (targetDepth == 0)
|
||||
return true;
|
||||
|
||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
bool stableWindowStarted = false;
|
||||
std::chrono::steady_clock::time_point stableSince;
|
||||
|
||||
while (true)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline)
|
||||
return false;
|
||||
|
||||
if (CompletedCountLocked() >= targetDepth)
|
||||
{
|
||||
if (stableDuration <= std::chrono::milliseconds::zero())
|
||||
return true;
|
||||
|
||||
if (!stableWindowStarted)
|
||||
{
|
||||
stableSince = now;
|
||||
stableWindowStarted = true;
|
||||
}
|
||||
|
||||
const auto stableDeadline = stableSince + stableDuration;
|
||||
if (now >= stableDeadline)
|
||||
return true;
|
||||
|
||||
mCondition.wait_until(lock, stableDeadline < deadline ? stableDeadline : deadline);
|
||||
continue;
|
||||
}
|
||||
|
||||
stableWindowStarted = false;
|
||||
mCondition.wait_until(lock, deadline, [&]() {
|
||||
return CompletedCountLocked() >= targetDepth;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void SystemFrameExchange::Clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mCompletedIndices.clear();
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
}
|
||||
mCondition.notify_all();
|
||||
}
|
||||
|
||||
SystemFrameExchangeMetrics SystemFrameExchange::Metrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
SystemFrameExchangeMetrics metrics = mCounters;
|
||||
metrics.capacity = mSlots.size();
|
||||
metrics.completedDepth = mCompletedIndices.size();
|
||||
|
||||
for (const Slot& slot : mSlots)
|
||||
{
|
||||
switch (slot.state)
|
||||
{
|
||||
case SystemFrameSlotState::Free:
|
||||
++metrics.freeCount;
|
||||
break;
|
||||
case SystemFrameSlotState::Rendering:
|
||||
++metrics.renderingCount;
|
||||
break;
|
||||
case SystemFrameSlotState::Completed:
|
||||
++metrics.completedCount;
|
||||
break;
|
||||
case SystemFrameSlotState::Scheduled:
|
||||
++metrics.scheduledCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::AcquireFreeLocked(SystemFrame& frame)
|
||||
{
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
Slot& slot = mSlots[index];
|
||||
if (slot.state != SystemFrameSlotState::Free)
|
||||
continue;
|
||||
|
||||
slot.state = SystemFrameSlotState::Rendering;
|
||||
++slot.generation;
|
||||
FillFrameLocked(index, frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::DropOldestCompletedLocked()
|
||||
{
|
||||
while (!mCompletedIndices.empty())
|
||||
{
|
||||
const std::size_t index = mCompletedIndices.front();
|
||||
mCompletedIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != SystemFrameSlotState::Completed)
|
||||
continue;
|
||||
|
||||
Slot& slot = mSlots[index];
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
++mCounters.completedDrops;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SystemFrameExchange::TrimCompletedLocked()
|
||||
{
|
||||
if (mConfig.maxCompletedFrames == 0)
|
||||
return;
|
||||
while (CompletedCountLocked() > mConfig.maxCompletedFrames)
|
||||
{
|
||||
if (!DropOldestCompletedLocked())
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
||||
{
|
||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||
}
|
||||
|
||||
void SystemFrameExchange::FillFrameLocked(std::size_t index, SystemFrame& frame)
|
||||
{
|
||||
Slot& slot = mSlots[index];
|
||||
frame.bytes = slot.bytes.empty() ? nullptr : slot.bytes.data();
|
||||
frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
||||
frame.width = mConfig.width;
|
||||
frame.height = mConfig.height;
|
||||
frame.pixelFormat = mConfig.pixelFormat;
|
||||
frame.index = index;
|
||||
frame.generation = slot.generation;
|
||||
frame.frameIndex = slot.frameIndex;
|
||||
}
|
||||
|
||||
std::size_t SystemFrameExchange::CompletedCountLocked() const
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (const Slot& slot : mSlots)
|
||||
{
|
||||
if (slot.state == SystemFrameSlotState::Completed)
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::size_t SystemFrameExchange::FrameByteCount() const
|
||||
{
|
||||
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||
}
|
||||
56
src/frames/SystemFrameExchange.h
Normal file
56
src/frames/SystemFrameExchange.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "SystemFrameTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class SystemFrameExchange
|
||||
{
|
||||
public:
|
||||
SystemFrameExchange() = default;
|
||||
explicit SystemFrameExchange(const SystemFrameExchangeConfig& config);
|
||||
|
||||
void Configure(const SystemFrameExchangeConfig& config);
|
||||
SystemFrameExchangeConfig Config() const;
|
||||
|
||||
bool AcquireForRender(SystemFrame& frame);
|
||||
bool PublishCompleted(const SystemFrame& frame);
|
||||
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
||||
bool ReleaseScheduledByBytes(void* bytes);
|
||||
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout);
|
||||
bool WaitForStableCompletedDepth(
|
||||
std::size_t targetDepth,
|
||||
std::chrono::milliseconds stableDuration,
|
||||
std::chrono::milliseconds timeout);
|
||||
void Clear();
|
||||
|
||||
SystemFrameExchangeMetrics Metrics() const;
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
std::vector<unsigned char> bytes;
|
||||
SystemFrameSlotState state = SystemFrameSlotState::Free;
|
||||
uint64_t generation = 1;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
bool AcquireFreeLocked(SystemFrame& frame);
|
||||
bool DropOldestCompletedLocked();
|
||||
void TrimCompletedLocked();
|
||||
bool IsValidLocked(const SystemFrame& frame) const;
|
||||
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
||||
std::size_t CompletedCountLocked() const;
|
||||
std::size_t FrameByteCount() const;
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
SystemFrameExchangeConfig mConfig;
|
||||
std::vector<Slot> mSlots;
|
||||
std::deque<std::size_t> mCompletedIndices;
|
||||
SystemFrameExchangeMetrics mCounters;
|
||||
};
|
||||
52
src/frames/SystemFrameTypes.h
Normal file
52
src/frames/SystemFrameTypes.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
enum class SystemFrameSlotState
|
||||
{
|
||||
Free,
|
||||
Rendering,
|
||||
Completed,
|
||||
Scheduled
|
||||
};
|
||||
|
||||
struct SystemFrameExchangeConfig
|
||||
{
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
unsigned rowBytes = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t maxCompletedFrames = 0;
|
||||
};
|
||||
|
||||
struct SystemFrame
|
||||
{
|
||||
void* bytes = nullptr;
|
||||
long rowBytes = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
std::size_t index = 0;
|
||||
uint64_t generation = 0;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
struct SystemFrameExchangeMetrics
|
||||
{
|
||||
std::size_t capacity = 0;
|
||||
std::size_t freeCount = 0;
|
||||
std::size_t renderingCount = 0;
|
||||
std::size_t completedCount = 0;
|
||||
std::size_t scheduledCount = 0;
|
||||
std::size_t completedDepth = 0;
|
||||
uint64_t acquiredFrames = 0;
|
||||
uint64_t completedFrames = 0;
|
||||
uint64_t scheduledFrames = 0;
|
||||
uint64_t completedDrops = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
uint64_t completedPollMisses = 0;
|
||||
};
|
||||
235
src/json/JsonWriter.cpp
Normal file
235
src/json/JsonWriter.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
#include "JsonWriter.h"
|
||||
|
||||
#include <iomanip>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr int kMaxDepth = 32;
|
||||
|
||||
void AppendHexEscape(std::ostringstream& stream, unsigned char value)
|
||||
{
|
||||
stream << "\\u"
|
||||
<< std::hex << std::uppercase << std::setw(4) << std::setfill('0')
|
||||
<< static_cast<int>(value)
|
||||
<< std::dec << std::nouppercase << std::setfill(' ');
|
||||
}
|
||||
}
|
||||
|
||||
void JsonWriter::BeginObject()
|
||||
{
|
||||
BeginValue();
|
||||
mStream << "{";
|
||||
PushScope(ScopeKind::Object);
|
||||
}
|
||||
|
||||
void JsonWriter::EndObject()
|
||||
{
|
||||
PopScope(ScopeKind::Object);
|
||||
mStream << "}";
|
||||
}
|
||||
|
||||
void JsonWriter::BeginArray()
|
||||
{
|
||||
BeginValue();
|
||||
mStream << "[";
|
||||
PushScope(ScopeKind::Array);
|
||||
}
|
||||
|
||||
void JsonWriter::EndArray()
|
||||
{
|
||||
PopScope(ScopeKind::Array);
|
||||
mStream << "]";
|
||||
}
|
||||
|
||||
void JsonWriter::Key(const std::string& name)
|
||||
{
|
||||
BeginKey();
|
||||
mStream << "\"" << EscapeString(name) << "\":";
|
||||
}
|
||||
|
||||
void JsonWriter::String(const std::string& value)
|
||||
{
|
||||
BeginValue();
|
||||
mStream << "\"" << EscapeString(value) << "\"";
|
||||
}
|
||||
|
||||
void JsonWriter::Bool(bool value)
|
||||
{
|
||||
BeginValue();
|
||||
mStream << (value ? "true" : "false");
|
||||
}
|
||||
|
||||
void JsonWriter::Null()
|
||||
{
|
||||
BeginValue();
|
||||
mStream << "null";
|
||||
}
|
||||
|
||||
void JsonWriter::Int(int64_t value)
|
||||
{
|
||||
BeginValue();
|
||||
mStream << value;
|
||||
}
|
||||
|
||||
void JsonWriter::UInt(uint64_t value)
|
||||
{
|
||||
BeginValue();
|
||||
mStream << value;
|
||||
}
|
||||
|
||||
void JsonWriter::Double(double value)
|
||||
{
|
||||
BeginValue();
|
||||
mStream << std::setprecision(15) << value;
|
||||
}
|
||||
|
||||
void JsonWriter::KeyString(const std::string& name, const std::string& value)
|
||||
{
|
||||
Key(name);
|
||||
String(value);
|
||||
}
|
||||
|
||||
void JsonWriter::KeyBool(const std::string& name, bool value)
|
||||
{
|
||||
Key(name);
|
||||
Bool(value);
|
||||
}
|
||||
|
||||
void JsonWriter::KeyNull(const std::string& name)
|
||||
{
|
||||
Key(name);
|
||||
Null();
|
||||
}
|
||||
|
||||
void JsonWriter::KeyInt(const std::string& name, int64_t value)
|
||||
{
|
||||
Key(name);
|
||||
Int(value);
|
||||
}
|
||||
|
||||
void JsonWriter::KeyUInt(const std::string& name, uint64_t value)
|
||||
{
|
||||
Key(name);
|
||||
UInt(value);
|
||||
}
|
||||
|
||||
void JsonWriter::KeyDouble(const std::string& name, double value)
|
||||
{
|
||||
Key(name);
|
||||
Double(value);
|
||||
}
|
||||
|
||||
std::string JsonWriter::StringValue() const
|
||||
{
|
||||
if (mScopeDepth != 0)
|
||||
throw std::logic_error("JSON document has unclosed scopes.");
|
||||
return mStream.str();
|
||||
}
|
||||
|
||||
void JsonWriter::Reset()
|
||||
{
|
||||
mStream.str(std::string());
|
||||
mStream.clear();
|
||||
mScopeDepth = 0;
|
||||
}
|
||||
|
||||
std::string JsonWriter::EscapeString(const std::string& value)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
for (unsigned char character : value)
|
||||
{
|
||||
switch (character)
|
||||
{
|
||||
case '"':
|
||||
stream << "\\\"";
|
||||
break;
|
||||
case '\\':
|
||||
stream << "\\\\";
|
||||
break;
|
||||
case '\b':
|
||||
stream << "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
stream << "\\f";
|
||||
break;
|
||||
case '\n':
|
||||
stream << "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
stream << "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
stream << "\\t";
|
||||
break;
|
||||
default:
|
||||
if (character < 0x20)
|
||||
AppendHexEscape(stream, character);
|
||||
else
|
||||
stream << character;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
void JsonWriter::BeginValue()
|
||||
{
|
||||
if (mScopeDepth == 0)
|
||||
return;
|
||||
|
||||
Scope& scope = mScopes[mScopeDepth - 1];
|
||||
if (scope.kind == ScopeKind::Object)
|
||||
{
|
||||
if (!scope.expectingValue)
|
||||
throw std::logic_error("JSON object value must follow a key.");
|
||||
scope.expectingValue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scope.first)
|
||||
mStream << ",";
|
||||
scope.first = false;
|
||||
}
|
||||
|
||||
void JsonWriter::BeginKey()
|
||||
{
|
||||
if (mScopeDepth == 0)
|
||||
throw std::logic_error("JSON key cannot be written outside an object.");
|
||||
|
||||
Scope& scope = mScopes[mScopeDepth - 1];
|
||||
if (scope.kind != ScopeKind::Object)
|
||||
throw std::logic_error("JSON key cannot be written inside an array.");
|
||||
if (scope.expectingValue)
|
||||
throw std::logic_error("JSON object key cannot be written before its previous value.");
|
||||
|
||||
if (!scope.first)
|
||||
mStream << ",";
|
||||
scope.first = false;
|
||||
scope.expectingValue = true;
|
||||
}
|
||||
|
||||
void JsonWriter::PushScope(ScopeKind kind)
|
||||
{
|
||||
if (mScopeDepth >= kMaxDepth)
|
||||
throw std::logic_error("JSON nesting is too deep.");
|
||||
|
||||
mScopes[mScopeDepth++] = Scope{ kind, true, false };
|
||||
}
|
||||
|
||||
void JsonWriter::PopScope(ScopeKind kind)
|
||||
{
|
||||
if (mScopeDepth == 0)
|
||||
throw std::logic_error("JSON scope underflow.");
|
||||
|
||||
Scope& scope = mScopes[mScopeDepth - 1];
|
||||
if (scope.kind != kind)
|
||||
throw std::logic_error("JSON scope kind mismatch.");
|
||||
if (scope.expectingValue)
|
||||
throw std::logic_error("JSON object key is missing a value.");
|
||||
|
||||
--mScopeDepth;
|
||||
}
|
||||
}
|
||||
60
src/json/JsonWriter.h
Normal file
60
src/json/JsonWriter.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
class JsonWriter
|
||||
{
|
||||
public:
|
||||
void BeginObject();
|
||||
void EndObject();
|
||||
void BeginArray();
|
||||
void EndArray();
|
||||
|
||||
void Key(const std::string& name);
|
||||
void String(const std::string& value);
|
||||
void Bool(bool value);
|
||||
void Null();
|
||||
void Int(int64_t value);
|
||||
void UInt(uint64_t value);
|
||||
void Double(double value);
|
||||
|
||||
void KeyString(const std::string& name, const std::string& value);
|
||||
void KeyBool(const std::string& name, bool value);
|
||||
void KeyNull(const std::string& name);
|
||||
void KeyInt(const std::string& name, int64_t value);
|
||||
void KeyUInt(const std::string& name, uint64_t value);
|
||||
void KeyDouble(const std::string& name, double value);
|
||||
|
||||
std::string StringValue() const;
|
||||
void Reset();
|
||||
|
||||
static std::string EscapeString(const std::string& value);
|
||||
|
||||
private:
|
||||
enum class ScopeKind
|
||||
{
|
||||
Object,
|
||||
Array
|
||||
};
|
||||
|
||||
struct Scope
|
||||
{
|
||||
ScopeKind kind = ScopeKind::Object;
|
||||
bool first = true;
|
||||
bool expectingValue = false;
|
||||
};
|
||||
|
||||
void BeginValue();
|
||||
void BeginKey();
|
||||
void PushScope(ScopeKind kind);
|
||||
void PopScope(ScopeKind kind);
|
||||
|
||||
std::ostringstream mStream;
|
||||
Scope mScopes[32];
|
||||
int mScopeDepth = 0;
|
||||
};
|
||||
}
|
||||
283
src/logging/Logger.cpp
Normal file
283
src/logging/Logger.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include "Logger.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
int Rank(LogLevel level)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel::Log:
|
||||
return 0;
|
||||
case LogLevel::Warning:
|
||||
return 1;
|
||||
case LogLevel::Error:
|
||||
return 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::string FormatTimestamp(std::chrono::system_clock::time_point timestamp)
|
||||
{
|
||||
const std::time_t time = std::chrono::system_clock::to_time_t(timestamp);
|
||||
std::tm localTime = {};
|
||||
localtime_s(&localTime, &time);
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S");
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string FormatRecord(const Logger::Record& record)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << FormatTimestamp(record.timestamp)
|
||||
<< " [" << LogLevelName(record.level) << "]"
|
||||
<< " [" << record.subsystem << "] "
|
||||
<< record.message;
|
||||
return stream.str();
|
||||
}
|
||||
}
|
||||
|
||||
const char* LogLevelName(LogLevel level)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel::Log:
|
||||
return "log";
|
||||
case LogLevel::Warning:
|
||||
return "warning";
|
||||
case LogLevel::Error:
|
||||
return "error";
|
||||
default:
|
||||
return "log";
|
||||
}
|
||||
}
|
||||
|
||||
Logger& Logger::Instance()
|
||||
{
|
||||
static Logger logger;
|
||||
return logger;
|
||||
}
|
||||
|
||||
Logger::~Logger()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void Logger::Start(LoggerConfig config)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mRunning)
|
||||
return;
|
||||
|
||||
mConfig = config;
|
||||
mStopping = false;
|
||||
mRunning = true;
|
||||
OpenFileSink();
|
||||
{
|
||||
std::lock_guard<std::mutex> countersLock(mCountersMutex);
|
||||
mCounters = LoggerCounters();
|
||||
}
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
}
|
||||
|
||||
void Logger::Stop()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mRunning && !mThread.joinable())
|
||||
return;
|
||||
mStopping = true;
|
||||
}
|
||||
mCondition.notify_all();
|
||||
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
CloseFileSink();
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
void Logger::Write(LogLevel level, const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
Enqueue(Record{ std::chrono::system_clock::now(), std::this_thread::get_id(), level, subsystem, message }, true);
|
||||
}
|
||||
|
||||
bool Logger::TryWrite(LogLevel level, const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
return Enqueue(Record{ std::chrono::system_clock::now(), std::this_thread::get_id(), level, subsystem, message }, false);
|
||||
}
|
||||
|
||||
void Logger::Log(const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
Write(LogLevel::Log, subsystem, message);
|
||||
}
|
||||
|
||||
void Logger::Warning(const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
Write(LogLevel::Warning, subsystem, message);
|
||||
}
|
||||
|
||||
void Logger::Error(const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
Write(LogLevel::Error, subsystem, message);
|
||||
}
|
||||
|
||||
LoggerCounters Logger::Counters() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mCountersMutex);
|
||||
return mCounters;
|
||||
}
|
||||
|
||||
bool Logger::IsRunning() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mRunning;
|
||||
}
|
||||
|
||||
bool Logger::ShouldWrite(LogLevel level) const
|
||||
{
|
||||
return Rank(level) >= Rank(mConfig.minimumLevel);
|
||||
}
|
||||
|
||||
bool Logger::Enqueue(Record record, bool block)
|
||||
{
|
||||
if (!ShouldWrite(record.level))
|
||||
return true;
|
||||
|
||||
std::unique_lock<std::mutex> lock(mMutex, std::defer_lock);
|
||||
if (block)
|
||||
{
|
||||
lock.lock();
|
||||
}
|
||||
else if (!lock.try_lock())
|
||||
{
|
||||
std::lock_guard<std::mutex> countersLock(mCountersMutex);
|
||||
++mCounters.dropped;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mRunning)
|
||||
{
|
||||
lock.unlock();
|
||||
WriteRecord(record);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mQueue.size() >= mConfig.maxQueuedMessages)
|
||||
{
|
||||
std::lock_guard<std::mutex> countersLock(mCountersMutex);
|
||||
++mCounters.dropped;
|
||||
return false;
|
||||
}
|
||||
|
||||
mQueue.push_back(std::move(record));
|
||||
{
|
||||
std::lock_guard<std::mutex> countersLock(mCountersMutex);
|
||||
++mCounters.queued;
|
||||
}
|
||||
lock.unlock();
|
||||
mCondition.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Logger::ThreadMain()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
Record record;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
mCondition.wait(lock, [this]() {
|
||||
return mStopping || !mQueue.empty();
|
||||
});
|
||||
|
||||
if (mQueue.empty())
|
||||
{
|
||||
if (mStopping)
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
record = std::move(mQueue.front());
|
||||
mQueue.pop_front();
|
||||
}
|
||||
WriteRecord(record);
|
||||
}
|
||||
}
|
||||
|
||||
void Logger::WriteRecord(const Record& record)
|
||||
{
|
||||
const std::string line = FormatRecord(record);
|
||||
|
||||
if (mConfig.writeToDebugOutput)
|
||||
OutputDebugStringA((line + "\n").c_str());
|
||||
|
||||
if (mConfig.writeToConsole)
|
||||
{
|
||||
if (record.level == LogLevel::Error)
|
||||
std::cerr << line << "\n";
|
||||
else
|
||||
std::cout << line << "\n";
|
||||
}
|
||||
if (mFile.is_open())
|
||||
{
|
||||
mFile << line << "\n";
|
||||
mFile.flush();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mCountersMutex);
|
||||
++mCounters.written;
|
||||
}
|
||||
|
||||
void Logger::OpenFileSink()
|
||||
{
|
||||
CloseFileSink();
|
||||
if (!mConfig.writeToFile || mConfig.filePath.empty())
|
||||
return;
|
||||
|
||||
std::error_code error;
|
||||
const std::filesystem::path logPath(mConfig.filePath);
|
||||
if (logPath.has_parent_path())
|
||||
std::filesystem::create_directories(logPath.parent_path(), error);
|
||||
mFile.open(logPath, std::ios::out | std::ios::app);
|
||||
}
|
||||
|
||||
void Logger::CloseFileSink()
|
||||
{
|
||||
if (mFile.is_open())
|
||||
mFile.close();
|
||||
}
|
||||
|
||||
bool TryLog(LogLevel level, const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
return Logger::Instance().TryWrite(level, subsystem, message);
|
||||
}
|
||||
|
||||
void Log(const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
Logger::Instance().Log(subsystem, message);
|
||||
}
|
||||
|
||||
void LogWarning(const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
Logger::Instance().Warning(subsystem, message);
|
||||
}
|
||||
|
||||
void LogError(const std::string& subsystem, const std::string& message)
|
||||
{
|
||||
Logger::Instance().Error(subsystem, message);
|
||||
}
|
||||
}
|
||||
101
src/logging/Logger.h
Normal file
101
src/logging/Logger.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
enum class LogLevel
|
||||
{
|
||||
Log,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
|
||||
struct LoggerConfig
|
||||
{
|
||||
LogLevel minimumLevel = LogLevel::Log;
|
||||
bool writeToConsole = true;
|
||||
bool writeToDebugOutput = true;
|
||||
bool writeToFile = true;
|
||||
std::string filePath = "logs/render-cadence-compositor.log";
|
||||
std::size_t maxQueuedMessages = 1024;
|
||||
};
|
||||
|
||||
struct LoggerCounters
|
||||
{
|
||||
uint64_t queued = 0;
|
||||
uint64_t written = 0;
|
||||
uint64_t dropped = 0;
|
||||
};
|
||||
|
||||
const char* LogLevelName(LogLevel level);
|
||||
|
||||
class Logger
|
||||
{
|
||||
public:
|
||||
struct Record
|
||||
{
|
||||
std::chrono::system_clock::time_point timestamp;
|
||||
std::thread::id threadId;
|
||||
LogLevel level = LogLevel::Log;
|
||||
std::string subsystem;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
static Logger& Instance();
|
||||
|
||||
Logger(const Logger&) = delete;
|
||||
Logger& operator=(const Logger&) = delete;
|
||||
|
||||
~Logger();
|
||||
|
||||
void Start(LoggerConfig config = LoggerConfig());
|
||||
void Stop();
|
||||
|
||||
void Write(LogLevel level, const std::string& subsystem, const std::string& message);
|
||||
bool TryWrite(LogLevel level, const std::string& subsystem, const std::string& message);
|
||||
|
||||
void Log(const std::string& subsystem, const std::string& message);
|
||||
void Warning(const std::string& subsystem, const std::string& message);
|
||||
void Error(const std::string& subsystem, const std::string& message);
|
||||
|
||||
LoggerCounters Counters() const;
|
||||
bool IsRunning() const;
|
||||
|
||||
private:
|
||||
Logger() = default;
|
||||
|
||||
bool ShouldWrite(LogLevel level) const;
|
||||
bool Enqueue(Record record, bool block);
|
||||
void ThreadMain();
|
||||
void WriteRecord(const Record& record);
|
||||
void OpenFileSink();
|
||||
void CloseFileSink();
|
||||
|
||||
LoggerConfig mConfig;
|
||||
std::ofstream mFile;
|
||||
std::thread mThread;
|
||||
mutable std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
std::deque<Record> mQueue;
|
||||
bool mStopping = false;
|
||||
bool mRunning = false;
|
||||
|
||||
mutable std::mutex mCountersMutex;
|
||||
LoggerCounters mCounters;
|
||||
};
|
||||
|
||||
bool TryLog(LogLevel level, const std::string& subsystem, const std::string& message);
|
||||
void Log(const std::string& subsystem, const std::string& message);
|
||||
void LogWarning(const std::string& subsystem, const std::string& message);
|
||||
void LogError(const std::string& subsystem, const std::string& message);
|
||||
}
|
||||
134
src/platform/HiddenGlWindow.cpp
Normal file
134
src/platform/HiddenGlWindow.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
#include "HiddenGlWindow.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr const char* kWindowClassName = "RenderCadenceCompositorHiddenGlWindow";
|
||||
}
|
||||
|
||||
HiddenGlWindow::~HiddenGlWindow()
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
|
||||
{
|
||||
return CreateShared(width, height, nullptr, nullptr, error);
|
||||
}
|
||||
|
||||
bool HiddenGlWindow::CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error)
|
||||
{
|
||||
Destroy();
|
||||
|
||||
mInstance = GetModuleHandle(nullptr);
|
||||
|
||||
WNDCLASSA wc = {};
|
||||
wc.style = CS_OWNDC;
|
||||
wc.lpfnWndProc = HiddenGlWindow::WindowProc;
|
||||
wc.hInstance = mInstance;
|
||||
wc.lpszClassName = kWindowClassName;
|
||||
|
||||
mClassAtom = RegisterClassA(&wc);
|
||||
if (mClassAtom == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
|
||||
{
|
||||
error = "RegisterClassA failed for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mWindow = CreateWindowA(
|
||||
kWindowClassName,
|
||||
"Render Cadence Compositor Hidden GL",
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
static_cast<int>(width),
|
||||
static_cast<int>(height),
|
||||
nullptr,
|
||||
nullptr,
|
||||
mInstance,
|
||||
nullptr);
|
||||
if (!mWindow)
|
||||
{
|
||||
error = "CreateWindowA failed for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mDc = GetDC(mWindow);
|
||||
if (!mDc)
|
||||
{
|
||||
error = "GetDC failed for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
PIXELFORMATDESCRIPTOR pfd = {};
|
||||
pfd.nSize = sizeof(pfd);
|
||||
pfd.nVersion = 1;
|
||||
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
|
||||
pfd.iPixelType = PFD_TYPE_RGBA;
|
||||
pfd.cColorBits = 32;
|
||||
pfd.cDepthBits = 0;
|
||||
pfd.iLayerType = PFD_MAIN_PLANE;
|
||||
|
||||
int pixelFormat = 0;
|
||||
if (sharedDeviceContext != nullptr)
|
||||
pixelFormat = GetPixelFormat(sharedDeviceContext);
|
||||
if (pixelFormat == 0)
|
||||
pixelFormat = ChoosePixelFormat(mDc, &pfd);
|
||||
if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd))
|
||||
{
|
||||
error = "Could not choose/set pixel format for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mGlrc = wglCreateContext(mDc);
|
||||
if (!mGlrc)
|
||||
{
|
||||
error = "wglCreateContext failed for hidden OpenGL window.";
|
||||
return false;
|
||||
}
|
||||
if (sharedContext != nullptr && wglShareLists(sharedContext, mGlrc) != TRUE)
|
||||
{
|
||||
error = "wglShareLists failed for hidden OpenGL shared context.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HiddenGlWindow::MakeCurrent() const
|
||||
{
|
||||
return mDc != nullptr && mGlrc != nullptr && wglMakeCurrent(mDc, mGlrc) == TRUE;
|
||||
}
|
||||
|
||||
void HiddenGlWindow::ClearCurrent() const
|
||||
{
|
||||
wglMakeCurrent(nullptr, nullptr);
|
||||
}
|
||||
|
||||
void HiddenGlWindow::Destroy()
|
||||
{
|
||||
ClearCurrent();
|
||||
|
||||
if (mGlrc)
|
||||
{
|
||||
wglDeleteContext(mGlrc);
|
||||
mGlrc = nullptr;
|
||||
}
|
||||
if (mWindow && mDc)
|
||||
{
|
||||
ReleaseDC(mWindow, mDc);
|
||||
mDc = nullptr;
|
||||
}
|
||||
if (mWindow)
|
||||
{
|
||||
DestroyWindow(mWindow);
|
||||
mWindow = nullptr;
|
||||
}
|
||||
|
||||
mInstance = nullptr;
|
||||
mClassAtom = 0;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK HiddenGlWindow::WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
return DefWindowProc(hwnd, message, wParam, lParam);
|
||||
}
|
||||
32
src/platform/HiddenGlWindow.h
Normal file
32
src/platform/HiddenGlWindow.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
class HiddenGlWindow
|
||||
{
|
||||
public:
|
||||
HiddenGlWindow() = default;
|
||||
HiddenGlWindow(const HiddenGlWindow&) = delete;
|
||||
HiddenGlWindow& operator=(const HiddenGlWindow&) = delete;
|
||||
~HiddenGlWindow();
|
||||
|
||||
bool Create(unsigned width, unsigned height, std::string& error);
|
||||
bool CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error);
|
||||
bool MakeCurrent() const;
|
||||
void ClearCurrent() const;
|
||||
void Destroy();
|
||||
|
||||
HDC DeviceContext() const { return mDc; }
|
||||
HGLRC Context() const { return mGlrc; }
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
HINSTANCE mInstance = nullptr;
|
||||
HWND mWindow = nullptr;
|
||||
HDC mDc = nullptr;
|
||||
HGLRC mGlrc = nullptr;
|
||||
ATOM mClassAtom = 0;
|
||||
};
|
||||
341
src/render/InputFrameTexture.cpp
Normal file
341
src/render/InputFrameTexture.cpp
Normal file
@@ -0,0 +1,341 @@
|
||||
#include "InputFrameTexture.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#ifndef GL_FRAMEBUFFER_BINDING
|
||||
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr GLuint kUyvyTextureUnit = 0;
|
||||
|
||||
const char* kDecodeVertexShader = R"GLSL(
|
||||
#version 430 core
|
||||
out vec2 vTexCoord;
|
||||
void main()
|
||||
{
|
||||
vec2 positions[3] = vec2[3](
|
||||
vec2(-1.0, -1.0),
|
||||
vec2( 3.0, -1.0),
|
||||
vec2(-1.0, 3.0));
|
||||
vec2 texCoords[3] = vec2[3](
|
||||
vec2(0.0, 0.0),
|
||||
vec2(2.0, 0.0),
|
||||
vec2(0.0, 2.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
vTexCoord = texCoords[gl_VertexID];
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
const char* kUyvyDecodeFragmentShader = R"GLSL(
|
||||
#version 430 core
|
||||
layout(binding = 0) uniform sampler2D uPackedUyvy;
|
||||
uniform vec2 uDecodedSize;
|
||||
in vec2 vTexCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
vec4 rec709YCbCr2rgba(float yByte, float cbByte, float crByte)
|
||||
{
|
||||
float y = (yByte - 16.0) / 219.0;
|
||||
float cb = (cbByte - 16.0) / 224.0 - 0.5;
|
||||
float cr = (crByte - 16.0) / 224.0 - 0.5;
|
||||
return vec4(
|
||||
y + 1.5748 * cr,
|
||||
y - 0.1873 * cb - 0.4681 * cr,
|
||||
y + 1.8556 * cb,
|
||||
1.0);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 decodedSize = ivec2(uDecodedSize);
|
||||
ivec2 outputCoord = ivec2(clamp(gl_FragCoord.xy, vec2(0.0), vec2(decodedSize - ivec2(1))));
|
||||
int sourceY = decodedSize.y - 1 - outputCoord.y;
|
||||
ivec2 packedCoord = ivec2(clamp(outputCoord.x / 2, 0, max(decodedSize.x / 2 - 1, 0)), sourceY);
|
||||
vec4 macroPixel = texelFetch(uPackedUyvy, packedCoord, 0) * 255.0;
|
||||
float ySample = (outputCoord.x & 1) != 0 ? macroPixel.a : macroPixel.g;
|
||||
fragColor = clamp(rec709YCbCr2rgba(ySample, macroPixel.r, macroPixel.b), vec4(0.0), vec4(1.0));
|
||||
}
|
||||
)GLSL";
|
||||
}
|
||||
|
||||
InputFrameTexture::~InputFrameTexture()
|
||||
{
|
||||
ShutdownGl();
|
||||
}
|
||||
|
||||
GLuint InputFrameTexture::PollAndUpload(InputFrameMailbox* mailbox)
|
||||
{
|
||||
if (mailbox == nullptr)
|
||||
return mTexture;
|
||||
|
||||
InputFrame frame;
|
||||
if (!mailbox->TryAcquireOldest(frame))
|
||||
{
|
||||
++mUploadMisses;
|
||||
mLastUploadMilliseconds = 0.0;
|
||||
return mTexture;
|
||||
}
|
||||
|
||||
if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Bgra8 && EnsureTexture(frame))
|
||||
{
|
||||
mLastFrameFormatSupported = true;
|
||||
const auto uploadStart = std::chrono::steady_clock::now();
|
||||
UploadBgra8FrameFlippedVertically(frame);
|
||||
const auto uploadEnd = std::chrono::steady_clock::now();
|
||||
mLastUploadMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(uploadEnd - uploadStart).count();
|
||||
++mUploadedFrames;
|
||||
}
|
||||
else if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Uyvy8 && EnsureTexture(frame) && EnsureRawUyvyTexture(frame) && EnsureDecodeProgram())
|
||||
{
|
||||
mLastFrameFormatSupported = true;
|
||||
const auto uploadStart = std::chrono::steady_clock::now();
|
||||
UploadUyvy8Frame(frame);
|
||||
DecodeUyvy8Frame(frame);
|
||||
const auto uploadEnd = std::chrono::steady_clock::now();
|
||||
mLastUploadMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(uploadEnd - uploadStart).count();
|
||||
++mUploadedFrames;
|
||||
}
|
||||
else
|
||||
{
|
||||
mLastFrameFormatSupported = frame.pixelFormat == VideoIOPixelFormat::Bgra8 || frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
|
||||
mLastUploadMilliseconds = 0.0;
|
||||
}
|
||||
|
||||
mailbox->Release(frame);
|
||||
return mTexture;
|
||||
}
|
||||
|
||||
void InputFrameTexture::ShutdownGl()
|
||||
{
|
||||
if (mTexture != 0)
|
||||
glDeleteTextures(1, &mTexture);
|
||||
if (mRawTexture != 0)
|
||||
glDeleteTextures(1, &mRawTexture);
|
||||
mTexture = 0;
|
||||
mRawTexture = 0;
|
||||
mWidth = 0;
|
||||
mHeight = 0;
|
||||
mRawWidth = 0;
|
||||
mRawHeight = 0;
|
||||
DestroyDecodeResources();
|
||||
}
|
||||
|
||||
bool InputFrameTexture::EnsureTexture(const InputFrame& frame)
|
||||
{
|
||||
if (frame.width == 0 || frame.height == 0)
|
||||
return false;
|
||||
|
||||
if (mTexture != 0 && mWidth == frame.width && mHeight == frame.height)
|
||||
return true;
|
||||
|
||||
ShutdownGl();
|
||||
glGenTextures(1, &mTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA8,
|
||||
static_cast<GLsizei>(frame.width),
|
||||
static_cast<GLsizei>(frame.height),
|
||||
0,
|
||||
GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
nullptr);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
mWidth = frame.width;
|
||||
mHeight = frame.height;
|
||||
return mTexture != 0;
|
||||
}
|
||||
|
||||
bool InputFrameTexture::EnsureRawUyvyTexture(const InputFrame& frame)
|
||||
{
|
||||
if (frame.width == 0 || frame.height == 0)
|
||||
return false;
|
||||
|
||||
const unsigned rawWidth = (frame.width + 1u) / 2u;
|
||||
if (mRawTexture != 0 && mRawWidth == rawWidth && mRawHeight == frame.height)
|
||||
return true;
|
||||
|
||||
if (mRawTexture != 0)
|
||||
glDeleteTextures(1, &mRawTexture);
|
||||
mRawTexture = 0;
|
||||
glGenTextures(1, &mRawTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA8,
|
||||
static_cast<GLsizei>(rawWidth),
|
||||
static_cast<GLsizei>(frame.height),
|
||||
0,
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
mRawWidth = rawWidth;
|
||||
mRawHeight = frame.height;
|
||||
return mRawTexture != 0;
|
||||
}
|
||||
|
||||
void InputFrameTexture::UploadBgra8FrameFlippedVertically(const InputFrame& frame)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
|
||||
|
||||
const unsigned char* sourceBytes = static_cast<const unsigned char*>(frame.bytes);
|
||||
for (unsigned destinationY = 0; destinationY < frame.height; ++destinationY)
|
||||
{
|
||||
const unsigned sourceY = frame.height - 1u - destinationY;
|
||||
const unsigned char* sourceRow = sourceBytes + static_cast<std::size_t>(sourceY) * static_cast<std::size_t>(frame.rowBytes);
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
0,
|
||||
static_cast<GLint>(destinationY),
|
||||
static_cast<GLsizei>(frame.width),
|
||||
1,
|
||||
GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
sourceRow);
|
||||
}
|
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void InputFrameTexture::UploadUyvy8Frame(const InputFrame& frame)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
static_cast<GLsizei>((frame.width + 1u) / 2u),
|
||||
static_cast<GLsizei>(frame.height),
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
frame.bytes);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void InputFrameTexture::DecodeUyvy8Frame(const InputFrame& frame)
|
||||
{
|
||||
GLint previousFramebuffer = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &previousFramebuffer);
|
||||
|
||||
if (mDecodeFramebuffer == 0)
|
||||
glGenFramebuffers(1, &mDecodeFramebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFramebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousFramebuffer));
|
||||
return;
|
||||
}
|
||||
|
||||
glViewport(0, 0, static_cast<GLsizei>(frame.width), static_cast<GLsizei>(frame.height));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glUseProgram(mDecodeProgram);
|
||||
const GLint decodedSizeLocation = glGetUniformLocation(mDecodeProgram, "uDecodedSize");
|
||||
if (decodedSizeLocation >= 0)
|
||||
glUniform2f(decodedSizeLocation, static_cast<GLfloat>(frame.width), static_cast<GLfloat>(frame.height));
|
||||
glActiveTexture(GL_TEXTURE0 + kUyvyTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||
glBindVertexArray(mDecodeVertexArray);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glUseProgram(0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousFramebuffer));
|
||||
}
|
||||
|
||||
bool InputFrameTexture::EnsureDecodeProgram()
|
||||
{
|
||||
if (mDecodeProgram != 0)
|
||||
return true;
|
||||
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kDecodeVertexShader, mDecodeVertexShader))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, kUyvyDecodeFragmentShader, mDecodeFragmentShader))
|
||||
return false;
|
||||
if (!LinkProgram(mDecodeVertexShader, mDecodeFragmentShader, mDecodeProgram))
|
||||
return false;
|
||||
|
||||
glUseProgram(mDecodeProgram);
|
||||
const GLint samplerLocation = glGetUniformLocation(mDecodeProgram, "uPackedUyvy");
|
||||
if (samplerLocation >= 0)
|
||||
glUniform1i(samplerLocation, static_cast<GLint>(kUyvyTextureUnit));
|
||||
glUseProgram(0);
|
||||
|
||||
if (mDecodeVertexArray == 0)
|
||||
glGenVertexArrays(1, &mDecodeVertexArray);
|
||||
return mDecodeProgram != 0 && mDecodeVertexArray != 0;
|
||||
}
|
||||
|
||||
void InputFrameTexture::DestroyDecodeResources()
|
||||
{
|
||||
if (mDecodeFramebuffer != 0)
|
||||
glDeleteFramebuffers(1, &mDecodeFramebuffer);
|
||||
if (mDecodeVertexArray != 0)
|
||||
glDeleteVertexArrays(1, &mDecodeVertexArray);
|
||||
if (mDecodeProgram != 0)
|
||||
glDeleteProgram(mDecodeProgram);
|
||||
if (mDecodeVertexShader != 0)
|
||||
glDeleteShader(mDecodeVertexShader);
|
||||
if (mDecodeFragmentShader != 0)
|
||||
glDeleteShader(mDecodeFragmentShader);
|
||||
mDecodeFramebuffer = 0;
|
||||
mDecodeVertexArray = 0;
|
||||
mDecodeProgram = 0;
|
||||
mDecodeVertexShader = 0;
|
||||
mDecodeFragmentShader = 0;
|
||||
}
|
||||
|
||||
bool InputFrameTexture::CompileShader(GLenum shaderType, const char* source, GLuint& shader)
|
||||
{
|
||||
shader = glCreateShader(shaderType);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compileResult = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult != GL_FALSE)
|
||||
return true;
|
||||
glDeleteShader(shader);
|
||||
shader = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InputFrameTexture::LinkProgram(GLuint vertexShader, GLuint fragmentShader, GLuint& program)
|
||||
{
|
||||
program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
glLinkProgram(program);
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult != GL_FALSE)
|
||||
return true;
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
return false;
|
||||
}
|
||||
50
src/render/InputFrameTexture.h
Normal file
50
src/render/InputFrameTexture.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "../frames/InputFrameMailbox.h"
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class InputFrameTexture
|
||||
{
|
||||
public:
|
||||
InputFrameTexture() = default;
|
||||
InputFrameTexture(const InputFrameTexture&) = delete;
|
||||
InputFrameTexture& operator=(const InputFrameTexture&) = delete;
|
||||
~InputFrameTexture();
|
||||
|
||||
GLuint PollAndUpload(InputFrameMailbox* mailbox);
|
||||
GLuint Texture() const { return mTexture; }
|
||||
uint64_t UploadedFrames() const { return mUploadedFrames; }
|
||||
uint64_t UploadMisses() const { return mUploadMisses; }
|
||||
double LastUploadMilliseconds() const { return mLastUploadMilliseconds; }
|
||||
bool LastFrameFormatSupported() const { return mLastFrameFormatSupported; }
|
||||
void ShutdownGl();
|
||||
|
||||
private:
|
||||
bool EnsureTexture(const InputFrame& frame);
|
||||
bool EnsureRawUyvyTexture(const InputFrame& frame);
|
||||
bool EnsureDecodeProgram();
|
||||
void UploadBgra8FrameFlippedVertically(const InputFrame& frame);
|
||||
void UploadUyvy8Frame(const InputFrame& frame);
|
||||
void DecodeUyvy8Frame(const InputFrame& frame);
|
||||
void DestroyDecodeResources();
|
||||
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader);
|
||||
static bool LinkProgram(GLuint vertexShader, GLuint fragmentShader, GLuint& program);
|
||||
|
||||
GLuint mTexture = 0;
|
||||
GLuint mRawTexture = 0;
|
||||
GLuint mDecodeFramebuffer = 0;
|
||||
GLuint mDecodeVertexArray = 0;
|
||||
GLuint mDecodeProgram = 0;
|
||||
GLuint mDecodeVertexShader = 0;
|
||||
GLuint mDecodeFragmentShader = 0;
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
unsigned mRawWidth = 0;
|
||||
unsigned mRawHeight = 0;
|
||||
uint64_t mUploadedFrames = 0;
|
||||
uint64_t mUploadMisses = 0;
|
||||
double mLastUploadMilliseconds = 0.0;
|
||||
bool mLastFrameFormatSupported = true;
|
||||
};
|
||||
49
src/render/RenderCadenceClock.cpp
Normal file
49
src/render/RenderCadenceClock.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#include "RenderCadenceClock.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
RenderCadenceClock::RenderCadenceClock(double frameDurationMilliseconds)
|
||||
{
|
||||
mFrameDuration = std::chrono::duration_cast<Duration>(std::chrono::duration<double, std::milli>(frameDurationMilliseconds));
|
||||
if (mFrameDuration <= Duration::zero())
|
||||
mFrameDuration = std::chrono::milliseconds(16);
|
||||
Reset();
|
||||
}
|
||||
|
||||
void RenderCadenceClock::Reset(TimePoint now)
|
||||
{
|
||||
mNextRenderTime = now;
|
||||
mPendingFrameAdvance = 1;
|
||||
mOverrunCount = 0;
|
||||
mSkippedFrameCount = 0;
|
||||
}
|
||||
|
||||
RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
||||
{
|
||||
Tick tick;
|
||||
if (now < mNextRenderTime)
|
||||
{
|
||||
tick.sleepFor = std::min(Duration(std::chrono::milliseconds(1)), mNextRenderTime - now);
|
||||
return tick;
|
||||
}
|
||||
|
||||
tick.due = true;
|
||||
mPendingFrameAdvance = 1;
|
||||
const Duration lateBy = now - mNextRenderTime;
|
||||
if (lateBy > mFrameDuration)
|
||||
{
|
||||
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
||||
mPendingFrameAdvance += tick.skippedFrames;
|
||||
++mOverrunCount;
|
||||
mSkippedFrameCount += tick.skippedFrames;
|
||||
}
|
||||
return tick;
|
||||
}
|
||||
|
||||
void RenderCadenceClock::MarkRendered(TimePoint now)
|
||||
{
|
||||
mNextRenderTime += mFrameDuration * mPendingFrameAdvance;
|
||||
mPendingFrameAdvance = 1;
|
||||
if (now - mNextRenderTime > mFrameDuration * 4)
|
||||
mNextRenderTime = now + mFrameDuration;
|
||||
}
|
||||
37
src/render/RenderCadenceClock.h
Normal file
37
src/render/RenderCadenceClock.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
class RenderCadenceClock
|
||||
{
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Duration = Clock::duration;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
struct Tick
|
||||
{
|
||||
bool due = false;
|
||||
uint64_t skippedFrames = 0;
|
||||
Duration sleepFor = Duration::zero();
|
||||
};
|
||||
|
||||
explicit RenderCadenceClock(double frameDurationMilliseconds = 1000.0 / 60.0);
|
||||
|
||||
void Reset(TimePoint now = Clock::now());
|
||||
Tick Poll(TimePoint now = Clock::now());
|
||||
void MarkRendered(TimePoint now = Clock::now());
|
||||
|
||||
Duration FrameDuration() const { return mFrameDuration; }
|
||||
TimePoint NextRenderTime() const { return mNextRenderTime; }
|
||||
uint64_t OverrunCount() const { return mOverrunCount; }
|
||||
uint64_t SkippedFrameCount() const { return mSkippedFrameCount; }
|
||||
|
||||
private:
|
||||
Duration mFrameDuration;
|
||||
TimePoint mNextRenderTime = Clock::now();
|
||||
uint64_t mPendingFrameAdvance = 1;
|
||||
uint64_t mOverrunCount = 0;
|
||||
uint64_t mSkippedFrameCount = 0;
|
||||
};
|
||||
389
src/render/RenderThread.cpp
Normal file
389
src/render/RenderThread.cpp
Normal file
@@ -0,0 +1,389 @@
|
||||
#include "RenderThread.h"
|
||||
|
||||
#include "../frames/InputFrameMailbox.h"
|
||||
#include "../frames/SystemFrameExchange.h"
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
#include "../logging/Logger.h"
|
||||
#include "../platform/HiddenGlWindow.h"
|
||||
#include "InputFrameTexture.h"
|
||||
#include "readback/Bgra8ReadbackPipeline.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "runtime/RuntimeRenderScene.h"
|
||||
#include "runtime/RuntimeShaderRenderer.h"
|
||||
#include "SimpleMotionRenderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
|
||||
mFrameExchange(frameExchange),
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
RenderThread::RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config) :
|
||||
mFrameExchange(frameExchange),
|
||||
mInputMailbox(inputMailbox),
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
RenderThread::~RenderThread()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool RenderThread::Start(std::string& error)
|
||||
{
|
||||
if (mThread.joinable())
|
||||
return true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||
mStarted = false;
|
||||
mStartupError.clear();
|
||||
}
|
||||
|
||||
mStopping.store(false, std::memory_order_release);
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
|
||||
std::unique_lock<std::mutex> lock(mStartupMutex);
|
||||
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() {
|
||||
return mStarted || !mStartupError.empty();
|
||||
}))
|
||||
{
|
||||
error = "Timed out starting render thread.";
|
||||
return false;
|
||||
}
|
||||
if (!mStartupError.empty())
|
||||
{
|
||||
error = mStartupError;
|
||||
lock.unlock();
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderThread::Stop()
|
||||
{
|
||||
mStopping.store(true, std::memory_order_release);
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
RenderThread::Metrics RenderThread::GetMetrics() const
|
||||
{
|
||||
Metrics metrics;
|
||||
metrics.renderedFrames = mRenderedFrames.load(std::memory_order_relaxed);
|
||||
metrics.completedReadbacks = mCompletedReadbacks.load(std::memory_order_relaxed);
|
||||
metrics.acquireMisses = mAcquireMisses.load(std::memory_order_relaxed);
|
||||
metrics.pboQueueMisses = mPboQueueMisses.load(std::memory_order_relaxed);
|
||||
metrics.clockOverruns = mClockOverruns.load(std::memory_order_relaxed);
|
||||
metrics.skippedFrames = mSkippedFrames.load(std::memory_order_relaxed);
|
||||
metrics.shaderBuildsCommitted = mShaderBuildsCommitted.load(std::memory_order_relaxed);
|
||||
metrics.shaderBuildFailures = mShaderBuildFailures.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameMilliseconds = mRenderFrameMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameBudgetUsedPercent = mRenderFrameBudgetUsedPercent.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameMaxMilliseconds = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.readbackQueueMilliseconds = mReadbackQueueMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.completedReadbackCopyMilliseconds = mCompletedReadbackCopyMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputFramesReceived = mInputFramesReceived.load(std::memory_order_relaxed);
|
||||
metrics.inputFramesDropped = mInputFramesDropped.load(std::memory_order_relaxed);
|
||||
metrics.inputConsumeMisses = mInputConsumeMisses.load(std::memory_order_relaxed);
|
||||
metrics.inputUploadMisses = mInputUploadMisses.load(std::memory_order_relaxed);
|
||||
metrics.inputReadyFrames = mInputReadyFrames.load(std::memory_order_relaxed);
|
||||
metrics.inputReadingFrames = mInputReadingFrames.load(std::memory_order_relaxed);
|
||||
metrics.inputLatestAgeMilliseconds = mInputLatestAgeMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputUploadMilliseconds = mInputUploadMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputFormatSupported = mInputFormatSupported.load(std::memory_order_relaxed);
|
||||
metrics.inputSignalPresent = mInputSignalPresent.load(std::memory_order_relaxed);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
void RenderThread::ThreadMain()
|
||||
{
|
||||
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting.");
|
||||
HiddenGlWindow window;
|
||||
std::string error;
|
||||
if (!window.Create(mConfig.width, mConfig.height, error))
|
||||
{
|
||||
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
|
||||
return;
|
||||
}
|
||||
std::unique_ptr<HiddenGlWindow> prepareWindow = std::make_unique<HiddenGlWindow>();
|
||||
if (!prepareWindow->CreateShared(mConfig.width, mConfig.height, window.DeviceContext(), window.Context(), error))
|
||||
{
|
||||
SignalStartupFailure(error.empty() ? "Runtime shader prepare shared context creation failed." : error);
|
||||
return;
|
||||
}
|
||||
if (!window.MakeCurrent())
|
||||
{
|
||||
SignalStartupFailure("OpenGL context creation failed.");
|
||||
return;
|
||||
}
|
||||
if (!ResolveGLExtensions())
|
||||
{
|
||||
SignalStartupFailure("OpenGL extension resolution failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleMotionRenderer renderer;
|
||||
RuntimeRenderScene runtimeRenderScene;
|
||||
Bgra8ReadbackPipeline readback;
|
||||
InputFrameTexture inputTexture;
|
||||
if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error))
|
||||
{
|
||||
SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error);
|
||||
return;
|
||||
}
|
||||
if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth))
|
||||
{
|
||||
SignalStartupFailure("Render pipeline initialization failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
RenderCadenceClock clock(mConfig.frameDurationMilliseconds);
|
||||
uint64_t frameIndex = 0;
|
||||
mRunning.store(true, std::memory_order_release);
|
||||
SignalStarted();
|
||||
|
||||
while (!mStopping.load(std::memory_order_acquire))
|
||||
{
|
||||
readback.ConsumeCompleted(
|
||||
[this](SystemFrame& frame) { return mFrameExchange.AcquireForRender(frame); },
|
||||
[this](const SystemFrame& frame) { return mFrameExchange.PublishCompleted(frame); },
|
||||
[this]() {
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
PublishReadbackMetrics(readback);
|
||||
|
||||
const auto now = RenderCadenceClock::Clock::now();
|
||||
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
||||
if (!tick.due)
|
||||
{
|
||||
if (tick.sleepFor > RenderCadenceClock::Duration::zero())
|
||||
std::this_thread::sleep_for(tick.sleepFor);
|
||||
continue;
|
||||
}
|
||||
|
||||
TryCommitReadyRuntimeShader(runtimeRenderScene);
|
||||
const GLuint videoInputTexture = inputTexture.PollAndUpload(mInputMailbox);
|
||||
PublishInputMetrics(inputTexture);
|
||||
if (!readback.RenderAndQueue(frameIndex, [this, &renderer, &runtimeRenderScene, videoInputTexture](uint64_t index) {
|
||||
if (runtimeRenderScene.HasLayers())
|
||||
runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height, videoInputTexture);
|
||||
else if (videoInputTexture != 0)
|
||||
renderer.RenderTexture(videoInputTexture);
|
||||
else
|
||||
renderer.RenderFrame(index);
|
||||
}))
|
||||
{
|
||||
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
PublishReadbackMetrics(readback);
|
||||
|
||||
CountRendered();
|
||||
++frameIndex;
|
||||
clock.MarkRendered(RenderCadenceClock::Clock::now());
|
||||
|
||||
mClockOverruns.store(clock.OverrunCount(), std::memory_order_relaxed);
|
||||
mSkippedFrames.store(clock.SkippedFrameCount(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < mConfig.pboDepth * 2; ++i)
|
||||
{
|
||||
readback.ConsumeCompleted(
|
||||
[this](SystemFrame& frame) { return mFrameExchange.AcquireForRender(frame); },
|
||||
[this](const SystemFrame& frame) { return mFrameExchange.PublishCompleted(frame); },
|
||||
[this]() {
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
PublishReadbackMetrics(readback);
|
||||
}
|
||||
|
||||
readback.Shutdown();
|
||||
inputTexture.ShutdownGl();
|
||||
runtimeRenderScene.ShutdownGl();
|
||||
renderer.ShutdownGl();
|
||||
window.ClearCurrent();
|
||||
mRunning.store(false, std::memory_order_release);
|
||||
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread stopped.");
|
||||
}
|
||||
|
||||
void RenderThread::SignalStarted()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||
mStarted = true;
|
||||
mStartupCondition.notify_all();
|
||||
}
|
||||
|
||||
void RenderThread::SignalStartupFailure(const std::string& error)
|
||||
{
|
||||
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Error, "render-thread", error);
|
||||
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||
mStartupError = error;
|
||||
mStartupCondition.notify_all();
|
||||
}
|
||||
|
||||
void RenderThread::CountRendered()
|
||||
{
|
||||
mRenderedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::CountCompleted()
|
||||
{
|
||||
mCompletedReadbacks.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::CountAcquireMiss()
|
||||
{
|
||||
mAcquireMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback)
|
||||
{
|
||||
const double renderMilliseconds = readback.LastRenderFrameMilliseconds();
|
||||
mRenderFrameMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||
if (mConfig.frameDurationMilliseconds > 0.0)
|
||||
{
|
||||
mRenderFrameBudgetUsedPercent.store(
|
||||
(renderMilliseconds / mConfig.frameDurationMilliseconds) * 100.0,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
else
|
||||
{
|
||||
mRenderFrameBudgetUsedPercent.store(0.0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
const double previousMax = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||
if (renderMilliseconds > previousMax)
|
||||
mRenderFrameMaxMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||
|
||||
mReadbackQueueMilliseconds.store(readback.LastReadbackQueueMilliseconds(), std::memory_order_relaxed);
|
||||
mCompletedReadbackCopyMilliseconds.store(readback.LastCompletedReadbackCopyMilliseconds(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
||||
{
|
||||
if (mInputMailbox != nullptr)
|
||||
{
|
||||
const InputFrameMailboxMetrics mailboxMetrics = mInputMailbox->Metrics();
|
||||
mInputFramesReceived.store(mailboxMetrics.submittedFrames, std::memory_order_relaxed);
|
||||
mInputFramesDropped.store(mailboxMetrics.droppedReadyFrames + mailboxMetrics.submitMisses, std::memory_order_relaxed);
|
||||
mInputConsumeMisses.store(mailboxMetrics.consumeMisses, std::memory_order_relaxed);
|
||||
mInputReadyFrames.store(mailboxMetrics.readyCount, std::memory_order_relaxed);
|
||||
mInputReadingFrames.store(mailboxMetrics.readingCount, std::memory_order_relaxed);
|
||||
mInputLatestAgeMilliseconds.store(mailboxMetrics.latestFrameAgeMilliseconds, std::memory_order_relaxed);
|
||||
mInputSignalPresent.store(mailboxMetrics.hasSubmittedFrame, std::memory_order_relaxed);
|
||||
}
|
||||
else
|
||||
{
|
||||
mInputFramesReceived.store(0, std::memory_order_relaxed);
|
||||
mInputFramesDropped.store(0, std::memory_order_relaxed);
|
||||
mInputConsumeMisses.store(0, std::memory_order_relaxed);
|
||||
mInputReadyFrames.store(0, std::memory_order_relaxed);
|
||||
mInputReadingFrames.store(0, std::memory_order_relaxed);
|
||||
mInputLatestAgeMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||
mInputSignalPresent.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
mInputUploadMisses.store(inputTexture.UploadMisses(), std::memory_order_relaxed);
|
||||
mInputUploadMilliseconds.store(inputTexture.LastUploadMilliseconds(), std::memory_order_relaxed);
|
||||
mInputFormatSupported.store(inputTexture.LastFrameFormatSupported(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
if (artifact.fragmentShaderSource.empty())
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mShaderArtifactMutex);
|
||||
mPendingShaderArtifact = artifact;
|
||||
mHasPendingShaderArtifact = true;
|
||||
}
|
||||
|
||||
void RenderThread::SubmitRuntimeRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderLayersMutex);
|
||||
mPendingRenderLayers = layers;
|
||||
mHasPendingRenderLayers = true;
|
||||
}
|
||||
|
||||
bool RenderThread::TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderArtifactMutex);
|
||||
if (!mHasPendingShaderArtifact)
|
||||
return false;
|
||||
|
||||
artifact = std::move(mPendingShaderArtifact);
|
||||
mPendingShaderArtifact = RuntimeShaderArtifact();
|
||||
mHasPendingShaderArtifact = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderThread::TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderLayersMutex);
|
||||
if (!mHasPendingRenderLayers)
|
||||
return false;
|
||||
|
||||
layers = std::move(mPendingRenderLayers);
|
||||
mPendingRenderLayers.clear();
|
||||
mHasPendingRenderLayers = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderThread::TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene)
|
||||
{
|
||||
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layers;
|
||||
std::string commitError;
|
||||
if (TryTakePendingRenderLayers(layers))
|
||||
{
|
||||
if (!runtimeRenderScene.CommitRenderLayers(layers, commitError))
|
||||
{
|
||||
RenderCadenceCompositor::TryLog(
|
||||
RenderCadenceCompositor::LogLevel::Error,
|
||||
"render-thread",
|
||||
"Runtime render-layer commit failed: " + commitError);
|
||||
mShaderBuildFailures.fetch_add(1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::TryLog(
|
||||
RenderCadenceCompositor::LogLevel::Log,
|
||||
"render-thread",
|
||||
"Runtime render layer snapshot committed.");
|
||||
mShaderBuildsCommitted.fetch_add(1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeShaderArtifact artifact;
|
||||
if (!TryTakePendingRuntimeShaderArtifact(artifact))
|
||||
return;
|
||||
|
||||
RenderCadenceCompositor::RuntimeRenderLayerModel layer;
|
||||
layer.id = artifact.layerId.empty() ? "runtime-layer-1" : artifact.layerId;
|
||||
layer.shaderId = artifact.shaderId;
|
||||
layer.artifact = artifact;
|
||||
layers.push_back(std::move(layer));
|
||||
if (!runtimeRenderScene.CommitRenderLayers(layers, commitError))
|
||||
{
|
||||
RenderCadenceCompositor::TryLog(
|
||||
RenderCadenceCompositor::LogLevel::Error,
|
||||
"render-thread",
|
||||
"Runtime shader GL commit failed: " + commitError);
|
||||
mShaderBuildFailures.fetch_add(1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::TryLog(
|
||||
RenderCadenceCompositor::LogLevel::Log,
|
||||
"render-thread",
|
||||
"Runtime shader committed: " + artifact.shaderId + ". " + artifact.message);
|
||||
mShaderBuildsCommitted.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
129
src/render/RenderThread.h
Normal file
129
src/render/RenderThread.h
Normal file
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
#include "RenderCadenceClock.h"
|
||||
#include "../runtime/RuntimeLayerModel.h"
|
||||
#include "../runtime/RuntimeShaderArtifact.h"
|
||||
#include "runtime/RuntimeRenderScene.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class SystemFrameExchange;
|
||||
class InputFrameMailbox;
|
||||
class InputFrameTexture;
|
||||
class Bgra8ReadbackPipeline;
|
||||
|
||||
class RenderThread
|
||||
{
|
||||
public:
|
||||
struct Config
|
||||
{
|
||||
unsigned width = 1920;
|
||||
unsigned height = 1080;
|
||||
double frameDurationMilliseconds = 1000.0 / 59.94;
|
||||
std::size_t pboDepth = 6;
|
||||
};
|
||||
|
||||
struct Metrics
|
||||
{
|
||||
uint64_t renderedFrames = 0;
|
||||
uint64_t completedReadbacks = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
uint64_t pboQueueMisses = 0;
|
||||
uint64_t clockOverruns = 0;
|
||||
uint64_t skippedFrames = 0;
|
||||
uint64_t shaderBuildsCommitted = 0;
|
||||
uint64_t shaderBuildFailures = 0;
|
||||
double renderFrameMilliseconds = 0.0;
|
||||
double renderFrameBudgetUsedPercent = 0.0;
|
||||
double renderFrameMaxMilliseconds = 0.0;
|
||||
double readbackQueueMilliseconds = 0.0;
|
||||
double completedReadbackCopyMilliseconds = 0.0;
|
||||
uint64_t inputFramesReceived = 0;
|
||||
uint64_t inputFramesDropped = 0;
|
||||
uint64_t inputConsumeMisses = 0;
|
||||
uint64_t inputUploadMisses = 0;
|
||||
std::size_t inputReadyFrames = 0;
|
||||
std::size_t inputReadingFrames = 0;
|
||||
double inputLatestAgeMilliseconds = 0.0;
|
||||
double inputUploadMilliseconds = 0.0;
|
||||
bool inputFormatSupported = true;
|
||||
bool inputSignalPresent = false;
|
||||
};
|
||||
|
||||
RenderThread(SystemFrameExchange& frameExchange, Config config);
|
||||
RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config);
|
||||
RenderThread(const RenderThread&) = delete;
|
||||
RenderThread& operator=(const RenderThread&) = delete;
|
||||
~RenderThread();
|
||||
|
||||
bool Start(std::string& error);
|
||||
void Stop();
|
||||
void SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact);
|
||||
void SubmitRuntimeRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||
|
||||
Metrics GetMetrics() const;
|
||||
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||
|
||||
private:
|
||||
void ThreadMain();
|
||||
void SignalStarted();
|
||||
void SignalStartupFailure(const std::string& error);
|
||||
void CountRendered();
|
||||
void CountCompleted();
|
||||
void CountAcquireMiss();
|
||||
void PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback);
|
||||
void PublishInputMetrics(const InputFrameTexture& inputTexture);
|
||||
void TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene);
|
||||
bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact);
|
||||
bool TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||
|
||||
SystemFrameExchange& mFrameExchange;
|
||||
InputFrameMailbox* mInputMailbox = nullptr;
|
||||
Config mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mRunning{ false };
|
||||
|
||||
mutable std::mutex mStartupMutex;
|
||||
std::condition_variable mStartupCondition;
|
||||
bool mStarted = false;
|
||||
std::string mStartupError;
|
||||
|
||||
std::atomic<uint64_t> mRenderedFrames{ 0 };
|
||||
std::atomic<uint64_t> mCompletedReadbacks{ 0 };
|
||||
std::atomic<uint64_t> mAcquireMisses{ 0 };
|
||||
std::atomic<uint64_t> mPboQueueMisses{ 0 };
|
||||
std::atomic<uint64_t> mClockOverruns{ 0 };
|
||||
std::atomic<uint64_t> mSkippedFrames{ 0 };
|
||||
std::atomic<uint64_t> mShaderBuildsCommitted{ 0 };
|
||||
std::atomic<uint64_t> mShaderBuildFailures{ 0 };
|
||||
std::atomic<double> mRenderFrameMilliseconds{ 0.0 };
|
||||
std::atomic<double> mRenderFrameBudgetUsedPercent{ 0.0 };
|
||||
std::atomic<double> mRenderFrameMaxMilliseconds{ 0.0 };
|
||||
std::atomic<double> mReadbackQueueMilliseconds{ 0.0 };
|
||||
std::atomic<double> mCompletedReadbackCopyMilliseconds{ 0.0 };
|
||||
std::atomic<uint64_t> mInputFramesReceived{ 0 };
|
||||
std::atomic<uint64_t> mInputFramesDropped{ 0 };
|
||||
std::atomic<uint64_t> mInputConsumeMisses{ 0 };
|
||||
std::atomic<uint64_t> mInputUploadMisses{ 0 };
|
||||
std::atomic<std::size_t> mInputReadyFrames{ 0 };
|
||||
std::atomic<std::size_t> mInputReadingFrames{ 0 };
|
||||
std::atomic<double> mInputLatestAgeMilliseconds{ 0.0 };
|
||||
std::atomic<double> mInputUploadMilliseconds{ 0.0 };
|
||||
std::atomic<bool> mInputFormatSupported{ true };
|
||||
std::atomic<bool> mInputSignalPresent{ false };
|
||||
|
||||
std::mutex mShaderArtifactMutex;
|
||||
bool mHasPendingShaderArtifact = false;
|
||||
RuntimeShaderArtifact mPendingShaderArtifact;
|
||||
|
||||
std::mutex mRenderLayersMutex;
|
||||
bool mHasPendingRenderLayers = false;
|
||||
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> mPendingRenderLayers;
|
||||
};
|
||||
313
src/render/SimpleMotionRenderer.cpp
Normal file
313
src/render/SimpleMotionRenderer.cpp
Normal file
@@ -0,0 +1,313 @@
|
||||
#include "SimpleMotionRenderer.h"
|
||||
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr GLuint kInputTextureUnit = 0;
|
||||
|
||||
const char* kTextureVertexShader = R"GLSL(
|
||||
#version 430 core
|
||||
out vec2 vTexCoord;
|
||||
void main()
|
||||
{
|
||||
vec2 positions[3] = vec2[3](
|
||||
vec2(-1.0, -1.0),
|
||||
vec2( 3.0, -1.0),
|
||||
vec2(-1.0, 3.0));
|
||||
vec2 texCoords[3] = vec2[3](
|
||||
vec2(0.0, 0.0),
|
||||
vec2(2.0, 0.0),
|
||||
vec2(0.0, 2.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
vTexCoord = texCoords[gl_VertexID];
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
const char* kTextureFragmentShader = R"GLSL(
|
||||
#version 430 core
|
||||
layout(binding = 0) uniform sampler2D uInputTexture;
|
||||
in vec2 vTexCoord;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = texture(uInputTexture, clamp(vTexCoord, vec2(0.0), vec2(1.0)));
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
const char* kPatternFragmentShader = R"GLSL(
|
||||
#version 430 core
|
||||
uniform vec2 uResolution;
|
||||
uniform float uFrameIndex;
|
||||
in vec2 vTexCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
vec3 hexColor(float r, float g, float b)
|
||||
{
|
||||
return vec3(r, g, b) / 255.0;
|
||||
}
|
||||
|
||||
vec3 smpteTop(float x)
|
||||
{
|
||||
if (x < 240.0) return hexColor(102.0, 102.0, 102.0);
|
||||
if (x < 445.0) return hexColor(191.0, 191.0, 191.0);
|
||||
if (x < 651.0) return hexColor(191.0, 191.0, 0.0);
|
||||
if (x < 857.0) return hexColor(0.0, 191.0, 191.0);
|
||||
if (x < 1063.0) return hexColor(0.0, 191.0, 0.0);
|
||||
if (x < 1269.0) return hexColor(191.0, 0.0, 191.0);
|
||||
if (x < 1475.0) return hexColor(191.0, 0.0, 0.0);
|
||||
if (x < 1680.0) return hexColor(0.0, 0.0, 191.0);
|
||||
return hexColor(102.0, 102.0, 102.0);
|
||||
}
|
||||
|
||||
vec3 smpteMiddleA(float x)
|
||||
{
|
||||
if (x < 240.0) return hexColor(0.0, 255.0, 255.0);
|
||||
if (x < 445.0) return hexColor(0.0, 63.0, 105.0);
|
||||
if (x < 1680.0) return hexColor(191.0, 191.0, 191.0);
|
||||
return hexColor(0.0, 0.0, 255.0);
|
||||
}
|
||||
|
||||
vec3 smpteMiddleB(float x)
|
||||
{
|
||||
if (x < 240.0) return hexColor(255.0, 255.0, 0.0);
|
||||
if (x < 445.0) return hexColor(65.0, 0.0, 119.0);
|
||||
if (x < 1475.0)
|
||||
{
|
||||
float ramp = clamp((x - 445.0) / (1475.0 - 445.0), 0.0, 1.0);
|
||||
return vec3(ramp);
|
||||
}
|
||||
if (x < 1680.0) return vec3(1.0);
|
||||
return hexColor(255.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
vec3 smpteBottom(float x)
|
||||
{
|
||||
if (x < 240.0) return hexColor(38.0, 38.0, 38.0);
|
||||
if (x < 549.0) return vec3(0.0);
|
||||
if (x < 960.0) return vec3(1.0);
|
||||
if (x < 1268.0) return vec3(0.0);
|
||||
if (x < 1337.0) return hexColor(5.0, 5.0, 5.0);
|
||||
if (x < 1405.0) return vec3(0.0);
|
||||
if (x < 1474.0) return hexColor(10.0, 10.0, 10.0);
|
||||
if (x < 1680.0) return vec3(0.0);
|
||||
return hexColor(38.0, 38.0, 38.0);
|
||||
}
|
||||
|
||||
vec3 smpteColor(vec2 uv)
|
||||
{
|
||||
vec2 pixel = vec2(clamp(uv.x, 0.0, 1.0), 1.0 - clamp(uv.y, 0.0, 1.0)) * vec2(1920.0, 1080.0);
|
||||
if (pixel.y < 630.0) return smpteTop(pixel.x);
|
||||
if (pixel.y < 720.0) return smpteMiddleA(pixel.x);
|
||||
if (pixel.y < 810.0) return smpteMiddleB(pixel.x);
|
||||
return smpteBottom(pixel.x);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 uv = clamp(vTexCoord, vec2(0.0), vec2(1.0));
|
||||
vec3 color = smpteColor(uv);
|
||||
|
||||
float t = uFrameIndex / 60.0;
|
||||
vec2 cubeSize = vec2(0.16, 0.20);
|
||||
vec2 cubeMin = vec2(
|
||||
(0.5 + 0.5 * sin(t * 1.7)) * (1.0 - cubeSize.x),
|
||||
(0.5 + 0.5 * sin(t * 1.1 + 0.8)) * (1.0 - cubeSize.y));
|
||||
vec2 cubeMax = cubeMin + cubeSize;
|
||||
bool insideCube = uv.x >= cubeMin.x && uv.x <= cubeMax.x && uv.y >= cubeMin.y && uv.y <= cubeMax.y;
|
||||
if (insideCube)
|
||||
{
|
||||
vec2 local = (uv - cubeMin) / cubeSize;
|
||||
vec3 cubeColor = vec3(1.0, 0.74 + 0.18 * sin(t * 2.1), 0.08);
|
||||
float edge = step(local.x, 0.04) + step(local.y, 0.04) + step(0.96, local.x) + step(0.96, local.y);
|
||||
color = edge > 0.0 ? vec3(1.0) : cubeColor;
|
||||
}
|
||||
|
||||
fragColor = vec4(color, 1.0);
|
||||
}
|
||||
)GLSL";
|
||||
}
|
||||
|
||||
bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
|
||||
{
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
return mWidth > 0 && mHeight > 0;
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::RenderFrame(uint64_t frameIndex)
|
||||
{
|
||||
if (!EnsurePatternProgram())
|
||||
{
|
||||
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
return;
|
||||
}
|
||||
|
||||
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glUseProgram(mPatternProgram);
|
||||
const GLint resolutionLocation = glGetUniformLocation(mPatternProgram, "uResolution");
|
||||
if (resolutionLocation >= 0)
|
||||
glUniform2f(resolutionLocation, static_cast<float>(mWidth), static_cast<float>(mHeight));
|
||||
const GLint frameIndexLocation = glGetUniformLocation(mPatternProgram, "uFrameIndex");
|
||||
if (frameIndexLocation >= 0)
|
||||
glUniform1f(frameIndexLocation, static_cast<float>(frameIndex));
|
||||
glBindVertexArray(mPatternVertexArray);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::RenderTexture(GLuint texture)
|
||||
{
|
||||
if (texture == 0 || !EnsureTextureProgram())
|
||||
{
|
||||
RenderFrame(0);
|
||||
return;
|
||||
}
|
||||
|
||||
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0 + kInputTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glUseProgram(mTextureProgram);
|
||||
glBindVertexArray(mTextureVertexArray);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::ShutdownGl()
|
||||
{
|
||||
DestroyPatternProgram();
|
||||
DestroyTextureProgram();
|
||||
mWidth = 0;
|
||||
mHeight = 0;
|
||||
}
|
||||
|
||||
bool SimpleMotionRenderer::EnsurePatternProgram()
|
||||
{
|
||||
if (mPatternProgram != 0)
|
||||
return true;
|
||||
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kTextureVertexShader, mPatternVertexShader))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, kPatternFragmentShader, mPatternFragmentShader))
|
||||
{
|
||||
DestroyPatternProgram();
|
||||
return false;
|
||||
}
|
||||
|
||||
mPatternProgram = glCreateProgram();
|
||||
glAttachShader(mPatternProgram, mPatternVertexShader);
|
||||
glAttachShader(mPatternProgram, mPatternFragmentShader);
|
||||
glLinkProgram(mPatternProgram);
|
||||
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(mPatternProgram, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
DestroyPatternProgram();
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenVertexArrays(1, &mPatternVertexArray);
|
||||
return mPatternVertexArray != 0;
|
||||
}
|
||||
|
||||
bool SimpleMotionRenderer::EnsureTextureProgram()
|
||||
{
|
||||
if (mTextureProgram != 0)
|
||||
return true;
|
||||
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kTextureVertexShader, mTextureVertexShader))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, kTextureFragmentShader, mTextureFragmentShader))
|
||||
{
|
||||
DestroyTextureProgram();
|
||||
return false;
|
||||
}
|
||||
|
||||
mTextureProgram = glCreateProgram();
|
||||
glAttachShader(mTextureProgram, mTextureVertexShader);
|
||||
glAttachShader(mTextureProgram, mTextureFragmentShader);
|
||||
glLinkProgram(mTextureProgram);
|
||||
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(mTextureProgram, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
DestroyTextureProgram();
|
||||
return false;
|
||||
}
|
||||
|
||||
glUseProgram(mTextureProgram);
|
||||
const GLint inputLocation = glGetUniformLocation(mTextureProgram, "uInputTexture");
|
||||
if (inputLocation >= 0)
|
||||
glUniform1i(inputLocation, static_cast<GLint>(kInputTextureUnit));
|
||||
glUseProgram(0);
|
||||
|
||||
glGenVertexArrays(1, &mTextureVertexArray);
|
||||
return mTextureVertexArray != 0;
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::DestroyTextureProgram()
|
||||
{
|
||||
if (mTextureProgram != 0)
|
||||
glDeleteProgram(mTextureProgram);
|
||||
if (mTextureVertexShader != 0)
|
||||
glDeleteShader(mTextureVertexShader);
|
||||
if (mTextureFragmentShader != 0)
|
||||
glDeleteShader(mTextureFragmentShader);
|
||||
if (mTextureVertexArray != 0)
|
||||
glDeleteVertexArrays(1, &mTextureVertexArray);
|
||||
mTextureProgram = 0;
|
||||
mTextureVertexShader = 0;
|
||||
mTextureFragmentShader = 0;
|
||||
mTextureVertexArray = 0;
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::DestroyPatternProgram()
|
||||
{
|
||||
if (mPatternProgram != 0)
|
||||
glDeleteProgram(mPatternProgram);
|
||||
if (mPatternVertexShader != 0)
|
||||
glDeleteShader(mPatternVertexShader);
|
||||
if (mPatternFragmentShader != 0)
|
||||
glDeleteShader(mPatternFragmentShader);
|
||||
if (mPatternVertexArray != 0)
|
||||
glDeleteVertexArrays(1, &mPatternVertexArray);
|
||||
mPatternProgram = 0;
|
||||
mPatternVertexShader = 0;
|
||||
mPatternFragmentShader = 0;
|
||||
mPatternVertexArray = 0;
|
||||
}
|
||||
|
||||
bool SimpleMotionRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader)
|
||||
{
|
||||
shader = glCreateShader(shaderType);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
|
||||
GLint compileResult = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult != GL_FALSE)
|
||||
return true;
|
||||
|
||||
glDeleteShader(shader);
|
||||
shader = 0;
|
||||
return false;
|
||||
}
|
||||
37
src/render/SimpleMotionRenderer.h
Normal file
37
src/render/SimpleMotionRenderer.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class SimpleMotionRenderer
|
||||
{
|
||||
public:
|
||||
SimpleMotionRenderer() = default;
|
||||
|
||||
bool InitializeGl(unsigned width, unsigned height);
|
||||
void RenderFrame(uint64_t frameIndex);
|
||||
void RenderTexture(GLuint texture);
|
||||
void ShutdownGl();
|
||||
|
||||
unsigned Width() const { return mWidth; }
|
||||
unsigned Height() const { return mHeight; }
|
||||
|
||||
private:
|
||||
bool EnsureTextureProgram();
|
||||
bool EnsurePatternProgram();
|
||||
void DestroyTextureProgram();
|
||||
void DestroyPatternProgram();
|
||||
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader);
|
||||
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
GLuint mPatternProgram = 0;
|
||||
GLuint mPatternVertexShader = 0;
|
||||
GLuint mPatternFragmentShader = 0;
|
||||
GLuint mPatternVertexArray = 0;
|
||||
GLuint mTextureProgram = 0;
|
||||
GLuint mTextureVertexShader = 0;
|
||||
GLuint mTextureFragmentShader = 0;
|
||||
GLuint mTextureVertexArray = 0;
|
||||
};
|
||||
160
src/render/readback/Bgra8ReadbackPipeline.cpp
Normal file
160
src/render/readback/Bgra8ReadbackPipeline.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
#include "Bgra8ReadbackPipeline.h"
|
||||
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
double MillisecondsSince(std::chrono::steady_clock::time_point start)
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
}
|
||||
}
|
||||
|
||||
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool Bgra8ReadbackPipeline::Initialize(unsigned width, unsigned height, std::size_t pboDepth)
|
||||
{
|
||||
Shutdown();
|
||||
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
mRowBytes = VideoIORowBytes(VideoIOPixelFormat::Bgra8, width);
|
||||
if (mWidth == 0 || mHeight == 0 || mRowBytes == 0)
|
||||
return false;
|
||||
|
||||
if (!CreateRenderTarget())
|
||||
{
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t byteCount = static_cast<std::size_t>(mRowBytes) * static_cast<std::size_t>(mHeight);
|
||||
if (!mPboRing.Initialize(pboDepth, byteCount))
|
||||
{
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Bgra8ReadbackPipeline::Shutdown()
|
||||
{
|
||||
mPboRing.Shutdown();
|
||||
DestroyRenderTarget();
|
||||
mWidth = 0;
|
||||
mHeight = 0;
|
||||
mRowBytes = 0;
|
||||
}
|
||||
|
||||
bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame)
|
||||
{
|
||||
if (mFramebuffer == 0 || !renderFrame)
|
||||
return false;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||
const auto renderStart = std::chrono::steady_clock::now();
|
||||
renderFrame(frameIndex);
|
||||
mLastRenderFrameMilliseconds = MillisecondsSince(renderStart);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
const auto queueStart = std::chrono::steady_clock::now();
|
||||
const bool queued = mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||
mLastReadbackQueueMilliseconds = MillisecondsSince(queueStart);
|
||||
return queued;
|
||||
}
|
||||
|
||||
void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||
const AcquireFrameCallback& acquireFrame,
|
||||
const PublishFrameCallback& publishFrame,
|
||||
const CounterCallback& onAcquireMiss,
|
||||
const CounterCallback& onCompleted)
|
||||
{
|
||||
if (!acquireFrame || !publishFrame)
|
||||
return;
|
||||
|
||||
PboReadbackRing::CompletedReadback readback;
|
||||
while (mPboRing.TryAcquireCompleted(readback))
|
||||
{
|
||||
const auto copyStart = std::chrono::steady_clock::now();
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
||||
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||
if (!mapped)
|
||||
{
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mPboRing.ReleaseCompleted(readback);
|
||||
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||
continue;
|
||||
}
|
||||
|
||||
SystemFrame frame;
|
||||
if (acquireFrame(frame))
|
||||
{
|
||||
const std::size_t byteCount = static_cast<std::size_t>(frame.rowBytes) * static_cast<std::size_t>(frame.height);
|
||||
if (frame.bytes != nullptr && byteCount <= readback.byteCount)
|
||||
{
|
||||
std::memcpy(frame.bytes, mapped, byteCount);
|
||||
frame.frameIndex = readback.frameIndex;
|
||||
frame.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
publishFrame(frame);
|
||||
if (onCompleted)
|
||||
onCompleted();
|
||||
}
|
||||
}
|
||||
else if (onAcquireMiss)
|
||||
{
|
||||
onAcquireMiss();
|
||||
}
|
||||
|
||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mPboRing.ReleaseCompleted(readback);
|
||||
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||
}
|
||||
}
|
||||
|
||||
bool Bgra8ReadbackPipeline::CreateRenderTarget()
|
||||
{
|
||||
glGenFramebuffers(1, &mFramebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||
|
||||
glGenTextures(1, &mTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA8,
|
||||
static_cast<GLsizei>(mWidth),
|
||||
static_cast<GLsizei>(mHeight),
|
||||
0,
|
||||
GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
nullptr);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
|
||||
|
||||
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return complete;
|
||||
}
|
||||
|
||||
void Bgra8ReadbackPipeline::DestroyRenderTarget()
|
||||
{
|
||||
if (mFramebuffer != 0)
|
||||
glDeleteFramebuffers(1, &mFramebuffer);
|
||||
if (mTexture != 0)
|
||||
glDeleteTextures(1, &mTexture);
|
||||
mFramebuffer = 0;
|
||||
mTexture = 0;
|
||||
}
|
||||
58
src/render/readback/Bgra8ReadbackPipeline.h
Normal file
58
src/render/readback/Bgra8ReadbackPipeline.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "PboReadbackRing.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
struct SystemFrame;
|
||||
|
||||
class Bgra8ReadbackPipeline
|
||||
{
|
||||
public:
|
||||
using RenderCallback = std::function<void(uint64_t frameIndex)>;
|
||||
using AcquireFrameCallback = std::function<bool(SystemFrame& frame)>;
|
||||
using PublishFrameCallback = std::function<bool(const SystemFrame& frame)>;
|
||||
using CounterCallback = std::function<void()>;
|
||||
|
||||
Bgra8ReadbackPipeline() = default;
|
||||
Bgra8ReadbackPipeline(const Bgra8ReadbackPipeline&) = delete;
|
||||
Bgra8ReadbackPipeline& operator=(const Bgra8ReadbackPipeline&) = delete;
|
||||
~Bgra8ReadbackPipeline();
|
||||
|
||||
bool Initialize(unsigned width, unsigned height, std::size_t pboDepth);
|
||||
void Shutdown();
|
||||
|
||||
bool RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame);
|
||||
void ConsumeCompleted(
|
||||
const AcquireFrameCallback& acquireFrame,
|
||||
const PublishFrameCallback& publishFrame,
|
||||
const CounterCallback& onAcquireMiss = {},
|
||||
const CounterCallback& onCompleted = {});
|
||||
|
||||
GLuint Framebuffer() const { return mFramebuffer; }
|
||||
unsigned Width() const { return mWidth; }
|
||||
unsigned Height() const { return mHeight; }
|
||||
unsigned RowBytes() const { return mRowBytes; }
|
||||
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
||||
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
||||
double LastRenderFrameMilliseconds() const { return mLastRenderFrameMilliseconds; }
|
||||
double LastReadbackQueueMilliseconds() const { return mLastReadbackQueueMilliseconds; }
|
||||
double LastCompletedReadbackCopyMilliseconds() const { return mLastCompletedReadbackCopyMilliseconds; }
|
||||
|
||||
private:
|
||||
bool CreateRenderTarget();
|
||||
void DestroyRenderTarget();
|
||||
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
unsigned mRowBytes = 0;
|
||||
GLuint mFramebuffer = 0;
|
||||
GLuint mTexture = 0;
|
||||
double mLastRenderFrameMilliseconds = 0.0;
|
||||
double mLastReadbackQueueMilliseconds = 0.0;
|
||||
double mLastCompletedReadbackCopyMilliseconds = 0.0;
|
||||
PboReadbackRing mPboRing;
|
||||
};
|
||||
138
src/render/readback/PboReadbackRing.cpp
Normal file
138
src/render/readback/PboReadbackRing.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "PboReadbackRing.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
PboReadbackRing::~PboReadbackRing()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool PboReadbackRing::Initialize(std::size_t depth, std::size_t byteCount)
|
||||
{
|
||||
Shutdown();
|
||||
if (depth == 0 || byteCount == 0)
|
||||
return false;
|
||||
|
||||
mSlots.resize(depth);
|
||||
mByteCount = byteCount;
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
glGenBuffers(1, &slot.pbo);
|
||||
if (slot.pbo == 0)
|
||||
{
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
|
||||
}
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PboReadbackRing::Shutdown()
|
||||
{
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
if (slot.fence)
|
||||
glDeleteSync(slot.fence);
|
||||
if (slot.pbo != 0)
|
||||
glDeleteBuffers(1, &slot.pbo);
|
||||
slot = {};
|
||||
}
|
||||
mSlots.clear();
|
||||
mWriteIndex = 0;
|
||||
mReadIndex = 0;
|
||||
mByteCount = 0;
|
||||
}
|
||||
|
||||
bool PboReadbackRing::QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex)
|
||||
{
|
||||
if (mSlots.empty())
|
||||
return false;
|
||||
|
||||
Slot& slot = mSlots[mWriteIndex];
|
||||
if (slot.inFlight || slot.acquired)
|
||||
{
|
||||
++mQueueMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
|
||||
glReadPixels(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
|
||||
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
slot.inFlight = slot.fence != nullptr;
|
||||
slot.frameIndex = frameIndex;
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
|
||||
if (!slot.inFlight)
|
||||
return false;
|
||||
|
||||
mWriteIndex = (mWriteIndex + 1) % mSlots.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PboReadbackRing::TryAcquireCompleted(CompletedReadback& readback)
|
||||
{
|
||||
if (mSlots.empty())
|
||||
return false;
|
||||
|
||||
for (std::size_t checked = 0; checked < mSlots.size(); ++checked)
|
||||
{
|
||||
Slot& slot = mSlots[mReadIndex];
|
||||
if (!slot.inFlight || slot.acquired || slot.fence == nullptr)
|
||||
{
|
||||
mReadIndex = (mReadIndex + 1) % mSlots.size();
|
||||
continue;
|
||||
}
|
||||
|
||||
const GLenum waitResult = glClientWaitSync(slot.fence, 0, 0);
|
||||
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
||||
return false;
|
||||
|
||||
slot.acquired = true;
|
||||
readback.pbo = slot.pbo;
|
||||
readback.frameIndex = slot.frameIndex;
|
||||
readback.byteCount = mByteCount;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PboReadbackRing::ReleaseCompleted(const CompletedReadback& readback)
|
||||
{
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
Slot& slot = mSlots[index];
|
||||
if (!slot.acquired || slot.pbo != readback.pbo)
|
||||
continue;
|
||||
ResetSlot(slot);
|
||||
mReadIndex = (index + 1) % mSlots.size();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void PboReadbackRing::DrainCompleted()
|
||||
{
|
||||
for (std::size_t pass = 0; pass < mSlots.size() * 2; ++pass)
|
||||
{
|
||||
CompletedReadback readback;
|
||||
if (!TryAcquireCompleted(readback))
|
||||
break;
|
||||
ReleaseCompleted(readback);
|
||||
}
|
||||
}
|
||||
|
||||
void PboReadbackRing::ResetSlot(Slot& slot)
|
||||
{
|
||||
if (slot.fence)
|
||||
glDeleteSync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
slot.inFlight = false;
|
||||
slot.acquired = false;
|
||||
slot.frameIndex = 0;
|
||||
}
|
||||
52
src/render/readback/PboReadbackRing.h
Normal file
52
src/render/readback/PboReadbackRing.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class PboReadbackRing
|
||||
{
|
||||
public:
|
||||
struct CompletedReadback
|
||||
{
|
||||
GLuint pbo = 0;
|
||||
uint64_t frameIndex = 0;
|
||||
std::size_t byteCount = 0;
|
||||
};
|
||||
|
||||
PboReadbackRing() = default;
|
||||
PboReadbackRing(const PboReadbackRing&) = delete;
|
||||
PboReadbackRing& operator=(const PboReadbackRing&) = delete;
|
||||
~PboReadbackRing();
|
||||
|
||||
bool Initialize(std::size_t depth, std::size_t byteCount);
|
||||
void Shutdown();
|
||||
|
||||
bool QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex);
|
||||
bool TryAcquireCompleted(CompletedReadback& readback);
|
||||
void ReleaseCompleted(const CompletedReadback& readback);
|
||||
void DrainCompleted();
|
||||
|
||||
std::size_t Depth() const { return mSlots.size(); }
|
||||
uint64_t QueueMisses() const { return mQueueMisses; }
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
GLuint pbo = 0;
|
||||
GLsync fence = nullptr;
|
||||
bool inFlight = false;
|
||||
bool acquired = false;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
void ResetSlot(Slot& slot);
|
||||
|
||||
std::vector<Slot> mSlots;
|
||||
std::size_t mWriteIndex = 0;
|
||||
std::size_t mReadIndex = 0;
|
||||
std::size_t mByteCount = 0;
|
||||
uint64_t mQueueMisses = 0;
|
||||
};
|
||||
271
src/render/runtime/RuntimeRenderScene.cpp
Normal file
271
src/render/runtime/RuntimeRenderScene.cpp
Normal file
@@ -0,0 +1,271 @@
|
||||
#include "RuntimeRenderScene.h"
|
||||
|
||||
#include "../../platform/HiddenGlWindow.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
#ifndef GL_FRAMEBUFFER_BINDING
|
||||
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||
#endif
|
||||
|
||||
RuntimeRenderScene::~RuntimeRenderScene()
|
||||
{
|
||||
ShutdownGl();
|
||||
}
|
||||
|
||||
bool RuntimeRenderScene::StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
|
||||
{
|
||||
return mPrepareWorker.Start(std::move(sharedWindow), error);
|
||||
}
|
||||
|
||||
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error)
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
std::vector<std::string> nextOrder;
|
||||
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
|
||||
nextOrder.reserve(layers.size());
|
||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||
{
|
||||
if (!layer.bypass)
|
||||
nextOrder.push_back(layer.id);
|
||||
}
|
||||
|
||||
for (auto layerIt = mLayers.begin(); layerIt != mLayers.end();)
|
||||
{
|
||||
const bool stillPresent = std::find(nextOrder.begin(), nextOrder.end(), layerIt->layerId) != nextOrder.end();
|
||||
if (stillPresent)
|
||||
{
|
||||
++layerIt;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (LayerProgram::PassProgram& pass : layerIt->passes)
|
||||
{
|
||||
if (pass.renderer)
|
||||
pass.renderer->ShutdownGl();
|
||||
}
|
||||
ReleasePendingPrograms(*layerIt);
|
||||
layerIt = mLayers.erase(layerIt);
|
||||
}
|
||||
|
||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||
{
|
||||
if (layer.bypass)
|
||||
continue;
|
||||
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||
continue;
|
||||
|
||||
const std::string fingerprint = Fingerprint(layer.artifact);
|
||||
LayerProgram* program = FindLayer(layer.id);
|
||||
if (!program)
|
||||
{
|
||||
LayerProgram next;
|
||||
next.layerId = layer.id;
|
||||
mLayers.push_back(std::move(next));
|
||||
program = &mLayers.back();
|
||||
}
|
||||
|
||||
bool hasReadyPass = false;
|
||||
for (const LayerProgram::PassProgram& pass : program->passes)
|
||||
{
|
||||
if (pass.renderer && pass.renderer->HasProgram())
|
||||
{
|
||||
hasReadyPass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && hasReadyPass)
|
||||
{
|
||||
for (LayerProgram::PassProgram& pass : program->passes)
|
||||
{
|
||||
if (pass.renderer)
|
||||
pass.renderer->UpdateArtifactState(layer.artifact);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (program->pendingFingerprint == fingerprint)
|
||||
continue;
|
||||
|
||||
ReleasePendingPrograms(*program);
|
||||
program->shaderId = layer.shaderId;
|
||||
program->pendingFingerprint = fingerprint;
|
||||
layersToPrepare.push_back(layer);
|
||||
}
|
||||
|
||||
mLayerOrder = std::move(nextOrder);
|
||||
if (!layersToPrepare.empty())
|
||||
mPrepareWorker.Submit(layersToPrepare);
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::ShutdownGl()
|
||||
{
|
||||
mPrepareWorker.Stop();
|
||||
for (LayerProgram& layer : mLayers)
|
||||
{
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (pass.renderer)
|
||||
pass.renderer->ShutdownGl();
|
||||
}
|
||||
ReleasePendingPrograms(layer);
|
||||
}
|
||||
mLayers.clear();
|
||||
mLayerOrder.clear();
|
||||
DestroyLayerTargets();
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::ConsumePreparedPrograms()
|
||||
{
|
||||
RuntimePreparedShaderProgram preparedProgram;
|
||||
while (mPrepareWorker.TryConsume(preparedProgram))
|
||||
{
|
||||
if (!preparedProgram.succeeded)
|
||||
{
|
||||
preparedProgram.ReleaseGl();
|
||||
continue;
|
||||
}
|
||||
|
||||
LayerProgram* layer = FindLayer(preparedProgram.layerId);
|
||||
if (!layer || layer->pendingFingerprint != preparedProgram.sourceFingerprint)
|
||||
{
|
||||
preparedProgram.ReleaseGl();
|
||||
continue;
|
||||
}
|
||||
|
||||
bool replacesExistingPendingPass = false;
|
||||
for (RuntimePreparedShaderProgram& existing : layer->pendingPreparedPrograms)
|
||||
{
|
||||
if (existing.passId != preparedProgram.passId)
|
||||
continue;
|
||||
existing.ReleaseGl();
|
||||
existing = std::move(preparedProgram);
|
||||
replacesExistingPendingPass = true;
|
||||
break;
|
||||
}
|
||||
if (!replacesExistingPendingPass)
|
||||
layer->pendingPreparedPrograms.push_back(std::move(preparedProgram));
|
||||
TryCommitPendingPrograms(*layer);
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::ReleasePendingPrograms(LayerProgram& layer)
|
||||
{
|
||||
for (RuntimePreparedShaderProgram& program : layer.pendingPreparedPrograms)
|
||||
program.ReleaseGl();
|
||||
layer.pendingPreparedPrograms.clear();
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer)
|
||||
{
|
||||
if (layer.pendingPreparedPrograms.empty())
|
||||
return;
|
||||
|
||||
const RuntimeShaderArtifact& artifact = layer.pendingPreparedPrograms.front().artifact;
|
||||
const std::size_t expectedPassCount = artifact.passes.empty() ? 1 : artifact.passes.size();
|
||||
if (layer.pendingPreparedPrograms.size() < expectedPassCount)
|
||||
return;
|
||||
|
||||
std::vector<LayerProgram::PassProgram> nextPasses;
|
||||
nextPasses.reserve(expectedPassCount);
|
||||
for (const RuntimeShaderPassArtifact& passArtifact : artifact.passes)
|
||||
{
|
||||
auto preparedIt = std::find_if(
|
||||
layer.pendingPreparedPrograms.begin(),
|
||||
layer.pendingPreparedPrograms.end(),
|
||||
[&passArtifact](const RuntimePreparedShaderProgram& prepared) {
|
||||
return prepared.passId == passArtifact.passId;
|
||||
});
|
||||
if (preparedIt == layer.pendingPreparedPrograms.end())
|
||||
return;
|
||||
|
||||
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||
std::string error;
|
||||
if (!nextRenderer->CommitPreparedProgram(*preparedIt, error))
|
||||
{
|
||||
ReleasePendingPrograms(layer);
|
||||
return;
|
||||
}
|
||||
|
||||
LayerProgram::PassProgram nextPass;
|
||||
nextPass.passId = preparedIt->passId;
|
||||
nextPass.inputNames = preparedIt->inputNames;
|
||||
nextPass.outputName = preparedIt->outputName.empty() ? preparedIt->passId : preparedIt->outputName;
|
||||
nextPass.renderer = std::move(nextRenderer);
|
||||
nextPasses.push_back(std::move(nextPass));
|
||||
}
|
||||
if (artifact.passes.empty())
|
||||
{
|
||||
RuntimePreparedShaderProgram& prepared = layer.pendingPreparedPrograms.front();
|
||||
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||
std::string error;
|
||||
if (!nextRenderer->CommitPreparedProgram(prepared, error))
|
||||
{
|
||||
ReleasePendingPrograms(layer);
|
||||
return;
|
||||
}
|
||||
|
||||
LayerProgram::PassProgram nextPass;
|
||||
nextPass.passId = prepared.passId;
|
||||
nextPass.inputNames = prepared.inputNames;
|
||||
nextPass.outputName = prepared.outputName.empty() ? prepared.passId : prepared.outputName;
|
||||
nextPass.renderer = std::move(nextRenderer);
|
||||
nextPasses.push_back(std::move(nextPass));
|
||||
}
|
||||
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (pass.renderer)
|
||||
pass.renderer->ShutdownGl();
|
||||
}
|
||||
layer.passes = std::move(nextPasses);
|
||||
layer.shaderId = artifact.shaderId;
|
||||
layer.sourceFingerprint = layer.pendingPreparedPrograms.front().sourceFingerprint;
|
||||
layer.pendingFingerprint.clear();
|
||||
layer.pendingPreparedPrograms.clear();
|
||||
}
|
||||
|
||||
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
||||
{
|
||||
for (LayerProgram& layer : mLayers)
|
||||
{
|
||||
if (layer.layerId == layerId)
|
||||
return &layer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId) const
|
||||
{
|
||||
for (const LayerProgram& layer : mLayers)
|
||||
{
|
||||
if (layer.layerId == layerId)
|
||||
return &layer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RuntimeRenderScene::LayerProgram::PassProgram* RuntimeRenderScene::FindPass(LayerProgram& layer, const std::string& passId)
|
||||
{
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (pass.passId == passId)
|
||||
return &pass;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string RuntimeRenderScene::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
const std::hash<std::string> hasher;
|
||||
std::string source;
|
||||
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
|
||||
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
|
||||
if (source.empty())
|
||||
source = artifact.fragmentShaderSource;
|
||||
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
|
||||
}
|
||||
64
src/render/runtime/RuntimeRenderScene.h
Normal file
64
src/render/runtime/RuntimeRenderScene.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "../runtime/RuntimeLayerModel.h"
|
||||
#include "RuntimeShaderPrepareWorker.h"
|
||||
#include "RuntimeShaderRenderer.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeRenderScene
|
||||
{
|
||||
public:
|
||||
RuntimeRenderScene() = default;
|
||||
RuntimeRenderScene(const RuntimeRenderScene&) = delete;
|
||||
RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete;
|
||||
~RuntimeRenderScene();
|
||||
|
||||
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
|
||||
bool HasLayers();
|
||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture = 0);
|
||||
void ShutdownGl();
|
||||
|
||||
private:
|
||||
struct LayerProgram
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string sourceFingerprint;
|
||||
std::string pendingFingerprint;
|
||||
std::vector<RuntimePreparedShaderProgram> pendingPreparedPrograms;
|
||||
struct PassProgram
|
||||
{
|
||||
std::string passId;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
std::unique_ptr<RuntimeShaderRenderer> renderer;
|
||||
};
|
||||
std::vector<PassProgram> passes;
|
||||
};
|
||||
|
||||
void ConsumePreparedPrograms();
|
||||
void ReleasePendingPrograms(LayerProgram& layer);
|
||||
void TryCommitPendingPrograms(LayerProgram& layer);
|
||||
bool EnsureLayerTargets(unsigned width, unsigned height);
|
||||
void DestroyLayerTargets();
|
||||
GLuint RenderLayer(LayerProgram& layer, uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture, GLuint layerInputTexture, GLuint outputFramebuffer, bool renderToOutput);
|
||||
LayerProgram* FindLayer(const std::string& layerId);
|
||||
const LayerProgram* FindLayer(const std::string& layerId) const;
|
||||
LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId);
|
||||
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
||||
|
||||
RuntimeShaderPrepareWorker mPrepareWorker;
|
||||
std::vector<LayerProgram> mLayers;
|
||||
std::vector<std::string> mLayerOrder;
|
||||
GLuint mLayerFramebuffers[4] = {};
|
||||
GLuint mLayerTextures[4] = {};
|
||||
unsigned mLayerTargetWidth = 0;
|
||||
unsigned mLayerTargetHeight = 0;
|
||||
};
|
||||
219
src/render/runtime/RuntimeRenderSceneRender.cpp
Normal file
219
src/render/runtime/RuntimeRenderSceneRender.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "RuntimeRenderScene.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef GL_FRAMEBUFFER_BINDING
|
||||
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||
#endif
|
||||
|
||||
bool RuntimeRenderScene::HasLayers()
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
const LayerProgram* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
continue;
|
||||
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||
{
|
||||
if (pass.renderer && pass.renderer->HasProgram())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
std::vector<LayerProgram*> readyLayers;
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
LayerProgram* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
continue;
|
||||
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||
{
|
||||
if (pass.renderer && pass.renderer->HasProgram())
|
||||
{
|
||||
readyLayers.push_back(layer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (readyLayers.empty())
|
||||
return;
|
||||
|
||||
GLint outputFramebuffer = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
|
||||
|
||||
if (readyLayers.size() == 1)
|
||||
{
|
||||
RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EnsureLayerTargets(width, height))
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||
RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Shader source contract:
|
||||
// - gVideoInput is the decoded current input texture for every layer in the stack.
|
||||
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
|
||||
GLuint layerInputTexture = videoInputTexture;
|
||||
std::size_t nextTargetIndex = 0;
|
||||
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
|
||||
{
|
||||
const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
|
||||
if (isFinalLayer)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
|
||||
layerInputTexture = mLayerTextures[nextTargetIndex];
|
||||
nextTargetIndex = 1 - nextTargetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
GLuint RuntimeRenderScene::RenderLayer(
|
||||
LayerProgram& layer,
|
||||
uint64_t frameIndex,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLuint videoInputTexture,
|
||||
GLuint layerInputTexture,
|
||||
GLuint outputFramebuffer,
|
||||
bool renderToOutput)
|
||||
{
|
||||
GLuint namedOutputs[2] = {};
|
||||
std::string namedOutputNames[2];
|
||||
std::size_t nextTargetIndex = 2;
|
||||
GLuint lastOutputTexture = layerInputTexture;
|
||||
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (!pass.renderer || !pass.renderer->HasProgram())
|
||||
continue;
|
||||
|
||||
GLuint sourceTexture = videoInputTexture;
|
||||
if (!pass.inputNames.empty())
|
||||
{
|
||||
const std::string& inputName = pass.inputNames.front();
|
||||
if (inputName == "videoInput")
|
||||
{
|
||||
sourceTexture = videoInputTexture;
|
||||
}
|
||||
else if (inputName != "layerInput")
|
||||
{
|
||||
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
|
||||
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
|
||||
// sampling and gVideoInput for the original input frame.
|
||||
for (std::size_t index = 0; index < 2; ++index)
|
||||
{
|
||||
if (namedOutputNames[index] == inputName)
|
||||
{
|
||||
sourceTexture = namedOutputs[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool writesLayerOutput = pass.outputName == "layerOutput";
|
||||
if (writesLayerOutput && renderToOutput)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
|
||||
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||
lastOutputTexture = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!EnsureLayerTargets(width, height))
|
||||
continue;
|
||||
|
||||
const std::size_t targetIndex = nextTargetIndex;
|
||||
nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
|
||||
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||
const std::size_t namedIndex = targetIndex - 2;
|
||||
namedOutputs[namedIndex] = mLayerTextures[targetIndex];
|
||||
namedOutputNames[namedIndex] = pass.outputName;
|
||||
lastOutputTexture = mLayerTextures[targetIndex];
|
||||
}
|
||||
|
||||
return lastOutputTexture;
|
||||
}
|
||||
|
||||
bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
return false;
|
||||
if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0
|
||||
&& mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0
|
||||
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
|
||||
return true;
|
||||
|
||||
DestroyLayerTargets();
|
||||
mLayerTargetWidth = width;
|
||||
mLayerTargetHeight = height;
|
||||
|
||||
glGenFramebuffers(4, mLayerFramebuffers);
|
||||
glGenTextures(4, mLayerTextures);
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA8,
|
||||
static_cast<GLsizei>(width),
|
||||
static_cast<GLsizei>(height),
|
||||
0,
|
||||
GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
nullptr);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroyLayerTargets();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::DestroyLayerTargets()
|
||||
{
|
||||
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
|
||||
glDeleteFramebuffers(4, mLayerFramebuffers);
|
||||
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
|
||||
glDeleteTextures(4, mLayerTextures);
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
mLayerFramebuffers[index] = 0;
|
||||
mLayerTextures[index] = 0;
|
||||
}
|
||||
mLayerTargetWidth = 0;
|
||||
mLayerTargetHeight = 0;
|
||||
}
|
||||
123
src/render/runtime/RuntimeShaderParams.cpp
Normal file
123
src/render/runtime/RuntimeShaderParams.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#include "RuntimeShaderParams.h"
|
||||
|
||||
#include "Std140Buffer.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace
|
||||
{
|
||||
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition)
|
||||
{
|
||||
ShaderParameterValue value;
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Float:
|
||||
value.numberValues = definition.defaultNumbers.empty() ? std::vector<double>{ 0.0 } : definition.defaultNumbers;
|
||||
break;
|
||||
case ShaderParameterType::Vec2:
|
||||
value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector<double>{ 0.0, 0.0 };
|
||||
break;
|
||||
case ShaderParameterType::Color:
|
||||
value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector<double>{ 1.0, 1.0, 1.0, 1.0 };
|
||||
break;
|
||||
case ShaderParameterType::Boolean:
|
||||
value.booleanValue = definition.defaultBoolean;
|
||||
break;
|
||||
case ShaderParameterType::Enum:
|
||||
value.enumValue = definition.defaultEnumValue;
|
||||
break;
|
||||
case ShaderParameterType::Text:
|
||||
value.textValue = definition.defaultTextValue;
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
value.numberValues = { 0.0, -1000000.0 };
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int EnumIndexForDefault(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
|
||||
{
|
||||
for (std::size_t optionIndex = 0; optionIndex < definition.enumOptions.size(); ++optionIndex)
|
||||
{
|
||||
if (definition.enumOptions[optionIndex].value == value.enumValue)
|
||||
return static_cast<int>(optionIndex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
double UtcSecondsOfDay()
|
||||
{
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
||||
const long long secondsPerDay = 24 * 60 * 60;
|
||||
long long daySeconds = seconds % secondsPerDay;
|
||||
if (daySeconds < 0)
|
||||
daySeconds += secondsPerDay;
|
||||
return static_cast<double>(daySeconds);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
uint64_t frameIndex,
|
||||
unsigned width,
|
||||
unsigned height)
|
||||
{
|
||||
std::vector<unsigned char> buffer;
|
||||
buffer.reserve(512);
|
||||
|
||||
AppendStd140Float(buffer, static_cast<float>(frameIndex) / 60.0f);
|
||||
AppendStd140Vec2(buffer, static_cast<float>(width), static_cast<float>(height));
|
||||
AppendStd140Vec2(buffer, static_cast<float>(width), static_cast<float>(height));
|
||||
AppendStd140Float(buffer, static_cast<float>(UtcSecondsOfDay()));
|
||||
AppendStd140Float(buffer, 0.0f);
|
||||
AppendStd140Float(buffer, 0.37f);
|
||||
AppendStd140Float(buffer, static_cast<float>(frameIndex));
|
||||
AppendStd140Float(buffer, 1.0f);
|
||||
AppendStd140Float(buffer, 0.0f);
|
||||
AppendStd140Int(buffer, 0);
|
||||
AppendStd140Int(buffer, 0);
|
||||
AppendStd140Int(buffer, 0);
|
||||
|
||||
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
||||
{
|
||||
const auto valueIt = artifact.parameterValues.find(definition.id);
|
||||
const ShaderParameterValue value = valueIt == artifact.parameterValues.end()
|
||||
? DefaultValueForDefinition(definition)
|
||||
: valueIt->second;
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Float:
|
||||
AppendStd140Float(buffer, value.numberValues.empty() ? 0.0f : static_cast<float>(value.numberValues[0]));
|
||||
break;
|
||||
case ShaderParameterType::Vec2:
|
||||
AppendStd140Vec2(buffer,
|
||||
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 0.0f,
|
||||
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 0.0f);
|
||||
break;
|
||||
case ShaderParameterType::Color:
|
||||
AppendStd140Vec4(buffer,
|
||||
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 1.0f,
|
||||
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 1.0f,
|
||||
value.numberValues.size() > 2 ? static_cast<float>(value.numberValues[2]) : 1.0f,
|
||||
value.numberValues.size() > 3 ? static_cast<float>(value.numberValues[3]) : 1.0f);
|
||||
break;
|
||||
case ShaderParameterType::Boolean:
|
||||
AppendStd140Int(buffer, value.booleanValue ? 1 : 0);
|
||||
break;
|
||||
case ShaderParameterType::Enum:
|
||||
AppendStd140Int(buffer, EnumIndexForDefault(definition, value));
|
||||
break;
|
||||
case ShaderParameterType::Text:
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
|
||||
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.resize(AlignStd140(buffer.size(), 16), 0);
|
||||
return buffer;
|
||||
}
|
||||
12
src/render/runtime/RuntimeShaderParams.h
Normal file
12
src/render/runtime/RuntimeShaderParams.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "../runtime/RuntimeShaderArtifact.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
uint64_t frameIndex,
|
||||
unsigned width,
|
||||
unsigned height);
|
||||
179
src/render/runtime/RuntimeShaderPrepareWorker.cpp
Normal file
179
src/render/runtime/RuntimeShaderPrepareWorker.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
#include "RuntimeShaderPrepareWorker.h"
|
||||
|
||||
#include "../../platform/HiddenGlWindow.h"
|
||||
#include "RuntimeShaderRenderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
RuntimeShaderPrepareWorker::~RuntimeShaderPrepareWorker()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool RuntimeShaderPrepareWorker::Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
|
||||
{
|
||||
if (mThread.joinable())
|
||||
return true;
|
||||
if (!sharedWindow || sharedWindow->DeviceContext() == nullptr || sharedWindow->Context() == nullptr)
|
||||
{
|
||||
error = "Runtime shader prepare worker needs an existing shared GL context.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mWindow = std::move(sharedWindow);
|
||||
mStopping.store(false, std::memory_order_release);
|
||||
mStarted.store(false, std::memory_order_release);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mStartupReady = false;
|
||||
mStartupError.clear();
|
||||
}
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() {
|
||||
return mStartupReady || !mStartupError.empty();
|
||||
}))
|
||||
{
|
||||
error = "Timed out starting runtime shader prepare worker.";
|
||||
lock.unlock();
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
if (!mStartupError.empty())
|
||||
{
|
||||
error = mStartupError;
|
||||
lock.unlock();
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeShaderPrepareWorker::Stop()
|
||||
{
|
||||
mStopping.store(true, std::memory_order_release);
|
||||
mCondition.notify_all();
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
|
||||
std::deque<RuntimePreparedShaderProgram> completed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mRequests.clear();
|
||||
completed.swap(mCompleted);
|
||||
}
|
||||
for (RuntimePreparedShaderProgram& program : completed)
|
||||
program.ReleaseGl();
|
||||
mWindow.reset();
|
||||
mStarted.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||
{
|
||||
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||
continue;
|
||||
|
||||
std::vector<RuntimeShaderPassArtifact> passes = layer.artifact.passes;
|
||||
if (passes.empty())
|
||||
{
|
||||
RuntimeShaderPassArtifact pass;
|
||||
pass.passId = "main";
|
||||
pass.fragmentShaderSource = layer.artifact.fragmentShaderSource;
|
||||
pass.outputName = "layerOutput";
|
||||
passes.push_back(std::move(pass));
|
||||
}
|
||||
|
||||
auto sameLayer = [&layer](const PrepareRequest& existing) {
|
||||
return existing.layerId == layer.id;
|
||||
};
|
||||
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
|
||||
|
||||
for (const RuntimeShaderPassArtifact& pass : passes)
|
||||
{
|
||||
PrepareRequest request;
|
||||
request.layerId = layer.id;
|
||||
request.shaderId = layer.shaderId;
|
||||
request.passId = pass.passId;
|
||||
request.sourceFingerprint = Fingerprint(layer.artifact);
|
||||
request.artifact = layer.artifact;
|
||||
request.passArtifact = pass;
|
||||
mRequests.push_back(std::move(request));
|
||||
}
|
||||
}
|
||||
mCondition.notify_one();
|
||||
}
|
||||
|
||||
bool RuntimeShaderPrepareWorker::TryConsume(RuntimePreparedShaderProgram& preparedProgram)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mCompleted.empty())
|
||||
return false;
|
||||
|
||||
preparedProgram = std::move(mCompleted.front());
|
||||
mCompleted.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeShaderPrepareWorker::ThreadMain()
|
||||
{
|
||||
if (!mWindow || !mWindow->MakeCurrent())
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mStartupError = "Runtime shader prepare worker could not make shared GL context current.";
|
||||
mStartupCondition.notify_all();
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mStartupReady = true;
|
||||
}
|
||||
mStarted.store(true, std::memory_order_release);
|
||||
mStartupCondition.notify_all();
|
||||
|
||||
while (!mStopping.load(std::memory_order_acquire))
|
||||
{
|
||||
PrepareRequest request;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
mCondition.wait(lock, [this]() {
|
||||
return mStopping.load(std::memory_order_acquire) || !mRequests.empty();
|
||||
});
|
||||
if (mStopping.load(std::memory_order_acquire))
|
||||
break;
|
||||
request = std::move(mRequests.front());
|
||||
mRequests.pop_front();
|
||||
}
|
||||
|
||||
RuntimePreparedShaderProgram preparedProgram;
|
||||
RuntimeShaderRenderer::BuildPreparedPassProgram(
|
||||
request.layerId,
|
||||
request.sourceFingerprint,
|
||||
request.artifact,
|
||||
request.passArtifact,
|
||||
preparedProgram);
|
||||
glFlush();
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mCompleted.push_back(std::move(preparedProgram));
|
||||
}
|
||||
|
||||
mWindow->ClearCurrent();
|
||||
}
|
||||
|
||||
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
const std::hash<std::string> hasher;
|
||||
std::string source;
|
||||
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
|
||||
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
|
||||
if (source.empty())
|
||||
source = artifact.fragmentShaderSource;
|
||||
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
|
||||
}
|
||||
58
src/render/runtime/RuntimeShaderPrepareWorker.h
Normal file
58
src/render/runtime/RuntimeShaderPrepareWorker.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeShaderProgram.h"
|
||||
#include "../../runtime/RuntimeLayerModel.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class HiddenGlWindow;
|
||||
|
||||
class RuntimeShaderPrepareWorker
|
||||
{
|
||||
public:
|
||||
RuntimeShaderPrepareWorker() = default;
|
||||
RuntimeShaderPrepareWorker(const RuntimeShaderPrepareWorker&) = delete;
|
||||
RuntimeShaderPrepareWorker& operator=(const RuntimeShaderPrepareWorker&) = delete;
|
||||
~RuntimeShaderPrepareWorker();
|
||||
|
||||
bool Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||
void Stop();
|
||||
|
||||
void Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||
bool TryConsume(RuntimePreparedShaderProgram& preparedProgram);
|
||||
|
||||
private:
|
||||
struct PrepareRequest
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string passId;
|
||||
std::string sourceFingerprint;
|
||||
RuntimeShaderArtifact artifact;
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
};
|
||||
|
||||
void ThreadMain();
|
||||
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
||||
|
||||
std::unique_ptr<HiddenGlWindow> mWindow;
|
||||
std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
std::deque<PrepareRequest> mRequests;
|
||||
std::deque<RuntimePreparedShaderProgram> mCompleted;
|
||||
std::condition_variable mStartupCondition;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mStarted{ false };
|
||||
bool mStartupReady = false;
|
||||
std::string mStartupError;
|
||||
};
|
||||
37
src/render/runtime/RuntimeShaderProgram.h
Normal file
37
src/render/runtime/RuntimeShaderProgram.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimePreparedShaderProgram
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string passId;
|
||||
std::string sourceFingerprint;
|
||||
RuntimeShaderArtifact artifact;
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
GLuint program = 0;
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
bool succeeded = false;
|
||||
std::string error;
|
||||
|
||||
void ReleaseGl()
|
||||
{
|
||||
if (program != 0)
|
||||
glDeleteProgram(program);
|
||||
if (vertexShader != 0)
|
||||
glDeleteShader(vertexShader);
|
||||
if (fragmentShader != 0)
|
||||
glDeleteShader(fragmentShader);
|
||||
program = 0;
|
||||
vertexShader = 0;
|
||||
fragmentShader = 0;
|
||||
}
|
||||
};
|
||||
320
src/render/runtime/RuntimeShaderRenderer.cpp
Normal file
320
src/render/runtime/RuntimeShaderRenderer.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#include "RuntimeShaderRenderer.h"
|
||||
|
||||
#include "RuntimeShaderParams.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||
constexpr GLuint kVideoInputTextureUnit = 0;
|
||||
constexpr GLuint kLayerInputTextureUnit = 1;
|
||||
|
||||
const char* kVertexShaderSource = R"GLSL(
|
||||
#version 430 core
|
||||
out vec2 vTexCoord;
|
||||
void main()
|
||||
{
|
||||
vec2 positions[3] = vec2[3](
|
||||
vec2(-1.0, -1.0),
|
||||
vec2( 3.0, -1.0),
|
||||
vec2(-1.0, 3.0));
|
||||
vec2 texCoords[3] = vec2[3](
|
||||
vec2(0.0, 0.0),
|
||||
vec2(2.0, 0.0),
|
||||
vec2(0.0, 2.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
vTexCoord = texCoords[gl_VertexID];
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
}
|
||||
|
||||
RuntimeShaderRenderer::~RuntimeShaderRenderer()
|
||||
{
|
||||
ShutdownGl();
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error)
|
||||
{
|
||||
if (!preparedProgram.succeeded || preparedProgram.program == 0)
|
||||
{
|
||||
error = preparedProgram.error.empty() ? "Prepared runtime shader program is not valid." : preparedProgram.error;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureStaticGlResources(error))
|
||||
return false;
|
||||
|
||||
DestroyProgram();
|
||||
mProgram = preparedProgram.program;
|
||||
mVertexShader = preparedProgram.vertexShader;
|
||||
mFragmentShader = preparedProgram.fragmentShader;
|
||||
mArtifact = preparedProgram.artifact;
|
||||
preparedProgram.program = 0;
|
||||
preparedProgram.vertexShader = 0;
|
||||
preparedProgram.fragmentShader = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
|
||||
mArtifact.parameterValues = artifact.parameterValues;
|
||||
mArtifact.message = artifact.message;
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::BuildPreparedProgram(
|
||||
const std::string& layerId,
|
||||
const std::string& sourceFingerprint,
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
RuntimePreparedShaderProgram& preparedProgram)
|
||||
{
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
passArtifact.passId = "main";
|
||||
passArtifact.fragmentShaderSource = artifact.fragmentShaderSource;
|
||||
passArtifact.outputName = "layerOutput";
|
||||
if (!artifact.passes.empty())
|
||||
passArtifact = artifact.passes.front();
|
||||
return BuildPreparedPassProgram(layerId, sourceFingerprint, artifact, passArtifact, preparedProgram);
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::BuildPreparedPassProgram(
|
||||
const std::string& layerId,
|
||||
const std::string& sourceFingerprint,
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
const RuntimeShaderPassArtifact& passArtifact,
|
||||
RuntimePreparedShaderProgram& preparedProgram)
|
||||
{
|
||||
preparedProgram = RuntimePreparedShaderProgram();
|
||||
preparedProgram.layerId = layerId;
|
||||
preparedProgram.shaderId = artifact.shaderId;
|
||||
preparedProgram.passId = passArtifact.passId;
|
||||
preparedProgram.sourceFingerprint = sourceFingerprint;
|
||||
preparedProgram.artifact = artifact;
|
||||
preparedProgram.passArtifact = passArtifact;
|
||||
preparedProgram.inputNames = passArtifact.inputNames;
|
||||
preparedProgram.outputName = passArtifact.outputName.empty() ? passArtifact.passId : passArtifact.outputName;
|
||||
|
||||
if (passArtifact.fragmentShaderSource.empty())
|
||||
{
|
||||
preparedProgram.error = "Cannot prepare an empty fragment shader.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BuildProgram(
|
||||
passArtifact.fragmentShaderSource,
|
||||
preparedProgram.program,
|
||||
preparedProgram.vertexShader,
|
||||
preparedProgram.fragmentShader,
|
||||
preparedProgram.error))
|
||||
{
|
||||
preparedProgram.ReleaseGl();
|
||||
return false;
|
||||
}
|
||||
|
||||
preparedProgram.succeeded = true;
|
||||
AssignSamplerUniforms(preparedProgram.program);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture, GLuint layerInputTexture)
|
||||
{
|
||||
if (mProgram == 0)
|
||||
return;
|
||||
|
||||
glViewport(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
UpdateGlobalParams(frameIndex, width, height);
|
||||
BindRuntimeTextures(sourceTexture, layerInputTexture);
|
||||
glBindVertexArray(mVertexArray);
|
||||
glUseProgram(mProgram);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::ShutdownGl()
|
||||
{
|
||||
DestroyProgram();
|
||||
DestroyStaticGlResources();
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::EnsureStaticGlResources(std::string& error)
|
||||
{
|
||||
if (mVertexArray == 0)
|
||||
glGenVertexArrays(1, &mVertexArray);
|
||||
if (mGlobalParamsBuffer == 0)
|
||||
{
|
||||
glGenBuffers(1, &mGlobalParamsBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
|
||||
glBufferData(GL_UNIFORM_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
}
|
||||
if (mFallbackSourceTexture == 0)
|
||||
{
|
||||
const unsigned char pixels[] = {
|
||||
0, 0, 0, 255,
|
||||
96, 64, 32, 255,
|
||||
64, 96, 160, 255,
|
||||
255, 255, 255, 255
|
||||
};
|
||||
glGenTextures(1, &mFallbackSourceTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mFallbackSourceTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
if (mVertexArray == 0 || mGlobalParamsBuffer == 0 || mFallbackSourceTexture == 0)
|
||||
{
|
||||
error = "Failed to create runtime shader GL resources.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error)
|
||||
{
|
||||
program = 0;
|
||||
vertexShader = 0;
|
||||
fragmentShader = 0;
|
||||
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kVertexShaderSource, vertexShader, error))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource.c_str(), fragmentShader, error))
|
||||
{
|
||||
glDeleteShader(vertexShader);
|
||||
vertexShader = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
glLinkProgram(program);
|
||||
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
std::array<char, 4096> log = {};
|
||||
GLsizei length = 0;
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), &length, log.data());
|
||||
error = std::string(log.data(), static_cast<std::size_t>(length));
|
||||
glDeleteProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
glDeleteShader(fragmentShader);
|
||||
program = 0;
|
||||
vertexShader = 0;
|
||||
fragmentShader = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint globalParamsIndex = glGetUniformBlockIndex(program, "GlobalParams");
|
||||
if (globalParamsIndex != GL_INVALID_INDEX)
|
||||
glUniformBlockBinding(program, globalParamsIndex, kGlobalParamsBindingPoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program)
|
||||
{
|
||||
glUseProgram(program);
|
||||
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
||||
if (videoInputLocation >= 0)
|
||||
glUniform1i(videoInputLocation, static_cast<GLint>(kVideoInputTextureUnit));
|
||||
const GLint videoInputArrayLocation = glGetUniformLocation(program, "gVideoInput_0");
|
||||
if (videoInputArrayLocation >= 0)
|
||||
glUniform1i(videoInputArrayLocation, static_cast<GLint>(kVideoInputTextureUnit));
|
||||
const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput");
|
||||
if (layerInputLocation >= 0)
|
||||
glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
||||
const GLint layerInputArrayLocation = glGetUniformLocation(program, "gLayerInput_0");
|
||||
if (layerInputArrayLocation >= 0)
|
||||
glUniform1i(layerInputArrayLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height)
|
||||
{
|
||||
std::vector<unsigned char>& buffer = mGlobalParamsScratch;
|
||||
buffer = BuildRuntimeShaderGlobalParamsStd140(mArtifact, frameIndex, width, height);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
|
||||
const GLsizeiptr bufferSize = static_cast<GLsizeiptr>(buffer.size());
|
||||
if (mGlobalParamsBufferSize != bufferSize)
|
||||
{
|
||||
glBufferData(GL_UNIFORM_BUFFER, bufferSize, buffer.data(), GL_DYNAMIC_DRAW);
|
||||
mGlobalParamsBufferSize = bufferSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
glBufferSubData(GL_UNIFORM_BUFFER, 0, bufferSize, buffer.data());
|
||||
}
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture)
|
||||
{
|
||||
const GLuint resolvedSourceTexture = sourceTexture != 0 ? sourceTexture : mFallbackSourceTexture;
|
||||
const GLuint resolvedLayerInputTexture = layerInputTexture != 0 ? layerInputTexture : resolvedSourceTexture;
|
||||
glActiveTexture(GL_TEXTURE0 + kVideoInputTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, resolvedSourceTexture);
|
||||
glActiveTexture(GL_TEXTURE0 + kLayerInputTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, resolvedLayerInputTexture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error)
|
||||
{
|
||||
shader = glCreateShader(shaderType);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
|
||||
GLint compileResult = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult != GL_FALSE)
|
||||
return true;
|
||||
|
||||
std::array<char, 4096> log = {};
|
||||
GLsizei length = 0;
|
||||
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), &length, log.data());
|
||||
error = std::string(log.data(), static_cast<std::size_t>(length));
|
||||
glDeleteShader(shader);
|
||||
shader = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::DestroyProgram()
|
||||
{
|
||||
if (mProgram != 0)
|
||||
glDeleteProgram(mProgram);
|
||||
if (mVertexShader != 0)
|
||||
glDeleteShader(mVertexShader);
|
||||
if (mFragmentShader != 0)
|
||||
glDeleteShader(mFragmentShader);
|
||||
mProgram = 0;
|
||||
mVertexShader = 0;
|
||||
mFragmentShader = 0;
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::DestroyStaticGlResources()
|
||||
{
|
||||
if (mGlobalParamsBuffer != 0)
|
||||
glDeleteBuffers(1, &mGlobalParamsBuffer);
|
||||
if (mVertexArray != 0)
|
||||
glDeleteVertexArrays(1, &mVertexArray);
|
||||
if (mFallbackSourceTexture != 0)
|
||||
glDeleteTextures(1, &mFallbackSourceTexture);
|
||||
mGlobalParamsBuffer = 0;
|
||||
mGlobalParamsBufferSize = 0;
|
||||
mVertexArray = 0;
|
||||
mFallbackSourceTexture = 0;
|
||||
}
|
||||
56
src/render/runtime/RuntimeShaderRenderer.h
Normal file
56
src/render/runtime/RuntimeShaderRenderer.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "RuntimeShaderProgram.h"
|
||||
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeShaderRenderer
|
||||
{
|
||||
public:
|
||||
RuntimeShaderRenderer() = default;
|
||||
RuntimeShaderRenderer(const RuntimeShaderRenderer&) = delete;
|
||||
RuntimeShaderRenderer& operator=(const RuntimeShaderRenderer&) = delete;
|
||||
~RuntimeShaderRenderer();
|
||||
|
||||
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
|
||||
bool HasProgram() const { return mProgram != 0; }
|
||||
void UpdateArtifactState(const RuntimeShaderArtifact& artifact);
|
||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture = 0, GLuint layerInputTexture = 0);
|
||||
void ShutdownGl();
|
||||
|
||||
static bool BuildPreparedProgram(
|
||||
const std::string& layerId,
|
||||
const std::string& sourceFingerprint,
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
RuntimePreparedShaderProgram& preparedProgram);
|
||||
static bool BuildPreparedPassProgram(
|
||||
const std::string& layerId,
|
||||
const std::string& sourceFingerprint,
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
const RuntimeShaderPassArtifact& passArtifact,
|
||||
RuntimePreparedShaderProgram& preparedProgram);
|
||||
|
||||
private:
|
||||
bool EnsureStaticGlResources(std::string& error);
|
||||
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error);
|
||||
static bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
|
||||
static void AssignSamplerUniforms(GLuint program);
|
||||
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
|
||||
void BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture);
|
||||
void DestroyProgram();
|
||||
void DestroyStaticGlResources();
|
||||
|
||||
RuntimeShaderArtifact mArtifact;
|
||||
GLuint mProgram = 0;
|
||||
GLuint mVertexShader = 0;
|
||||
GLuint mFragmentShader = 0;
|
||||
GLuint mVertexArray = 0;
|
||||
GLuint mGlobalParamsBuffer = 0;
|
||||
GLsizeiptr mGlobalParamsBufferSize = 0;
|
||||
GLuint mFallbackSourceTexture = 0;
|
||||
std::vector<unsigned char> mGlobalParamsScratch;
|
||||
};
|
||||
379
src/runtime/RuntimeLayerModel.cpp
Normal file
379
src/runtime/RuntimeLayerModel.cpp
Normal file
@@ -0,0 +1,379 @@
|
||||
#include "RuntimeLayerModel.h"
|
||||
|
||||
#include "RuntimeParameterUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
bool RuntimeLayerModel::InitializeSingleLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
Clear();
|
||||
if (shaderId.empty())
|
||||
{
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Shader '" + shaderId + "' is not in the supported shader catalog.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Layer layer;
|
||||
layer.id = AllocateLayerId();
|
||||
layer.shaderId = shaderPackage->id;
|
||||
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||
layer.message = "Runtime Slang build is waiting to start.";
|
||||
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||
mLayers.push_back(std::move(layer));
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error)
|
||||
{
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Shader '" + shaderId + "' is not in the supported shader catalog.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Layer layer;
|
||||
layer.id = AllocateLayerId();
|
||||
layer.shaderId = shaderPackage->id;
|
||||
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||
layer.message = "Runtime Slang build is waiting to start.";
|
||||
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||
layerId = layer.id;
|
||||
mLayers.push_back(std::move(layer));
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::RemoveLayer(const std::string& layerId, std::string& error)
|
||||
{
|
||||
for (auto layerIt = mLayers.begin(); layerIt != mLayers.end(); ++layerIt)
|
||||
{
|
||||
if (layerIt->id != layerId)
|
||||
continue;
|
||||
|
||||
mLayers.erase(layerIt);
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::ReorderLayer(const std::string& layerId, int targetIndex, std::string& error)
|
||||
{
|
||||
auto layerIt = std::find_if(mLayers.begin(), mLayers.end(), [&layerId](const Layer& layer) {
|
||||
return layer.id == layerId;
|
||||
});
|
||||
if (layerIt == mLayers.end())
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetIndex < 0)
|
||||
targetIndex = 0;
|
||||
if (targetIndex >= static_cast<int>(mLayers.size()))
|
||||
targetIndex = static_cast<int>(mLayers.size()) - 1;
|
||||
|
||||
Layer layer = std::move(*layerIt);
|
||||
mLayers.erase(layerIt);
|
||||
std::size_t destinationIndex = static_cast<std::size_t>(targetIndex);
|
||||
if (destinationIndex > mLayers.size())
|
||||
destinationIndex = mLayers.size();
|
||||
mLayers.insert(mLayers.begin() + static_cast<std::ptrdiff_t>(destinationIndex), std::move(layer));
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::SetLayerBypass(const std::string& layerId, bool bypass, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
layer->bypass = bypass;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Shader '" + shaderId + "' is not in the supported shader catalog.";
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->shaderId = shaderPackage->id;
|
||||
layer->shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||
layer->buildState = RuntimeLayerBuildState::Pending;
|
||||
layer->message = "Runtime Slang build is waiting to start.";
|
||||
layer->renderReady = false;
|
||||
layer->artifact = RuntimeShaderArtifact();
|
||||
InitializeDefaultParameterValues(*layer, *shaderPackage);
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderParameterDefinition* definition = FindParameterDefinition(*layer, parameterId);
|
||||
if (!definition)
|
||||
{
|
||||
error = "Unknown parameter id '" + parameterId + "' for layer " + layerId + ".";
|
||||
return false;
|
||||
}
|
||||
|
||||
ShaderParameterValue normalizedValue;
|
||||
if (definition->type == ShaderParameterType::Trigger)
|
||||
{
|
||||
const auto currentIt = layer->parameterValues.find(parameterId);
|
||||
const double previousCount = currentIt == layer->parameterValues.end() || currentIt->second.numberValues.empty()
|
||||
? 0.0
|
||||
: currentIt->second.numberValues.front();
|
||||
normalizedValue.numberValues = { previousCount + 1.0, RuntimeElapsedSeconds() };
|
||||
}
|
||||
else if (!NormalizeAndValidateParameterValue(*definition, value, normalizedValue, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->parameterValues[parameterId] = normalizedValue;
|
||||
if (layer->renderReady)
|
||||
layer->artifact.parameterValues = layer->parameterValues;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::ResetParameters(const std::string& layerId, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->parameterValues.clear();
|
||||
for (const ShaderParameterDefinition& definition : layer->parameterDefinitions)
|
||||
layer->parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||
if (layer->renderReady)
|
||||
layer->artifact.parameterValues = layer->parameterValues;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeLayerModel::Clear()
|
||||
{
|
||||
mLayers.clear();
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->buildState = RuntimeLayerBuildState::Pending;
|
||||
layer->message = message;
|
||||
layer->renderReady = false;
|
||||
layer->artifact = RuntimeShaderArtifact();
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error)
|
||||
{
|
||||
Layer* layer = artifact.layerId.empty() ? FindFirstLayerForShader(artifact.shaderId) : FindLayer(artifact.layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = artifact.layerId.empty()
|
||||
? "No runtime layer is waiting for shader artifact: " + artifact.shaderId
|
||||
: "No runtime layer is waiting for shader artifact on layer: " + artifact.layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->shaderName = artifact.displayName.empty() ? artifact.shaderId : artifact.displayName;
|
||||
layer->buildState = RuntimeLayerBuildState::Ready;
|
||||
layer->message = artifact.message;
|
||||
layer->renderReady = true;
|
||||
layer->artifact = artifact;
|
||||
layer->artifact.parameterValues = layer->parameterValues;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::MarkBuildFailedForShader(const std::string& shaderId, const std::string& message)
|
||||
{
|
||||
Layer* layer = FindFirstLayerForShader(shaderId);
|
||||
if (!layer)
|
||||
return false;
|
||||
|
||||
std::string error;
|
||||
return MarkBuildFailed(layer->id, message, error);
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::MarkBuildFailed(const std::string& layerId, const std::string& message, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->buildState = RuntimeLayerBuildState::Failed;
|
||||
layer->message = message;
|
||||
layer->renderReady = false;
|
||||
layer->artifact = RuntimeShaderArtifact();
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::MarkRenderCommitFailed(const std::string& layerId, const std::string& message, std::string& error)
|
||||
{
|
||||
return MarkBuildFailed(layerId, message, error);
|
||||
}
|
||||
|
||||
RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const
|
||||
{
|
||||
RuntimeLayerModelSnapshot snapshot;
|
||||
snapshot.compileSucceeded = true;
|
||||
|
||||
for (const Layer& layer : mLayers)
|
||||
{
|
||||
snapshot.displayLayers.push_back(ToReadModel(layer));
|
||||
if (!layer.message.empty() && snapshot.compileMessage.empty())
|
||||
snapshot.compileMessage = layer.message;
|
||||
if (layer.buildState == RuntimeLayerBuildState::Failed)
|
||||
snapshot.compileSucceeded = false;
|
||||
if (layer.renderReady)
|
||||
{
|
||||
RuntimeRenderLayerModel renderLayer;
|
||||
renderLayer.id = layer.id;
|
||||
renderLayer.shaderId = layer.shaderId;
|
||||
renderLayer.bypass = layer.bypass;
|
||||
renderLayer.artifact = layer.artifact;
|
||||
renderLayer.artifact.parameterValues = layer.parameterValues;
|
||||
snapshot.renderLayers.push_back(std::move(renderLayer));
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot.compileMessage.empty())
|
||||
snapshot.compileMessage = mLayers.empty() ? "Runtime shader build disabled." : "Runtime shader build has not completed yet.";
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
std::string RuntimeLayerModel::FirstLayerId() const
|
||||
{
|
||||
return mLayers.empty() ? std::string() : mLayers.front().id;
|
||||
}
|
||||
|
||||
RuntimeLayerModel::Layer* RuntimeLayerModel::FindLayer(const std::string& layerId)
|
||||
{
|
||||
for (Layer& layer : mLayers)
|
||||
{
|
||||
if (layer.id == layerId)
|
||||
return &layer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const RuntimeLayerModel::Layer* RuntimeLayerModel::FindLayer(const std::string& layerId) const
|
||||
{
|
||||
for (const Layer& layer : mLayers)
|
||||
{
|
||||
if (layer.id == layerId)
|
||||
return &layer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RuntimeLayerModel::Layer* RuntimeLayerModel::FindFirstLayerForShader(const std::string& shaderId)
|
||||
{
|
||||
for (Layer& layer : mLayers)
|
||||
{
|
||||
if (layer.shaderId == shaderId)
|
||||
return &layer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void RuntimeLayerModel::InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage)
|
||||
{
|
||||
layer.parameterDefinitions = shaderPackage.parameters;
|
||||
layer.parameterValues.clear();
|
||||
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
|
||||
layer.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||
}
|
||||
|
||||
const ShaderParameterDefinition* RuntimeLayerModel::FindParameterDefinition(const Layer& layer, const std::string& parameterId)
|
||||
{
|
||||
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
|
||||
{
|
||||
if (definition.id == parameterId)
|
||||
return &definition;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string RuntimeLayerModel::AllocateLayerId()
|
||||
{
|
||||
return "runtime-layer-" + std::to_string(mNextLayerNumber++);
|
||||
}
|
||||
|
||||
RuntimeLayerReadModel RuntimeLayerModel::ToReadModel(const Layer& layer)
|
||||
{
|
||||
RuntimeLayerReadModel readModel;
|
||||
readModel.id = layer.id;
|
||||
readModel.shaderId = layer.shaderId;
|
||||
readModel.shaderName = layer.shaderName;
|
||||
readModel.bypass = layer.bypass;
|
||||
readModel.buildState = layer.buildState;
|
||||
readModel.message = layer.message;
|
||||
readModel.renderReady = layer.renderReady;
|
||||
readModel.parameterDefinitions = layer.parameterDefinitions;
|
||||
readModel.parameterValues = layer.parameterValues;
|
||||
return readModel;
|
||||
}
|
||||
|
||||
double RuntimeLayerModel::RuntimeElapsedSeconds() const
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||
}
|
||||
}
|
||||
101
src/runtime/RuntimeLayerModel.h
Normal file
101
src/runtime/RuntimeLayerModel.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "RuntimeShaderArtifact.h"
|
||||
#include "SupportedShaderCatalog.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
enum class RuntimeLayerBuildState
|
||||
{
|
||||
Pending,
|
||||
Ready,
|
||||
Failed
|
||||
};
|
||||
|
||||
struct RuntimeLayerReadModel
|
||||
{
|
||||
std::string id;
|
||||
std::string shaderId;
|
||||
std::string shaderName;
|
||||
bool bypass = false;
|
||||
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
||||
std::string message;
|
||||
bool renderReady = false;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
};
|
||||
|
||||
struct RuntimeRenderLayerModel
|
||||
{
|
||||
std::string id;
|
||||
std::string shaderId;
|
||||
bool bypass = false;
|
||||
RuntimeShaderArtifact artifact;
|
||||
};
|
||||
|
||||
struct RuntimeLayerModelSnapshot
|
||||
{
|
||||
bool compileSucceeded = true;
|
||||
std::string compileMessage;
|
||||
std::vector<RuntimeLayerReadModel> displayLayers;
|
||||
std::vector<RuntimeRenderLayerModel> renderLayers;
|
||||
};
|
||||
|
||||
class RuntimeLayerModel
|
||||
{
|
||||
public:
|
||||
bool InitializeSingleLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& error);
|
||||
void Clear();
|
||||
|
||||
bool AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error);
|
||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||
bool ReorderLayer(const std::string& layerId, int targetIndex, std::string& error);
|
||||
bool SetLayerBypass(const std::string& layerId, bool bypass, std::string& error);
|
||||
bool SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error);
|
||||
bool ResetParameters(const std::string& layerId, std::string& error);
|
||||
bool MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error);
|
||||
bool MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error);
|
||||
bool MarkBuildFailedForShader(const std::string& shaderId, const std::string& message);
|
||||
bool MarkBuildFailed(const std::string& layerId, const std::string& message, std::string& error);
|
||||
bool MarkRenderCommitFailed(const std::string& layerId, const std::string& message, std::string& error);
|
||||
|
||||
RuntimeLayerModelSnapshot Snapshot() const;
|
||||
std::string FirstLayerId() const;
|
||||
|
||||
private:
|
||||
struct Layer
|
||||
{
|
||||
std::string id;
|
||||
std::string shaderId;
|
||||
std::string shaderName;
|
||||
bool bypass = false;
|
||||
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
||||
std::string message;
|
||||
bool renderReady = false;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
RuntimeShaderArtifact artifact;
|
||||
};
|
||||
|
||||
Layer* FindLayer(const std::string& layerId);
|
||||
const Layer* FindLayer(const std::string& layerId) const;
|
||||
Layer* FindFirstLayerForShader(const std::string& shaderId);
|
||||
static void InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage);
|
||||
static const ShaderParameterDefinition* FindParameterDefinition(const Layer& layer, const std::string& parameterId);
|
||||
std::string AllocateLayerId();
|
||||
static RuntimeLayerReadModel ToReadModel(const Layer& layer);
|
||||
double RuntimeElapsedSeconds() const;
|
||||
|
||||
std::vector<Layer> mLayers;
|
||||
uint64_t mNextLayerNumber = 1;
|
||||
std::chrono::steady_clock::time_point mStartTime = std::chrono::steady_clock::now();
|
||||
};
|
||||
}
|
||||
27
src/runtime/RuntimeShaderArtifact.h
Normal file
27
src/runtime/RuntimeShaderArtifact.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeShaderPassArtifact
|
||||
{
|
||||
std::string passId;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
};
|
||||
|
||||
struct RuntimeShaderArtifact
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string displayName;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<RuntimeShaderPassArtifact> passes;
|
||||
std::string message;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
};
|
||||
75
src/runtime/RuntimeShaderBridge.cpp
Normal file
75
src/runtime/RuntimeShaderBridge.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "RuntimeShaderBridge.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
RuntimeShaderBridge::~RuntimeShaderBridge()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void RuntimeShaderBridge::Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError)
|
||||
{
|
||||
Start(std::string(), shaderId, std::move(onArtifactReady), std::move(onError));
|
||||
}
|
||||
|
||||
void RuntimeShaderBridge::Start(const std::string& layerId, const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError)
|
||||
{
|
||||
Stop();
|
||||
if (shaderId.empty())
|
||||
return;
|
||||
|
||||
mLayerId = layerId;
|
||||
mOnArtifactReady = std::move(onArtifactReady);
|
||||
mOnError = std::move(onError);
|
||||
mStopping.store(false, std::memory_order_release);
|
||||
mFinished.store(false, std::memory_order_release);
|
||||
mCompiler.StartShaderBuild(shaderId);
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
}
|
||||
|
||||
void RuntimeShaderBridge::RequestStop()
|
||||
{
|
||||
mStopping.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void RuntimeShaderBridge::Stop()
|
||||
{
|
||||
RequestStop();
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
mCompiler.Stop();
|
||||
mLayerId.clear();
|
||||
mOnArtifactReady = ArtifactCallback();
|
||||
mOnError = ErrorCallback();
|
||||
mFinished.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool RuntimeShaderBridge::CanStopWithoutWaiting() const
|
||||
{
|
||||
return mFinished.load(std::memory_order_acquire) && !mCompiler.Running();
|
||||
}
|
||||
|
||||
void RuntimeShaderBridge::ThreadMain()
|
||||
{
|
||||
while (!mStopping.load(std::memory_order_acquire))
|
||||
{
|
||||
RuntimeSlangShaderBuild build;
|
||||
if (mCompiler.TryConsume(build))
|
||||
{
|
||||
if (build.succeeded)
|
||||
{
|
||||
build.artifact.layerId = mLayerId;
|
||||
if (mOnArtifactReady)
|
||||
mOnArtifactReady(build.artifact);
|
||||
}
|
||||
else if (mOnError)
|
||||
{
|
||||
mOnError(build.message);
|
||||
}
|
||||
mFinished.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
mFinished.store(true, std::memory_order_release);
|
||||
}
|
||||
38
src/runtime/RuntimeShaderBridge.h
Normal file
38
src/runtime/RuntimeShaderBridge.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeShaderArtifact.h"
|
||||
#include "RuntimeSlangShaderCompiler.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class RuntimeShaderBridge
|
||||
{
|
||||
public:
|
||||
using ArtifactCallback = std::function<void(const RuntimeShaderArtifact&)>;
|
||||
using ErrorCallback = std::function<void(const std::string&)>;
|
||||
|
||||
RuntimeShaderBridge() = default;
|
||||
RuntimeShaderBridge(const RuntimeShaderBridge&) = delete;
|
||||
RuntimeShaderBridge& operator=(const RuntimeShaderBridge&) = delete;
|
||||
~RuntimeShaderBridge();
|
||||
|
||||
void Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError);
|
||||
void Start(const std::string& layerId, const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError);
|
||||
void RequestStop();
|
||||
void Stop();
|
||||
bool CanStopWithoutWaiting() const;
|
||||
|
||||
private:
|
||||
void ThreadMain();
|
||||
|
||||
RuntimeSlangShaderCompiler mCompiler;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mFinished{ true };
|
||||
std::string mLayerId;
|
||||
ArtifactCallback mOnArtifactReady;
|
||||
ErrorCallback mOnError;
|
||||
};
|
||||
159
src/runtime/RuntimeSlangShaderCompiler.cpp
Normal file
159
src/runtime/RuntimeSlangShaderCompiler.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "RuntimeSlangShaderCompiler.h"
|
||||
|
||||
#include "ShaderCompiler.h"
|
||||
#include "ShaderPackageRegistry.h"
|
||||
#include "ShaderTypes.h"
|
||||
#include "SupportedShaderCatalog.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::filesystem::path FindRepoRoot()
|
||||
{
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
for (;;)
|
||||
{
|
||||
if (std::filesystem::exists(current / "shaders" / "happy-accident" / "shader.slang") &&
|
||||
std::filesystem::exists(current / "runtime" / "templates" / "shader_wrapper.slang.in"))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
const std::filesystem::path parent = current.parent_path();
|
||||
if (parent.empty() || parent == current)
|
||||
return std::filesystem::current_path();
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RuntimeSlangShaderCompiler::~RuntimeSlangShaderCompiler()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void RuntimeSlangShaderCompiler::StartHappyAccidentBuild()
|
||||
{
|
||||
StartShaderBuild("happy-accident");
|
||||
}
|
||||
|
||||
void RuntimeSlangShaderCompiler::StartShaderBuild(const std::string& shaderId)
|
||||
{
|
||||
if (mRunning.load(std::memory_order_acquire))
|
||||
return;
|
||||
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mReadyBuild = RuntimeSlangShaderBuild();
|
||||
}
|
||||
|
||||
mRunning.store(true, std::memory_order_release);
|
||||
mThread = std::thread([this, shaderId]() {
|
||||
RuntimeSlangShaderBuild build = BuildShader(shaderId);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mReadyBuild = std::move(build);
|
||||
mReadyBuild.available = true;
|
||||
}
|
||||
mRunning.store(false, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
void RuntimeSlangShaderCompiler::Stop()
|
||||
{
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
mRunning.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool RuntimeSlangShaderCompiler::TryConsume(RuntimeSlangShaderBuild& build)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mReadyBuild.available)
|
||||
return false;
|
||||
|
||||
build = std::move(mReadyBuild);
|
||||
mReadyBuild = RuntimeSlangShaderBuild();
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::string& shaderId) const
|
||||
{
|
||||
RuntimeSlangShaderBuild build;
|
||||
build.artifact.shaderId = shaderId;
|
||||
|
||||
try
|
||||
{
|
||||
const std::filesystem::path repoRoot = FindRepoRoot();
|
||||
const std::filesystem::path shaderDir = repoRoot / "shaders" / shaderId;
|
||||
const std::filesystem::path runtimeBuildDir = repoRoot / "runtime" / "generated" / "render-cadence-compositor";
|
||||
|
||||
ShaderPackageRegistry registry(0);
|
||||
ShaderPackage shaderPackage;
|
||||
std::string error;
|
||||
if (!registry.ParseManifest(shaderDir / "shader.json", shaderPackage, error))
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = error.empty() ? "Shader manifest parse failed." : error;
|
||||
return build;
|
||||
}
|
||||
const RenderCadenceCompositor::ShaderSupportResult support =
|
||||
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||
if (!support.supported)
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = support.reason;
|
||||
return build;
|
||||
}
|
||||
|
||||
ShaderCompiler compiler(
|
||||
repoRoot,
|
||||
runtimeBuildDir / (shaderId + ".wrapper.slang"),
|
||||
runtimeBuildDir / (shaderId + ".generated.glsl"),
|
||||
runtimeBuildDir / (shaderId + ".patched.glsl"),
|
||||
0);
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
std::string fragmentShaderSource;
|
||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error))
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = error.empty() ? "Slang compile failed." : error;
|
||||
return build;
|
||||
}
|
||||
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
passArtifact.passId = pass.id;
|
||||
passArtifact.fragmentShaderSource = std::move(fragmentShaderSource);
|
||||
passArtifact.inputNames = pass.inputNames;
|
||||
passArtifact.outputName = pass.outputName;
|
||||
build.artifact.passes.push_back(std::move(passArtifact));
|
||||
}
|
||||
|
||||
const auto end = std::chrono::steady_clock::now();
|
||||
const double milliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(end - start).count();
|
||||
build.succeeded = true;
|
||||
build.artifact.shaderId = shaderPackage.id;
|
||||
build.artifact.displayName = shaderPackage.displayName;
|
||||
build.artifact.parameterDefinitions = shaderPackage.parameters;
|
||||
if (!build.artifact.passes.empty())
|
||||
build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource;
|
||||
build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
|
||||
build.message = build.artifact.message;
|
||||
return build;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = exception.what();
|
||||
return build;
|
||||
}
|
||||
}
|
||||
39
src/runtime/RuntimeSlangShaderCompiler.h
Normal file
39
src/runtime/RuntimeSlangShaderCompiler.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeShaderArtifact.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
struct RuntimeSlangShaderBuild
|
||||
{
|
||||
bool available = false;
|
||||
bool succeeded = false;
|
||||
RuntimeShaderArtifact artifact;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
class RuntimeSlangShaderCompiler
|
||||
{
|
||||
public:
|
||||
RuntimeSlangShaderCompiler() = default;
|
||||
RuntimeSlangShaderCompiler(const RuntimeSlangShaderCompiler&) = delete;
|
||||
RuntimeSlangShaderCompiler& operator=(const RuntimeSlangShaderCompiler&) = delete;
|
||||
~RuntimeSlangShaderCompiler();
|
||||
|
||||
void StartHappyAccidentBuild();
|
||||
void StartShaderBuild(const std::string& shaderId);
|
||||
void Stop();
|
||||
bool TryConsume(RuntimeSlangShaderBuild& build);
|
||||
bool Running() const { return mRunning.load(std::memory_order_acquire); }
|
||||
|
||||
private:
|
||||
RuntimeSlangShaderBuild BuildShader(const std::string& shaderId) const;
|
||||
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mRunning{ false };
|
||||
std::mutex mMutex;
|
||||
RuntimeSlangShaderBuild mReadyBuild;
|
||||
};
|
||||
112
src/runtime/SupportedShaderCatalog.cpp
Normal file
112
src/runtime/SupportedShaderCatalog.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "SupportedShaderCatalog.h"
|
||||
|
||||
#include "ShaderPackageRegistry.h"
|
||||
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
|
||||
{
|
||||
if (shaderPackage.passes.empty())
|
||||
return { false, "Shader package has no render passes." };
|
||||
|
||||
if (shaderPackage.temporal.enabled)
|
||||
return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." };
|
||||
|
||||
if (shaderPackage.feedback.enabled)
|
||||
return { false, "RenderCadenceCompositor currently supports only stateless shaders; feedback storage is not enabled in this app." };
|
||||
|
||||
if (!shaderPackage.textureAssets.empty())
|
||||
return { false, "RenderCadenceCompositor does not load shader texture assets yet; texture-backed shaders need a CPU-prepared asset handoff first." };
|
||||
|
||||
if (!shaderPackage.fontAssets.empty())
|
||||
return { false, "RenderCadenceCompositor does not load shader font assets yet; text shaders need a CPU-prepared asset handoff first." };
|
||||
|
||||
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters)
|
||||
{
|
||||
if (parameter.type == ShaderParameterType::Text)
|
||||
return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." };
|
||||
}
|
||||
|
||||
bool writesLayerOutput = false;
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
if (pass.sourcePath.empty())
|
||||
{
|
||||
return { false, "Shader pass '" + pass.id + "' has no source." };
|
||||
}
|
||||
if (pass.outputName == "layerOutput")
|
||||
writesLayerOutput = true;
|
||||
for (const std::string& inputName : pass.inputNames)
|
||||
{
|
||||
if (inputName == "videoInput" || inputName == "layerInput")
|
||||
continue;
|
||||
bool matchesNamedOutput = false;
|
||||
for (const ShaderPassDefinition& outputPass : shaderPackage.passes)
|
||||
{
|
||||
if (outputPass.outputName == inputName)
|
||||
{
|
||||
matchesNamedOutput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchesNamedOutput)
|
||||
return { false, "Shader pass '" + pass.id + "' references unknown input '" + inputName + "'." };
|
||||
}
|
||||
}
|
||||
if (!writesLayerOutput)
|
||||
return { false, "Shader package must write a pass output named 'layerOutput'." };
|
||||
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error)
|
||||
{
|
||||
mShaders.clear();
|
||||
mPackagesById.clear();
|
||||
|
||||
if (shaderRoot.empty())
|
||||
{
|
||||
error = "Shader library path is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
ShaderPackageRegistry registry(maxTemporalHistoryFrames);
|
||||
std::map<std::string, ShaderPackage> packagesById;
|
||||
std::vector<std::string> packageOrder;
|
||||
std::vector<ShaderPackageStatus> packageStatuses;
|
||||
if (!registry.Scan(shaderRoot, packagesById, packageOrder, packageStatuses, error))
|
||||
return false;
|
||||
|
||||
for (const std::string& packageId : packageOrder)
|
||||
{
|
||||
const auto packageIt = packagesById.find(packageId);
|
||||
if (packageIt == packagesById.end())
|
||||
continue;
|
||||
|
||||
const ShaderPackage& shaderPackage = packageIt->second;
|
||||
const ShaderSupportResult support = CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||
if (!support.supported)
|
||||
continue;
|
||||
|
||||
SupportedShaderSummary summary;
|
||||
summary.id = shaderPackage.id;
|
||||
summary.name = shaderPackage.displayName.empty() ? shaderPackage.id : shaderPackage.displayName;
|
||||
summary.description = shaderPackage.description;
|
||||
summary.category = shaderPackage.category;
|
||||
mShaders.push_back(std::move(summary));
|
||||
mPackagesById[shaderPackage.id] = shaderPackage;
|
||||
}
|
||||
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
const ShaderPackage* SupportedShaderCatalog::FindPackage(const std::string& shaderId) const
|
||||
{
|
||||
const auto packageIt = mPackagesById.find(shaderId);
|
||||
return packageIt == mPackagesById.end() ? nullptr : &packageIt->second;
|
||||
}
|
||||
}
|
||||
39
src/runtime/SupportedShaderCatalog.h
Normal file
39
src/runtime/SupportedShaderCatalog.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct SupportedShaderSummary
|
||||
{
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string category;
|
||||
};
|
||||
|
||||
struct ShaderSupportResult
|
||||
{
|
||||
bool supported = false;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage);
|
||||
|
||||
class SupportedShaderCatalog
|
||||
{
|
||||
public:
|
||||
bool Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error);
|
||||
const std::vector<SupportedShaderSummary>& Shaders() const { return mShaders; }
|
||||
const ShaderPackage* FindPackage(const std::string& shaderId) const;
|
||||
|
||||
private:
|
||||
std::vector<SupportedShaderSummary> mShaders;
|
||||
std::map<std::string, ShaderPackage> mPackagesById;
|
||||
};
|
||||
}
|
||||
183
src/telemetry/CadenceTelemetry.h
Normal file
183
src/telemetry/CadenceTelemetry.h
Normal file
@@ -0,0 +1,183 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct CadenceTelemetrySnapshot
|
||||
{
|
||||
double sampleSeconds = 0.0;
|
||||
double renderFps = 0.0;
|
||||
double scheduleFps = 0.0;
|
||||
std::size_t freeFrames = 0;
|
||||
std::size_t completedFrames = 0;
|
||||
std::size_t scheduledFrames = 0;
|
||||
uint64_t renderedTotal = 0;
|
||||
uint64_t scheduledTotal = 0;
|
||||
uint64_t completedPollMisses = 0;
|
||||
uint64_t scheduleFailures = 0;
|
||||
uint64_t completedDrops = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
uint64_t completions = 0;
|
||||
uint64_t displayedLate = 0;
|
||||
uint64_t dropped = 0;
|
||||
uint64_t clockOverruns = 0;
|
||||
uint64_t clockSkippedFrames = 0;
|
||||
uint64_t shaderBuildsCommitted = 0;
|
||||
uint64_t shaderBuildFailures = 0;
|
||||
double renderFrameMilliseconds = 0.0;
|
||||
double renderFrameBudgetUsedPercent = 0.0;
|
||||
double renderFrameMaxMilliseconds = 0.0;
|
||||
double readbackQueueMilliseconds = 0.0;
|
||||
double completedReadbackCopyMilliseconds = 0.0;
|
||||
uint64_t inputFramesReceived = 0;
|
||||
uint64_t inputFramesDropped = 0;
|
||||
uint64_t inputConsumeMisses = 0;
|
||||
uint64_t inputUploadMisses = 0;
|
||||
std::size_t inputReadyFrames = 0;
|
||||
std::size_t inputReadingFrames = 0;
|
||||
double inputLatestAgeMilliseconds = 0.0;
|
||||
double inputUploadMilliseconds = 0.0;
|
||||
bool inputFormatSupported = true;
|
||||
bool inputSignalPresent = false;
|
||||
double inputCaptureFps = 0.0;
|
||||
double inputConvertMilliseconds = 0.0;
|
||||
double inputSubmitMilliseconds = 0.0;
|
||||
uint64_t inputNoSignalFrames = 0;
|
||||
uint64_t inputUnsupportedFrames = 0;
|
||||
uint64_t inputSubmitMisses = 0;
|
||||
std::string inputCaptureFormat = "none";
|
||||
bool deckLinkBufferedAvailable = false;
|
||||
uint64_t deckLinkBuffered = 0;
|
||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||
bool deckLinkScheduleLeadAvailable = false;
|
||||
int64_t deckLinkPlaybackStreamTime = 0;
|
||||
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||
int64_t deckLinkScheduleLeadFrames = 0;
|
||||
uint64_t deckLinkScheduleRealignments = 0;
|
||||
};
|
||||
|
||||
class CadenceTelemetry
|
||||
{
|
||||
public:
|
||||
template <typename SystemFrameExchange, typename Output, typename OutputThread>
|
||||
CadenceTelemetrySnapshot Sample(
|
||||
const SystemFrameExchange& exchange,
|
||||
const Output& output,
|
||||
const OutputThread& outputThread)
|
||||
{
|
||||
const auto now = Clock::now();
|
||||
const double seconds = mHasLastSample
|
||||
? std::chrono::duration_cast<std::chrono::duration<double>>(now - mLastSampleTime).count()
|
||||
: 0.0;
|
||||
|
||||
const auto exchangeMetrics = exchange.Metrics();
|
||||
const auto outputMetrics = output.Metrics();
|
||||
const auto threadMetrics = outputThread.Metrics();
|
||||
|
||||
CadenceTelemetrySnapshot snapshot;
|
||||
snapshot.sampleSeconds = seconds;
|
||||
snapshot.renderedTotal = exchangeMetrics.completedFrames;
|
||||
snapshot.scheduledTotal = exchangeMetrics.scheduledFrames;
|
||||
snapshot.freeFrames = exchangeMetrics.freeCount;
|
||||
snapshot.completedFrames = exchangeMetrics.completedCount;
|
||||
snapshot.scheduledFrames = exchangeMetrics.scheduledCount;
|
||||
snapshot.completedPollMisses = threadMetrics.completedPollMisses;
|
||||
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
||||
? outputMetrics.scheduleFailures
|
||||
: threadMetrics.scheduleFailures;
|
||||
snapshot.completedDrops = exchangeMetrics.completedDrops;
|
||||
snapshot.acquireMisses = exchangeMetrics.acquireMisses;
|
||||
snapshot.completions = outputMetrics.completions;
|
||||
snapshot.displayedLate = outputMetrics.displayedLate;
|
||||
snapshot.dropped = outputMetrics.dropped;
|
||||
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
|
||||
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
|
||||
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
|
||||
snapshot.deckLinkScheduleLeadAvailable = outputMetrics.scheduleLeadAvailable;
|
||||
snapshot.deckLinkPlaybackStreamTime = outputMetrics.playbackStreamTime;
|
||||
snapshot.deckLinkPlaybackFrameIndex = outputMetrics.playbackFrameIndex;
|
||||
snapshot.deckLinkNextScheduleFrameIndex = outputMetrics.nextScheduleFrameIndex;
|
||||
snapshot.deckLinkScheduleLeadFrames = outputMetrics.scheduleLeadFrames;
|
||||
snapshot.deckLinkScheduleRealignments = outputMetrics.scheduleRealignmentCount;
|
||||
|
||||
if (mHasLastSample && seconds > 0.0)
|
||||
{
|
||||
snapshot.renderFps = static_cast<double>(snapshot.renderedTotal - mLastRenderedFrames) / seconds;
|
||||
snapshot.scheduleFps = static_cast<double>(snapshot.scheduledTotal - mLastScheduledFrames) / seconds;
|
||||
}
|
||||
|
||||
mLastSampleTime = now;
|
||||
mLastRenderedFrames = snapshot.renderedTotal;
|
||||
mLastScheduledFrames = snapshot.scheduledTotal;
|
||||
mHasLastSample = true;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
template <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread>
|
||||
CadenceTelemetrySnapshot Sample(
|
||||
const SystemFrameExchange& exchange,
|
||||
const Output& output,
|
||||
const OutputThread& outputThread,
|
||||
const RenderThread& renderThread)
|
||||
{
|
||||
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread);
|
||||
const auto renderMetrics = renderThread.GetMetrics();
|
||||
snapshot.clockOverruns = renderMetrics.clockOverruns;
|
||||
snapshot.clockSkippedFrames = renderMetrics.skippedFrames;
|
||||
snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted;
|
||||
snapshot.shaderBuildFailures = renderMetrics.shaderBuildFailures;
|
||||
snapshot.renderFrameMilliseconds = renderMetrics.renderFrameMilliseconds;
|
||||
snapshot.renderFrameBudgetUsedPercent = renderMetrics.renderFrameBudgetUsedPercent;
|
||||
snapshot.renderFrameMaxMilliseconds = renderMetrics.renderFrameMaxMilliseconds;
|
||||
snapshot.readbackQueueMilliseconds = renderMetrics.readbackQueueMilliseconds;
|
||||
snapshot.completedReadbackCopyMilliseconds = renderMetrics.completedReadbackCopyMilliseconds;
|
||||
snapshot.inputFramesReceived = renderMetrics.inputFramesReceived;
|
||||
snapshot.inputFramesDropped = renderMetrics.inputFramesDropped;
|
||||
snapshot.inputConsumeMisses = renderMetrics.inputConsumeMisses;
|
||||
snapshot.inputUploadMisses = renderMetrics.inputUploadMisses;
|
||||
snapshot.inputReadyFrames = renderMetrics.inputReadyFrames;
|
||||
snapshot.inputReadingFrames = renderMetrics.inputReadingFrames;
|
||||
snapshot.inputLatestAgeMilliseconds = renderMetrics.inputLatestAgeMilliseconds;
|
||||
snapshot.inputUploadMilliseconds = renderMetrics.inputUploadMilliseconds;
|
||||
snapshot.inputFormatSupported = renderMetrics.inputFormatSupported;
|
||||
snapshot.inputSignalPresent = renderMetrics.inputSignalPresent;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
template <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread, typename InputEdge>
|
||||
CadenceTelemetrySnapshot Sample(
|
||||
const SystemFrameExchange& exchange,
|
||||
const Output& output,
|
||||
const OutputThread& outputThread,
|
||||
const RenderThread& renderThread,
|
||||
const InputEdge& inputEdge)
|
||||
{
|
||||
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread, renderThread);
|
||||
const auto inputMetrics = inputEdge.Metrics();
|
||||
snapshot.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
||||
snapshot.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
||||
snapshot.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
||||
snapshot.inputUnsupportedFrames = inputMetrics.unsupportedFrames;
|
||||
snapshot.inputSubmitMisses = inputMetrics.submitMisses;
|
||||
snapshot.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none";
|
||||
if (snapshot.sampleSeconds > 0.0)
|
||||
snapshot.inputCaptureFps = static_cast<double>(inputMetrics.capturedFrames - mLastInputCapturedFrames) / snapshot.sampleSeconds;
|
||||
mLastInputCapturedFrames = inputMetrics.capturedFrames;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
Clock::time_point mLastSampleTime = Clock::now();
|
||||
uint64_t mLastRenderedFrames = 0;
|
||||
uint64_t mLastScheduledFrames = 0;
|
||||
uint64_t mLastInputCapturedFrames = 0;
|
||||
bool mHasLastSample = false;
|
||||
};
|
||||
}
|
||||
83
src/telemetry/CadenceTelemetryJson.h
Normal file
83
src/telemetry/CadenceTelemetryJson.h
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include "CadenceTelemetry.h"
|
||||
#include "../json/JsonWriter.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetrySnapshot& snapshot)
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyDouble("sampleSeconds", snapshot.sampleSeconds);
|
||||
writer.KeyDouble("renderFps", snapshot.renderFps);
|
||||
writer.KeyDouble("scheduleFps", snapshot.scheduleFps);
|
||||
writer.KeyUInt("free", static_cast<uint64_t>(snapshot.freeFrames));
|
||||
writer.KeyUInt("completed", static_cast<uint64_t>(snapshot.completedFrames));
|
||||
writer.KeyUInt("scheduled", static_cast<uint64_t>(snapshot.scheduledFrames));
|
||||
writer.KeyUInt("renderedTotal", snapshot.renderedTotal);
|
||||
writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal);
|
||||
writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses);
|
||||
writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures);
|
||||
writer.KeyUInt("completedDrops", snapshot.completedDrops);
|
||||
writer.KeyUInt("acquireMisses", snapshot.acquireMisses);
|
||||
writer.KeyUInt("completions", snapshot.completions);
|
||||
writer.KeyUInt("late", snapshot.displayedLate);
|
||||
writer.KeyUInt("dropped", snapshot.dropped);
|
||||
writer.KeyUInt("clockOverruns", snapshot.clockOverruns);
|
||||
writer.KeyUInt("clockSkippedFrames", snapshot.clockSkippedFrames);
|
||||
writer.KeyUInt("clockOveruns", snapshot.clockOverruns);
|
||||
writer.KeyUInt("clockSkipped", snapshot.clockSkippedFrames);
|
||||
writer.KeyUInt("shaderCommitted", snapshot.shaderBuildsCommitted);
|
||||
writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures);
|
||||
writer.KeyDouble("renderFrameMs", snapshot.renderFrameMilliseconds);
|
||||
writer.KeyDouble("renderFrameBudgetUsedPercent", snapshot.renderFrameBudgetUsedPercent);
|
||||
writer.KeyDouble("renderFrameMaxMs", snapshot.renderFrameMaxMilliseconds);
|
||||
writer.KeyDouble("readbackQueueMs", snapshot.readbackQueueMilliseconds);
|
||||
writer.KeyDouble("completedReadbackCopyMs", snapshot.completedReadbackCopyMilliseconds);
|
||||
writer.KeyUInt("inputFramesReceived", snapshot.inputFramesReceived);
|
||||
writer.KeyUInt("inputFramesDropped", snapshot.inputFramesDropped);
|
||||
writer.KeyUInt("inputConsumeMisses", snapshot.inputConsumeMisses);
|
||||
writer.KeyUInt("inputUploadMisses", snapshot.inputUploadMisses);
|
||||
writer.KeyUInt("inputReadyFrames", static_cast<uint64_t>(snapshot.inputReadyFrames));
|
||||
writer.KeyUInt("inputReadingFrames", static_cast<uint64_t>(snapshot.inputReadingFrames));
|
||||
writer.KeyDouble("inputLatestAgeMs", snapshot.inputLatestAgeMilliseconds);
|
||||
writer.KeyDouble("inputUploadMs", snapshot.inputUploadMilliseconds);
|
||||
writer.KeyBool("inputFormatSupported", snapshot.inputFormatSupported);
|
||||
writer.KeyBool("inputSignalPresent", snapshot.inputSignalPresent);
|
||||
writer.KeyDouble("inputCaptureFps", snapshot.inputCaptureFps);
|
||||
writer.KeyDouble("inputConvertMs", snapshot.inputConvertMilliseconds);
|
||||
writer.KeyDouble("inputSubmitMs", snapshot.inputSubmitMilliseconds);
|
||||
writer.KeyUInt("inputNoSignalFrames", snapshot.inputNoSignalFrames);
|
||||
writer.KeyUInt("inputUnsupportedFrames", snapshot.inputUnsupportedFrames);
|
||||
writer.KeyUInt("inputSubmitMisses", snapshot.inputSubmitMisses);
|
||||
writer.KeyString("inputCaptureFormat", snapshot.inputCaptureFormat);
|
||||
writer.KeyBool("deckLinkBufferedAvailable", snapshot.deckLinkBufferedAvailable);
|
||||
writer.Key("deckLinkBuffered");
|
||||
if (snapshot.deckLinkBufferedAvailable)
|
||||
writer.UInt(snapshot.deckLinkBuffered);
|
||||
else
|
||||
writer.Null();
|
||||
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds);
|
||||
writer.KeyBool("deckLinkScheduleLeadAvailable", snapshot.deckLinkScheduleLeadAvailable);
|
||||
writer.Key("deckLinkScheduleLeadFrames");
|
||||
if (snapshot.deckLinkScheduleLeadAvailable)
|
||||
writer.Int(snapshot.deckLinkScheduleLeadFrames);
|
||||
else
|
||||
writer.Null();
|
||||
writer.KeyUInt("deckLinkPlaybackFrameIndex", snapshot.deckLinkPlaybackFrameIndex);
|
||||
writer.KeyUInt("deckLinkNextScheduleFrameIndex", snapshot.deckLinkNextScheduleFrameIndex);
|
||||
writer.KeyInt("deckLinkPlaybackStreamTime", snapshot.deckLinkPlaybackStreamTime);
|
||||
writer.KeyUInt("deckLinkScheduleRealignments", snapshot.deckLinkScheduleRealignments);
|
||||
writer.EndObject();
|
||||
}
|
||||
|
||||
inline std::string CadenceTelemetryToJson(const CadenceTelemetrySnapshot& snapshot)
|
||||
{
|
||||
JsonWriter writer;
|
||||
WriteCadenceTelemetryJson(writer, snapshot);
|
||||
return writer.StringValue();
|
||||
}
|
||||
}
|
||||
122
src/telemetry/TelemetryHealthMonitor.h
Normal file
122
src/telemetry/TelemetryHealthMonitor.h
Normal file
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
|
||||
#include "CadenceTelemetry.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct TelemetryHealthMonitorConfig
|
||||
{
|
||||
std::chrono::milliseconds interval = std::chrono::seconds(1);
|
||||
std::size_t scheduledStarvationThreshold = 0;
|
||||
};
|
||||
|
||||
class TelemetryHealthMonitor
|
||||
{
|
||||
public:
|
||||
explicit TelemetryHealthMonitor(TelemetryHealthMonitorConfig config = TelemetryHealthMonitorConfig()) :
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
TelemetryHealthMonitor(const TelemetryHealthMonitor&) = delete;
|
||||
TelemetryHealthMonitor& operator=(const TelemetryHealthMonitor&) = delete;
|
||||
|
||||
~TelemetryHealthMonitor()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
template <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread>
|
||||
void Start(const SystemFrameExchange& exchange, const Output& output, const OutputThread& outputThread, const RenderThread& renderThread)
|
||||
{
|
||||
if (mRunning)
|
||||
return;
|
||||
mStopping = false;
|
||||
mThread = std::thread([this, &exchange, &output, &outputThread, &renderThread]() {
|
||||
CadenceTelemetry telemetry;
|
||||
CadenceTelemetrySnapshot previous;
|
||||
bool hasPrevious = false;
|
||||
while (!mStopping)
|
||||
{
|
||||
std::this_thread::sleep_for(mConfig.interval);
|
||||
const CadenceTelemetrySnapshot snapshot = telemetry.Sample(exchange, output, outputThread, renderThread);
|
||||
ReportHealth(snapshot, hasPrevious ? &previous : nullptr);
|
||||
previous = snapshot;
|
||||
hasPrevious = true;
|
||||
}
|
||||
});
|
||||
mRunning = true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mStopping = true;
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
private:
|
||||
void ReportHealth(const CadenceTelemetrySnapshot& snapshot, const CadenceTelemetrySnapshot* previous) const
|
||||
{
|
||||
if (!previous)
|
||||
return;
|
||||
|
||||
const uint64_t lateDelta = snapshot.displayedLate - previous->displayedLate;
|
||||
const uint64_t droppedDelta = snapshot.dropped - previous->dropped;
|
||||
const uint64_t scheduleFailureDelta = snapshot.scheduleFailures - previous->scheduleFailures;
|
||||
|
||||
if (droppedDelta > 0 || lateDelta > 0)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta
|
||||
<< " droppedDelta=" << droppedDelta
|
||||
<< " totalLate=" << snapshot.displayedLate
|
||||
<< " totalDropped=" << snapshot.dropped
|
||||
<< " scheduleLead=";
|
||||
if (snapshot.deckLinkScheduleLeadAvailable)
|
||||
message << snapshot.deckLinkScheduleLeadFrames;
|
||||
else
|
||||
message << "n/a";
|
||||
message << " realignments=" << snapshot.deckLinkScheduleRealignments;
|
||||
LogWarning("telemetry", message.str());
|
||||
}
|
||||
|
||||
if (scheduleFailureDelta > 0)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "DeckLink schedule failures increased: delta=" << scheduleFailureDelta
|
||||
<< " total=" << snapshot.scheduleFailures;
|
||||
LogWarning("telemetry", message.str());
|
||||
}
|
||||
|
||||
const bool appScheduledStarved = snapshot.scheduledFrames <= mConfig.scheduledStarvationThreshold
|
||||
&& snapshot.scheduledTotal > 0;
|
||||
const bool deckLinkStarved = snapshot.deckLinkBufferedAvailable && snapshot.deckLinkBuffered == 0;
|
||||
if (appScheduledStarved || deckLinkStarved)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "Output buffer starvation detected: scheduled=" << snapshot.scheduledFrames
|
||||
<< " decklinkBuffered=";
|
||||
if (snapshot.deckLinkBufferedAvailable)
|
||||
message << snapshot.deckLinkBuffered;
|
||||
else
|
||||
message << "n/a";
|
||||
message << " renderFps=" << snapshot.renderFps
|
||||
<< " scheduleFps=" << snapshot.scheduleFps;
|
||||
LogError("telemetry", message.str());
|
||||
}
|
||||
}
|
||||
|
||||
TelemetryHealthMonitorConfig mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mRunning{ false };
|
||||
};
|
||||
}
|
||||
367
src/video/DeckLinkInput.cpp
Normal file
367
src/video/DeckLinkInput.cpp
Normal file
@@ -0,0 +1,367 @@
|
||||
#include "DeckLinkInput.h"
|
||||
|
||||
#include "DeckLinkVideoIOFormat.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <new>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool FindInputDisplayMode(IDeckLinkInput* input, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode)
|
||||
{
|
||||
if (input == nullptr || foundMode == nullptr)
|
||||
return false;
|
||||
|
||||
*foundMode = nullptr;
|
||||
CComPtr<IDeckLinkDisplayModeIterator> iterator;
|
||||
if (input->GetDisplayModeIterator(&iterator) != S_OK)
|
||||
return false;
|
||||
|
||||
return FindDeckLinkDisplayMode(iterator, targetMode, foundMode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DeckLinkInputCallback::DeckLinkInputCallback(DeckLinkInput& owner) :
|
||||
mOwner(owner)
|
||||
{
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::QueryInterface(REFIID iid, LPVOID* ppv)
|
||||
{
|
||||
if (ppv == nullptr)
|
||||
return E_POINTER;
|
||||
if (iid == IID_IUnknown || iid == IID_IDeckLinkInputCallback)
|
||||
{
|
||||
*ppv = static_cast<IDeckLinkInputCallback*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
*ppv = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE DeckLinkInputCallback::AddRef()
|
||||
{
|
||||
return ++mRefCount;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE DeckLinkInputCallback::Release()
|
||||
{
|
||||
const ULONG refCount = --mRefCount;
|
||||
if (refCount == 0)
|
||||
delete this;
|
||||
return refCount;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket*)
|
||||
{
|
||||
if (videoFrame != nullptr)
|
||||
mOwner.HandleFrameArrived(videoFrame);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
|
||||
{
|
||||
mOwner.HandleFormatChanged();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
DeckLinkInput::DeckLinkInput(InputFrameMailbox& mailbox) :
|
||||
mMailbox(mailbox)
|
||||
{
|
||||
}
|
||||
|
||||
DeckLinkInput::~DeckLinkInput()
|
||||
{
|
||||
ReleaseResources();
|
||||
}
|
||||
|
||||
bool DeckLinkInput::Initialize(const DeckLinkInputConfig& config, std::string& error)
|
||||
{
|
||||
ReleaseResources();
|
||||
mConfig = config;
|
||||
Log("decklink-input", "Initializing DeckLink input for " + config.videoFormat.displayName + ".");
|
||||
|
||||
if (!DiscoverInput(config, error))
|
||||
return false;
|
||||
|
||||
if (mInput->EnableVideoInput(config.videoFormat.displayMode, mCapturePixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
||||
{
|
||||
error = "DeckLink input setup failed while enabling " +
|
||||
std::string(mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8") +
|
||||
" input for " + config.videoFormat.displayName + ".";
|
||||
ReleaseResources();
|
||||
return false;
|
||||
}
|
||||
Log(
|
||||
"decklink-input",
|
||||
std::string("DeckLink input enabled in ") + (mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8 raw capture") + " mode.");
|
||||
|
||||
mCallback.Attach(new (std::nothrow) DeckLinkInputCallback(*this));
|
||||
if (mCallback == nullptr)
|
||||
{
|
||||
error = "DeckLink input setup failed while creating the capture callback.";
|
||||
ReleaseResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mInput->SetCallback(mCallback) != S_OK)
|
||||
{
|
||||
error = "DeckLink input setup failed while installing the capture callback.";
|
||||
ReleaseResources();
|
||||
return false;
|
||||
}
|
||||
Log("decklink-input", "DeckLink input callback installed.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkInput::Start(std::string& error)
|
||||
{
|
||||
if (mInput == nullptr)
|
||||
{
|
||||
error = "DeckLink input has not been initialized.";
|
||||
return false;
|
||||
}
|
||||
if (mRunning.load(std::memory_order_acquire))
|
||||
return true;
|
||||
|
||||
if (mInput->StartStreams() != S_OK)
|
||||
{
|
||||
error = "DeckLink input stream failed to start.";
|
||||
return false;
|
||||
}
|
||||
mRunning.store(true, std::memory_order_release);
|
||||
Log("decklink-input", "DeckLink input stream started.");
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckLinkInput::Stop()
|
||||
{
|
||||
if (mInput != nullptr && mRunning.exchange(false, std::memory_order_acq_rel))
|
||||
{
|
||||
mInput->StopStreams();
|
||||
Log("decklink-input", "DeckLink input stream stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
void DeckLinkInput::ReleaseResources()
|
||||
{
|
||||
Stop();
|
||||
if (mInput != nullptr)
|
||||
{
|
||||
mInput->SetCallback(nullptr);
|
||||
mInput->DisableVideoInput();
|
||||
}
|
||||
mCallback.Release();
|
||||
mInput.Release();
|
||||
}
|
||||
|
||||
DeckLinkInputMetrics DeckLinkInput::Metrics() const
|
||||
{
|
||||
DeckLinkInputMetrics metrics;
|
||||
metrics.capturedFrames = mCapturedFrames.load(std::memory_order_relaxed);
|
||||
metrics.noInputSourceFrames = mNoInputSourceFrames.load(std::memory_order_relaxed);
|
||||
metrics.unsupportedFrames = mUnsupportedFrames.load(std::memory_order_relaxed);
|
||||
metrics.submitMisses = mSubmitMisses.load(std::memory_order_relaxed);
|
||||
metrics.convertMilliseconds = mConvertMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.submitMilliseconds = mSubmitMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.captureFormat = CaptureFormatName();
|
||||
return metrics;
|
||||
}
|
||||
|
||||
VideoIOPixelFormat DeckLinkInput::CapturePixelFormat() const
|
||||
{
|
||||
return mCapturePixelFormat == bmdFormat8BitYUV ? VideoIOPixelFormat::Uyvy8 : VideoIOPixelFormat::Bgra8;
|
||||
}
|
||||
|
||||
void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||
{
|
||||
if (inputFrame == nullptr)
|
||||
return;
|
||||
|
||||
if ((inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource)
|
||||
{
|
||||
mNoInputSourceFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedNoInputSource.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input callback reports no input source.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputFrame->GetWidth() != static_cast<long>(mMailbox.Config().width) ||
|
||||
inputFrame->GetHeight() != static_cast<long>(mMailbox.Config().height))
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame dimensions do not match the configured mailbox.");
|
||||
return;
|
||||
}
|
||||
|
||||
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
|
||||
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, reinterpret_cast<void**>(&inputFrameBuffer)) != S_OK)
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame does not expose IDeckLinkVideoBuffer.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame buffer could not be opened for read access.");
|
||||
return;
|
||||
}
|
||||
|
||||
void* bytes = nullptr;
|
||||
inputFrameBuffer->GetBytes(&bytes);
|
||||
bool submitted = false;
|
||||
if (mCapturePixelFormat == bmdFormat8BitBGRA)
|
||||
submitted = SubmitBgra8Frame(inputFrame, bytes);
|
||||
else if (mCapturePixelFormat == bmdFormat8BitYUV)
|
||||
submitted = SubmitUyvy8Frame(inputFrame, bytes);
|
||||
else
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
if (!submitted)
|
||||
{
|
||||
mSubmitMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedSubmitMiss.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame could not be submitted to InputFrameMailbox.");
|
||||
}
|
||||
mCapturedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expectedFirstFrame = false;
|
||||
if (mLoggedFirstFrame.compare_exchange_strong(expectedFirstFrame, true, std::memory_order_relaxed))
|
||||
{
|
||||
TryLog(
|
||||
LogLevel::Log,
|
||||
"decklink-input",
|
||||
std::string("First DeckLink ") + (mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8 raw") + " input frame submitted to InputFrameMailbox.");
|
||||
}
|
||||
|
||||
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
||||
}
|
||||
|
||||
void DeckLinkInput::HandleFormatChanged()
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input format changed; input edge does not auto-switch formats yet.");
|
||||
}
|
||||
|
||||
bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string& error)
|
||||
{
|
||||
CComPtr<IDeckLinkIterator> iterator;
|
||||
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&iterator));
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "DeckLink input discovery failed. Blackmagic DeckLink drivers may not be installed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IDeckLink> deckLink;
|
||||
while (iterator->Next(&deckLink) == S_OK)
|
||||
{
|
||||
CComPtr<IDeckLinkInput> candidateInput;
|
||||
if (deckLink->QueryInterface(IID_IDeckLinkInput, reinterpret_cast<void**>(&candidateInput)) == S_OK && candidateInput != nullptr)
|
||||
{
|
||||
CComPtr<IDeckLinkDisplayMode> displayMode;
|
||||
if (FindInputDisplayMode(candidateInput, config.videoFormat.displayMode, &displayMode) &&
|
||||
SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitBGRA))
|
||||
{
|
||||
mInput = candidateInput;
|
||||
mCapturePixelFormat = bmdFormat8BitBGRA;
|
||||
Log("decklink-input", "DeckLink input device selected for BGRA8 capture.");
|
||||
return true;
|
||||
}
|
||||
if (displayMode != nullptr &&
|
||||
SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitYUV))
|
||||
{
|
||||
mInput = candidateInput;
|
||||
mCapturePixelFormat = bmdFormat8BitYUV;
|
||||
Log("decklink-input", "DeckLink input device selected for UYVY8 raw capture with render-thread GPU decode.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
deckLink.Release();
|
||||
}
|
||||
|
||||
error = "No DeckLink input device supports BGRA8 or UYVY8 capture for " + config.videoFormat.displayName + ".";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DeckLinkInput::SupportsInputFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat) const
|
||||
{
|
||||
if (input == nullptr)
|
||||
return false;
|
||||
|
||||
BOOL supported = FALSE;
|
||||
BMDDisplayMode actualMode = bmdModeUnknown;
|
||||
const HRESULT result = input->DoesSupportVideoMode(
|
||||
bmdVideoConnectionUnspecified,
|
||||
displayMode,
|
||||
pixelFormat,
|
||||
bmdNoVideoInputConversion,
|
||||
bmdSupportedVideoModeDefault,
|
||||
&actualMode,
|
||||
&supported);
|
||||
return result == S_OK && supported != FALSE;
|
||||
}
|
||||
|
||||
bool DeckLinkInput::SubmitBgra8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes)
|
||||
{
|
||||
if (inputFrame == nullptr || bytes == nullptr)
|
||||
return false;
|
||||
const uint64_t frameIndex = mCapturedFrames.load(std::memory_order_relaxed);
|
||||
mConvertMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||
const auto submitStart = std::chrono::steady_clock::now();
|
||||
const bool submitted = mMailbox.SubmitFrame(bytes, static_cast<unsigned>(inputFrame->GetRowBytes()), frameIndex);
|
||||
const auto submitEnd = std::chrono::steady_clock::now();
|
||||
mSubmitMilliseconds.store(
|
||||
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(submitEnd - submitStart).count(),
|
||||
std::memory_order_relaxed);
|
||||
return submitted;
|
||||
}
|
||||
|
||||
bool DeckLinkInput::SubmitUyvy8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes)
|
||||
{
|
||||
if (inputFrame == nullptr || bytes == nullptr)
|
||||
return false;
|
||||
|
||||
const unsigned width = static_cast<unsigned>(inputFrame->GetWidth());
|
||||
const unsigned height = static_cast<unsigned>(inputFrame->GetHeight());
|
||||
const long sourceRowBytes = inputFrame->GetRowBytes();
|
||||
if (width == 0 || height == 0 || sourceRowBytes < static_cast<long>(width * 2u))
|
||||
return false;
|
||||
|
||||
mConvertMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||
const uint64_t frameIndex = mCapturedFrames.load(std::memory_order_relaxed);
|
||||
const auto submitStart = std::chrono::steady_clock::now();
|
||||
const bool submitted = mMailbox.SubmitFrame(bytes, static_cast<unsigned>(sourceRowBytes), frameIndex);
|
||||
const auto submitEnd = std::chrono::steady_clock::now();
|
||||
mSubmitMilliseconds.store(
|
||||
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(submitEnd - submitStart).count(),
|
||||
std::memory_order_relaxed);
|
||||
return submitted;
|
||||
}
|
||||
|
||||
const char* DeckLinkInput::CaptureFormatName() const
|
||||
{
|
||||
if (mInput == nullptr)
|
||||
return "none";
|
||||
if (mCapturePixelFormat == bmdFormat8BitBGRA)
|
||||
return "BGRA8";
|
||||
if (mCapturePixelFormat == bmdFormat8BitYUV)
|
||||
return "UYVY8";
|
||||
return "unsupported";
|
||||
}
|
||||
}
|
||||
96
src/video/DeckLinkInput.h
Normal file
96
src/video/DeckLinkInput.h
Normal file
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include "../frames/InputFrameMailbox.h"
|
||||
#include "DeckLinkAPI_h.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
|
||||
#include <atlbase.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkInputConfig
|
||||
{
|
||||
VideoFormat videoFormat;
|
||||
};
|
||||
|
||||
struct DeckLinkInputMetrics
|
||||
{
|
||||
uint64_t capturedFrames = 0;
|
||||
uint64_t noInputSourceFrames = 0;
|
||||
uint64_t unsupportedFrames = 0;
|
||||
uint64_t submitMisses = 0;
|
||||
double convertMilliseconds = 0.0;
|
||||
double submitMilliseconds = 0.0;
|
||||
const char* captureFormat = "none";
|
||||
};
|
||||
|
||||
class DeckLinkInput;
|
||||
|
||||
class DeckLinkInputCallback final : public IDeckLinkInputCallback
|
||||
{
|
||||
public:
|
||||
explicit DeckLinkInputCallback(DeckLinkInput& owner);
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket) override;
|
||||
HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode* newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags) override;
|
||||
|
||||
private:
|
||||
DeckLinkInput& mOwner;
|
||||
std::atomic<ULONG> mRefCount{ 1 };
|
||||
};
|
||||
|
||||
class DeckLinkInput
|
||||
{
|
||||
public:
|
||||
DeckLinkInput(InputFrameMailbox& mailbox);
|
||||
DeckLinkInput(const DeckLinkInput&) = delete;
|
||||
DeckLinkInput& operator=(const DeckLinkInput&) = delete;
|
||||
~DeckLinkInput();
|
||||
|
||||
bool Initialize(const DeckLinkInputConfig& config, std::string& error);
|
||||
bool Start(std::string& error);
|
||||
void Stop();
|
||||
void ReleaseResources();
|
||||
|
||||
bool IsInitialized() const { return mInput != nullptr; }
|
||||
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||
VideoIOPixelFormat CapturePixelFormat() const;
|
||||
DeckLinkInputMetrics Metrics() const;
|
||||
|
||||
void HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame);
|
||||
void HandleFormatChanged();
|
||||
|
||||
private:
|
||||
bool DiscoverInput(const DeckLinkInputConfig& config, std::string& error);
|
||||
bool SupportsInputFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat) const;
|
||||
bool SubmitBgra8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes);
|
||||
bool SubmitUyvy8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes);
|
||||
const char* CaptureFormatName() const;
|
||||
|
||||
InputFrameMailbox& mMailbox;
|
||||
DeckLinkInputConfig mConfig;
|
||||
BMDPixelFormat mCapturePixelFormat = bmdFormat8BitBGRA;
|
||||
CComPtr<IDeckLinkInput> mInput;
|
||||
CComPtr<DeckLinkInputCallback> mCallback;
|
||||
std::atomic<bool> mRunning{ false };
|
||||
std::atomic<uint64_t> mCapturedFrames{ 0 };
|
||||
std::atomic<uint64_t> mNoInputSourceFrames{ 0 };
|
||||
std::atomic<uint64_t> mUnsupportedFrames{ 0 };
|
||||
std::atomic<uint64_t> mSubmitMisses{ 0 };
|
||||
std::atomic<double> mConvertMilliseconds{ 0.0 };
|
||||
std::atomic<double> mSubmitMilliseconds{ 0.0 };
|
||||
std::atomic<bool> mLoggedFirstFrame{ false };
|
||||
std::atomic<bool> mLoggedNoInputSource{ false };
|
||||
std::atomic<bool> mLoggedUnsupportedFrame{ false };
|
||||
std::atomic<bool> mLoggedSubmitMiss{ false };
|
||||
};
|
||||
}
|
||||
87
src/video/DeckLinkInputThread.h
Normal file
87
src/video/DeckLinkInputThread.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkInput.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkInputThreadConfig
|
||||
{
|
||||
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(100);
|
||||
};
|
||||
|
||||
class DeckLinkInputThread
|
||||
{
|
||||
public:
|
||||
DeckLinkInputThread(DeckLinkInput& input, DeckLinkInputThreadConfig config = DeckLinkInputThreadConfig()) :
|
||||
mInput(input),
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
DeckLinkInputThread(const DeckLinkInputThread&) = delete;
|
||||
DeckLinkInputThread& operator=(const DeckLinkInputThread&) = delete;
|
||||
|
||||
~DeckLinkInputThread()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool Start(std::string& error)
|
||||
{
|
||||
if (mThread.joinable())
|
||||
return true;
|
||||
mStartSucceeded.store(false, std::memory_order_release);
|
||||
mStartCompleted.store(false, std::memory_order_release);
|
||||
mStopping.store(false, std::memory_order_release);
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
|
||||
while (!mStartCompleted.load(std::memory_order_acquire))
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
|
||||
if (mStartSucceeded.load(std::memory_order_acquire))
|
||||
return true;
|
||||
|
||||
error = mStartError;
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mStopping.store(true, std::memory_order_release);
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
private:
|
||||
void ThreadMain()
|
||||
{
|
||||
std::string error;
|
||||
if (!mInput.Start(error))
|
||||
{
|
||||
mStartError = error;
|
||||
mStartCompleted.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
mStartSucceeded.store(true, std::memory_order_release);
|
||||
mStartCompleted.store(true, std::memory_order_release);
|
||||
while (!mStopping.load(std::memory_order_acquire))
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
mInput.Stop();
|
||||
}
|
||||
|
||||
DeckLinkInput& mInput;
|
||||
DeckLinkInputThreadConfig mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mStartCompleted{ false };
|
||||
std::atomic<bool> mStartSucceeded{ false };
|
||||
std::string mStartError;
|
||||
};
|
||||
}
|
||||
112
src/video/DeckLinkOutput.cpp
Normal file
112
src/video/DeckLinkOutput.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "DeckLinkOutput.h"
|
||||
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
DeckLinkOutput::~DeckLinkOutput()
|
||||
{
|
||||
ReleaseResources();
|
||||
}
|
||||
|
||||
bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error)
|
||||
{
|
||||
mConfig = config;
|
||||
mCompletionCallback = completionCallback;
|
||||
|
||||
VideoFormatSelection formats;
|
||||
formats.output = config.outputVideoMode;
|
||||
if (!mSession.DiscoverDevicesAndModes(formats, error))
|
||||
return false;
|
||||
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
|
||||
return false;
|
||||
if (!mSession.ConfigureOutput(
|
||||
[this](const VideoIOCompletion& completion) { HandleCompletion(completion); },
|
||||
formats.output,
|
||||
config.externalKeyingEnabled,
|
||||
error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!mSession.PrepareOutputSchedule())
|
||||
{
|
||||
error = "DeckLink output schedule preparation failed.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkOutput::StartScheduledPlayback(std::string& error)
|
||||
{
|
||||
if (mSession.StartScheduledPlayback())
|
||||
return true;
|
||||
error = "DeckLink scheduled playback failed to start.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DeckLinkOutput::ScheduleFrame(const VideoIOOutputFrame& frame)
|
||||
{
|
||||
return mSession.ScheduleOutputFrame(frame);
|
||||
}
|
||||
|
||||
void DeckLinkOutput::Stop()
|
||||
{
|
||||
mSession.Stop();
|
||||
}
|
||||
|
||||
void DeckLinkOutput::ReleaseResources()
|
||||
{
|
||||
mSession.ReleaseResources();
|
||||
}
|
||||
|
||||
const VideoIOState& DeckLinkOutput::State() const
|
||||
{
|
||||
return mSession.State();
|
||||
}
|
||||
|
||||
DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
|
||||
{
|
||||
DeckLinkOutputMetrics metrics;
|
||||
metrics.completions = mCompletions.load();
|
||||
metrics.displayedLate = mDisplayedLate.load();
|
||||
metrics.dropped = mDropped.load();
|
||||
metrics.flushed = mFlushed.load();
|
||||
|
||||
const VideoIOState& state = mSession.State();
|
||||
metrics.scheduleFailures = state.deckLinkScheduleFailureCount;
|
||||
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
||||
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
||||
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
||||
metrics.scheduleLeadAvailable = state.deckLinkScheduleLeadAvailable;
|
||||
metrics.playbackStreamTime = state.deckLinkPlaybackStreamTime;
|
||||
metrics.playbackFrameIndex = state.deckLinkPlaybackFrameIndex;
|
||||
metrics.nextScheduleFrameIndex = state.deckLinkNextScheduleFrameIndex;
|
||||
metrics.scheduleLeadFrames = state.deckLinkScheduleLeadFrames;
|
||||
metrics.scheduleRealignmentCount = state.deckLinkScheduleRealignmentCount;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
void DeckLinkOutput::HandleCompletion(const VideoIOCompletion& completion)
|
||||
{
|
||||
++mCompletions;
|
||||
switch (completion.result)
|
||||
{
|
||||
case VideoIOCompletionResult::DisplayedLate:
|
||||
++mDisplayedLate;
|
||||
break;
|
||||
case VideoIOCompletionResult::Dropped:
|
||||
++mDropped;
|
||||
break;
|
||||
case VideoIOCompletionResult::Flushed:
|
||||
++mFlushed;
|
||||
break;
|
||||
case VideoIOCompletionResult::Completed:
|
||||
case VideoIOCompletionResult::Unknown:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (mCompletionCallback)
|
||||
mCompletionCallback(completion);
|
||||
}
|
||||
}
|
||||
69
src/video/DeckLinkOutput.h
Normal file
69
src/video/DeckLinkOutput.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "DeckLinkSession.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkOutputConfig
|
||||
{
|
||||
VideoFormat outputVideoMode;
|
||||
bool externalKeyingEnabled = false;
|
||||
bool outputAlphaRequired = false;
|
||||
};
|
||||
|
||||
struct DeckLinkOutputMetrics
|
||||
{
|
||||
uint64_t completions = 0;
|
||||
uint64_t displayedLate = 0;
|
||||
uint64_t dropped = 0;
|
||||
uint64_t flushed = 0;
|
||||
uint64_t scheduleFailures = 0;
|
||||
bool actualBufferedFramesAvailable = false;
|
||||
uint64_t actualBufferedFrames = 0;
|
||||
double scheduleCallMilliseconds = 0.0;
|
||||
bool scheduleLeadAvailable = false;
|
||||
int64_t playbackStreamTime = 0;
|
||||
uint64_t playbackFrameIndex = 0;
|
||||
uint64_t nextScheduleFrameIndex = 0;
|
||||
int64_t scheduleLeadFrames = 0;
|
||||
uint64_t scheduleRealignmentCount = 0;
|
||||
};
|
||||
|
||||
class DeckLinkOutput
|
||||
{
|
||||
public:
|
||||
using CompletionCallback = std::function<void(const VideoIOCompletion&)>;
|
||||
|
||||
DeckLinkOutput() = default;
|
||||
DeckLinkOutput(const DeckLinkOutput&) = delete;
|
||||
DeckLinkOutput& operator=(const DeckLinkOutput&) = delete;
|
||||
~DeckLinkOutput();
|
||||
|
||||
bool Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error);
|
||||
bool StartScheduledPlayback(std::string& error);
|
||||
bool ScheduleFrame(const VideoIOOutputFrame& frame);
|
||||
void Stop();
|
||||
void ReleaseResources();
|
||||
|
||||
const VideoIOState& State() const;
|
||||
DeckLinkOutputMetrics Metrics() const;
|
||||
|
||||
private:
|
||||
void HandleCompletion(const VideoIOCompletion& completion);
|
||||
|
||||
DeckLinkSession mSession;
|
||||
DeckLinkOutputConfig mConfig;
|
||||
CompletionCallback mCompletionCallback;
|
||||
std::atomic<uint64_t> mCompletions{ 0 };
|
||||
std::atomic<uint64_t> mDisplayedLate{ 0 };
|
||||
std::atomic<uint64_t> mDropped{ 0 };
|
||||
std::atomic<uint64_t> mFlushed{ 0 };
|
||||
};
|
||||
}
|
||||
127
src/video/DeckLinkOutputThread.h
Normal file
127
src/video/DeckLinkOutputThread.h
Normal file
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
#include "DeckLinkOutput.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkOutputThreadConfig
|
||||
{
|
||||
std::size_t targetBufferedFrames = 4;
|
||||
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(1);
|
||||
};
|
||||
|
||||
struct DeckLinkOutputThreadMetrics
|
||||
{
|
||||
uint64_t scheduledFrames = 0;
|
||||
uint64_t completedPollMisses = 0;
|
||||
uint64_t scheduleFailures = 0;
|
||||
};
|
||||
|
||||
template <typename SystemFrameExchange>
|
||||
class DeckLinkOutputThread
|
||||
{
|
||||
public:
|
||||
DeckLinkOutputThread(DeckLinkOutput& output, SystemFrameExchange& exchange, DeckLinkOutputThreadConfig config = DeckLinkOutputThreadConfig()) :
|
||||
mOutput(output),
|
||||
mExchange(exchange),
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
DeckLinkOutputThread(const DeckLinkOutputThread&) = delete;
|
||||
DeckLinkOutputThread& operator=(const DeckLinkOutputThread&) = delete;
|
||||
|
||||
~DeckLinkOutputThread()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool Start()
|
||||
{
|
||||
if (mRunning)
|
||||
return true;
|
||||
mStopping = false;
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
mRunning = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mStopping = true;
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
DeckLinkOutputThreadMetrics Metrics() const
|
||||
{
|
||||
DeckLinkOutputThreadMetrics metrics;
|
||||
metrics.scheduledFrames = mScheduledFrames.load();
|
||||
metrics.completedPollMisses = mCompletedPollMisses.load();
|
||||
metrics.scheduleFailures = mScheduleFailures.load();
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private:
|
||||
void ThreadMain()
|
||||
{
|
||||
while (!mStopping)
|
||||
{
|
||||
const auto exchangeMetrics = mExchange.Metrics();
|
||||
const auto outputMetrics = mOutput.Metrics();
|
||||
const std::size_t bufferedFrames = outputMetrics.actualBufferedFramesAvailable
|
||||
? static_cast<std::size_t>(outputMetrics.actualBufferedFrames)
|
||||
: exchangeMetrics.scheduledCount;
|
||||
if (bufferedFrames >= mConfig.targetBufferedFrames)
|
||||
{
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
continue;
|
||||
}
|
||||
SystemFrame frame;
|
||||
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||
{
|
||||
++mCompletedPollMisses;
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
continue;
|
||||
}
|
||||
|
||||
VideoIOOutputFrame outputFrame;
|
||||
outputFrame.bytes = frame.bytes;
|
||||
outputFrame.nativeBuffer = frame.bytes;
|
||||
outputFrame.rowBytes = frame.rowBytes;
|
||||
outputFrame.width = frame.width;
|
||||
outputFrame.height = frame.height;
|
||||
outputFrame.pixelFormat = frame.pixelFormat;
|
||||
|
||||
if (!mOutput.ScheduleFrame(outputFrame))
|
||||
{
|
||||
++mScheduleFailures;
|
||||
mExchange.ReleaseScheduledByBytes(frame.bytes);
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
continue;
|
||||
}
|
||||
|
||||
++mScheduledFrames;
|
||||
}
|
||||
}
|
||||
|
||||
DeckLinkOutput& mOutput;
|
||||
SystemFrameExchange& mExchange;
|
||||
DeckLinkOutputThreadConfig mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mRunning{ false };
|
||||
std::atomic<uint64_t> mScheduledFrames{ 0 };
|
||||
std::atomic<uint64_t> mCompletedPollMisses{ 0 };
|
||||
std::atomic<uint64_t> mScheduleFailures{ 0 };
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user