Skip to main content

Graph Pipeline

A Graph Pipeline in KraftShade provides a more flexible approach to shader operations compared to a Serial Pipeline. Instead of a linear sequence, a Graph Pipeline allows you to create complex, non-linear processing flows where you have precise control over the input and output of each step.

Understanding Graph Pipelines​

Graph pipelines are ideal for:

  • Complex multi-pass rendering techniques
  • Effects that require intermediate results
  • Scenarios where you need precise control over buffer management
  • Advanced compositing operations

The key characteristic of a graph pipeline is its non-linear nature - you explicitly define where each step gets its input from and where it renders its output to.

Creating a Graph Pipeline​

You can create a graph pipeline using the graphSteps function within a pipeline context:

pipeline(windowSurface) {
graphSteps(targetBuffer = windowSurface) {
// Graph steps defined here
}
}

Parameters:

  • targetBuffer: The final output buffer where the result will be rendered
  • A lambda block where you define the steps of the graph pipeline

Adding Steps to a Graph Pipeline​

Within a graph pipeline, you have several ways to add steps:

Basic Step​

The most basic way to add a step is with the step function:

graphSteps(targetBuffer = windowSurface) {
step(
shader = MyCustomShader(),
targetBuffer = buffer1,
setupAction = { shader ->
// Configure shader parameters
}
)
}

Parameters:

  • shader: The shader to use for this step
  • targetBuffer: The buffer to render the output to
  • setupAction: An optional lambda to configure the shader

Step with Input Texture​

For shaders that require an input texture, you can use stepWithInputTexture:

graphSteps(targetBuffer = windowSurface) {
stepWithInputTexture(
shader = BlurKraftShader(),
inputTexture = sourceTexture,
targetBuffer = buffer1
)
}

This method has two variants:

  1. For constant textures (loaded from a bitmap)
  2. For texture providers (like buffer references)

Buffer Management in Graph Pipelines​

One of the key features of graph pipelines is explicit buffer management. You can create buffer references and use them as inputs and outputs for different steps:

graphSteps(targetBuffer = windowSurface) {
// Create buffer references
val (horizontalBlurBuffer, verticalBlurBuffer) = createBufferReferences(
"horizontal-blur",
"vertical-blur"
)

// First pass: horizontal blur
stepWithInputTexture(
shader = BlurKraftShader(),
inputTexture = sourceTexture,
targetBuffer = horizontalBlurBuffer
) { shader ->
shader.direction = BlurDirection.HORIZONTAL
}

// Second pass: vertical blur using the result of the first pass
stepWithInputTexture(
shader = BlurKraftShader(),
inputTexture = horizontalBlurBuffer,
targetBuffer = verticalBlurBuffer
) { shader ->
shader.direction = BlurDirection.VERTICAL
}

// Final pass: blend the blurred result with the original
stepWithInputTexture(
shader = AlphaBlendKraftShader(),
inputTexture = sourceTexture,
targetBuffer = graphTargetBuffer
) { shader ->
shader.setSecondInputTexture(verticalBlurBuffer)
shader.mixturePercent = 0.8f
}
}

This example demonstrates a two-pass blur effect followed by a blend operation, which would be difficult to express in a serial pipeline.

Custom Run Steps​

For operations that don't fit the shader model, you can use a custom run step:

graphSteps(targetBuffer = windowSurface) {
step("Custom operation") { runContext ->
// Custom OpenGL operations here
}
}

Differences from Serial Pipeline​

The main differences between graph and serial pipelines are:

  1. Explicit Buffer Management: In a graph pipeline, you explicitly specify the target buffer for each step, while in a serial pipeline, the buffers are managed automatically using a ping-pong mechanism.

  2. Non-Linear Flow: Graph pipelines allow for non-linear processing flows, where steps can use the outputs of any previous step as input, not just the immediately preceding step.

  3. Multiple Inputs: Graph pipelines make it easier to work with shaders that require multiple input textures, such as blend operations.

  4. Reuse of Intermediate Results: You can reuse the output of a step multiple times in different parts of the pipeline.

Implementation Details​

The GraphPipelineSetupScope class provides the implementation for graph pipelines. It extends BasePipelineSetupScope and adds methods specific to graph pipelines:

@KraftShadeDsl
class GraphPipelineSetupScope(
glEnv: GlEnv,
pipeline: Pipeline,
val graphTargetBuffer: GlBufferProvider,
) : BasePipelineSetupScope(glEnv, pipeline) {
// Methods for adding steps to the graph pipeline
}

The graphTargetBuffer property represents the final output buffer for the graph pipeline.

Real-World Example: Two-Pass Blur​

Here's a complete example that implements a two-pass Gaussian blur effect using a graph pipeline:

pipeline(windowSurface) {
graphSteps(targetBuffer = windowSurface) {
// Create buffer references for intermediate results
val (horizontalBlurBuffer, verticalBlurBuffer) = createBufferReferences(
"horizontal-blur",
"vertical-blur"
)

// First pass: horizontal blur
stepWithInputTexture(
shader = GaussianBlurKraftShader(),
inputTexture = bitmap.asTexture(),
targetBuffer = horizontalBlurBuffer
) { shader ->
shader.blurSize = 10f
shader.horizontal = true
}

// Second pass: vertical blur
stepWithInputTexture(
shader = GaussianBlurKraftShader(),
inputTexture = horizontalBlurBuffer,
targetBuffer = verticalBlurBuffer
) { shader ->
shader.blurSize = 10f
shader.horizontal = false
}

// Final pass: apply vignette to the blurred result
stepWithInputTexture(
shader = VignetteKraftShader(),
inputTexture = verticalBlurBuffer,
targetBuffer = graphTargetBuffer
) { shader ->
shader.vignetteStart = 0.8f
shader.vignetteEnd = 0.3f
}
}
}

This creates a processing graph that:

  1. Applies a horizontal Gaussian blur to the input image
  2. Applies a vertical Gaussian blur to the result of step 1
  3. Applies a vignette effect to the fully blurred image
  4. Renders the final result to the window surface

When to Use Graph Pipelines​

Consider using graph pipelines when:

  • You need to implement multi-pass effects like bloom, depth of field, or shadow mapping
  • You want to reuse intermediate results in different parts of your pipeline
  • You need precise control over buffer allocation and management
  • Your effect requires non-linear processing flows

For simpler linear effects, a Serial Pipeline might be more appropriate. For complex effects that combine both approaches, consider using a Nested Pipeline.