Tutorials

Migrating from 3.22.8 to 3.23.0

BBMOD 3.23.0 Intermediate Migration Guide

Created: 5.5.2026


This guide is for projects that already run on BBMOD 3.22.8 and need to upgrade to 3.23.0 with minimal surprises. We focus on the API changes that can prevent your project from running correctly and provide practical before/after replacements.

Contents

Quick migration checklist

  1. Replace queue priority based setup with BBMOD_ERenderQueue categories.
  2. Replace old stateful render queue command chains with Draw* command calls.
  3. Update all queued DrawSprite* calls to pass material as the first argument.
  4. Mark BBMOD_Material.HashDirty as true whenever custom material state changes.
  5. Ensure your custom get_hash includes every render-affecting custom field.
  6. If you use custom dynamic-batch DataWriter, update it for 3.23.0 and add the new DataFilter member.
  7. Remove BBMOD_MaterialPropertyBlock and obsolete OnApply callback usage.

Render queues use categories now

Queue ordering is now driven by BBMOD_ERenderQueue category values (Terrain, Opaque, Transparent, Sky) instead of mutable queue priorities.

Before (3.22.8)

var _queues = bbmod_render_queues_get();
var _queueTransparent = _queues[0];
_queueTransparent.set_priority(200);

matGlass.RenderQueue = _queueTransparent;

After (3.23.0)

var _queueTransparent = bbmod_render_queue_get(BBMOD_ERenderQueue.Transparent);

matGlass.RenderQueue = BBMOD_ERenderQueue.Transparent;

Use bbmod_render_queue_get when you need a queue instance and use enum values when setting material queue category.

DrawSprite queue commands require material first

In 3.23.0, queued sprite drawing methods on BBMOD_RenderQueue require material as the first argument.

Before (3.22.8)

var _queue = bbmod_render_queue_get(BBMOD_ERenderQueue.Transparent);
_queue.DrawSpriteExt(sprMarker, 0, x, y, 1, 1, 0, c_white, 1);

After (3.23.0)

var _queue = bbmod_render_queue_get(BBMOD_ERenderQueue.Transparent);
var _spriteMat = BBMOD_MATERIAL_DEFAULT.clone();
_spriteMat.BaseOpacity = sprite_get_texture(sprMarker, 0);

_queue.DrawSpriteExt(_spriteMat, sprMarker, 0, x, y, 1, 1, 0, c_white, 1);

The same material-first update applies to DrawSprite, DrawSpriteGeneral, DrawSpritePart, DrawSpritePos, DrawSpriteTiled, and related variants.

Material hashes and HashDirty

In 3.23.0, material hashes are central for deterministic sorting and batching. If a custom material field changes but hash stays stale, render ordering and batch grouping can become incorrect.

Before (3.22.8)

// Custom fields changed directly, but hash invalidation was often skipped.
matWet.Wetness = clamp(targetWetness, 0, 1);
matWet.FlowSpeed = max(flowSpeed, 0);

After (3.23.0)

// Route changes through setters that invalidate hash only when needed.
matWet
    .set_wetness(targetWetness)
    .set_flow_speed(flowSpeed);

Recommended rule: every field that can change shader/material behavior must either set HashDirty = true on change, or be included in immutable construction.

Dynamic batching migration for custom DataWriter

In 3.22.8, many projects only customized BBMOD_DynamicBatch.DataWriter. In 3.23.0, BBMOD_DynamicBatch.DataFilter is a new member and should be provided together with custom writer logic.

Before (3.22.8 style custom DataWriter only)

batch = new BBMOD_DynamicBatch(model);
batch.SlotsPerInstance = 15;

batch.DataWriter = function(_out, _index)
{
    var _base = _index;

    // Position
    _out[@ _base] = x;
    _out[@ _base + 1] = y;
    _out[@ _base + 2] = z;
    // Uniform scale
    _out[@ _base + 3] = image_xscale;
    // Rotation quaternion
    new BBMOD_Quaternion()
        .FromAxisAngle(BBMOD_VEC3_UP, image_angle)
        .ToArray(_out, _base + 4);
    // ID bytes
    _out[@ _base + 8] = ((id & $000000FF) >> 0) / 255.0;
    _out[@ _base + 9] = ((id & $0000FF00) >> 8) / 255.0;
    _out[@ _base + 10] = ((id & $00FF0000) >> 16) / 255.0;
    _out[@ _base + 11] = ((id & $FF000000) >> 24) / 255.0;

    // Custom per-instance payload written by project-specific code.
    _out[@ _base + 12] = WindPhase;
    _out[@ _base + 13] = WindStrength;
    _out[@ _base + 14] = LodFactor;
};

