Skip to main content

Best Practices

This guide provides recommendations and best practices for using the KraftShade Pipeline DSL effectively. Following these guidelines will help you create efficient, maintainable, and performant shader pipelines.

Pipeline Design

Choose the Right Pipeline Type

Select the appropriate pipeline type based on your needs:

  • Serial Pipeline: Use for linear sequences of effects where each step builds on the previous one.
  • Graph Pipeline: Use for complex effects that require multiple passes or non-linear processing.
  • Nested Pipeline: Use for modular, reusable effect components or to organize complex pipelines.
// Simple linear processing - use Serial Pipeline
serialSteps(bitmap.asTexture(), windowSurface) {
step(ContrastKraftShader())
step(SaturationKraftShader())
}

// Complex multi-pass effect - use Graph Pipeline
graphSteps(targetBuffer = windowSurface) {
// Create buffer references
val (horizontalBlurBuffer, verticalBlurBuffer) = createBufferReferences(
"horizontal-blur",
"vertical-blur"
)

// Multiple passes with explicit buffer management
stepWithInputTexture(shader, inputTexture, horizontalBlurBuffer)
stepWithInputTexture(shader, horizontalBlurBuffer, verticalBlurBuffer)
stepWithInputTexture(shader, verticalBlurBuffer, graphTargetBuffer)
}

Keep Pipelines Modular

Break down complex effects into smaller, reusable components:

// Reusable blur component
fun GraphPipelineSetupScope.applyTwoPassBlur(
inputTexture: TextureProvider,
outputBuffer: GlBufferProvider,
blurSize: Float
) {
val (horizontalBlurBuffer) = createBufferReferences("horizontal-blur")

// Horizontal pass
stepWithInputTexture(
shader = GaussianBlurKraftShader(),
inputTexture = inputTexture,
targetBuffer = horizontalBlurBuffer
) { shader ->
shader.blurSize = blurSize
shader.horizontal = true
}

// Vertical pass
stepWithInputTexture(
shader = GaussianBlurKraftShader(),
inputTexture = horizontalBlurBuffer,
targetBuffer = outputBuffer
) { shader ->
shader.blurSize = blurSize
shader.horizontal = false
}
}

// Usage
graphSteps(targetBuffer = windowSurface) {
applyTwoPassBlur(bitmap.asTexture(), graphTargetBuffer, 5f)
}

Use Descriptive Names

Give your buffers and steps descriptive names to improve readability and debugging:

// Good: Descriptive buffer names
val (horizontalBlurBuffer, verticalBlurBuffer) = createBufferReferences(
"horizontal-blur",
"vertical-blur"
)

// Good: Descriptive step purpose
step("Apply vignette effect") { runContext ->
// Implementation
}

Performance Optimization

Minimize Render Passes

Each step in a pipeline requires a render pass, which has overhead. Minimize the number of passes when possible:

// Less efficient: Two separate steps
step(ContrastKraftShader()) { shader ->
shader.contrast = 1.5f
}
step(SaturationKraftShader()) { shader ->
shader.saturation = 0.8f
}

// More efficient: Combined shader if possible
step(ContrastAndSaturationKraftShader()) { shader ->
shader.contrast = 1.5f
shader.saturation = 0.8f
}

Code Organization

Group related operations together using nested pipelines:

serialSteps(inputTexture, targetBuffer) {
// Color adjustments group
serialStep {
step(ContrastKraftShader())
step(SaturationKraftShader())
step(BrightnessKraftShader())
}

// Artistic effects group
serialStep {
step(VignetteKraftShader())
step(CrosshatchKraftShader())
}
}

Extract Reusable Components

Extract commonly used effect combinations into extension functions or use PipelineModifier for more complex reusable components:

// Extension function for a common effect combination
suspend fun SerialTextureInputPipelineScope.applyBasicColorAdjustments(
contrast: Float = 1.0f,
saturation: Float = 1.0f,
brightness: Float = 0.0f
) {
step(ContrastKraftShader()) { shader ->
shader.contrast = contrast
}

step(SaturationKraftShader()) { shader ->
shader.saturation = saturation
}

step(BrightnessKraftShader()) { shader ->
shader.brightness = brightness
}
}

// Usage
serialSteps(inputTexture, targetBuffer) {
applyBasicColorAdjustments(contrast = 1.2f, saturation = 0.8f)
// Other steps...
}

Use Higher-Level Abstractions When Appropriate

For simple cases, use higher-level abstractions like kraftBitmap:

// Simple, concise approach for basic effects
val processedBitmap = kraftBitmap(context, inputBitmap) {
serialPipeline {
step(ContrastKraftShader(1.5f))
step(SaturationKraftShader(0.8f))
}
}

