This specification is maintained by the AgentRules Working Group. Have enhancements or want to claim maintainership?Submit Specification →
mobilecritical PriorityID: kmp-hardware-surface-synchronization

Deterministic 120Hz Surface Sync and Teardown Synchronization in KMP Compose

Enforces synchronous lifecycle fencing and atomic frame pacing between Compose Multiplatform render loops and native display surfaces (Android SurfaceView/Choreographer and iOS CAMetalLayer/CADisplayLink). Prevents BufferQueue abandonment, SIGSEGV crashes on invalidated ANativeWindow handles, and CAMetalLayer presentation on detached layers during 120Hz ProMotion transitions.

Prohibited Anti-Pattern: Asynchronous surface release racing against active frame-pacing threads during composition teardown

Export Ready-to-Drop Configuration

Select format below to copy code or download file directly.

---
description: Deterministic 120Hz Surface Sync and Teardown Synchronization in KMP Compose - Enforces synchronous lifecycle fencing and atomic frame pacing between Compose Multiplatform render loops and native display surfaces (Android SurfaceView/Choreographer and iOS CAMetalLayer/CADisplayLink). Prevents BufferQueue abandonment, SIGSEGV crashes on invalidated ANativeWindow handles, and CAMetalLayer presentation on detached layers during 120Hz ProMotion transitions.
globs: **/commonMain/**/render/*.kt, **/androidMain/**/surface/*.kt, **/iosMain/**/metal/*.kt, **/platform/**/display/*.kt
alwaysApply: true
verified_by: "agentrules.org"
license: "CC-BY-4.0"
source: "https://agentrules.org/rules/kmp-hardware-surface-synchronization"
---

# Deterministic 120Hz Surface Sync and Teardown Synchronization in KMP Compose

> **Severity**: CRITICAL | **Category**: mobile
> **Anti-Pattern Warning**: Asynchronous surface release racing against active frame-pacing threads during composition teardown

## System Constraints
You are a Principal Mobile Graphics Architect enforcing memory safety, zero-tearing, and deterministic 120Hz ProMotion/Choreographer synchronization in Compose Multiplatform. You must prevent native surface teardown race conditions by enforcing synchronous pipeline draining, atomic surface state machines, and lifecycle-gated frame dispatch across Android SurfaceView and iOS CAMetalLayer implementations.

## Strict Guidelines
- Engineers MUST ALWAYS implement a synchronous hardware fence in surfaceDestroyed (Android) and viewWillDisappear/deinit (iOS) that joins or halts the render loop BEFORE the native surface is freed by the platform windowing subsystem.
- Engineers MUST NOT issue GPU commands or enqueue buffer swap requests against an ANativeWindow or CAMetalLayer without first holding a non-blocking read-lock on the surface lifecycle token.
- Code SHOULD dynamically configure CADisplayLink (iOS) with CAFrameRateRange(minimum = 60.0, preferred = 120.0, maximum = 120.0) and link Choreographer.FrameCallback (Android) to the Window's preferredDisplayModeId to maintain variable refresh rate (VRR) alignment.
- Services MUST atomically transition the rendering state machine to `TornDown` prior to detaching native surface buffers, rejecting any pending or in-flight composition frames deterministically.
- Components NEVER execute native buffer swaps or call eglSwapBuffers/commitCommandBuffer inside asynchronous Coroutine dispatchers that lack cancellation coordination with the platform VSYNC callback.

## Bad vs Good Implementations
### Android SurfaceView Teardown Synchronization

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: surfaceDestroyed returns immediately while an uncoordinated background coroutine continues to invoke native graphics routines on the destroyed Surface, triggering SIGSEGV or 'BufferQueue has been abandoned' crashes.
```kotlin
class UnsafeSurfaceRenderer : SurfaceHolder.Callback {
    private val coroutineScope = CoroutineScope(Dispatchers.Default)
    private var surface: Surface? = null

    override fun surfaceCreated(holder: SurfaceHolder) {
        surface = holder.surface
        coroutineScope.launch {
            while (isActive) {
                // Race condition: continues drawing after surfaceDestroyed returns
                surface?.let { nativeRenderToSurface(it) }
                delay(8) // Rough 120Hz poll without Choreographer sync
            }
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // BUG: Releases handle asynchronously; nativeRenderToSurface crashes with BufferQueue abandoned
        coroutineScope.launch {
            surface = null
        }
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) {}
    private external fun nativeRenderToSurface(surface: Surface)
}
```

#### Production Standard (Correct)
> **Standard**: Acquires an exclusive write lock in surfaceDestroyed to guarantee all active Choreographer frame callbacks drain completely before native resources and buffer allocations are freed.
```kotlin
class SafeSurfaceRenderer(private val choreographer: Choreographer) : SurfaceHolder.Callback {
    private val renderLock = java.util.concurrent.locks.ReentrantReadWriteLock()
    @Volatile private var isAttached = false
    private var nativeHandle: Long = 0L

    private val frameCallback = object : Choreographer.FrameCallback {
        override fun doFrame(frameTimeNanos: Long) {
            renderLock.readLock().lock()
            try {
                if (isAttached && nativeHandle != 0L) {
                    nativeRenderFrame(nativeHandle, frameTimeNanos)
                    choreographer.postFrameCallback(this)
                }
            } finally {
                renderLock.readLock().unlock()
            }
        }
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        renderLock.writeLock().lock()
        try {
            nativeHandle = nativeInitSurface(holder.surface)
            isAttached = true
            choreographer.postFrameCallback(frameCallback)
        } finally {
            renderLock.writeLock().unlock()
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Synchronously halt frame callback and drain rendering pipeline before callback returns
        choreographer.removeFrameCallback(frameCallback)
        renderLock.writeLock().lock()
        try {
            isAttached = false
            nativeTeardownSurface(nativeHandle)
            nativeHandle = 0L
        } finally {
            renderLock.writeLock().unlock()
        }
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) {
        renderLock.writeLock().lock()
        try {
            if (nativeHandle != 0L) nativeResize(nativeHandle, w, h)
        } finally {
            renderLock.writeLock().unlock()
        }
    }

    private external fun nativeInitSurface(surface: Surface): Long
    private external fun nativeRenderFrame(handle: Long, frameTimeNanos: Long)
    private external fun nativeResize(handle: Long, w: Int, h: Int)
    private external fun nativeTeardownSurface(handle: Long)
}
```

### iOS CAMetalLayer DisplayLink 120Hz ProMotion Safe Lifecycle

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: Failing to synchronously call invalidate() and retain-cycle cleanups on CADisplayLink allows pending display refresh events to invoke nextDrawable() on a layer whose backing framebuffer is being dismantled, causing Metal GPU validation assertions.
```kotlin
class MetalLayerCoordinator(private val metalLayer: CAMetalLayer) {
    private var displayLink: CADisplayLink? = null

    fun start() {
        displayLink = CADisplayLink.displayLinkWithTarget(
            target = this,
            selector = NSSelectorFromString("renderFrame")
        )
        displayLink?.addToRunLoop(NSRunLoop.mainRunLoop, NSRunLoopCommonModes)
    }

    fun onDeinit() {
        // BUG: Fails to invalidate displayLink synchronously on the owning runloop
        displayLink?.paused = true
    }

    @ObjCAction
    fun renderFrame() {
        // Metal drawable acquisition races with layer destruction
        val drawable = metalLayer.nextDrawable() ?: return
        // Draw frame...
    }
}
```

#### Production Standard (Correct)
> **Standard**: Sets up deterministic 120Hz frame ranges, validates atomic run state within an autoreleasepool, and synchronously drains in-flight GPU commands while invalidating CADisplayLink before layer destruction.
```kotlin
class MetalLayerCoordinator(
    private val metalLayer: CAMetalLayer,
    private val commandQueue: MTLCommandQueueProtocol
) {
    private var displayLink: CADisplayLink? = null
    private val isRenderingActive = atomic(false)

    fun start() {
        val link = CADisplayLink.displayLinkWithTarget(
            target = this,
            selector = NSSelectorFromString("renderFrame:")
        )
        // Enforce 120Hz ProMotion range via iOS 15+ API
        link.preferredFrameRateRange = CAFrameRateRangeMake(60.0f, 120.0f, 120.0f)
        link.addToRunLoop(NSRunLoop.mainRunLoop, NSRunLoopCommonModes)
        displayLink = link
        isRenderingActive.value = true
    }

    fun stopSynchronously() {
        isRenderingActive.value = false
        displayLink?.let {
            it.invalidate()
            it.removeFromRunLoop(NSRunLoop.mainRunLoop, NSRunLoopCommonModes)
        }
        displayLink = null
        // Drain GPU command buffer pipeline synchronously
        val drainBuffer = commandQueue.commandBuffer()
        drainBuffer?.commit()
        drainBuffer?.waitUntilCompleted()
    }

    @ObjCAction
    fun renderFrame(link: CADisplayLink) {
        if (!isRenderingActive.value) return
        autoreleasepool {
            val drawable = metalLayer.nextDrawable() ?: return@autoreleasepool
            // Dispatch frame synchronized with ProMotion VSYNC
        }
    }
}
```

Recommended MCP Servers (mcp-serverlist.com)

Equip agent with system tools to verify code for this stack.

Recommended Agent Skills (askills.dev)

Ready-to-use executable skill functions tailored to this stack.

Architectural Specification & Invariant Rationale

Architectural Invariant Design: Deterministic 120Hz Surface Sync and Teardown Synchronization in KMP Compose

Deterministic 120Hz Surface Sync and Teardown Synchronization in KMP Compose defines an industrial-grade behavioral standard for mobile engineering. Enforces synchronous lifecycle fencing and atomic frame pacing between Compose Multiplatform render loops and native display surfaces (Android SurfaceView/Choreographer and iOS CAMetalLayer/CADisplayLink). Prevents BufferQueue abandonment, SIGSEGV crashes on invalidated ANativeWindow handles, and CAMetalLayer presentation on detached layers during 120Hz ProMotion transitions. Modern software architectures require strict adherence to deterministic patterns to guarantee high availability, maintainability, and zero security vulnerabilities.

Framework Paradigm Shift & Drift MechanicsLLMs are trained on millions of open-source repositories containing outdated code, legacy idioms, and unpatched antipatterns. When coding assistants generate implementations for this stack, they default to common statistical tokens: Asynchronous surface release racing against active frame-pacing threads during composition teardown Without an explicit system instruction file, developers spend hours debugging silent failures.
Failure Modes & Production Blast Radius

Hazard Analysis: What Breaks Without This Rule

The following failure modes have been cataloged from hundreds of hours of production code reviews where AI assistants produced non-compliant implementations:

Failure ModeAI Symptom (The Anti-Pattern)SeverityProduction Impact & Remedy
AI Hallucination & Deprecation DriftAsynchronous surface release racing against active frame-pacing threads during composition teardownCritical
Violating this industrial standard introduces regression bugs, security risks, and unexpected runtime failures in production.
Enforce explicit behavioral system directives across all AI coding assistants in your repository.
Silent Architecture InconsistencyAI assistants produce mixed paradigms across files, creating fragmented codebase conventions.Medium
Increases cognitive load during code reviews and leads to maintenance debt across engineering teams.
Pin project conventions in .cursor/rules, CLAUDE.md, and AGENTS.md so all team agents follow identical guardrails.
Architectural Directives in Detail

Step-by-Step Production Directives

Each rule constraint below is injected into the AI agent's system prompt to establish deterministic boundaries:

1

Engineers MUST ALWAYS implement a synchronous hardware fence in surfaceDestroyed (Android) and viewWillDisappear/deinit (iOS) that joins or halts the render loop BEFORE the native surface is freed by the platform windowing subsystem.

Engineers MUST ALWAYS implement a synchronous hardware fence in surfaceDestroyed (Android) and viewWillDisappear/deinit (iOS) that joins or halts the render loop BEFORE the native surface is freed by the platform windowing subsystem.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
2

Engineers MUST NOT issue GPU commands or enqueue buffer swap requests against an ANativeWindow or CAMetalLayer without first holding a non-blocking read-lock on the surface lifecycle token.

Engineers MUST NOT issue GPU commands or enqueue buffer swap requests against an ANativeWindow or CAMetalLayer without first holding a non-blocking read-lock on the surface lifecycle token.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
3

Code SHOULD dynamically configure CADisplayLink (iOS) with CAFrameRateRange(minimum = 60.0, preferred = 120.0, maximum = 120.0) and link Choreographer.FrameCallback (Android) to the Window's preferredDisplayModeId to maintain variable refresh rate (VRR) alignment.

Code SHOULD dynamically configure CADisplayLink (iOS) with CAFrameRateRange(minimum = 60.0, preferred = 120.0, maximum = 120.0) and link Choreographer.FrameCallback (Android) to the Window's preferredDisplayModeId to maintain variable refresh rate (VRR) alignment.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
4

Services MUST atomically transition the rendering state machine to `TornDown` prior to detaching native surface buffers, rejecting any pending or in-flight composition frames deterministically.

Services MUST atomically transition the rendering state machine to `TornDown` prior to detaching native surface buffers, rejecting any pending or in-flight composition frames deterministically.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
5

Components NEVER execute native buffer swaps or call eglSwapBuffers/commitCommandBuffer inside asynchronous Coroutine dispatchers that lack cancellation coordination with the platform VSYNC callback.

Components NEVER execute native buffer swaps or call eglSwapBuffers/commitCommandBuffer inside asynchronous Coroutine dispatchers that lack cancellation coordination with the platform VSYNC callback.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
Technical Questions & Answers

Frequently Asked Technical Questions

Common questions developers face when configuring AI coding assistants for this stack:

Q:Why is Deterministic 120Hz Surface Sync and Teardown Synchronization in KMP Compose critical for AI coding assistants?

Enforces synchronous lifecycle fencing and atomic frame pacing between Compose Multiplatform render loops and native display surfaces (Android SurfaceView/Choreographer and iOS CAMetalLayer/CADisplayLink). Prevents BufferQueue abandonment, SIGSEGV crashes on invalidated ANativeWindow handles, and CAMetalLayer presentation on detached layers during 120Hz ProMotion transitions. Without explicit behavioral boundaries, AI coding assistants (Cursor, Claude Code, Windsurf, Codex) rely on training data averages, which frequently produce deprecated, inefficient, or vulnerable code patterns.

Q:How does adopting this rule prevent AI hallucinations?

This rule explicitly declares what the AI must avoid (Asynchronous surface release racing against active frame-pacing threads during composition teardown) and prescribes verified positive examples. Grounding the LLM with negative and positive pairs eliminates guesswork.

Q:Which AI coding environments are supported?

This specification is multi-platform compatible: Cursor (.cursor/rules/*.mdc & .cursorrules), Anthropic Claude Code (CLAUDE.md), Windsurf (.windsurfrules), Codex / Cline (AGENTS.md), and GitHub Copilot (.github/copilot-instructions.md).

Q:Can I combine this rule with other technology stacks?

Yes. Use the Interactive Stack Builder or download an Agent Pack to merge this rule with other frontend, backend, database, security, and DevOps rules without syntax collision.

Open Specification · CC BY 4.0·Free for Personal, Team & Commercial In-Repo Use

Citation & Open Source Attribution Guidelines

This specification is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). You are completely free to adapt and share this rule in your workspace. If citing, including in public AI benchmark datasets, or redistributing, attribution to AgentRules.org is required.

Related MOBILE Rules

View all rules →
1 Rule in Stack

Deterministic