openapi: 3.0.3
info:
  title: Video Shader Toys Control API
  version: 0.1.0
  description: |
    REST API exposed by the local Video Shader Toys control server.

    The API is intended for local control tools and the bundled React UI. All mutating
    endpoints return a small action result object.

    WebSocket state streaming is planned for the control UI but is not currently served
    by RenderCadenceCompositor. Clients should poll `/api/state` until `/ws` is implemented.
servers:
  - url: http://127.0.0.1:8080
    description: Default local control server
tags:
  - name: State
    description: Runtime state and status.
  - name: Static
    description: Bundled control UI and static assets served by the local host.
  - name: Docs
    description: OpenAPI and Swagger UI documentation served by the local host.
  - name: Layers
    description: Layer stack control.
  - name: Stack Presets
    description: Save and recall layer stack presets.
  - name: Runtime
    description: Runtime actions.
paths:
  /:
    get:
      tags: [Static]
      summary: Serve the bundled control UI
      description: Returns the built React control UI `index.html` from `ui/dist`.
      operationId: getControlUiRoot
      responses:
        "200":
          description: Control UI HTML.
          content:
            text/html:
              schema:
                type: string
        "404":
          description: UI bundle was not found.
          content:
            text/plain:
              schema:
                type: string
  /index.html:
    get:
      tags: [Static]
      summary: Serve the bundled control UI index file
      description: Returns the built React control UI `index.html` from `ui/dist`.
      operationId: getControlUiIndex
      responses:
        "200":
          description: Control UI HTML.
          content:
            text/html:
              schema:
                type: string
        "404":
          description: UI bundle was not found.
          content:
            text/plain:
              schema:
                type: string
  /assets/{assetPath}:
    get:
      tags: [Static]
      summary: Serve a bundled control UI asset
      description: Serves files from `ui/dist/assets`. The server rejects unsafe relative paths and guesses the content type from the file extension.
      operationId: getControlUiAsset
      parameters:
        - name: assetPath
          in: path
          required: true
          description: Relative asset path below `ui/dist/assets`.
          schema:
            type: string
      responses:
        "200":
          description: Static asset.
          content:
            text/javascript:
              schema:
                type: string
            text/css:
              schema:
                type: string
            image/svg+xml:
              schema:
                type: string
            image/png:
              schema:
                type: string
                format: binary
            text/plain:
              schema:
                type: string
        "404":
          description: Asset was not found or the path was unsafe.
          content:
            text/plain:
              schema:
                type: string
  /docs:
    get:
      tags: [Docs]
      summary: Serve Swagger UI
      description: Returns a small Swagger UI page pointed at `/docs/openapi.yaml`.
      operationId: getSwaggerUi
      responses:
        "200":
          description: Swagger UI HTML.
          content:
            text/html:
              schema:
                type: string
  /docs/:
    get:
      tags: [Docs]
      summary: Serve Swagger UI
      description: Alias for `/docs`.
      operationId: getSwaggerUiWithTrailingSlash
      responses:
        "200":
          description: Swagger UI HTML.
          content:
            text/html:
              schema:
                type: string
  /docs/openapi.yaml:
    get:
      tags: [Docs]
      summary: Serve the OpenAPI document
      operationId: getOpenApiDocumentFromDocs
      responses:
        "200":
          description: OpenAPI YAML document.
          content:
            application/yaml:
              schema:
                type: string
        "404":
          description: OpenAPI document was not found.
          content:
            text/plain:
              schema:
                type: string
  /openapi.yaml:
    get:
      tags: [Docs]
      summary: Serve the OpenAPI document
      description: Alias for `/docs/openapi.yaml`.
      operationId: getOpenApiDocument
      responses:
        "200":
          description: OpenAPI YAML document.
          content:
            application/yaml:
              schema:
                type: string
        "404":
          description: OpenAPI document was not found.
          content:
            text/plain:
              schema:
                type: string
  /api/state:
    get:
      tags: [State]
      summary: Get current runtime state
      operationId: getRuntimeState
      responses:
        "200":
          description: Current runtime state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RuntimeState"
  /api/layers/add:
    post:
      tags: [Layers]
      summary: Add a layer using a shader package
      operationId: addLayer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddLayerRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/layers/remove:
    post:
      tags: [Layers]
      summary: Remove a layer
      operationId: removeLayer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LayerIdRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/layers/move:
    post:
      tags: [Layers]
      summary: Move a layer by direction
      description: Moves a layer up or down by one or more positions, depending on the signed direction value.
      operationId: moveLayer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MoveLayerRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/layers/reorder:
    post:
      tags: [Layers]
      summary: Move a layer to an absolute index
      operationId: reorderLayer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReorderLayerRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/layers/set-bypass:
    post:
      tags: [Layers]
      summary: Set layer bypass state
      operationId: setLayerBypass
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetBypassRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/layers/set-shader:
    post:
      tags: [Layers]
      summary: Change the shader package used by a layer
      operationId: setLayerShader
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetShaderRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/layers/update-parameter:
    post:
      tags: [Layers]
      summary: Update a layer parameter value
      operationId: updateLayerParameter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateParameterRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/layers/reset-parameters:
    post:
      tags: [Layers]
      summary: Reset a layer's parameters to shader defaults
      operationId: resetLayerParameters
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LayerIdRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/stack-presets/save:
    post:
      tags: [Stack Presets]
      summary: Save the current layer stack as a preset
      operationId: saveStackPreset
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PresetNameRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/stack-presets/load:
    post:
      tags: [Stack Presets]
      summary: Load a layer stack preset
      operationId: loadStackPreset
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PresetNameRequest"
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/reload:
    post:
      tags: [Runtime]
      summary: Reload shaders
      description: Rescans the shader library, re-reads manifests, queues shader compilation, and refreshes shader availability/errors. If a changed shader fails, the previous working stack remains active where possible.
      operationId: reloadShaders
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
  /api/screenshot:
    post:
      tags: [Runtime]
      summary: Queue a PNG screenshot of the final output render target
      description: Captures the next completed output render target and writes it under `runtime/screenshots/`.
      operationId: queueScreenshot
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
      responses:
        "200":
          $ref: "#/components/responses/ActionOk"
        "400":
          $ref: "#/components/responses/ActionError"
