# Migration Guide
**Moving Between Tiers - Progressive Disclosure in Action**
This guide shows you how to move between shdrlib's three tiers, mix tiers in the same project, and progressively adopt more or less control as your needs change.
---
## Table of Contents
- [Overview](#overview)
- [EZ → EX Migration](#ez--ex-migration)
- [EX → CORE Migration](#ex--core-migration)
- [CORE → EX Migration](#core--ex-migration)
- [Mixing Tiers](#mixing-tiers)
- [When to Drop Down](#when-to-drop-down)
- [When to Move Up](#when-to-move-up)
- [Common Patterns](#common-patterns)
---
## Overview
### Progressive Disclosure
shdrlib's three-tier design enables **progressive disclosure** - start simple, add complexity only when needed:
```
EZ (Simple) ←→ EX (Balanced) ←→ CORE (Control)
```
**Key principle:** All tiers interoperate seamlessly.
### Migration Paths
| EZ → EX | Need explicit control | Production app outgrows EZ |
| EX → CORE | Need custom patterns | Building engine/framework |
| CORE → EX | Want safety | Reducing boilerplate |
| Mixed | Any combo | Use right tool for each job |
---
## EZ → EX Migration
### Why Migrate?
Your prototype is becoming a production application and you need:
- Explicit configuration control
- Type-safe resource management
- Custom descriptor layouts
- Fine-grained synchronization
### Step-by-Step Migration
#### Before (EZ Tier)
```rust
use shdrlib::ez::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// One-liner setup
let mut renderer = EzRenderer::new()?;
// One-liner pipeline
let pipeline = renderer.quick_pipeline(VERT_SHADER, FRAG_SHADER)?;
// Simple render loop
renderer.render_frame(|frame| {
frame.bind_pipeline(pipeline)?;
frame.set_viewport(0.0, 0.0, 800.0, 600.0);
frame.set_scissor(0, 0, 800, 600);
frame.draw(3, 1, 0, 0);
Ok(())
})?;
Ok(())
}
```
#### After (EX Tier)
```rust
use shdrlib::{
core::ShaderStage,
ex::{RuntimeManager, RuntimeConfig, ShaderManager, PipelineBuilder},
};
use ash::vk;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Explicit initialization
let mut runtime = RuntimeManager::new(RuntimeConfig {
app_name: "My App".to_string(),
enable_validation: cfg!(debug_assertions),
in_flight_frames: 2,
})?;
let mut shaders = ShaderManager::new(runtime.device())?;
// Explicit shader compilation
let vert_id = shaders.add_shader(
VERT_SHADER,
ShaderStage::Vertex,
"main_vert"
)?;
let frag_id = shaders.add_shader(
FRAG_SHADER,
ShaderStage::Fragment,
"main_frag"
)?;
// Explicit pipeline configuration
let pipeline_id = shaders.build_pipeline(
PipelineBuilder::new()
.vertex_shader(shaders.get_shader(vert_id)?.handle(), "main")
.fragment_shader(shaders.get_shader(frag_id)?.handle(), "main")
.color_attachment_formats(vec![vk::Format::R8G8B8A8_UNORM])
.viewport(0.0, 0.0, 800.0, 600.0)
.scissor(0, 0, 800, 600),
"main_pipeline"
)?;
// Explicit frame management
let frame = runtime.begin_frame()?;
let pipeline = shaders.get_pipeline(pipeline_id)?;
// Record commands
let cmd = frame.command_buffer;
unsafe {
let device = runtime.device().handle();
device.cmd_bind_pipeline(
cmd,
vk::PipelineBindPoint::GRAPHICS,
pipeline.handle()
);
device.cmd_draw(cmd, 3, 1, 0, 0);
}
runtime.end_frame(&Default::default())?;
Ok(())
}
```
### What Changed?
| **Setup** | 1 line | 5-10 lines |
| **Shader compilation** | Implicit | Explicit with IDs |
| **Pipeline creation** | 1 function call | Builder pattern |
| **Frame management** | Closure-based | Manual begin/end |
| **Resource management** | Automatic | Type-safe IDs |
| **Configuration** | Defaults | Explicit |
### Benefits Gained
- ✅ **Explicit control** over all settings
- ✅ **Type-safe IDs** prevent resource confusion
- ✅ **Better error handling** with detailed errors
- ✅ **Shader hot-reloading** via ShaderManager
- ✅ **Custom pipeline configs** via PipelineBuilder
- ✅ **Production patterns** ready for real apps
### What Stays the Same
- ✅ **Performance** - Still zero-cost
- ✅ **Safety** - Still memory safe
- ✅ **Shaders** - Same GLSL code
- ✅ **Access to CORE** - Can still drop down
---
## EX → CORE Migration
### Why Migrate?
You're building an engine and need:
- Custom memory allocation strategies
- Non-standard pipeline configurations
- Direct Vulkan control
- Integration with existing Vulkan code
### Step-by-Step Migration
#### Before (EX Tier)
```rust
use shdrlib::ex::*;
let mut runtime = RuntimeManager::new(RuntimeConfig::default())?;
let mut shaders = ShaderManager::new(runtime.device())?;
let shader_id = shaders.add_shader(SHADER, ShaderStage::Vertex, "shader")?;
let pipeline_id = shaders.build_pipeline(
PipelineBuilder::new()
.vertex_shader(shaders.get_shader(shader_id)?.handle(), "main")
.color_attachment_formats(vec![vk::Format::R8G8B8A8_UNORM]),
"pipeline"
)?;
```
#### After (CORE Tier)
```rust
use shdrlib::core::*;
use ash::vk;
// Manual instance creation
let instance = Instance::new(InstanceCreateInfo {
app_name: "My Engine".to_string(),
api_version: vk::API_VERSION_1_3,
enable_validation: false,
..Default::default()
})?;
// Manual device creation
let physical_device = /* select physical device */;
let device = Device::new(&instance, physical_device, &DeviceCreateInfo {
queue_infos: vec![/* queue configs */],
enabled_features: /* features */,
..Default::default()
})?;
// Manual shader compilation
let shader = Shader::from_glsl(&device, SHADER, ShaderStage::Vertex)?;
// Manual pipeline creation (100+ lines of configuration)
let pipeline = Pipeline::new(&device, &PipelineCreateInfo {
// ... extensive configuration ...
})?;
```
### What Changed?
| **Setup** | RuntimeManager | Manual instance/device |
| **Shader compilation** | ShaderManager | Direct Shader API |
| **Pipeline creation** | PipelineBuilder | Raw Vulkan structs |
| **Resource IDs** | Type-safe | Raw Vulkan handles |
| **Lifetime safety** | Automatic | Manual |
| **Error handling** | Typed errors | Direct Vulkan errors |
### Responsibilities Gained
- ⚠️ **Manual drop order** - You manage lifetimes
- ⚠️ **Queue selection** - You pick queue families
- ⚠️ **Memory allocation** - You allocate/bind memory
- ⚠️ **Synchronization** - You manage fences/semaphores
- ⚠️ **Validation** - You interpret Vulkan errors
### Benefits Gained
- ✅ **Complete control** over every Vulkan call
- ✅ **Custom allocators** possible
- ✅ **No abstractions** - pure Vulkan
- ✅ **Direct ash access** - integrate with any Vulkan code
- ✅ **Framework building** - build your own EX tier
---
## CORE → EX Migration
### Why Migrate?
You've built a working CORE tier system but want:
- Automatic lifetime management
- Less boilerplate code
- Type-safe resource tracking
- Easier maintenance
### Step-by-Step Migration
#### Before (CORE Tier)
```rust
use shdrlib::core::*;
struct MyRenderer {
// Manual drop order management
pipeline: Pipeline,
shader: Shader,
device: Device,
instance: Instance,
}
impl MyRenderer {
fn new() -> Result<Self, Box<dyn std::error::Error>> {
let instance = Instance::new(/* ... */)?;
let device = Device::new(&instance, /* ... */)?;
let shader = Shader::from_glsl(&device, /* ... */)?;
let pipeline = Pipeline::new(&device, /* ... */)?;
Ok(Self { pipeline, shader, device, instance })
}
}
```
#### After (EX Tier)
```rust
use shdrlib::ex::*;
struct MyRenderer {
// Automatic drop order - no manual management needed!
runtime: RuntimeManager,
shaders: ShaderManager,
}
impl MyRenderer {
fn new() -> Result<Self, Box<dyn std::error::Error>> {
let runtime = RuntimeManager::new(RuntimeConfig::default())?;
let mut shaders = ShaderManager::new(runtime.device())?;
// Resources managed by IDs
let shader_id = shaders.add_shader(/* ... */)?;
let pipeline_id = shaders.build_pipeline(/* ... */)?;
Ok(Self { runtime, shaders })
}
}
```
### Benefits Gained
- ✅ **Automatic safety** - Rust ownership ensures correct order
- ✅ **4x-8x less code** - Managers handle boilerplate
- ✅ **Type-safe IDs** - Can't confuse resources
- ✅ **Helper utilities** - Common patterns in one line
- ✅ **Same performance** - Still zero-cost
---
## Mixing Tiers
### The Power of Progressive Disclosure
**You don't have to pick just one tier!** Use the right tool for each job.
### Pattern 1: EZ for Main Rendering, CORE for Custom Resources
```rust
// Start with EZ for ease of use
let mut renderer = EzRenderer::new()?;
let pipeline = renderer.quick_pipeline(VERT, FRAG)?;
// Drop to CORE for custom buffer with specific memory properties
let device = renderer.runtime().device();
let custom_buffer = shdrlib::core::Buffer::new(
device,
&vk::BufferCreateInfo {
size: 1024,
usage: vk::BufferUsageFlags::STORAGE_BUFFER,
// Custom configuration...
..Default::default()
}
)?;
// Back to EZ for rendering
renderer.render_frame(|frame| {
frame.bind_pipeline(pipeline)?;
// Use custom_buffer in rendering...
Ok(())
})?;
```
### Pattern 2: EX for Main App, CORE for Specific Optimization
```rust
// EX tier for main application
let mut runtime = RuntimeManager::new(RuntimeConfig::default())?;
let mut shaders = ShaderManager::new(runtime.device())?;
// Drop to CORE for custom memory pool
let device = runtime.device();
let memory_pool = create_custom_memory_pool(device)?;
// Use EX helpers with CORE allocations
let buffer = shdrlib::ex::helpers::create_buffer_with_memory(
device,
&buffer_info,
memory_pool // Custom memory
)?;
```
### Pattern 3: CORE Engine, EZ for Debug Visualization
```rust
// Your engine uses CORE tier
struct MyEngine {
device: Device,
// ... other CORE components
}
impl MyEngine {
fn debug_visualize(&self) {
// Use EZ tier for quick debug rendering
let mut debug_renderer = EzRenderer::from_device(self.device.clone())?;
let pipeline = debug_renderer.quick_pipeline(DEBUG_VERT, DEBUG_FRAG)?;
debug_renderer.render_frame(|frame| {
// Quick debug visualization
frame.bind_pipeline(pipeline)?;
frame.draw(debug_vertex_count, 1, 0, 0);
Ok(())
})?;
}
}
```
---
## When to Drop Down
### From EZ to EX
Drop down when you need:
- ✅ Explicit pipeline configuration
- ✅ Custom descriptor layouts
- ✅ Shader hot-reloading
- ✅ Multi-pass rendering
- ✅ Type-safe resource management
**Signs it's time:**
- You're fighting EZ tier limitations
- Need fine-grained control over resources
- Building production application
### From EX to CORE
Drop down when you need:
- ✅ Custom memory allocation strategies
- ✅ Non-standard pipeline configurations
- ✅ Direct Vulkan handle access
- ✅ Integration with existing Vulkan code
- ✅ Building your own framework
**Signs it's time:**
- You're working around EX tier abstractions
- Need patterns not supported by EX
- Building an engine/framework
---
## When to Move Up
### From CORE to EX
Move up when:
- ✅ Tired of managing lifetimes manually
- ✅ Want to reduce boilerplate
- ✅ Need better error messages
- ✅ Building application, not engine
**Signs it's time:**
- Spending too much time on boilerplate
- Hit by lifetime bugs
- Want team members to understand code easier
### From EX to EZ
Move up when:
- ✅ Prototyping new ideas
- ✅ Teaching graphics concepts
- ✅ Building simple tools
- ✅ Want maximum simplicity
**Signs it's time:**
- Don't need explicit configuration
- Want fastest possible iteration
- Focus on shaders, not setup
---
## Common Patterns
### Pattern: Gradual Migration
Don't migrate all at once - do it incrementally:
```rust
// Week 1: Start with EZ
let renderer = EzRenderer::new()?;
// Week 2: Access EX components when needed
let shader_manager = renderer.shader_manager_mut();
let custom_pipeline = shader_manager.build_pipeline(/* custom config */)?;
// Week 3: Full EX migration when ready
let runtime = RuntimeManager::new(RuntimeConfig::default())?;
let shaders = ShaderManager::new(runtime.device())?;
```
### Pattern: Tier-Specific Modules
Organize code by tier:
```rust
mod core_engine {
// CORE tier: Your engine foundation
use shdrlib::core::*;
}
mod ex_renderer {
// EX tier: Application rendering
use shdrlib::ex::*;
}
mod ez_debug {
// EZ tier: Debug visualization
use shdrlib::ez::*;
}
```
### Pattern: Abstraction Escape Hatches
Always provide access to lower tiers:
```rust
struct MyRenderer {
ez_renderer: EzRenderer,
}
impl MyRenderer {
// Normal API uses EZ
pub fn render(&mut self) { /* EZ API */ }
// Advanced users can access EX
pub fn shader_manager(&mut self) -> &mut ShaderManager {
self.ez_renderer.shader_manager_mut()
}
// Experts can access CORE
pub fn device(&self) -> &Device {
self.ez_renderer.runtime().device()
}
}
```
---
## Summary
**Progressive Disclosure = Flexibility**
- ✅ **Start simple** with EZ tier
- ✅ **Add control** with EX tier
- ✅ **Go deep** with CORE tier
- ✅ **Mix freely** - use right tool for each job
- ✅ **Migrate gradually** - no need to rewrite everything
**Key Takeaway:** shdrlib tiers are **not mutually exclusive**. Use them together!
**Next Steps:**
- Try mixing tiers in your project
- Read tier-specific guides for details
- Check examples for mixed-tier patterns
---
**Last Updated:** October 30, 2025