batch.submit(undefined, _batchData, _ids);

After (3.23.0)

batch = new BBMOD_DynamicBatch(model);
batch.SlotsPerInstance = 16;

batch.DataWriter = function(_out, _index)
{
    var _base = _index;

    // Position
    _out[@ _base] = x;
    _out[@ _base + 1] = y;
    _out[@ _base + 2] = z;
    // Uniform scale
    _out[@ _base + 3] = image_xscale;
    // Rotation quaternion
    new BBMOD_Quaternion()
        .FromAxisAngle(BBMOD_VEC3_UP, image_angle)
        .ToArray(_out, _base + 4);
    // ID bytes
    _out[@ _base + 8] = ((id & $000000FF) >> 0) / 255.0;
    _out[@ _base + 9] = ((id & $0000FF00) >> 8) / 255.0;
    _out[@ _base + 10] = ((id & $00FF0000) >> 16) / 255.0;
    _out[@ _base + 11] = ((id & $FF000000) >> 24) / 255.0;

    // Custom per-instance payload.
    _out[@ _base + 12] = WindPhase;
    _out[@ _base + 13] = WindStrength;
    _out[@ _base + 14] = LodFactor;

    // Effective dither used by batched shaders in 3.23.0.
    _out[@ _base + 15] = 1.0;
};

batch.DataFilter = function(
    _mesh,
    _matrix,
    _batchData,
    _ids,
    _instances,
    _visibleInstancesHint = undefined,
    _ditherEnableSnapshot = bbmod_dither_get_enabled(),
    _ditherValueSnapshot = bbmod_dither_get_value()
)
{
    var _result = {
        BatchData: _batchData,
        VisibleInstances: 0,
        FrustumCulledInstances: 0,
        DistanceCulledInstances: 0,
        FadeData: undefined,
        SkipDraw: false,
    };

    var _slotsPerInstance = SlotsPerInstance;
    bbmod_assert(_slotsPerInstance >= 16);

    var _ditherEnabled = _ditherEnableSnapshot;
    var _ditherValue = clamp(_ditherValueSnapshot, 0.0, 1.0);
    var _ditherCullAll = (_ditherEnabled && _ditherValue <= 0.0);

    // No per-instance IDs provided (for example some custom producers).
    if (!is_array(_ids))
    {
        var _visibleFallback = 0;
        if (is_real(_visibleInstancesHint))
        {
            _visibleFallback = max(real(_visibleInstancesHint), 0.0);
        }
        else if (is_array(_batchData))
        {
            if (array_length(_batchData) > 0 && is_array(_batchData[0]))
            {
                var _batchIndex = 0;
                repeat(array_length(_batchData))
                {
                    _visibleFallback += ceil(array_length(_batchData[_batchIndex++]) / _slotsPerInstance);
                }
            }
            else
            {
                _visibleFallback = ceil(array_length(_batchData) / _slotsPerInstance);
            }
        }

        _result.VisibleInstances = _ditherCullAll ? 0 : _visibleFallback;
        _result.DistanceCulledInstances = _ditherCullAll ? _visibleFallback : 0;
        _result.SkipDraw = (_result.VisibleInstances <= 0);
        return _result;
    }

    // Flat IDs + flat payload variant for minimal custom pipelines.
    var _visibleInstances = 0;
    var _distanceCulledInstances = 0;
    var _instanceIndex = 0;
    repeat(array_length(_ids))
    {
        var _idCurrent = _ids[_instanceIndex];
        var _index = _instanceIndex * _slotsPerInstance;
        var _keep = (_idCurrent != 0);

        if (_keep && _instances != undefined && ds_list_find_index(_instances, _idCurrent) == -1)
        {
            _keep = false;
        }

        if (_keep && _ditherCullAll)
        {
            _keep = false;
            ++_distanceCulledInstances;
        }

        if (!_keep)
        {
            var _slot = _index;
            repeat(_slotsPerInstance)
            {
                _batchData[@ _slot++] = 0.0;
            }
        }
        else
        {
            if (_ditherEnabled)
            {
                _batchData[@(_index + 15)] = clamp(_ditherValue * _batchData[@(_index + 14)], 0.0, 1.0);
            }
            else
            {
                _batchData[@(_index + 15)] = 1.0;
            }

            ++_visibleInstances;
        }

        ++_instanceIndex;
    }

    _result.BatchData = _batchData;
    _result.VisibleInstances = _visibleInstances;
    _result.DistanceCulledInstances = _distanceCulledInstances;
    _result.SkipDraw = (_visibleInstances <= 0);
    return _result;
};

