Hotline
A high-performance, hot-reload graphics engine.
Install / Use
/learn @polymonster/HotlineREADME
Hotline
Hotline is a graphics engine and live coding tool that allows you to edit code, shaders, and render state without restating the application.
<img src="https://raw.githubusercontent.com/polymonster/polymonster.github.io/master/images/hotline/geom3.gif" width="100%"/>Checkout a live demo!.
Features
- An easy to use cross platform graphics/compute/os api for rapid development.
- Hot reloadable, live coding environment (shaders, render configuration, code).
- Plugin architexture, flexible and extendible.
- os - Operating system API; window, input, etc.
- gfx - Concise low level graphics API.
- pmfx - High level, data driven, ergonomic graphics API.
- av - Hardware accelerated av decoding.
- imgui - Full featured dear ImGui implementation with viewports and docking.
- ecs - Entity component system using
bevy_ecs. - Client - Live reloading coding environment client application.
- Standalone Library - Use as a standalone library and integrate into your own applications.
- Examples - A focus on modern rendering examples (gpu-driven, multi-threaded, bindless).
Supported Platforms
Windows with Direct3D12 is the first fully supported platform, macOS with metal is available in a branch and should be complete soon.
Using the Client
For the time being it is recommended to use the repository from GitHub if you want to use the example plugins or standalone examples. If you just want to use the library then crates.io is suitable. There are some difficulties with publishing data and plugins which I hope to iron out in time.
Building / Fetching Data
The hotline-data repository is required to build and serve data for the examples and the example plugins, it is included as a submodule of this repository, you can clone with submodules as so:
git clone https://github.com/polymonster/hotline.git --recursive
You can add the submodule after cloning or update the submodule to keep it in-sync with the main repository as follows:
git submodule update --init --recursive
Running The Client
You can run the binary client which allows code to be reloaded through plugins. There are some plugins already provided with the repository:
// build the hotline library and data
cargo build
// build plugins
cargo build -p ecs -p ecs_examples
// or the whole workspace
cargo build --workspace
// run the client
cargo run client
Any code changes made to the plugin libs will cause a rebuild and reload to happen with the client still running. You can also edit the shaders where hlsl files make up the shader code and pmfx files allow you to specify pipeline state objects in config files. Any changes detected to pmfx shaders will be rebuilt and all modified pipelines or views will be rebuilt.
Building from Visual Studio Code
There are included tasks and launch files for vscode including configurations for the client and the examples. Launching the client from vscode in debug or release will build the core hotline lib, client, data and plugins.
Adding Plugins
Plugins are loaded by passing a directory to add_plugin_lib which contains a Cargo.toml and is a dynamic library. They can be opened interactively in the client using the File > Open from the main menu bar by selecting the Cargo.toml.
The basic Cargo.toml setup looks like this:
[package]
name = "ecs_examples"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cylib"]
[dependencies]
hotline-rs = { path = "../.." }
You can provide your own plugin implementations using the Plugin trait. A basic plugin can hook itself by implementing a few functions:
impl Plugin<gfx_platform::Device, os_platform::App> for EmptyPlugin {
fn create() -> Self {
EmptyPlugin {
}
}
fn setup(&mut self, client: Client<gfx_platform::Device, os_platform::App>)
-> Client<gfx_platform::Device, os_platform::App> {
println!("plugin setup");
client
}
fn update(&mut self, client: client::Client<gfx_platform::Device, os_platform::App>)
-> Client<gfx_platform::Device, os_platform::App> {
println!("plugin update");
client
}
fn unload(&mut self, client: Client<gfx_platform::Device, os_platform::App>)
-> Client<gfx_platform::Device, os_platform::App> {
println!("plugin unload");
client
}
fn ui(&mut self, client: Client<gfx_platform::Device, os_platform::App>)
-> Client<gfx_platform::Device, os_platform::App> {
println!("plugin ui");
client
}
}
// the macro instantiates the plugin with a c-abi so it can be loaded dynamically.
hotline_plugin![EmptyPlugin];
ecs
There is a core entity component system plugin which builds on top of bevy_ecs. It allows you to supply your own systems and build schedules dynamically. It is possible to load and find new ecs systems in different dynamic libraries. You can register and instantiate demos which are collections of setup, update and render systems.
Registering Demos
Systems can be imported dynamically from different plugins, in order to do so they need to be hooked into a function which can be located dynamically by the ecs plugin. You can implement a function called get_demos_<lib_name>, which returns a list of available demos inside a plugin named <lib_name>.
/// Register demo names
#[no_mangle]
pub fn get_demos_ecs_examples() -> Vec<String> {
demos![
"primitives",
"draw_indexed",
"draw_indexed_push_constants",
// ..
]
}
You can then provide an initialisation function named after the demo this returns a ScheduleInfo for which systems to run:
/// Init function for primitives demo
#[no_mangle]
pub fn primitives(client: &mut Client<gfx_platform::Device, os_platform::App>) -> ScheduleInfo {
// load resources we may need
client.pmfx.load(&hotline_rs::get_data_path("shaders/debug").as_str()).unwrap();
// fill out info
ScheduleInfo {
setup: systems![
"setup_primitives"
],
update: systems![
"update_cameras",
"update_main_camera_config"
],
render_graph: "mesh_debug"
}
}
The setup, update and render systems are tagged with the attribute macro #[export_update_fn], #[export_render_fn] or #[export_compute_fn].
Setup Systems
You can supply setup systems to add entities and set up a scene, these will be executed once at startup and then anytime when a dynamic code reload happens the world will be cleared and the setup systems will be re-executed. This allows changes to setup systems to appear in the live client. You can add multiple setup systems and they will be executed concurrently.
#[no_mangle]
#[export_update_fn]
pub fn setup_cube(
mut device: ResMut<DeviceRes>,
mut commands: Commands) {
let pos = Mat4f::from_translation(Vec3f::unit_y() * 10.0);
let scale = Mat4f::from_scale(splat3f(10.0));
let cube_mesh = hotline_rs::primitives::create_cube_mesh(&mut device.0);
commands.spawn((
Position(Vec3f::zero()),
Velocity(Vec3f::one()),
MeshComponent(cube_mesh.clone()),
WorldMatrix(pos * scale)
));
Ok(())
}
Update Systems
You can also supply your own update systems to animate and move your entities.
#[export_update_fn]
fn update_cameras(
app: Res<AppRes>,
main_window: Res<MainWindowRes>,
mut query: Query<(&mut Position, &mut Rotation, &mut ViewProjectionMatrix), With<Camera>>
) -> Result<(), {
let app = &app.0;
for (mut position, mut rotation, mut view_proj) in &mut query {
// ..
}
}
Render Systems
You can specify render graphs in pmfx that set up views, which get dispatched into render functions. All render systems run concurrently on the CPU, the command buffers they generate are executed in an order determined by the pmfx render graph and it's dependencies.
#[export_render_fn]
pub fn render_meshes(
pmfx: &Res<PmfxRes>,
view: &pmfx::View<gfx_platform::Device>,
cmd_buf: &mut <gfx_platform::Device as Device>::CmdBuf,
mesh_draw_query: Query<(&WorldMatrix, &MeshComponent)>) -> Result<(), hotline_rs::Error> {
let fmt = view.pass.get_format_hash();
let mesh_debug = pmfx.get_render_pipeline_for_format(&view.view_pipeline, fmt)?;
let camera = pmfx.get_camera_constants(&view.camera)?;
cmd_buf.set_render_pipeline(&mesh_debug);
cmd_buf.push_render_constants(mesh_debug, 0, 0, 16 * 3, 0, gfx::as_u8_slice(camera));
// make draw calls
for (world_matrix, mesh) in &mesh_draw_query {
cmd_buf.push_render_constants(mesh_debug, 1, 0, 16, 0, &world_matrix.0);
cmd_buf.set_index_buffer(&mesh.0.ib);
cmd_buf.set_vertex_buffer(&
