Recently I’ve come across some weird things about Unity that you might never ever need to know. But I think I’d still put them here in case one day I might need to recall them. These knowledges are small enough to not be worth separate posts, so, well, let them grow in here then.
1. Graphics Buffers are not mapped to persistent physical memory
Whenever you create a ComputeBuffer or GraphicsBuffer, what you have in hand is not always the same chunk of memory. Unity may reallocate the underlying physical memory, but keeps the managed buffer reference intact.
In other words, the managed reference of ComputeBuffer or GraphicsBuffer is always the same, but the underlying physical memory address may change without Unity telling you this. Calling GetNativePtr might give you different results even on the same ComputeBuffer.
I discovered this when I tried to develop a tool that tracks buffer allocation and deallocation. Back then, I thought the physical memory address is persistent (via GetNativePtr), but then I noticed a significant amount of non-released buffers in my tool, despite walking through the debugger it shows that all these buffers’ Dispose methods are properly called. Well, when I match a dispose call with a creation call, I checked two things - managed reference address and the native pointer. And this is the moment I found that despite the managed reference stays the same, the native pointer had changed.
Well, what does it mean? It means you should never cache the native pointer. In day-to-day Unity development, this is almost irrelevant. But this is crucial when it comes to Native Render Plugins, where you pass this pointer to your plugin. You must always call GetNativePtr just before calling your plugin code.
… Which means there might be another artifact - two different managed references pointing to the same piece of physical memory. This is fairly common in render graph implementations - if two logical resources have compatible descriptors and their lifespans do not overlap, then we could alias them to the same physical memory. But this is just my educated guess - I haven’t encountered a case where this knowledge becomes significant when using ComputeBuffer or GraphicsBuffer.
2. GPU Flush to Zero
When a floating point number is too small, it gets flushed to 0. This is a logical way to combat precision issues related to floats, but it would also induce subtle bugs that could be hard to track if you are not aware of it.
I noticed this behavior when trying to pack data on the CPU side and send the data to the GPU. Essentially, I have 2 halves, and I pack them to float. When the first half is 0, obviously, the packed float becomes extraordinarily small. While this number is preserved when passed to the GPU, the GPU simply thinks it is noise and flushes the entire float to 0, making the second half erroneously 0 as well. In RenderDoc, the symptom would be that the value becomes 0 after stepping through any arithmetic on the said value.
If you want finer technical details, here’s the NVIDIA article on denormal flushing.
Simply put, do not use floats to pack values; always use unsigned integers.