components:
  responses:
    ActionOk:
      description: Action succeeded.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ActionResponse"
          example:
            ok: true
    ActionError:
      description: Action failed or request JSON was invalid.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ActionResponse"
          example:
            ok: false
            error: Unknown layer id.
  schemas:
    ActionResponse:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
        error:
          type: string
          description: Present when the request fails or when the runtime returns a diagnostic.
      additionalProperties: false
    AddLayerRequest:
      type: object
      required: [shaderId]
      properties:
        shaderId:
          type: string
          example: studio-color
      additionalProperties: false
    LayerIdRequest:
      type: object
      required: [layerId]
      properties:
        layerId:
          type: string
          example: layer-1
      additionalProperties: false
    MoveLayerRequest:
      type: object
      required: [layerId, direction]
      properties:
        layerId:
          type: string
        direction:
          type: integer
          description: Signed movement direction. Negative moves toward the top of the stack; positive moves toward the bottom.
          example: -1
      additionalProperties: false
    ReorderLayerRequest:
      type: object
      required: [layerId, targetIndex]
      properties:
        layerId:
          type: string
        targetIndex:
          type: integer
          minimum: 0
          example: 0
      additionalProperties: false
    SetBypassRequest:
      type: object
      required: [layerId, bypass]
      properties:
        layerId:
          type: string
        bypass:
          type: boolean
      additionalProperties: false
    SetShaderRequest:
      type: object
      required: [layerId, shaderId]
      properties:
        layerId:
          type: string
        shaderId:
          type: string
          example: vhs
      additionalProperties: false
    UpdateParameterRequest:
      type: object
      required: [layerId, parameterId, value]
      properties:
        layerId:
          type: string
        parameterId:
          type: string
          example: brightness
        value:
          description: Parameter value. Type depends on the shader parameter definition.
          oneOf:
            - type: number
            - type: boolean
            - type: string
            - type: array
              items:
                type: number
          example: 1.25
      additionalProperties: false
    PresetNameRequest:
      type: object
      required: [presetName]
      properties:
        presetName:
          type: string
          example: live-show-look
      additionalProperties: false
    RuntimeState:
      type: object
      properties:
        app:
          $ref: "#/components/schemas/AppState"
        runtime:
          $ref: "#/components/schemas/RuntimeStatus"
        video:
          $ref: "#/components/schemas/VideoStatus"
        decklink:
          $ref: "#/components/schemas/DeckLinkStatus"
        videoIO:
          $ref: "#/components/schemas/VideoIOStatus"
        performance:
          $ref: "#/components/schemas/PerformanceStatus"
        backendPlayout:
          $ref: "#/components/schemas/BackendPlayoutStatus"
        runtimeEvents:
          $ref: "#/components/schemas/RuntimeEventStatus"
        shaders:
          type: array
          items:
            $ref: "#/components/schemas/ShaderSummary"
        stackPresets:
          type: array
          items:
            type: string
        layers:
          type: array
          items:
            $ref: "#/components/schemas/LayerState"
    AppState:
      type: object
      properties:
        serverPort:
          type: number
        oscPort:
          type: number
        oscBindAddress:
          type: string
        oscSmoothing:
          type: number
        autoReload:
          type: boolean
        maxTemporalHistoryFrames:
          type: number
        previewFps:
          type: number
        enableExternalKeying:
          type: boolean
        inputVideoFormat:
          type: string
        inputFrameRate:
          type: string
        outputVideoFormat:
          type: string
        outputFrameRate:
          type: string
    RuntimeStatus:
      type: object
      properties:
        layerCount:
          type: number
        compileSucceeded:
          type: boolean
        compileMessage:
          type: string
    VideoStatus:
      type: object
      properties:
        hasSignal:
          type: boolean
        width:
          type: number
        height:
          type: number
        modeName:
          type: string
    DeckLinkStatus:
      type: object
      deprecated: true
      description: Legacy DeckLink-specific status object. Prefer `videoIO` for new clients.
      properties:
        modelName:
          type: string
        supportsInternalKeying:
          type: boolean
        supportsExternalKeying:
          type: boolean
        keyerInterfaceAvailable:
          type: boolean
        externalKeyingRequested:
          type: boolean
        externalKeyingActive:
          type: boolean
        statusMessage:
          type: string
    VideoIOStatus:
      type: object
      properties:
        backend:
          type: string
          example: decklink
        modelName:
          type: string
        supportsInternalKeying:
          type: boolean
        supportsExternalKeying:
          type: boolean
        keyerInterfaceAvailable:
          type: boolean
        externalKeyingRequested:
          type: boolean
        externalKeyingActive:
          type: boolean
        statusMessage:
          type: string
    PerformanceStatus:
      type: object
      properties:
        frameBudgetMs:
          type: number
        renderMs:
          type: number
        smoothedRenderMs:
          type: number
        budgetUsedPercent:
          type: number
        completionIntervalMs:
          type: number
        smoothedCompletionIntervalMs:
          type: number
        maxCompletionIntervalMs:
          type: number
        lateFrameCount:
          type: number
        droppedFrameCount:
          type: number
        flushedFrameCount:
          type: number
    BackendPlayoutStatus:
      type: object
      properties:
        lifecycleState:
          type: string
          example: running
        degraded:
          type: boolean
        statusMessage:
          type: string
        lateFrameCount:
          type: number
        droppedFrameCount:
          type: number
        flushedFrameCount:
          type: number
        readyQueue:
          $ref: "#/components/schemas/BackendReadyQueueStatus"
        outputRender:
          $ref: "#/components/schemas/BackendOutputRenderStatus"
        recovery:
          $ref: "#/components/schemas/BackendPlayoutRecoveryStatus"
    BackendReadyQueueStatus:
      type: object
      properties:
        depth:
          type: number
          description: Current number of ready output frames.
        capacity:
          type: number
          description: Maximum ready output frames currently allowed.
        minDepth:
          type: number
          description: Minimum observed ready queue depth since backend worker start.
        maxDepth:
          type: number
          description: Maximum observed ready queue depth since backend worker start.
        zeroDepthCount:
          type: number
          description: Number of observed samples where the ready queue was empty.
        pushedCount:
          type: number
        poppedCount:
          type: number
        droppedCount:
          type: number
        underrunCount:
          type: number
    BackendOutputRenderStatus:
      type: object
      properties:
        renderMs:
          type: number
          description: Most recent output render duration in milliseconds.
        smoothedRenderMs:
          type: number
          description: Smoothed output render duration in milliseconds.
        maxRenderMs:
          type: number
          description: Maximum observed output render duration in milliseconds.
        acquireFrameMs:
          type: number
          description: Time spent acquiring a writable backend output frame in milliseconds.
        renderRequestMs:
          type: number
          description: Time spent executing the render-thread output frame request in milliseconds.
        endAccessMs:
          type: number
          description: Time spent ending write access to the backend output frame in milliseconds.
        queueWaitMs:
          type: number
          description: Time the output render request spent waiting for the render thread in milliseconds.
        drawMs:
          type: number
          description: Time spent drawing, blitting, packing, and flushing the output frame in milliseconds.
        fenceWaitMs:
          type: number
          description: Time spent waiting for the async readback fence in milliseconds.
        mapMs:
          type: number
          description: Time spent mapping the async readback pixel buffer in milliseconds.
        readbackCopyMs:
          type: number
          description: Time spent copying async readback bytes into the backend output frame in milliseconds.
        cachedCopyMs:
          type: number
          description: Time spent copying the cached output frame when async readback is not ready in milliseconds.
        asyncQueueMs:
          type: number
          description: Time spent queueing the next async readback in milliseconds.
        asyncQueueBufferMs:
          type: number
          description: Time spent orphaning or allocating the async readback pixel buffer in milliseconds.
        asyncQueueSetupMs:
          type: number
          description: Time spent applying readback pixel-store, framebuffer, and pixel-pack-buffer state in milliseconds.
        asyncQueueReadPixelsMs:
          type: number
          description: Time spent issuing glReadPixels for the async readback in milliseconds.
        asyncQueueFenceMs:
          type: number
          description: Time spent creating the async readback fence in milliseconds.
        syncReadMs:
          type: number
          description: Time spent in bootstrap synchronous readback in milliseconds.
        asyncReadbackMissCount:
          type: number
          description: Count of output render requests where async readback was not ready.
        cachedFallbackCount:
          type: number
          description: Count of output render requests served from the cached output frame.
        syncFallbackCount:
          type: number
          description: Count of output render requests that used bootstrap synchronous readback.
    BackendPlayoutRecoveryStatus:
      type: object
      properties:
        completionResult:
          type: string
          enum: [Completed, DisplayedLate, Dropped, Flushed, Unknown]
        completedFrameIndex:
          type: number
        scheduledFrameIndex:
          type: number
        scheduledLeadFrames:
          type: number
        measuredLagFrames:
          type: number
        catchUpFrames:
          type: number
        lateStreak:
          type: number
        dropStreak:
          type: number
    RuntimeEventStatus:
      type: object
      properties:
        queue:
          $ref: "#/components/schemas/RuntimeEventQueueStatus"
        dispatch:
          $ref: "#/components/schemas/RuntimeEventDispatchStatus"
    RuntimeEventQueueStatus:
      type: object
      properties:
        name:
          type: string
        depth:
          type: number
        capacity:
          type: number
        droppedCount:
          type: number
        oldestEventAgeMs:
          type: number
    RuntimeEventDispatchStatus:
      type: object
      properties:
        dispatchCallCount:
          type: number
        dispatchedEventCount:
          type: number
        handlerInvocationCount:
          type: number
        handlerFailureCount:
          type: number
        lastDispatchDurationMs:
          type: number
        maxDispatchDurationMs:
          type: number
    ShaderSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        category:
          type: string
        available:
          type: boolean
          description: False when the shader package exists but failed manifest or compile validation.
        error:
          type: string
          description: Error text for unavailable shader packages.
        temporal:
          $ref: "#/components/schemas/TemporalState"
        feedback:
          $ref: "#/components/schemas/FeedbackState"
    TemporalState:
      type: object
      properties:
        enabled:
          type: boolean
        historySource:
          type: string
          enum: [source, preLayerInput, none]
        requestedHistoryLength:
          type: number
        effectiveHistoryLength:
          type: number
    FeedbackState:
      type: object
      properties:
        enabled:
          type: boolean
        writePass:
          type: string
    LayerState:
      type: object
      properties:
        id:
          type: string
        shaderId:
          type: string
        shaderName:
          type: string
        bypass:
          type: boolean
        temporal:
          $ref: "#/components/schemas/TemporalState"
        parameters:
          type: array
          items:
            $ref: "#/components/schemas/ParameterState"
    ParameterState:
      type: object
      properties:
        id:
          type: string
        label:
          type: string
        description:
          type: string
          description: Short helper text shown under the parameter label in the control UI.
        type:
          type: string
          enum: [float, vec2, color, bool, enum, text, trigger]
        defaultValue:
          description: Default parameter value from the shader manifest.
          oneOf:
            - type: number
            - type: boolean
            - type: string
            - type: array
              items:
                type: number
        min:
          type: array
          items:
            type: number
        max:
          type: array
          items:
            type: number
        step:
          type: array
          items:
            type: number
        options:
          type: array
          items:
            $ref: "#/components/schemas/ParameterOption"
        maxLength:
          type: number
          description: Maximum length for text parameters.
        font:
          type: string
          description: Font asset id used by text parameters, when declared.
        value:
          description: Current parameter value.
          oneOf:
            - type: number
            - type: boolean
            - type: string
            - type: array
              items:
                type: number
    ParameterOption:
      type: object
      properties:
        value:
          type: string
        label:
          type: string
