# Integrating Charton with Bevy (Decoupled Architecture)
When attempting to embed charts into a powerful game engine like Bevy, you often run into the brutal reality of the Rust ecosystem: WGPU Version Hell. If the `wgpu` version required by Bevy mismatches the one used by your charting library, the compiler's type system will flat-out reject your build.
Charton's `WgpuRenderer` provides an elegant Decoupled Rendering solution to bypass this entirely. It spins up its own independent GPU context in the background, renders the chart off-screen, reads the pixels back to CPU memory (`Vec<u8>`), and then uploads them to Bevy as a standard texture. This design allows you to completely ignore dependency conflicts between the two ecosystems.
Here is the complete integration logic and a step-by-step breakdown of the source code.
## Cargo Dependencies
In your host Bevy project, you only need the core `bevy` library and `charton` with the `wgpu` and `png` feature enabled. Because the rendering is completely decoupled, you do not need to manually import `wgpu` or `pollster` here.
```toml
[package]
name = "bevy"
version = "0.1.0"
edition = "2024"
[dependencies]
charton = { version = "0.5", features = ["wgpu", "png"] }
bevy = "0.18"
```
## Step-by-Step Architecture Breakdown
The core of this integration is "separation of concerns." We break the entire lifecycle down into four clear steps:
### Step 1: Initialize the Host App (App Config)
Configure the base Bevy plugins. Because modern Bevy strongly types `WindowResolution` to avoid floating-point precision issues, we must pass an explicit integer tuple `(800, 600)`. We then mount our core rendering logic into the `Startup` schedule, ensuring it only runs once.
### Step 2: Headless Chart Generation (Phase 1: Charton Domain)
The `WgpuRenderer` initializes silently in the background. It doesn't rely on any visible window; instead, it allocates an off-screen canvas in VRAM, draws the chart primitives, and overlays text via the CPU. Finally, calling `.render()` yields a pure, raw `Vec<u8>` array of RGBA pixels.
### Step 3: The Asset Handshake (Phase 2: Memory Bridge)
Once we have the pixels on the CPU side, we wrap them in a Bevy Image struct. We explicitly define the texture format as `TextureFormat::Rgba8Unorm` to match Charton's output. By calling `images.add(chart_image)`, we register this raw memory block into Bevy's asset pipeline and receive a lightweight pointer: `Handle<Image>`.
### Step 4: Modern Component Spawning (Phase 3: Bevy Domain)
Thanks to modern Bevy's Required Components feature, we can say goodbye to bloated `Bundles`. By simply spawning the core `Camera2d` and `Sprite::from_image(texture_handle)` components, Bevy's underlying engine automatically detects dependencies and injects `Transform`, `Visibility`, and other required components to perfectly center the chart on screen.
## The Complete Implementation
Here is the complete, standalone rendering example that compiles and runs flawlessly in a Bevy 0.18 environment:
```rust
use bevy::prelude::*;
use bevy::asset::RenderAssetUsages;
use bevy::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
use bevy::core_pipeline::tonemapping::Tonemapping;
use charton::prelude::*;
use charton::render::WgpuRenderer;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Charton Bevy 0.18 Decoupled Example".into(),
resolution: (800, 600).into(), // Strictly requires a (u32, u32) integer tuple
..default()
}),
..default()
}))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
) {
// 1. Initialize 2D Camera
commands.spawn((
Camera2d,
Tonemapping::None,
));
// ==========================================
// Phase 1: Chart Rendering (Headless & Decoupled)
// ==========================================
// Spin up an independent GPU instance for off-screen rendering
let mut charton_renderer = WgpuRenderer::new();
let ds = load_dataset("mtcars").unwrap();
let chart = Chart::build(&ds)
.unwrap()
.mark_point()
.unwrap()
.encode((
alt::x("wt"),
alt::y("mpg"),
alt::color("gear").with_scale(Scale::Discrete),
))
.unwrap()
.configure_theme(|t| t.with_label_size(20.0).with_tick_label_size(18.0))
.with_size(800, 600);
// The perfect black box: compress complex chart rendering into a pure byte array
let pixels = charton_renderer.render(&chart, 800, 600, 1.0).unwrap();
// ==========================================
// Phase 2: Bevy 0.18 Integration
// ==========================================
let size = Extent3d {
width: 800,
height: 600,
depth_or_array_layers: 1,
};
// Construct an Image asset recognized by Bevy
let chart_image = Image::new(
size,
TextureDimension::D2,
pixels,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
);
// Store the image into Bevy's asset pipeline and acquire the unique handle
let texture_handle = images.add(chart_image);
// 2. Render the Sprite (SpriteBundle is deprecated; Required Components handles the rest)
commands.spawn(Sprite::from_image(texture_handle));
}
```
## Architectural Takeaways
**Performance Strategy: The One-Shot Rule**
Placing `WgpuRenderer` inside Bevy's `Startup` system is critical to this architecture. Memory readback (transferring data from GPU to CPU, then back to GPU) is an incredibly expensive operation. If you drop this into the `Update` schedule—which runs 60 times a second—you will block the main thread and tank your framerate. Treating the chart as a static asset, generating it once, and caching it is the best practice for maintaining game-level performance.
Through this approach, it doesn't matter how fast the Bevy community iterates its rendering pipeline, nor does it matter how many major versions separate the `wgpu` dependencies. As long as `Vec<u8>` remains a universal memory bridge, Charton can seamlessly integrate into any complex Bevy project with minimal effort and maximum stability.