bedrock-render is a tile renderer for Minecraft Bedrock worlds. It depends on
bedrock-world for Bedrock LevelDB, NBT, chunk, subchunk, height, and biome
queries. Rendering, palettes, tile planning, image encoding, cancellation,
threading, previews, and benchmarks live in this separate crate so the world
parser remains lightweight.
This repository is designed to be checked out independently. The current MSRV is
Rust 1.88, and the default feature set includes async, webp, and gpu.
CPU-only consumers can build with --no-default-features and opt into the image
formats they need.
- Rendering is always an X/Z plane.
Yis only a sampling parameter. - Tiles are controlled by
RenderLayout. The default layout is16x16chunks per tile, one block per pixel, producing a256x256image. Largerblocks_per_pixelvalues keep the same world area but render lower-detail images for fast large-world previews. - Arbitrary rectangular chunk regions are supported through
ChunkRegion. - Web-map paths are deterministic and cache-safe:
<world>/<signature>/r<renderer>-p<palette>/<dimension>/<mode>/<layout>/<tile_x>/<tile_z>.<ext>. - Tile batches use bounded worker threads.
Autoresolves from the selected execution profile:Exportuses host logical CPUs,Interactivecaps work to a smaller foreground-safe pool. Explicit thread counts support1..=512. - Web-map export uses a global chunk-bake queue, per-wave dynamic memory budget,
parallel tile compose/encode, and a bounded MPMC writer queue so CPU workers
are not serialized behind
fs::write. - Interactive frontends should create one
MapRenderSessionper opened world. A session holds the renderer, tile cache, and diagnostics context so panning and zooming do not reopen the world or rebuild cache state for every batch. RenderMemoryBudget::Autouses a bounded cache budget for chunk bakes and export waves.FixedBytesandDisabledare available for offline tooling.- Long operations support explicit cancellation and progress callbacks.
RenderMode::Biome { y }: biome color map sampled at the requested Y layer.RenderMode::RawBiomeLayer { y }: diagnostic biome-id color map.RenderMode::LayerBlocks { y }: fixed block layer map at world Y.RenderMode::SurfaceBlocks: main top-down terrain map. Each X/Z column starts from the Bedrock height map, scans down to the highest renderable block, applies biome tint, and blends transparent water over the solid block below.RenderMode::HeightMap: height gradient from Bedrock Data2D/Data3D records.RenderMode::CaveSlice { y }: fixed Y cave diagnostic map for air/solid/water/lava.
SurfaceBlocks follows the same core data flow used by BedrockMap's terrain
bake path: use chunk.get_height(x,z), scan downward in that column until a
renderable block is found, then bake terrain/biome/height data. The default
terrain preview applies lightweight height-normal shading so slopes remain visible;
the height map remains a separate diagnostic image. Missing chunk or
missing height records are treated as absent terrain, not as gray map pixels.
Fixed Y rendering remains available through LayerBlocks { y }.
Unknown blocks are rendered as opaque purple diagnostic pixels. Missing chunks,
missing height maps, and empty terrain are transparent and counted separately in
RenderDiagnostics.
use std::sync::Arc;
use bedrock_render::{
ChunkRegion, ImageFormat, MapRenderer, RenderExecutionProfile, RenderLayout,
RenderMemoryBudget, RenderMode, RenderOptions, RenderPalette, RenderThreadingOptions,
};
let renderer = MapRenderer::new(Arc::new(world), RenderPalette::default());
let region = ChunkRegion::new(dimension, -32, -32, 31, 31);
let layout = RenderLayout {
chunks_per_tile: 16,
blocks_per_pixel: 1,
pixels_per_block: 1,
};
let tiles = renderer.render_region_tiles_blocking(
region,
RenderMode::HeightMap,
layout,
RenderOptions {
format: ImageFormat::WebP,
threading: RenderThreadingOptions::Auto,
execution_profile: RenderExecutionProfile::Export,
memory_budget: RenderMemoryBudget::Auto,
..RenderOptions::default()
},
)?;MapRenderSession is the high-performance path for GPUI and other interactive
map viewers. It streams tile events as work completes, reuses the world handle
and tile cache, supports cancellation, and can cull missing chunks with the new
bedrock-world render-index APIs before baking.
use std::sync::Arc;
use bedrock_render::{
ChunkRegion, ImageFormat, MapRenderSession, MapRenderSessionConfig,
MapRenderer, RenderCachePolicy, RenderCancelFlag, RenderExecutionProfile,
RenderLayout, RenderMode, RenderOptions, RenderPalette, RenderTilePriority,
TileStreamEvent,
};
let renderer = MapRenderer::new(Arc::new(world), RenderPalette::default());
let session = MapRenderSession::new(
renderer,
MapRenderSessionConfig {
cache_root: "target/bedrock-render-cache".into(),
world_id: "viewer".into(),
world_signature: "leveldb-manifest-and-leveldat-signature".into(),
cull_missing_chunks: true,
..MapRenderSessionConfig::default()
},
);
let layout = RenderLayout::default();
let region = ChunkRegion::new(dimension, -32, -32, 31, 31);
let planned_tiles = MapRenderer::plan_region_tiles(region, RenderMode::SurfaceBlocks, layout)?;
let cancel = RenderCancelFlag::new();
session.render_web_tiles_streaming_blocking(
&planned_tiles,
RenderOptions {
format: ImageFormat::WebP,
execution_profile: RenderExecutionProfile::Interactive,
cache_policy: RenderCachePolicy::Use,
cancel: Some(cancel.clone()),
priority: RenderTilePriority::DistanceFrom { tile_x: 0, tile_z: 0 },
..RenderOptions::default()
},
|event| {
match event {
TileStreamEvent::Cached { planned, encoded } => {
// Decode or hand encoded bytes to the UI image cache.
}
TileStreamEvent::Rendered { planned, tile } => {
// Present tile.rgba immediately and keep tile.encoded for cache.
}
TileStreamEvent::Failed { planned, error } => {
eprintln!("tile failed: {error}");
}
TileStreamEvent::Progress(progress) => {
eprintln!("tiles {}/{}", progress.completed_tiles, progress.total_tiles);
}
TileStreamEvent::Complete { diagnostics, stats } => {
eprintln!("cache hits={} gpu={:?}", stats.cache_hits, stats.resolved_backend);
}
}
Ok(())
},
)?;Legacy callers that used render_web_tiles_blocking usually waited for an
entire batch before updating UI:
renderer.render_web_tiles_blocking(&planned_tiles, options, |planned, tile| {
write_tile(planned, tile)?;
Ok(())
})?;Prefer a long-lived session and stream tile events into the frontend:
let session = MapRenderSession::new(renderer, MapRenderSessionConfig::default());
let cancel = RenderCancelFlag::new();
session.render_web_tiles_streaming_blocking(
&planned_tiles,
RenderOptions {
cancel: Some(cancel),
cache_policy: RenderCachePolicy::Use,
..options
},
|event| {
enqueue_tile_event(event)?;
Ok(())
},
)?;The old batch APIs remain available for export tools, but new interactive code should treat the session streaming API as the primary entry point.
The render_streaming_session example demonstrates the same event flow against
a local world:
cargo run --example render_streaming_session -- <world_path>
The preview example generates six atlas PNGs plus web-map tile folders:
cargo run --example render_preview --features png
Optional arguments:
render_preview <world_path> <output_dir> <center_tile_x> <center_tile_z> \
<viewport_tiles> <layer_y> <cave_y> <chunks_per_tile>
Preview output layout:
<output_dir>/
biome-viewport.png
raw-biome-viewport.png
layer-y64-viewport.png
surface-viewport.png
heightmap-viewport.png
cave-y32-viewport.png
web-tiles/sample/signature/r2-p1/overworld/heightmap/16c-1bpp/21/12.png
render_web_map exports WebP tiles plus a self-contained static HTML viewer.
It is intended for validating the renderer and for generating shareable web-map
artifacts without requiring GPUI or a CDN.
When --region is omitted, the example discovers and renders the loaded chunk
bounds for each selected dimension.
cargo run --example render_web_map -- \
--world ../bedrock-world/tests/fixtures/sample-bedrock-world \
--out target/bedrock-web-map \
--dimensions overworld,nether,end \
--mode surface,heightmap,biome,layer \
--chunks-per-tile 16 \
--chunks-per-region 32 \
--blocks-per-pixel 4 \
--threads auto \
--profile export \
--memory-budget auto \
--pipeline-depth 256 \
--tile-batch-size auto \
--writer-threads 2 \
--write-queue-capacity 256 \
--stats \
--force
Output layout:
target/bedrock-web-map/
viewer.html
map-layout.json
map-data.js
tiles/<dimension>/<mode>/<layout>/<tile_z>/<tile_x>.webp
map-layout.json is the automatically generated map layout for tools and
external frontends. map-data.js embeds only the minimal layout constants and
dynamically provides tileBounds(), tileId(), and tilePath(). Tile locations
are derived from the fixed
tiles/<dimension>/<mode>/<layout>/<tile_z>/<tile_x>.webp rule, so the HTML
viewer can load the data as a normal script from file:// without using
fetch() or hitting browser CORS restrictions. Export writes viewer.html,
map-layout.json, and map-data.js before opening and scanning the world; as
dimension bounds are discovered and each mode finishes, map-layout.json and
map-data.js are refreshed with updated layout and stats. The viewer
periodically reloads map-data.js and retries visible tiles, so images appear
incrementally while export is still running.
It supports dimension switching, mode switching, drag panning, wheel zoom, tile
coordinates, and load-error reporting. For large
worlds, use --region for a bounded export or increase --blocks-per-pixel
before exporting the whole loaded chunk bounds. --profile export --threads auto
is the recommended default for 8-core/16-thread machines. Use
--profile interactive for UI-like previews, and use a fixed thread value up to
512 only for offline exports where the storage device can keep up. --stats
prints planned tiles, unique chunks, baked chunks, bake/encode/write time, cache
hits, and peak cache bytes so CPU under-utilization can be traced to I/O,
encoding, or chunk baking. Missing chunks, missing
subchunks, and unloaded fixed-Y areas are transparent. Opaque purple pixels
indicate an actual block name that was read from the world but had no palette
mapping.
The default palette is embedded in the crate and in compiled binaries. It ships with two auditable JSON sources:
data/colors/bedrock-block-color.json
data/colors/bedrock-biome-color.json
RenderPalette::default() builds from the embedded JSON sources, so normal
rendering does not require external palette files. The JSON files remain
available through RenderPalette::builtin_block_color_json() and
RenderPalette::builtin_biome_color_json() for auditing and tooling.
The built-in data is maintained inside this project for Bedrock rendering; the
loader also understands legacy object-map palette JSON so applications can
import their own licensed color data without depending on another renderer.
For projects that have their own licensed color data, RenderPalette also
supports additional user-provided JSON:
--palette-json target/bedrock-block-color.json
--palette-json target/bedrock-biome-color.json
JSON is an import/override format. Tintable blocks use mask colors in the block
source and receive their final render color from biome tint data. The JSON
loader accepts combined objects
with schema_version / sources / blocks / defaults / biomes, object maps such as
{"minecraft:stone":"#7d7d7d"}, arrays with name / id plus color, and
legacy multi-texture object-map shapes for local user-provided reference data.
Palette source maintenance commands:
cargo run --example palette_tool -- audit --check
cargo run --example palette_tool -- generate-clean-room --check
cargo run --example palette_tool -- normalize --check
Source policy and public references are documented in docs/PALETTE_SOURCES.md.
Latest local reference-palette smoke run:
cargo run --example render_web_map -- \
--world ../bedrock-world/tests/fixtures/sample-bedrock-world \
--out target/bedrock-web-map-ref-palette \
--region 0,0,15,15 \
--mode surface,heightmap,biome,layer \
--y 64 \
--palette-json target/bedrock-block-color.json \
--palette-json target/bedrock-biome-color.json \
--force
loaded palette JSON target\bedrock-block-color.json: block_colors=1211 biome_colors=0 skipped=0
loaded palette JSON target\bedrock-biome-color.json: block_colors=0 biome_colors=88 skipped=0
overworld surface tiles=1 missing=0 transparent=0 unknown=0
overworld layer-y64 tiles=1 missing=0 transparent=12544 unknown=0
Latest local debug preview run:
cargo run --example render_preview --features png
surface diagnostics: missing_chunks=0 missing_heightmaps=0 unknown_blocks=0 fallback_pixels=0
preview output: target/bedrock-render-preview
Latest local WebP web-map smoke run:
cargo run --example render_web_map -- \
--world ../bedrock-world/tests/fixtures/sample-bedrock-world \
--out target/bedrock-web-map \
--region 0,0,15,15 \
--mode surface,heightmap,biome,layer \
--threads auto \
--tile-batch-size auto \
--writer-threads 2 \
--write-queue-capacity 64 \
--force
Generated viewer.html, map-layout.json, map-data.js, and WebP tiles for overworld/nether/end.
LayerBlocks transparent pixels are expected for unloaded fixed-Y areas.
These images are generated by the preview tool with the current default palette. The terrain view references BedrockMap-style top-down map behavior while remaining part of this standalone public renderer.
Biome colors sampled on an X/Z plane at the configured Y layer.
Diagnostic biome-id map. Unknown or sparse ids are intentionally visible.
Fixed world-Y block layer rendered as an X/Z plane. This is not a side section.
Primary top-down terrain map. Each X/Z column uses the chunk height map, scans
down to the highest renderable block, applies biome tint, and blends transparent
water with the block below. Lightweight height-normal shading is enabled by default
so terrain does not look completely flat. SurfaceRenderOptions::block_boundaries
adds a subtle 2D per-block outline and height-contact shadow so cliffs, paths,
and same-color blocks remain readable without switching to a 2.5D view. Use
HeightMap for elevation analysis.
Height gradient derived from Bedrock Data2D/Data3D height records.
Fixed Y cave diagnostic map for air, solid blocks, water, and lava.
Biome,RawBiomeLayer,LayerBlocks,HeightMap, andCaveSliceprefetch only the chunk records needed by the tile.- Height data is fetched once per chunk through
bedrock-worldand cached as a compact16x16height array inside the tile context. SurfaceBlocksdoes not scan missing-height columns from world top to bottom. Missing chunk/height data is counted in diagnostics and emitted as absent terrain.- Subchunk access uses
SubChunk::block_state_at(local_x, local_y, local_z)so renderer code does not duplicate palette index math. - Web export uses a region-first bake pipeline.
--chunks-per-regioncontrols the bake cache unit;32is the default for large exports, while16is better for small bounded validation runs or lower interactive latency. - Web export does not keep every baked region for the full map resident at once.
The renderer splits baking, composition, and writing into waves bounded by
--memory-budget.--pipeline-depthand--write-queue-capacityonly bound encoded tiles waiting for disk writes. SurfaceBlocksuses chunk bake: each chunk is reduced to a16x16terrain image first, then region planes and WebP tiles are assembled from baked chunk pixels.- The static viewer uses
RenderLayoutauto-scaling. Small worlds default to full detail; larger worlds can use 2/4/8 blocks per pixel without changing the visible tile coverage. - Cache keys include world path hash, world file signature, renderer version, palette version, dimension, render mode, Y layer, and layout. All-transparent stale cached tiles are rejected by the UI and regenerated.
ImageFormat::Rgbais the lowest-latency UI path. WebP/PNG are intended for cache/export/preview paths.RenderOptions::gpucontrols GPU compose scheduling.max_in_flight=0,batch_size=0,batch_pixels=0,submit_workers=0,readback_workers=0,buffer_pool_bytes=0, andstaging_pool_bytes=0use profile-aware defaults; export workloads allow more in-flight GPU work than interactive workloads.RenderOptions::cpucontrols bounded CPU queue depth and chunk batch sizing, whileRenderOptions::prioritylets interactive sessions render the current viewport center before distant tiles.- GPU compose runs with bounded concurrent in-flight jobs on one
wgpudevice. Failed GPU tiles fall back to CPU and reportgpu_fallback_reason; stats also expose GPU batch tiles, submit/readback workers, and buffer/staging pool reuse. - The static web-map example writes WebP tiles under
tiles/<dimension>/<mode>/<layout>/<tile_z>/<tile_x>.webp; it does not leave gaps between tiles and uses transparent pixels for absent world data. - Viewer integrations can start from the world's
level.datspawn point, support dimension/custom-dimension switching, and store WebP web-map tiles under an application cache directory.
cargo fmt --all -- --check
cargo clippy --all-features --all-targets -- -D warnings
cargo test --all-features
cargo test --no-default-features
cargo doc --no-deps --all-features
cargo rustdoc --lib --all-features -- -D missing_docs
cargo bench --bench render --all-features
The default benchmark suite measures tile rendering, chunk baking, and small batch behavior. Full web-map export benchmarks are opt-in:
$env:BEDROCK_RENDER_FULL_BENCH='1'
cargo bench --bench render --all-features
Remove-Item Env:\BEDROCK_RENDER_FULL_BENCH
More details are in docs/API.md, docs/TESTING.md, and docs/BENCHMARKS.md.
- Surface rendering is top-down and BedrockMap-style. Web-map export now uses a global chunk-bake queue and bounded GPU compose queue, but cross-process persistent chunk-bake reuse is still a future optimization.
- V1 does not implement entity markers or labels.