Error Handling and Debugging

Use Proper Scope Methods

Use the appropriate methods for each pipeline scope to avoid errors:

// In a serial pipeline, use serialStep instead of serialSteps
serialSteps(inputTexture, targetBuffer) {
// Correct: Use serialStep for nested serial pipeline
serialStep {
// Steps here...
}

// Correct: Use graphStep for nested graph pipeline
graphStep { inputTexture ->
// Steps here...
}
}

Add Debug Information

Include debug information in your pipeline steps:

// Add purpose for debug
step("Apply vignette effect") { runContext ->
// Implementation
}

// Use descriptive buffer names
val (blurBuffer) = createBufferReferences("gaussian-blur-result")

Check Buffer Sizes

Be aware of buffer sizes, especially when working with different input sources:

// Add a check or log for buffer sizes
step("Check buffer sizes") { runContext ->
val bufferSize = getPoolBufferSize()
KraftLogger.d("Current buffer size: $bufferSize")
}

Resource Management

Release Resources When Done

Ensure resources are properly released when no longer needed:

// Use the use() extension function for automatic resource cleanup
GlEnv(context).use { env ->
env.execute {
// Pipeline operations...
}
} // GlEnv is automatically released here

Reuse Textures When Possible

For frequently used textures, consider reusing them:

// Load texture once
val commonTexture = LoadedTexture(bitmap)

// Use it multiple times
stepWithInputTexture(shader1, commonTexture, buffer1)
stepWithInputTexture(shader2, commonTexture, buffer2)

Advanced Techniques

Combine with Serialized Effects

You can combine DSL-defined pipelines with serialized effects:

// Apply a serialized effect within a pipeline
serialSteps(inputTexture, targetBuffer) {
// Regular steps
step(ContrastKraftShader())

// Apply a serialized effect
step(serializedEffect, targetBuffer)

// More regular steps
step(VignetteKraftShader())
}

Use Custom Run Steps for Complex Operations

For operations that don't fit the shader model, use custom run steps:

step("Custom OpenGL operations") { runContext ->
// Direct OpenGL operations
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBufferId)
// More OpenGL code...
}

Leverage Input Parameters

Use the Input<T> type for dynamic shader parameters:

// Create an input parameter
val saturationInput = mutableInput(1.0f)

// Use it in a shader
step(SaturationKraftShader()) { shader ->
shader.saturation = saturationInput.get()
}

// Later, update the input value
saturationInput.set(0.5f)

Common Pitfalls to Avoid

Avoid Excessive Nesting

While nesting is powerful, excessive nesting can make your code hard to follow:

// Avoid: Deep nesting
serialSteps(inputTexture, targetBuffer) {
serialStep {
graphStep { inputTexture ->
serialSteps(inputTexture, graphTargetBuffer) {
// Too deep!
}
}
}
}

// Better: Flatten when possible
serialSteps(inputTexture, targetBuffer) {
// First group of operations
step(ContrastKraftShader())

// Complex operation as a single graph step
graphStep { inputTexture ->
// Graph operations here
}

// Final operations
step(VignetteKraftShader())
}

Don't Ignore Buffer Lifecycle

Be careful about buffer references that cross pipeline boundaries:

// Problematic: Using a buffer reference outside its scope
val (buffer) = createBufferReferences("temp-buffer")
graphSteps(targetBuffer) {
stepWithInputTexture(shader, inputTexture, buffer)
}
// buffer might be recycled here!
stepWithInputTexture(shader, buffer, outputBuffer) // Potential issue!

// Better: Keep buffer usage within its scope
graphSteps(targetBuffer) {
val (buffer) = createBufferReferences("temp-buffer")
stepWithInputTexture(shader, inputTexture, buffer)
stepWithInputTexture(shader, buffer, graphTargetBuffer)
}

Avoid Unnecessary Buffer Creation

Don't create new buffers when you can reuse existing ones:

// Avoid: Creating new buffers for each effect
serialSteps(inputTexture, targetBuffer) {
for (i in 0 until 10) {
val (tempBuffer) = createBufferReferences("effect-$i") // Inefficient!
// Use tempBuffer...
}
}

// Better: Reuse buffers
serialSteps(inputTexture, targetBuffer) {
// Serial pipeline automatically uses ping-pong buffers
for (i in 0 until 10) {
step(EffectKraftShader()) { shader ->
shader.intensity = i / 10f
}
}
}

Conclusion

By following these best practices, you can create efficient, maintainable, and performant shader pipelines with the KraftShade Pipeline DSL. Remember that the right approach depends on your specific use case, so adapt these guidelines as needed.

For more information, refer to the documentation on: