Skip to content

Instantly share code, notes, and snippets.

@jakubtomsu
Last active November 2, 2025 13:50
Show Gist options
  • Select an option

  • Save jakubtomsu/5ee4fdfee23df893f6f61b4692dcf895 to your computer and use it in GitHub Desktop.

Select an option

Save jakubtomsu/5ee4fdfee23df893f6f61b4692dcf895 to your computer and use it in GitHub Desktop.

Revisions

  1. jakubtomsu revised this gist Jul 3, 2024. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion fontstash_sokol_gfx.odin
    Original file line number Diff line number Diff line change
    @@ -24,7 +24,8 @@ RENDERER_DEFAULT_FONT_ATLAS_SIZE :: 512
    Renderer :: struct {
    font_ctx: fontstash.FontContext,
    font_indexes: [Text_Font]i32,
    font_atlas_size: IVec2,
    font_atlas_size: [2]i32,
    // Fixed size array with len, you can use a dynamic array as well
    font_instances: Array(BUDGET_FONT_QUADS, Renderer_Font_Instance),
    font_pip: sg.Pipeline,
    font_instance_buf: sg.Buffer,
  2. jakubtomsu revised this gist Jul 3, 2024. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions fontstash_sokol_gfx.odin
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,8 @@
    // This is an example usage of vendor:fontstash with sokol_gfx.
    // By Jakub Tomšů
    //
    // https://gist.github.com/jakubtomsu/5ee4fdfee23df893f6f61b4692dcf895
    //
    // This won't compile on it's own, but it contains all of the interesting parts.
    // It should be pretty obvious how to modify it to your needs, if not let me know.
    //
  3. jakubtomsu created this gist Jul 3, 2024.
    210 changes: 210 additions & 0 deletions fontstash_sokol_gfx.odin
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,210 @@
    // This is an example usage of vendor:fontstash with sokol_gfx.
    // By Jakub Tomšů
    //
    // This won't compile on it's own, but it contains all of the interesting parts.
    // It should be pretty obvious how to modify it to your needs, if not let me know.
    //
    // The genral per-frame work is this:
    // - renderer_draw_text appends quads to a cpu-side buffer
    // - the quads get uploaded to the gpu as an per-instance vertex buffer
    // - update atlas if dirty or resized.
    // Note: sokol doesn't have partial writes, we always need to destroy it and create it again.
    // - draw all characters in a single instanced draw call
    package game

    import "../shaders"
    import sg "../sokol/gfx"
    import "core:fmt" // <-- this contains output of sokol-shdc
    import "vendor:fontstash"

    RENDERER_DEFAULT_FONT_ATLAS_SIZE :: 512

    Renderer :: struct {
    font_ctx: fontstash.FontContext,
    font_indexes: [Text_Font]i32,
    font_atlas_size: IVec2,
    font_instances: Array(BUDGET_FONT_QUADS, Renderer_Font_Instance),
    font_pip: sg.Pipeline,
    font_instance_buf: sg.Buffer,
    font_ibuf: sg.Buffer,
    font_img: sg.Image,
    }


    // Quad
    Renderer_Font_Instance :: struct {
    pos_min: [2]f32,
    pos_max: [2]f32,
    uv_min: [2]u16,
    uv_max: [2]u16,
    color: [4]u8,
    }

    Text_Align_Horizontal :: enum u8 {
    Left = u8(fontstash.AlignHorizontal.LEFT),
    Center = u8(fontstash.AlignHorizontal.CENTER),
    Right = u8(fontstash.AlignHorizontal.RIGHT),
    }

    Text_Align_Vertical :: enum u8 {
    Top = u8(fontstash.AlignVertical.TOP),
    Middle = u8(fontstash.AlignVertical.MIDDLE),
    Bottom = u8(fontstash.AlignVertical.BOTTOM),
    Baseline = u8(fontstash.AlignVertical.BASELINE),
    }

    Text_Font :: enum u8 {
    Default = 0,
    Debug,
    }

    @(rodata)
    _text_font_path := [Text_Font]string {
    .Default = "assets/Noto_Sans_JP/static/NotoSansJP-Regular.ttf",
    .Debug = "assets/JetBrainsMono-Regular.ttf",
    }

    renderer_draw_text :: proc(
    ren: ^Renderer,
    text: string,
    pos: Vec2,
    size: f32 = 12,
    color := COLOR_WHITE,
    blur: f32 = 0,
    spacing: f32 = 0,
    font: Text_Font = .Default,
    align_h: Text_Align_Horizontal = .Left,
    align_v: Text_Align_Vertical = .Baseline,
    ) {
    col := color_to_color8(color)

    // Easier than dealing with fontstash state stack...
    state := fontstash.__getState(&ren.font_ctx)
    state^ = {
    size = size,
    blur = blur,
    spacing = spacing,
    font = 0,
    ah = fontstash.AlignHorizontal(align_h),
    av = fontstash.AlignVertical(align_v),
    }

    inv_screen := 1.0 / vec_cast(f32, ren.screen_size)

    for iter := fontstash.TextIterInit(&ren.font_ctx, pos.x, pos.y, text); true; {
    quad: fontstash.Quad
    fontstash.TextIterNext(&ren.font_ctx, &iter, &quad) or_break

    ds_append(
    &ren.font_instances,
    Renderer_Font_Instance {
    // Transform quads into NDC
    pos_min = (Vec2{quad.x0, quad.y0} * inv_screen) * 2.0 - 1.0,
    pos_max = (Vec2{quad.x1, quad.y1} * inv_screen) * 2.0 - 1.0,
    uv_min = {norm_pack_fast(u16, quad.s0), norm_pack_fast(u16, quad.t0)},
    uv_max = {norm_pack_fast(u16, quad.s1), norm_pack_fast(u16, quad.t1)},
    color = col,
    },
    )
    }
    }

    renderer_init :: proc(ren: ^Renderer) {
    fontstash.Init(&ren.font_ctx, RENDERER_DEFAULT_FONT_ATLAS_SIZE, RENDERER_DEFAULT_FONT_ATLAS_SIZE, .BOTTOMLEFT)

    for font in Text_Font {
    index := fontstash.AddFontPath(&ren.font_ctx, fmt.tprint(font), _text_font_path[font])
    ren.font_indexes[font] = i32(index)
    }

    ren.font_pip = sg.make_pipeline(
    {
    label = "font-pip",
    shader = sg.make_shader(shaders.font_shader_desc(sg.query_backend())),
    layout = {
    buffers = {0 = {step_func = .PER_INSTANCE}},
    attrs = {
    shaders.ATTR_font_vs_inst_pos_min = {format = .FLOAT2},
    shaders.ATTR_font_vs_inst_pos_max = {format = .FLOAT2},
    shaders.ATTR_font_vs_inst_uv_min = {format = .USHORT2N},
    shaders.ATTR_font_vs_inst_uv_max = {format = .USHORT2N},
    shaders.ATTR_font_vs_inst_color = {format = .UBYTE4N},
    },
    },
    index_type = .UINT16,
    color_count = 1,
    colors = {0 = {pixel_format = RENDERER_GBUF_COLOR_PIXEL_FORMAT, blend = RENDERER_PREMULTIPLIED_ALPHA_BLEND_STATE}},
    depth = {pixel_format = .DEPTH, write_enabled = false, compare = .ALWAYS},
    cull_mode = .NONE,
    },
    )

    ren.font_instance_buf = sg.make_buffer(
    {label = "font-instance-buf", type = .VERTEXBUFFER, usage = .STREAM, size = size_of(ren.font_instances.data)},
    )

    ren.font_ibuf = sg.make_buffer(
    {label = "font-ibuf", type = .INDEXBUFFER, usage = .IMMUTABLE, data = sg_range_slice([]u16{0, 1, 2, 1, 2, 3})},
    )

    // ...
    }

    renderer_update :: proc(ren: ^Renderer) {
    // Update atlas and fontstash
    {
    if ren.font_instances.len > 0 {
    sg.update_buffer(ren.font_instance_buf, sg_range_slice(ds_slice(&ren.font_instances)))
    }

    atlas_size := IVec2{i32(ren.font_ctx.width), i32(ren.font_ctx.height)}
    dirty_texture :=
    ren.font_ctx.dirtyRect[0] < ren.font_ctx.dirtyRect[2] && ren.font_ctx.dirtyRect[1] < ren.font_ctx.dirtyRect[3]

    // Note dirty_texture: sokol doesn't have partial texture updates
    if ren.font_atlas_size != atlas_size || dirty_texture {
    ren.font_atlas_size = atlas_size

    sg.destroy_image(ren.font_img)
    ren.font_img = sg.make_image(
    {
    label = "font-img",
    type = ._2D,
    width = atlas_size.x,
    height = atlas_size.y,
    num_slices = 1,
    num_mipmaps = 1,
    pixel_format = .R8,
    sample_count = 1,
    usage = .IMMUTABLE,
    data = {subimage = {0 = {0 = sg_range_slice(ren.font_ctx.textureData)}}},
    },
    )
    }

    ren.font_ctx.state_count = 0
    fontstash.Reset(&ren.font_ctx)
    }

    // Draw characters
    if ren.font_instances.len > 0 {
    sg.apply_pipeline(ren.font_pip)

    // clamp to border
    sg.apply_bindings(
    {
    vertex_buffers = {0 = ren.font_instance_buf},
    index_buffer = ren.font_ibuf,
    fs = {
    images = {shaders.SLOT_font_tex = ren.font_img},
    samplers = {shaders.SLOT_font_smp = ren.linear_clamp_to_edge_smp},
    },
    },
    )

    sg.draw(0, 6, ren.font_instances.len)
    ren.font_instances.len = 0
    }

    // swapchain pass...
    }
    45 changes: 45 additions & 0 deletions shaders.glsl
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,45 @@
    // This file should live in the shaders/ directory along with the shaders.odin generated by sokol-shdc

    // build with this on windows:
    // sokol-tools-bin\\bin\\win32\\sokol-shdc.exe -i source/shaders/shaders.glsl -o source/shaders/shaders.odin -l hlsl5 -f sokol_odin

    @header package shaders
    @header import sg "../sokol/gfx"

    @vs font_vs
    in vec2 inst_pos_min;
    in vec2 inst_pos_max;
    in vec2 inst_uv_min;
    in vec2 inst_uv_max;
    in vec4 inst_color;

    out vec2 vert_uv;
    out vec4 vert_color;

    void main() {
    // Note: we could change the quad definition to use offset+size instead for both position and UVs instead of the bit hacks.
    bool left = bool(gl_VertexIndex & 1);
    bool bottom = bool((gl_VertexIndex >> 1) & 1);
    vec2 pos = vec2(left ? inst_pos_min.x : inst_pos_max.x, bottom ? inst_pos_min.y : inst_pos_max.y);
    vec2 uv = vec2(left ? inst_uv_min.x : inst_uv_max.x, bottom ? inst_uv_min.y : inst_uv_max.y);
    gl_Position = vec4(pos, 0.0, 1.0);
    vert_uv = uv;
    vert_color = inst_color;
    }
    @end

    @fs font_fs
    in vec2 vert_uv;
    in vec4 vert_color;

    out vec4 frag_color;

    uniform texture2D font_tex;
    uniform sampler font_smp;

    void main() {
    frag_color = vert_color * vec4(texture(sampler2D(font_tex, font_smp), vert_uv).r);
    }
    @end

    @program font font_vs font_fs