The software rasterizer has a recovery path for running out of code space:
SetupDraw returns false, the caller resets the cache and asks again. It has
never been able to run.
ReserveMemory only had a pxAssert, which is compiled out of a release build,
so it always handed back a pointer. GetDefaultFunction has no other way to
fail, so SetupDraw could not return false, so ResetCodeCache never ran. What
happens instead is that the bump pointer walks off the end of the reserve,
and since the software renderer sits last in the code arena, that is the end
of the arena.
It would not have worked if it had run, either. Clear emptied the codegen map
and rewound the pointer but left the active map holding pointers into memory
about to be handed out again, so the next lookup would have jumped into
whatever replaced it.
So: ReserveMemory reports full, Clear drops the active map along with the
codegen map, and a null is deliberately not cached on the way out. That last
one matters more than it looks. The active map is consulted before anything
else, so an entry cached during the failure would have survived the reset
that was supposed to fix it and gone on answering null for that selector for
the rest of the run.
Nothing here is iOS specific. It reads the same on every platform, we are
just the ones with a reason to have been looking.