batch.submit(undefined, _batchData, _ids, _visibleInstances);

_visibleInstances is optional, but recommended for no-ID paths where you already know visible count.

Replacing MaterialPropertyBlock and OnApply

BBMOD_MaterialPropertyBlock and OnApply are obsolete in 3.23.0. The production-grade replacement is a custom material struct that extends a BBMOD material, overrides apply(_vertexFormat), exposes safe setters, and includes custom fields in get_hash().

Before (3.22.8)

var _props = new BBMOD_MaterialPropertyBlock();
_props.Values["u_Time"] = current_time * 0.001;
bbmod_material_props_set(_props);
matWater.OnApply = function()
{
    shader_set_uniform_f(shader_get_uniform(shader_current(), "u_Wetness"), wetness);
};

model.submit([matWater]);
bbmod_material_props_reset();

After (3.23.0)

function MAT_Water(_shader): BBMOD_DefaultMaterial(_shader) constructor
{
    static DefaultMaterial_apply = apply;
    static DefaultMaterial_get_hash = get_hash;

    Wetness = 0.0;
    FlowSpeed = 0.0;

    __uniformWetness = {};
    __uniformFlowSpeed = {};

    static __get_uniform = function(_cache, _shaderRaw, _name)
    {
        var _key = string(_shaderRaw);
        if (!variable_struct_exists(_cache, _key))
        {
            _cache[$ _key] = shader_get_uniform(_shaderRaw, _name);
        }
        return _cache[$ _key];
    };

    static set_wetness = function(_wetness)
    {
        _wetness = clamp(_wetness, 0.0, 1.0);
        if (Wetness != _wetness)
        {
            Wetness = _wetness;
            HashDirty = true;
        }
        return self;
    };

    static set_flow_speed = function(_flowSpeed)
    {
        _flowSpeed = max(_flowSpeed, 0.0);
        if (FlowSpeed != _flowSpeed)
        {
            FlowSpeed = _flowSpeed;
            HashDirty = true;
        }
        return self;
    };

    static apply = function(_vertexFormat)
    {
        if (!DefaultMaterial_apply(_vertexFormat))
        {
            return false;
        }

        var _shaderRaw = shader_current();
        var _uWetness = __get_uniform(__uniformWetness, _shaderRaw, "u_Wetness");
        var _uFlow = __get_uniform(__uniformFlowSpeed, _shaderRaw, "u_FlowSpeed");

        if (_uWetness >= 0)
        {
            shader_set_uniform_f(_uWetness, Wetness);
        }
        if (_uFlow >= 0)
        {
            shader_set_uniform_f(_uFlow, FlowSpeed);
        }

        return true;
    };

    static get_hash = function()
    {
        if (!HashDirty)
        {
            return __hash;
        }

        var _hash = DefaultMaterial_get_hash();
        _hash = bbmod_hash_combine(_hash, floor(Wetness * 65535.0));
        _hash = bbmod_hash_combine(_hash, floor(FlowSpeed * 65535.0));

        __hash = _hash;
        HashDirty = false;
        return __hash;
    };
}

matWater = new MAT_Water(BBMOD_SHADER_DEFAULT)
    .set_wetness(0.35)
    .set_flow_speed(1.25);

// Per-frame update path.
matWater.set_wetness(targetWetness);

model.submit([matWater]);

This pattern preserves deterministic sorting, avoids repeated uniform lookups, and keeps hash invalidation tightly coupled to state changes.

Smoke test after upgrade

After replacing the APIs above, verify these points in one small test room:

  1. Opaque, transparent, sky and terrain objects render in expected order.
  2. Queued sprites still render (no missing material argument errors).
  3. Materials that change custom fields are re-sorted correctly (hash updates as expected).
  4. Dynamic batches still draw all expected instances and custom DataFilter callbacks run.
  5. No runtime code depends on bbmod_material_props_set/get/reset or OnApply.

Once these checks pass, your 3.22.8 project should be safely aligned with 3.23.0 rendering and batching changes.