Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.jetbrains.skia.*
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.internal.fastForEach
import org.jetbrains.skiko.redrawer.Direct3DRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.redrawer.RedrawerManager
import java.awt.*
import java.awt.Color
import java.awt.Component
import java.awt.Dimension
import java.awt.Graphics
import java.awt.Point
import java.awt.event.*
Expand All @@ -36,10 +36,11 @@ actual open class SkiaLayer internal constructor(
private val analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
actual val pixelGeometry: PixelGeometry = PixelGeometry.UNKNOWN,
/**
* Whether this layer fills its entire host window. When true, platform-specific window-level
* optimizations may be used (e.g., on macOS, driving the interactive live-resize redraw from the
* window). Set to false when the layer is embedded as a Swing component somewhere in the window's
* hierarchy rather than covering the whole window, so those window-level paths are disabled.
* Whether this layer fills its entire host window.
*
* When true, platform-specific window-level optimizations may be used (e.g., on macOS, driving the interactive
* live-resize redraw from the window). Set to false when the layer is embedded as a Swing component somewhere in
* the window's hierarchy rather than covering the whole window, so those window-level paths are disabled.
*/
internal val fillsWindow: Boolean = false,
) : JComponent(), Accessible {
Expand Down Expand Up @@ -124,7 +125,7 @@ actual open class SkiaLayer internal constructor(
@Suppress("DEPRECATION")
super.reshape(x, y, width, height)

redrawer?.onPlatformComponentResized()
redrawer?.onLayerComponentResized()
}

override fun getInputMethodRequests(): InputMethodRequests? {
Expand All @@ -144,8 +145,8 @@ actual open class SkiaLayer internal constructor(
return canReceiveFocus(cause) && super.requestFocusInWindow(cause)
}

private fun canReceiveFocus(cause: FocusEvent.Cause?) = cause != FocusEvent.Cause.MOUSE_EVENT ||
isRequestFocusEnabled
private fun canReceiveFocus(cause: FocusEvent.Cause?) =
cause != FocusEvent.Cause.MOUSE_EVENT || isRequestFocusEnabled
}
@Suppress("LeakingThis")
add(backedLayer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,23 @@ internal class MetalContextHandler(
makeMetalContext(device.ptr)
)

/** Presents the frame asynchronously (off the main thread). Used for every frame outside a live resize. */
fun finishFrame() = finishFrame(device.ptr)
/**
* Presents the frame asynchronously (off the AppKit main thread).
*/
fun finishFrameAsync() = finishFrameAsync(device.ptr)

/**
* Presents the frame synchronously, joining the ambient window-resize transaction.
* Must be called on the AppKit main thread during a live resize.
* Presents the frame synchronously, in the calling thread.
*
* Used in two scenarios:
* - During live-resize it is called on the AppKit main thread, to join the ambient window-resize transaction.
* - Before showing the window, it is called while the layer is already displayable (but not yet showing), so the
* window's first on-screen frame draws content instead of flashing its background.
*/
fun finishFrameInLiveResize() = finishFrameInLiveResize(device.ptr)
fun finishFrameSync() = finishFrameSync(device.ptr)

private external fun makeMetalContext(device: Long): Long
private external fun makeMetalRenderTarget(device: Long, width: Int, height: Int): Long
private external fun finishFrame(device: Long)
private external fun finishFrameInLiveResize(device: Long)
private external fun finishFrameAsync(device: Long)
private external fun finishFrameSync(device: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,26 @@ internal abstract class AWTRedrawer(
check(!isDisposed) { "${this.javaClass.simpleName} is disposed" }
}

override fun onLayerComponentResized() {
syncBoundsFromPlatformComponent()

if (!layer.isShowing && layer.isDisplayable && (layer.width > 0) && (layer.height > 0)) {
renderBeforeShown()
return
}

needRender(throttledToVsync = false)
}

/**
* Renders and presents a frame when the layer is already displayable but not yet showing.
* This is needed so we have a frame ready when the window is first shown, to prevent the window background
* flashing.
*/
protected open fun renderBeforeShown(): Boolean {
renderImmediately()
return true
}

override fun isTransparentBackgroundSupported() = defaultIsTransparentBackgroundSupported(layer)
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ internal class Direct3DRedrawer(
super.dispose()
}

override fun onPlatformComponentResized() {
override fun onLayerComponentResized() {
// During live resize, the layer tells us its size directly; the AWT size is not in sync
if (!isHandlingLiveResizeNow) {
super.onPlatformComponentResized()
super.onLayerComponentResized()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ internal class MetalRedrawer(
}
}

override fun renderBeforeShown(): Boolean {
checkDisposed()
update()
inDrawScope {
if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
performDraw(finishFrame = false)
}
}
performNativeDrawAction {
contextHandler.finishFrameSync()
}
return true
}

private suspend fun draw() {
inDrawScope {
// Move drawing to another thread to free the main thread
Expand Down Expand Up @@ -175,14 +189,10 @@ internal class MetalRedrawer(
}

private fun LayerDrawScope.performDraw(finishFrame: Boolean = true) {
synchronized(drawLock) {
if (!isDisposed) {
autoreleasepool {
contextHandler.draw()
if (finishFrame) {
contextHandler.finishFrame()
}
}
performNativeDrawAction {
contextHandler.draw()
if (finishFrame) {
contextHandler.finishFrameAsync()
}
}
}
Expand All @@ -208,10 +218,10 @@ internal class MetalRedrawer(
}
}

override fun onPlatformComponentResized() {
override fun onLayerComponentResized() {
// During live resize, the layer tells us its size directly; the AWT size is not in sync
if (!isHandlingLiveResizeNow) {
super.onPlatformComponentResized()
super.onLayerComponentResized()
}
}

Expand Down Expand Up @@ -244,7 +254,7 @@ internal class MetalRedrawer(
// The present must run on the AppKit main thread to join the resize transaction
synchronized(drawLock) {
if (!isDisposed) {
contextHandler.finishFrameInLiveResize()
contextHandler.finishFrameSync()
}
}
}
Expand Down Expand Up @@ -280,6 +290,19 @@ internal class MetalRedrawer(
setLayerVisible(device.ptr, isVisible)
}

/**
* Wraps [block] in the necessary machinery needed to call native, drawing-related, code.
*/
private inline fun performNativeDrawAction(block: () -> Unit) {
synchronized(drawLock) {
if (!isDisposed) {
autoreleasepool { // This is needed only if the call is not on the AppKit thread
block()
}
}
}
}

private external fun createMetalDevice(window: Long, transparency: Boolean, frameBuffering: Int, adapter: Long, platformInfo: Long, liveResizeEnabled: Boolean): Long
private external fun disposeDevice(device: Long)
private external fun resizeLayers(device: Long, x: Int, y: Int, width: Int, height: Int)
Expand Down
46 changes: 24 additions & 22 deletions skiko/src/awtMain/objectiveC/macos/MetalContextHandler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ static void finishFrame(jlong devicePtr, void (^present)(MetalDevice *device, id
}
}

/// Presents the current drawable asynchronously — skiko's default, used for every frame outside a live
/// resize. Called off the AppKit main thread (from the background frame loop) so present work doesn't
/// Presents the current drawable asynchronously — the default, used under normal circumstances.
/// Called off the AppKit main thread (from the background frame loop) so present work doesn't
/// destabilize FPS on the main thread.
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_context_MetalContextHandler_finishFrame(
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_context_MetalContextHandler_finishFrameAsync(
JNIEnv *env, jobject contextHandler, jlong devicePtr)
{
finishFrame(devicePtr, ^(MetalDevice *device, id<CAMetalDrawable> currentDrawable, id<MTLCommandBuffer> commandBuffer) {
Expand Down Expand Up @@ -120,33 +120,35 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_context_MetalContextHandler_fini
});
}

/// Presents the current drawable synchronously, joining the ambient window-resize CATransaction.
/// Must be called on the AppKit main thread during a live resize (from drawFrameWhileLiveResizing),
/// where the ambient CATransaction — from AWTMetalLayer.setBounds, committing the window's new size —
/// flushes the present. This is the sole presenter for the duration of the resize.
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_context_MetalContextHandler_finishFrameInLiveResize(
/// Presents the current drawable synchronously
/// This is used:
/// - During live-resize, on the AppKit main thread, to join the ambient resize transaction
/// - When the window is displayable but not yet shown, to make sure the first visible frame already has the content,
/// avoiding flashing the layer background.
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_context_MetalContextHandler_finishFrameSync(
JNIEnv *env, jobject contextHandler, jlong devicePtr)
{
finishFrame(devicePtr, ^(MetalDevice *device, id<CAMetalDrawable> currentDrawable, id<MTLCommandBuffer> commandBuffer) {
/// presentsWithTransaction is YES for the whole resize session — the live-resize start
/// observer (MetalRedrawer.mm) sets it, the end observer clears it. Holding it layer-wide is
/// safe because during a resize the main thread is the sole presenter; the only frames that
/// could reach the async finishFrame path are stragglers, and they're dropped there rather
/// than presenting under YES.
/// During live-resize:
/// presentsWithTransaction is YES for the whole resize session — the live-resize start
/// observer (MetalRedrawer.mm) sets it, the end observer clears it. Holding it layer-wide is
/// safe because during a resize the main thread is the sole presenter; the only frames that
/// could reach the async finishFrame path are stragglers, and they're dropped there rather
/// than presenting under YES.
/// Present synchronously so the drawable swap joins the ambient window resize transaction
/// (no nested begin/commit — that would split it back out). commit + waitUntilScheduled
/// guarantees the drawing command buffer (submitted by Skia earlier, ahead of this one in
/// the queue) is scheduled first.
///
/// Present synchronously so the drawable swap joins the ambient window resize transaction
/// (no nested begin/commit — that would split it back out). commit + waitUntilScheduled
/// guarantees the drawing command buffer (submitted by Skia earlier, ahead of this one in
/// the queue) is scheduled first.
///
/// No drawable-vs-layer size guard is needed here (unlike the async finishFrame path): setBounds
/// set drawableSize, then we acquired the drawable, rendered, committed and present it all
/// synchronously on this thread inside one transaction, so the layer size cannot change underneath.
/// In draw-before-visible:
/// commit + waitUntilScheduled guarantees the drawing command buffer (submitted by Skia earlier,
/// ahead of this one in the queue) is scheduled first, so the present below can't outrun the
/// rendering.
[commandBuffer commit];
[commandBuffer waitUntilScheduled];
[currentDrawable present];
});
}

} // extern C
#endif
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.jetbrains.skiko

import org.jetbrains.skia.Canvas
import org.jetbrains.skia.FilterTileMode
import org.jetbrains.skia.ImageFilter
import org.jetbrains.skia.Paint
import org.jetbrains.skia.Rect
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.Point
import javax.swing.JWindow
import javax.swing.SwingUtilities

/**
* Standalone harness launched in a FRESH JVM by the `no window flash on first show` test.
*
* The first-show background flash only reproduces on the very first window displayed in a process, so the
* test cannot observe it from its own (long-since-warmed-up) JVM. Instead it spawns this harness per run:
* a brand-new process whose single window is genuinely its first, using the render API and color passed in
* argv (`renderApi rgb`). The window bounds are hard-coded to cover the pixel the parent samples.
*
* The frame it draws is deliberately made expensive by repeated full-window blur passes, forcing real GPU work
* to stress the fix as hard as possible.
*/
object FirstWindowShowFlashHelper {
// Hard-coded window bounds. The parent (SkiaLayerTest."no window flash on first show") positions its
// background window so that the pixel it samples (the background's center) falls within these bounds.
private val WINDOW_LOCATION = Point(400, 400)
private val WINDOW_SIZE = Dimension(600, 600)

@JvmStatic
fun main(args: Array<String>) {
val renderApi = GraphicsApi.valueOf(args[0])
val color = Color(args[1].toInt())

SwingUtilities.invokeLater {
val layer = SkiaLayer(properties = SkiaLayerProperties(renderApi = renderApi))
layer.renderDelegate = object : SkikoRenderDelegate {
private val paint = Paint().also { it.color = color.rgb }
private val loadFill = Paint().also { it.color = Color.GRAY.rgb }
private val layerPaint = Paint().also {
it.imageFilter = ImageFilter.makeBlur(30f, 30f, FilterTileMode.CLAMP)
}

override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
val rect = Rect(0f, 0f, width.toFloat(), height.toFloat())
repeat(1000) {
canvas.saveLayer(rect, layerPaint)
canvas.drawRect(rect, loadFill)
canvas.restore()
}
canvas.drawRect(rect, paint)
}
}

// Use JWindow, not JFrame because the latter is shown with a fade-in animation on Windows,
// which breaks the color comparison in the test
JWindow().apply {
contentPane.add(layer, BorderLayout.CENTER)
location = WINDOW_LOCATION
size = WINDOW_SIZE
// Ensure our window is above the parent's background window
isAlwaysOnTop = true
isVisible = true
}
}
}
}
Loading