fix: tick native watchdog before SetFormat to prevent black screen - #355
Conversation
When the surface is born as RGB565 on Vulkan, the RGB565 guard in SurfaceChanged calls SetFormat(RGBA8888) to trigger a surface recreate. Previously, this reset surfaceEvent and returned early, blocking the draw thread on surfaceEvent.Wait(5000) while the UI thread was stuck in the synchronous SetFormat call. This blocked the entire managed runtime from sending heartbeats, causing the native watchdog to fire at 10s and producing a black screen. The fix sets surfaceEvent BEFORE calling SetFormat, so the draw thread can proceed with the current (soon-to-be-recreated) surface. The new surface triggers another SurfaceChanged which sets surfaceEvent again with the correct RGBA8888 format. Fixes the issue where the APK shows a black screen after the last build.
When the surface is born as RGB565 on Vulkan, the RGB565 guard in SurfaceChanged calls SetFormat(RGBA8888) which triggers a synchronous surface teardown on the UI thread. This blocks the UI thread for hundreds of milliseconds, preventing the Update thread from sending heartbeats to the native watchdog. The native watchdog (10s default) then fires, killing the process and producing a black screen. The fix calls NativeWatchdog.Heartbeat() right before SetFormat to reset the watchdog timer from the UI thread. The native watchdog checks g_lastHeartbeatMonotonicSec which is updated by a simple atomic store — safe to call from any thread and never throws. This is safer than the previous approach of setting surfaceEvent before SetFormat, which introduced a data race between the draw thread reading surfaceGlobalRef and SurfaceDestroyed freeing it.
|
Warning Review limit reached
Your plan currently allows 1 review/hour. Refill in 19 minutes and 27 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The previous commit removed surfaceEvent.Reset() which introduced a dangling pointer race: the draw thread could read surfaceGlobalRef while SurfaceDestroyed is freeing it. The fix keeps the NativeWatchdog.Heartbeat() call before SetFormat (to prevent the watchdog timeout) but restores the surfaceEvent.Reset() to prevent the draw thread from proceeding with a stale surface handle. The sequence is now: 1. Tick native watchdog (resets 10s timer) 2. surfaceEvent.Reset() (prevents draw thread from using stale handle) 3. SetFormat(RGBA8888) (triggers surface teardown) 4. return (prevents fall-through) The new surface triggers SurfaceChanged again which sets surfaceEvent with the correct RGBA8888 format.
On fresh installs where framework.ini doesn't exist yet, write 'Renderer = OpenGL' as the default instead of letting the framework fall through to Vulkan (which is the default for Automatic on Android when the GPU reports Vulkan support). Vulkan causes black screens on several Adreno GPU families (7xx series in particular) because the Veldrid Vulkan backend either times out its 5s SurfaceHandle poll or hands a stale ANativeWindow to vkCreateAndroidSurfaceKHR. Plus the synchronous SetFormat(RGBA8888) call blocks the UI thread, preventing managed heartbeats and triggering the native watchdog. OpenGL ES is the safer default; users can switch to Vulkan in Settings → Graphics → Renderer if their device handles it well. Also includes the NativeWatchdog.Heartbeat() tick before SetFormat (from previous commit) as a safety net for devices that already have Vulkan configured.
Problem
After the last build, the APK shows a black screen on launch. The app starts but never renders any frames.
Root Cause
The native crash log shows the watchdog fired with 'no managed heartbeat ever observed (Update thread did not tick)'. The sequence:
Fix
Call NativeWatchdog.Heartbeat() right before SetFormat to reset the watchdog timer from the UI thread. The native watchdog checks g_lastHeartbeatMonotonicSec which is updated by a simple atomic store — safe to call from any thread.
This approach was chosen over setting surfaceEvent before SetFormat because the latter introduces a data race: the draw thread could read surfaceGlobalRef while SurfaceDestroyed is freeing it, causing a use-after-free Vulkan driver crash.
Changes