BRGRegistrationTracker
Before reading this page, make sure you've read System Overview to understand how prototypes, render signatures, chunks, and pools work together.
BRGRegistrationTracker is the resource tracking layer that both BRGRegisterer and the built-in registerers use internally. It manages prototype reference counting, chunk tracking with automatic overflow, and full cleanup on shutdown — without requiring a GameObject.
Use BRGRegistrationTracker directly when you need to manage BRG instances from a non-MonoBehaviour context (ECS systems, static managers, network callbacks, etc.). For MonoBehaviour-based registration, use Custom Registerer (BRGRegisterer) instead, which wraps a tracker with automatic OnEnable/OnDisable lifecycle.
Creating a Tracker
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
namespace BRGInstancedRenderer
{
public class MyManager
{
BRGRegistrationTracker _tracker;
int _protoIndex = -1;
ChunkLink _chunk;
public void Start(GameObject prefab)
{
_tracker = new BRGRegistrationTracker("MyManager");
// Connect to BRGRenderer singleton
if (!_tracker.Initialize()) return;
// 1. Register render signature
_protoIndex = _tracker.RegisterRenderSignatureFromPrefab(prefab);
if (_protoIndex < 0) return;
// 2. Allocate a chunk
_chunk = _tracker.AllocChunk();
if (!_chunk.IsValid) return;
// 3. Build instance data
var instances = new NativeArray<InstanceData>(100, Allocator.TempJob);
for (int i = 0; i < 100; i++)
{
instances[i] = new InstanceData
{
position = new float3(i * 2f, 0f, 0f),
rotation = quaternion.identity,
scale = new float3(1f),
signatureIndex = _protoIndex,
color = new Color32(255, 255, 255, 255)
};
}
// 4. Write to chunk
var bounds = new Bounds(Vector3.zero, new Vector3(200f, 10f, 10f));
_tracker.WriteChunk(_chunk, instances, bounds);
instances.Dispose();
}
public void Stop()
{
// Frees all chunks, unregisters all prototypes, disconnects from BRGRenderer
_tracker?.Shutdown();
}
}
}
Lifecycle
| Method | Description |
|---|---|
new BRGRegistrationTracker(string name) |
Create a tracker. The name is used in log messages. |
bool Initialize() |
Connect to BRGRenderer.Instance. Returns false if the singleton is unavailable. |
void Shutdown() |
Free all tracked chunks, unregister all prototypes, and disconnect. Safe to call multiple times. |
| Property | Type | Description |
|---|---|---|
| BRGSystem | BRGRenderer | The singleton reference. Set after Initialize(), null after Shutdown(). |
| IsRegistered | bool | true while the tracker is initialized. |
| Name | string | Display name for log messages. |
Callbacks
You can set callbacks for initialization and shutdown, which is how BRGRegisterer hooks in its OnInitialize / OnShutdown methods:
var tracker = new BRGRegistrationTracker("MyTracker");
tracker.InitializeCallback = () => { /* register protos, alloc chunks, etc. */ };
tracker.ShutdownCallback = () => { /* clean up local state only */ };
tracker.Initialize(); // calls InitializeCallback after connecting
If InitializeCallback throws, the tracker automatically calls ShutdownCallback and shuts down to prevent partial initialization.
Structs
InstanceData
Per-instance data for registration and updates.
| Field | Type | Description |
|---|---|---|
| position | float3 | World-space position. |
| rotation | quaternion | World-space rotation. |
| scale | float3 | Local scale. |
| signatureIndex | int | Global signature/prototype index returned by RegisterRenderSignatureFromPrefab or RegisterRenderSignature. |
| color | Color32 | Per-instance color tint. Requires Enable Per-Instance Color on the Config. Not uploaded by WriteChunk, AddInstance, or AddInstances — those paths only upload position / rotation / scale / signatureIndex. To apply color, call StageColor(link, color) (or SetColorUnsafe(link, color)) after the chunk is written, typically from ChunkWritten / OnChunkWritten. |
PrototypeData
The asset definition — meshes, materials, LODs, and other data extracted from a prefab. This is the "what does it look like" data. Accessed via RenderSignature.asset.
| Field | Type | Description |
|---|---|---|
| sourcePrefab | GameObject | Original prefab (used for identity comparison). |
| mesh | Mesh | Base mesh (LOD 0). |
| materials | Material[] | Materials per submesh (LOD 0). |
| lodMeshes | Mesh[] | Additional LOD meshes (LOD 1+). |
| lodMaterials | Material[][] | Materials per LOD level. |
| lodScreenSizes | float[] | Screen relative heights for LOD transitions. |
| lodFadeTransitionWidths | float[] | Crossfade width per LOD level. |
| lodCastsShadows | bool[] | Whether each LOD casts shadows. |
| lodMotionVectorModes | MotionVectorGenerationMode[] | Per-LOD motion vector mode. |
| lodGroupSize | float | Reference size for LOD calculations. |
| fadeMode | LODFadeMode | Crossfade mode from LODGroup. |
| percentageFadeMask | uint | Bit N = LOD N uses percentage fade (SpeedTree). |
| speedTreeRendererID | int | Renderer instance ID for SpeedTree wind lookup. |
| usesMeshLod | bool | Unity 6.2+ Mesh LOD instead of separate meshes per level. |
| prefitCullBounds | PrefitCullBounds[] | Optional pre-computed tight cull sphere per LOD. Skips runtime bounds measurement entirely. See Prefit Cull Bounds. |
PrefitCullBounds
A tightened cull sphere for one LOD, supplied at registration instead of being measured at runtime.
| Field | Type | Description |
|---|---|---|
| centre | float3 | Sphere centre in mesh-local space. |
| radius | float | Sphere radius. Must be > 0 for the entry to be considered valid. |
| cylRadius | float | Radius of the bounding cylinder about the Y axis, used for tighter tests on upright geometry like trees and grass. |
| IsValid | bool | true when radius > 0. Invalid entries are ignored and that LOD falls back to runtime fitting. |
See Prefit Cull Bounds for how these are produced and consumed.
RenderSignature
The full registration unit — combines a PrototypeData asset with render behavior configuration. This is what you pass to RegisterRenderSignature when you need explicit control over render settings.
The same prefab can be registered multiple times with different render settings. Deduplication uses both sourcePrefab and a hash of the config fields, so two RenderSignatures with the same prefab but different shadow settings are treated as separate prototypes.
| Field | Type | Default | Description |
|---|---|---|---|
| asset | PrototypeData | — | The mesh/material/LOD data. |
| motionVectorMode | MotionVectorGenerationMode | — | Motion vector generation mode. |
| animateCrossFading | bool | — | Whether to use animated crossfade. |
| maxShadowCascade | int | 4 | Max shadow cascades. |
| minShadowLOD | int | 0 | Minimum LOD for shadow mesh. |
| maxShadowDistance | float | 0 | Resolved max shadow distance, 0 = global. Fed from the assigned Prototype Group's shadow caster distance culling. |
| forceDisableShadows | bool | false | Never cast shadows, regardless of the source renderer's shadow casting mode. |
| maxShadowCrossfadeCascade | int | -99 | Cascade-specific crossfade control. -99 = use the global config value. |
| lodFadeDuration | float | 0 | Animated crossfade duration override. |
| prototypeGroup | BRGPrototypeGroup | null | Optional group for shared LOD bias, density, and shadow culling. See BRG Prototype Group. |
| enableDistanceDensity | bool | false | Enable per-instance distance density culling. |
| distanceDensityMode | DistanceDensityMode | ScreenSize | ScreenSize or RawDistance. |
| densityStartScreenSize | float | 0.1 | Screen-size percentage where thinning begins (Screen Size mode). |
| densityStartDistance | float | 50 | Distance where thinning begins (Raw Distance mode). |
| densityEndDistance | float | 150 | Distance where thinning reaches minimum (Raw Distance mode). |
| densityMinFraction | float | 0.3 | Minimum density fraction at max distance. 0 = cull all; 0.3 = keep 30%. |
| densityFadeWidth | float | 0.05 | Crossfade width for density transitions. |
| layer | int | 0 | GameObject layer. |
| renderingLayerMask | uint | 0xffffffff | URP/HDRP rendering layer mask. |
Most users can simply call RegisterRenderSignatureFromPrefab(GameObject), which extracts a RenderSignature from the prefab automatically (including any BRG Prototype Extra Data settings). You can also call ExtractRenderSignature(GameObject) to get a RenderSignature without registering it, modify it, and then pass it to RegisterRenderSignature.
The defaults above are the struct's own, used when you build a
RenderSignatureby hand. They don't all match the inspector defaults on BRG Prototype Extra Data — extraction overwrites them with whatever that component holds.
ChunkLink
Handle to an allocated BRG chunk. Tracked for automatic cleanup on shutdown. Includes a generation counter for ABA-safe liveness checks.
| Member | Type | Description |
|---|---|---|
| Id | int | Internal chunk identifier. |
| Generation | int | Generation counter — incremented when the chunk slot is reused, so a stale ChunkLink won't accidentally point at a new chunk. |
| IsValid | bool | true if the chunk was successfully allocated (Id >= 0). |
| IsAlive | bool | true if the chunk is still allocated and matches its generation. Use this for ABA-safe checks before operating on a stored ChunkLink. |
| NumActivePools | int | How many pools the chunk currently occupies. |
| GetPoolGlobalIndex(int localIdx) | int | Global index of the pool at the given local position within the chunk. |
| NumInstancesInPool(int localIdx) | int | Instance count in the pool at the given local position. |
| Invalid | static | Sentinel value for an invalid chunk. |
InstanceLink
Handle to a single instance — the chunk it lives in plus its global slot index. The ChunkId may be an overflow chunk, not the primary ChunkLink.Id.
InstanceLink is a passive handle — all per-instance operations (move, color, remove, etc.) are performed via methods on the tracker or registerer. See Tracked Staging and Unsafe Direct Modification below.
| Property | Type | Description |
|---|---|---|
| ChunkId | int | The chunk this instance belongs to (may be an overflow chunk). |
| GlobalBRGSlotIndex | int | Global slot index in the instance buffer. |
| IsValid | bool | true if the slot index is >= 0. |
| Invalid | static | Sentinel value (-1, -1). |
Prefit Cull Bounds
Every registered mesh needs a cull sphere. By default BRG measures one at registration time, walking the mesh's vertices to fit a sphere tighter than the bounding-box corner. That costs time on the registering thread and requires the mesh to be readable.
Prefit bounds skip that work. If PrototypeData.prefitCullBounds holds a valid entry for an LOD, that sphere is used directly and no measurement happens.
Where they come from
For prefabs, they're baked automatically in the editor — as long as the prefab has a BRG Prototype Extra Data component on it. Prefabs without one are skipped and fall back to runtime measurement.
Tip
Add BRG Prototype Extra Data to your prototype prefabs even if you don't need any of its overrides. Its presence is what enables cull bounds baking, which removes the measurement cost from prototype registration.
A bake is queued when:
- A prototype prefab is imported (saved, reimported, or first added to the project).
- A prototype registers at runtime without usable baked bounds. This is edit-mode only, and retried at most once per prefab per domain reload.
The measured bounds are written into the BRG Prototype Extra Data component and the prefab asset is saved, so the values are part of the prefab and should be committed. They're small — a centre, a radius, and a cylinder radius per LOD.
Baking never happens during import itself. Prefabs are queued and the bake runs afterwards on a delayed callback, since asset writes aren't legal inside an import callback.
Cases that skip the bake
| Case | Behaviour |
|---|---|
Prefab has no BRGPrototypeExtraData |
Skipped entirely. |
| Prefab produces more than one render signature | Skipped — per-LOD bounds can't be mapped back to a single component. Measures at runtime instead. |
| Prefab is currently open in Prefab Mode | Deferred until the prefab stage closes, then baked. |
| No LOD could be measured | Nothing is written. |
| Bounds already match what would be baked | Nothing is written, so no redundant asset save. |
Supplying them yourself
When building a RenderSignature by hand — procedural meshes, runtime-generated LODs, or your own asset pipeline — populate asset.prefitCullBounds with one entry per LOD:
var sig = tracker.ExtractRenderSignature(prefab);
sig.asset.prefitCullBounds = new[]
{
new PrefitCullBounds { centre = lod0Centre, radius = lod0Radius, cylRadius = lod0CylRadius },
new PrefitCullBounds { centre = lod1Centre, radius = lod1Radius, cylRadius = lod1CylRadius },
};
int protoIndex = tracker.RegisterRenderSignature(sig);
Entries are matched by LOD index. Leave an entry with radius = 0 to let that one LOD fall back to runtime fitting.
Fallback behaviour
| Situation | Result |
|---|---|
| Valid prefit entry for the LOD | Used directly, no measurement. |
| No entry, and Allow Fallback Runtime Bounds Fitting is on | Measured at registration. Non-readable meshes are measured by async readback and use the box radius until it lands. |
| No entry, and that option is off | Falls back to the bounding-box corner radius. |
| A prefit shows up while an async measurement is still in flight | The readback is discarded and the prefit is applied immediately. |
A completed fit is never overwritten — once a mesh's bounds have landed, a later prefit for the same mesh is ignored.
The in-flight case comes up when one mesh is shared by several prototypes: the first to register has no prefit and starts a readback, and a later one does have a prefit. Rather than wait for a measurement it no longer needs, the pending readback is marked superseded and the prefit takes effect right away.
See Allow Fallback Runtime Bounds Fitting for the config toggle.
Render Signature Registration
All registrations are tracked and automatically unregistered on Shutdown().
| Method | Description |
|---|---|
RenderSignature ExtractRenderSignature(GameObject prefab) |
Extract a RenderSignature from a prefab without registering it. |
int RegisterRenderSignatureFromPrefab(GameObject prefab) |
Register a single signature extracted from the prefab. Returns the global prototype index, or -1 on failure. |
int[] RegisterRenderSignaturesFromPrefab(GameObject prefab) |
Register multiple signatures extracted from a prefab that produces more than one (e.g. multi-renderer prefabs). Returns the array of global indices. |
int RegisterRenderSignature(RenderSignature signature) |
Register an explicit RenderSignature. Returns the global prototype index, or -1 on failure. |
void UnregisterRenderSignature(int globalIdx) |
Manually unregister a single prototype. Removes it from the tracker's auto-cleanup list. |
Chunk Management
All chunks are tracked and automatically freed on Shutdown().
| Method | Description |
|---|---|
int MaxInstancesPerChunk |
Maximum instances a single chunk can hold (driven by Max Pool Refs Per Chunk × 16 in the Config). |
ChunkLink AllocChunk() |
Allocate a new chunk. |
void WriteChunk(ChunkLink, NativeArray<InstanceData>, Bounds, float cullDistance = 0, float instanceCullDistance = float.MaxValue) |
Write instances to a chunk. Automatically splits across overflow chunks if instance count exceeds MaxInstancesPerChunk. Overflow chunks share bounds with the primary chunk. |
void ReleaseChunk(ChunkLink) |
Free a chunk and all its overflow chunks. Idempotent. |
void RecalculateChunkBounds(ChunkLink, Bounds) |
Update chunk culling bounds (applies to primary + all overflows). |
void SetChunkEnabled(ChunkLink, bool) |
Enable or disable a chunk for rendering (applies to primary + all overflows). Disabled chunks skip culling and rendering. |
bool GetChunkEnabled(ChunkLink) |
Returns whether the chunk is currently enabled. |
void SetRenderEnabled(bool) |
Enable/disable rendering for ALL chunks owned by this tracker at once. |
WriteChunk Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| link | ChunkLink | — | The chunk to write to. |
| instances | NativeArray<InstanceData> | — | Instance data array. |
| bounds | Bounds | — | World-space AABB used for hierarchical culling. Should tightly enclose all instances. |
| cullDistance | float | 0 | Per-chunk max render distance. 0 = unlimited. |
| instanceCullDistance | float | float.MaxValue | Per-instance max render distance applied during GPU culling. |
Instance Lifecycle
| Method | Description |
|---|---|
InstanceLink AddInstance(ChunkLink, InstanceData) |
Add a single instance. Tries the primary chunk, then existing overflow chunks, then allocates a new overflow if needed. Returns InstanceLink.Invalid on failure or for a stale/freed ChunkLink. |
InstanceLink[] AddInstances(ChunkLink, NativeArray<InstanceData>, int count) |
Add multiple instances with automatic overflow. Returns one InstanceLink per added instance. |
InstanceLink MoveInstanceToChunk(InstanceLink source, ChunkLink dest, InstanceData) |
Add to destination first, then remove from source. Returns InstanceLink.Invalid if the destination is full — source untouched in that case. |
Lookups
| Method | Description |
|---|---|
InstanceLink GetInstanceLink(ChunkLink, int localIndex) |
Resolve a logical instance index (0..GetTotalChunkInstanceCount − 1, spanning primary + overflow) into a current InstanceLink. Returns InstanceLink.Invalid if the chunk is freed or the index is out of range. |
int GetTotalChunkInstanceCount(ChunkLink) |
Total instance count including overflow chunks. |
int GetOverflowChunkCount(ChunkLink) |
Number of overflow chunks attached to this primary. |
void GetPerChunkInstanceCounts(ChunkLink, List<int>) |
Instance counts per sub-chunk: primary at index 0, then each overflow. |
Tracked Staging
For most use cases, you should use the staged Stage* API. These methods accumulate per-slot writes and merge them so that multiple updates to the same instance within one frame collapse into a single GPU write. The staged changes are flushed automatically during BRGRenderer's next update.
| Method | Description |
|---|---|
StagePosition(InstanceLink \| int slot, float3) |
Update position. |
StageRotation(InstanceLink \| int slot, quaternion) |
Update rotation. |
StageScale(InstanceLink \| int slot, float3) |
Update scale. |
StageMoveAndRotate(InstanceLink \| int slot, float3, quaternion) |
Update position and rotation. |
StageMove(InstanceLink \| int slot, float3, quaternion, float3) |
Update full TRS. |
StageColor(InstanceLink \| int slot, Color32) |
Update per-instance color. |
StageSetEnabled(InstanceLink \| int slot, bool) |
Enable or disable a single instance. Disabled instances stay in the buffer but are culled. |
StageRemove(InstanceLink) / StageRemove(int chunkId, int slot) |
Remove an instance. Also drops any pending staged writes for that slot. |
// move + color + enable in one frame -> single merged GPU write
tracker.StageMoveAndRotate(link, newPos, newRot);
tracker.StageColor(link, Color.red);
tracker.StageSetEnabled(link, true);
Batched Staging
When you already have per-instance results in a NativeArray — typically produced by a Burst job — the batch overloads submit the whole array in one call instead of looping over per-instance Stage* calls. They still go through the tracked path, so per-slot merging still applies.
| Method | Description |
|---|---|
StageTRSBatch(NativeArray<BRGRenderer.GPUTRSPatchEntry> entries, int count) |
Stage a batch of TRS patches, merging with any pending writes per slot. Entries whose flags have no patch bits set are skipped. |
StageColorBatch(NativeArray<BRGRenderer.GPUColorEntry> entries, int count) |
Stage a batch of colors, merging with any pending writes per slot. |
// entries filled by a Burst job, then submitted in one call
tracker.StageTRSBatch(patchEntries, count);
tracker.StageColorBatch(colorEntries, count);
Unsafe Direct Modification
The *Unsafe variants are direct pass-throughs to BRGRenderer with no buffering, no merging, and no conflict resolution. Use them only when you know what you're doing — for example, in performance-critical hot paths where you've already deduplicated writes yourself, or when you need writes to land immediately.
| Method | Description |
|---|---|
SetPositionUnsafe(InstanceLink \| int slot, float3) |
Direct position write. |
SetRotationUnsafe(InstanceLink \| int slot, quaternion) |
Direct rotation write. |
SetScaleUnsafe(InstanceLink \| int slot, float3) |
Direct scale write. |
MoveAndRotateUnsafe(InstanceLink \| int slot, float3, quaternion) |
Position + rotation. |
MoveUnsafe(InstanceLink \| int slot, float3, quaternion, float3) |
Full TRS. |
SetColorUnsafe(InstanceLink \| int slot, Color32) |
Direct color write. |
SetEnabledUnsafe(InstanceLink \| int slot, bool) |
Direct enable/disable. |
RemoveUnsafe(InstanceLink) / RemoveUnsafe(int chunkId, int slot) |
Direct removal. |
SampleLightProbeUnsafe(InstanceLink \| int slot, float3 position) |
Re-sample light probes at a position. |
Batched Unsafe Writes
The fastest path at scale. These bulk-stage a whole NativeArray straight into the core staging buffers, skipping both the per-instance call overhead and the tracker's merge dictionary. Same one-write-per-slot contract as the other *Unsafe methods.
| Method | Description |
|---|---|
SetTRSBatchUnsafe(NativeArray<BRGRenderer.GPUTRSPatchEntry> entries, NativeArray<int> slotIndices, int count, bool skipFlipProcessing = false) |
Bulk-stage TRS patches directly. slotIndices[i] must match entries[i].slotIndex. Entries carrying the scale flag may be modified in place. |
SetColorBatchUnsafe(NativeArray<BRGRenderer.GPUColorEntry> entries, int count) |
Bulk-stage per-instance colors directly. |
// job wrote patches[] + slots[] + colorEntries[]; submit in two calls
tracker.SetTRSBatchUnsafe(patches, slots, count);
tracker.SetColorBatchUnsafe(colorEntries, count);
Batch Entry Structs
Both structs live on BRGRenderer and are Burst-compatible, so a job can write them directly.
public struct GPUTRSPatchEntry // 48 bytes, sequential layout
{
public uint slotIndex; // InstanceLink.GlobalBRGSlotIndex
public uint flags; // bit 0 = position, bit 1 = rotation, bit 2 = scale
public float3 position;
public float4 rotation; // quaternion.value
public float3 scale;
}
public struct GPUColorEntry
{
public uint slotIndex;
public uint packedColor; // see BRGRegistrationTracker.PackColor
}
Helpers on BRGRegistrationTracker for filling those structs from inside a Burst job:
| Member | Description |
|---|---|
const uint kPatchPosition (1u) |
Flag bit — this entry writes position. |
const uint kPatchRotation (2u) |
Flag bit — this entry writes rotation. |
const uint kPatchScale (4u) |
Flag bit — this entry writes scale. |
static uint PackColor(in Color32 color) |
Packs a Color32 into the GPU encoding used by GPUColorEntry.packedColor. Burst-compatible. |
Set only the flags you're actually writing — an entry that just moves an instance sets kPatchPosition and leaves rotation / scale untouched.
Mixing staged and unsafe writes on the same slot in the same frame is undefined — pick one path.
Same-path duplicates are unsafe too. The direct/unsafe write path is append-only: two
*Unsafecalls on the same slot in one frame — or two calls to the equivalentBRGRenderer.SetInstance*methods on the same slot in one frame — race, landing in a single GPU dispatch with an arbitrary winner. Partial-flag TRS writes (e.g. twoSetPositionUnsafe+ aSetRotationUnsafein the same frame) can also lose components. The same applies inside a batch:SetTRSBatchUnsafe/SetColorBatchUnsafearrays must not contain two entries pointing at the sameslotIndex. If your code path might touch the same slot more than once per frame, useStage*or theStage*Batchoverloads — they deduplicate in the tracker dictionary and are safe against this. SomeBRGRendererdirect methods carry the same warning in their XML doc comments.
Removed slots never inherit stale writes.
RemoveInstanceandRemoveUnsafeflush any pending direct TRS patches, color writes, and sig overrides onto the dying instance before the slot can be reused. A same-frame remove → re-add always receives only the new instance's data. This is a floor guarantee ("writes never outlive a slot") on top of the general "one write per slot per frame" rule — it doesn't add per-frame dedup to the direct path, it just prevents cross-instance contamination.
ChunkWritten Event
When WriteChunk is called, the underlying GPU writes may be deferred (e.g. for a buffer resize). The ChunkWritten event fires once per primary chunk after all of its sub-chunks (primary + overflow) have completed their writes, so you can safely call GetInstanceLink afterward.
tracker.ChunkWritten += (chunkId) =>
{
var link = tracker.GetInstanceLink(myChunkLink, 0);
// cache or modify the instance
};
Difference from BRGRegisterer
BRGRegisterer is a thin MonoBehaviour wrapper around BRGRegistrationTracker. It adds:
- Automatic
Initialize()onOnEnable,Shutdown()onOnDisable - Editor support via
[ExecuteAlways]with deferred initialization - Abstract
OnInitialize()/OnShutdown()hooks via the callback system - A virtual
OnChunkWritten(int)hook for theChunkWrittenevent - Protected method forwarding so subclasses don't touch the tracker directly
If your registration logic lives in a MonoBehaviour, use Custom Registerer. If it doesn't, use BRGRegistrationTracker directly.