Skip to content

Fix Android Vulkan blit synchronization and pipeline blits - #186

Draft
ruccho wants to merge 3 commits into
feature/native-loggingfrom
feature/android-vulkan-blit-fixes
Draft

Fix Android Vulkan blit synchronization and pipeline blits#186
ruccho wants to merge 3 commits into
feature/native-loggingfrom
feature/android-vulkan-blit-fixes

Conversation

@ruccho

@ruccho ruccho commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #174 (feature/native-logging); bb8bbc8 switches the new code from println! to the log crate introduced there.

Fixes two problems in the Android Vulkan blit path (unienc_android_mc), the readback-free path that renders the source texture into a HardwareBuffer handed to MediaCodec:

  1. Rendering artifacts in the host project — reported as striped artifacts in dynamic shadows that appear on some frames and not others. The blit is issued right before the next frame's shadow caster pass, and the plugin event was submitting to Unity's graphics queue without the synchronization Unity requires, alongside a few Vulkan spec violations.
  2. Recording frame rate lower than the readback path while the game's frame rate is unaffected. Each push awaited the whole GPU round trip and pushes are serialized, so the encoder could only accept one frame per round trip (several frames of latency).

Synchronization with Unity and spec fixes (47e7994)

  • The plugin event was configured with GraphicsQueueAccess::Allow and flags = ModifiesCommandBuffersState only, dropping the default EnsurePreviousFrameSubmission. vkQueueSubmit from the render thread could therefore race Unity's submission thread. The event now uses DontCare with the default flags, and the submission goes through IUnityGraphicsVulkan::AccessQueue(flush = true), which Unity invokes where queue access is guaranteed and after its own pending command buffers are submitted.
  • The source VkImage was read from the raw GetNativeTexturePtr() pointer with no layout transition or barrier while the descriptor declared SHADER_READ_ONLY_OPTIMAL. It is now acquired through AccessTexture(..., PipelineBarrier), so Unity records the transition and keeps tracking the layout. The VkFormat reported by Unity replaces the GraphicsFormat mapping table.
  • vkFreeCommandBuffers ran on a blocking-pool thread while the render thread allocated from the same VkCommandPool (externally synchronized object). Command buffers are now taken from a mutex-guarded pool and recycled after the fence signals instead of being freed.
  • The render pass' final_layout was PRESENT_SRC_KHR while the following release barrier declared old_layout = COLOR_ATTACHMENT_OPTIMAL; both are now COLOR_ATTACHMENT_OPTIMAL.
  • The queue family ownership release to VK_QUEUE_FAMILY_EXTERNAL used VK_QUEUE_FAMILY_IGNORED as the source; it now uses Unity's graphics queue family.

Pipelined blits (6bb84fe)

  • push returns as soon as the graphics event is issued. A single completion task awaits the outstanding blits in issue order and queues each frame to MediaCodec, so successive frames overlap on the GPU and the recording frame rate follows the frame provider instead of the round-trip latency.
  • In-flight frames are bounded by a semaphore (MAX_BLITS_IN_FLIGHT = 2, below the ImageWriter's max_images = 3) so dequeueInputImage never has to wait; the dequeue itself runs on the blocking pool.
  • End of stream is signalled by the completion task once the pending frames have been queued (previously done synchronously in Drop). Errors from the completion task are surfaced by the next push.

Testing

  • cargo ndk -t aarch64-linux-android --platform 26 build / clippy for unienc_android_mc pass with no new warnings; host cargo check -p unienc_c passes.
  • Verified on an Android device.

🤖 Generated with Claude Code

ruccho and others added 3 commits September 11, 2026 18:45
…und trip

On Android the blit path awaited the whole round trip inside `push`:
the graphics event is drained once per frame on the C# side, the render
thread executes it later, and the fence signals only after the GPU has
also finished the work Unity submitted before it. Pushes are serialized
by the encoder input lock, so the recording frame rate was capped at one
frame per round trip (several frames), while the readback path overlaps
its requests and was not affected.

`push` now returns as soon as the event is issued. A completion task
awaits the outstanding blits in issue order and queues each frame to
MediaCodec, so successive frames overlap on the GPU. In-flight frames are
bounded by a semaphore below the ImageWriter's max images so that
`dequeueInputImage` never has to wait, and the dequeue itself runs on the
blocking pool instead of an executor worker.

End of stream is signalled by the completion task once the pending
frames have been queued, since `Drop` cannot wait for them. Errors from
the completion task are surfaced by the next `push`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ruccho
ruccho force-pushed the feature/android-vulkan-blit-fixes branch from 96d6f39 to bb8bbc8 Compare September 11, 2026 09:47
@ruccho
ruccho changed the base branch from main to feature/native-logging September 11, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant