Header menu logo Mibo

Rendering in Mibo

Mibo separates what you draw from how you draw it. Your view function decides what should appear on screen. An IRenderer handles the GPU work. This split lets you write pure view logic that is easy to test while keeping rendering details where they belong.

There's no dispatch?

Traditional MVU frameworks let views emit messages through event handlers. Mibo does not. This is because unlike usual MVU setups, what you render on screen does not depend on user input or state changes. Instead, the update function and subscriptions handle those concerns.

// mibo usually requires a function like this
let view ctx model buffer =
    buffer.Draw(draw {
        // Your drawing code here
    })
      .Submit()

You can test this function by verifying the commands it produces. No graphics device required.

The IRenderer Contract

IRenderer<'Model> is the seam between your code and the GPU:

type IRenderer<'Model> =
    abstract member Draw: GameContext * 'Model * GameTime -> unit

The runtime calls each registered renderer once per frame, passing the current model. How the renderer uses that model is up to its implementation. Some renderers accept a view function and translate model state into commands. Others access the model directly and issue draw calls immediately.

Register renderers when building your program:

Program.mkProgram init update
|> Program.withRenderer (fun game -> myRenderer :> IRenderer<Model>)

Multiple Renderers

You can register multiple renderers. They execute in the order you added them.

Each renderer receives the same model instance but can interpret it differently. A common setup uses two renderers: one for a 3D world and another for 2D UI overlays.

Program.mkProgram init update
|> Program.withRenderer (fun game ->
    PipelineRenderer.create config worldView game :> IRenderer<Model>)
|> Program.withRenderer (fun game ->
    Batch2DRenderer.create uiView game :> IRenderer<Model>)

Renderers are independent unless you explicitly share state through the model. The first renderer can clear the screen, render a 3D scene, and leave the depth buffer. The second can then draw 2D elements on top. Or they can each clear and manage their own render targets. The runtime does not impose a specific policy.

RenderBuffer Pattern

Built-in renderers like Batch2DRenderer and PipelineRenderer use a buffer-based approach:

  1. The renderer creates a RenderBuffer specialized to its command types
  2. Each frame it clears the buffer and calls your view function to fill it
  3. It sorts and batches commands for efficient GPU submission
  4. It executes the commands

The buffer type determines what commands you can submit. A 2D renderer uses RenderBuffer<int<RenderLayer>, RenderCmd2D>. A 3D pipeline renderer uses RenderBuffer<unit, RenderCommand>. The renderer's factory function captures your view and determines the buffer type you work with.

This pattern works well when you need sorting, batching, or complex scene management. The renderer handles those details. Your view stays focused on what to draw.

Custom Renderers

If the built in renderers do not do the work you need, or you feel constrained and need direct GPU control, you may implement IRenderer yourself. You decide whether to use a buffer or draw immediately.

Buffer approach (when you need sorting or batching):

type BufferedRenderer<'Model>(game: Game, view) =
    let buffer = RenderBuffer<unit, MyCommand>()

    interface IRenderer<'Model> with
        member _.Draw(ctx, model, gameTime) =
            buffer.Clear()
            view ctx model buffer
            // Sort, batch, and execute commands

Immediate approach (when you want direct control):

type ImmediateRenderer(game: Game) =
    interface IRenderer<Model> with
        member _.Draw(ctx, model, gameTime) =
            // Direct GPU access
            ctx.GraphicsDevice.Clear(Color.CornflowerBlue)
            // Custom draw logic based on model

Both approaches are valid. Use what fits your rendering needs.

Built-in Offerings

Mibo provides renderers that handle common scenarios so you can focus on game logic rather than rendering boilerplate:

These are available if you want them. You are not required to use them. If they do not fit your needs, implement IRenderer and take full control.

See the dedicated pages for details on specific renderers and features:

Note: The older Batch3DRenderer is deprecated. Use PipelineRenderer for new 3D projects.

val view: ctx: 'a -> model: 'b -> buffer: 'c -> 'd
val ctx: 'a
val model: 'b
val buffer: 'c
type IRenderer<'Model> = abstract Draw: 'b * 'Model * 'c -> unit
'Model
type unit = Unit
Multiple items
type BufferedRenderer<'Model> = interface IRenderer<'Model> new: game: 'a * view: ('b -> 'Model -> 'c -> unit) -> BufferedRenderer<'Model>

--------------------
new: game: 'a * view: ('b -> 'Model -> 'c -> unit) -> BufferedRenderer<'Model>
val game: 'a
val view: ('a -> 'Model -> 'b -> unit)
val buffer: 'a
val model: 'Model
val gameTime: 'a
Multiple items
type ImmediateRenderer = interface obj new: game: obj -> ImmediateRenderer override Draw: ctx: 'a * model: 'b * gameTime: 'c -> 'd

--------------------
new: game: obj -> ImmediateRenderer
val game: obj
val gameTime: 'c

Type something to start searching.