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
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: