Unverified Commit c5cdb928 authored by Nikolai Rykunov's avatar Nikolai Rykunov Committed by GitHub

Introduce Software and Metal offscreen rendering on Graphics for Swing interop (#720)

---------
Co-authored-by: 's avatarIgor Demin <igordmn@users.noreply.github.com>
parent e3b0ef8d
package SkiaAwtSample
import org.jetbrains.skiko.*
import org.jetbrains.skia.*
import org.jetbrains.skia.paragraph.*
import org.jetbrains.skiko.*
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.PI
class ClocksAwt(private val layer: SkiaLayer) : SkikoView {
open class ClocksAwt(private val scaleProvider: () -> Float) : SkikoView {
constructor(layer: SkiaLayer) : this({ layer.contentScale })
private val typeface = Typeface.makeFromFile("fonts/JetBrainsMono-Regular.ttf")
private val font = Font(typeface, 13f).apply {
edging = FontEdging.SUBPIXEL_ANTI_ALIAS
......@@ -84,7 +86,7 @@ class ClocksAwt(private val layer: SkiaLayer) : SkikoView {
}
val paragraph = ParagraphBuilder(style, fontCollection)
.pushStyle(TextStyle().setColor(0xFF000000.toInt()))
.addText("JRE: ${System.getProperty("java.vendor")}, ${System.getProperty("java.runtime.version")}, Graphics API: ${layer.renderApi} ✿゚ $currentSystemTheme")
.addText("JRE: ${System.getProperty("java.vendor")}, ${System.getProperty("java.runtime.version")} $currentSystemTheme")
.popStyle()
.build()
paragraph.layout(Float.POSITIVE_INFINITY)
......@@ -93,8 +95,9 @@ class ClocksAwt(private val layer: SkiaLayer) : SkikoView {
// Alpha layers test
val rectW = 100f
val rectH = 100f
val left = (width / layer.contentScale - rectW) / 2f
val top = (height / layer.contentScale - rectH) / 2f
val scale = scaleProvider()
val left = (width / scale - rectW) / 2f
val top = (height / scale - rectH) / 2f
val pictureRecorder = PictureRecorder()
val pictureCanvas = pictureRecorder.beginRecording(
Rect.makeLTRB(left, top, left + rectW, top + rectH)
......
package SkiaAwtSample
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.FPSCounter
import java.awt.BorderLayout
import java.awt.Color
import javax.swing.*
fun swingSkiaResize() = SwingUtilities.invokeLater {
val window = JFrame()
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
window.title = "Swing Window with Compose"
var skiaPanel: SkiaSwingPanel? = null
val fpsCounter = FPSCounter(logOnTick = true)
val skikoView = object : ClocksAwt({ skiaPanel!!.graphicsConfiguration.defaultTransform.scaleX.toFloat() }) {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
fpsCounter.tick()
super.onRender(canvas, width, height, nanoTime)
skiaPanel!!.repaint()
}
}
skiaPanel = SkiaSwingPanel(skikoView)
val leftPanel = JPanel().apply {
background = Color.CYAN
}
val rightPanel = JPanel(BorderLayout()).apply {
add(skiaPanel, BorderLayout.CENTER)
}
val splitter = JSplitPane(JSplitPane.HORIZONTAL_SPLIT).apply {
isContinuousLayout = true
leftComponent = leftPanel
rightComponent = rightPanel
}
window.add(splitter)
window.setSize(350, 200)
window.setLocationRelativeTo(null)
window.isVisible = true
}
\ No newline at end of file
package SkiaAwtSample
import org.jetbrains.skiko.ClipComponent
import org.jetbrains.skiko.SkikoView
import org.jetbrains.skiko.swing.SkiaSwingLayer
import java.awt.Color
import java.awt.Component
import javax.swing.JLayeredPane
class SkiaSwingPanel(skikoView: SkikoView) : JLayeredPane() {
val layer = SkiaSwingLayer(skikoView)
init {
layout = null
background = Color.white
}
override fun add(component: Component): Component {
layer.clipComponents.add(ClipComponent(component))
return super.add(component, Integer.valueOf(0))
}
override fun doLayout() {
layer.setBounds(0, 0, width, height)
}
override fun addNotify() {
super.addNotify()
super.add(layer, Integer.valueOf(10))
}
override fun removeNotify() {
layer.dispose()
super.removeNotify()
}
}
package SkiaAwtSample
import org.jetbrains.skiko.GenericSkikoView
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.*
import org.jetbrains.skiko.GenericSkikoView
fun makeButton(
text: String,
......@@ -57,7 +57,8 @@ fun swingSkia() = SwingUtilities.invokeLater {
),
BorderLayout.EAST
)
window.contentPane.add(
val southButtonsPanel = JPanel(BorderLayout())
southButtonsPanel.add(
makeButton(
text = "New Window",
action = {
......@@ -66,6 +67,16 @@ fun swingSkia() = SwingUtilities.invokeLater {
),
BorderLayout.SOUTH
)
southButtonsPanel.add(
makeButton(
text = "Resize test window",
action = {
swingSkiaResize()
}
),
BorderLayout.CENTER
)
window.contentPane.add(southButtonsPanel, BorderLayout.SOUTH)
window.contentPane.add(panel, BorderLayout.CENTER)
window.setSize(800, 600)
......
package org.jetbrains.skiko
/**
* Provides [MetalAdapter] that holds pointer to native [MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)
* chosen using [adapterPriority]
*
* @see "src/awtMain/objectiveC/macos/MetalApi.mm"
*/
internal fun chooseMetalAdapter(adapterPriority: GpuPriority): MetalAdapter {
val adapter = chooseAdapter(adapterPriority.ordinal)
val adapterName = getAdapterName(adapter)
val adapterMemorySize = getAdapterMemorySize(adapter)
return MetalAdapter(adapter, adapterName, adapterMemorySize)
}
/**
* @param ptr pointer for native [MTLDevice](https://developer.apple.com/documentation/metal/mtldevice)
* @param name the full name of the vendor device.
* @param memorySize approximation of how much memory this device can use with good performance.
*/
internal data class MetalAdapter(val ptr: Long, val name: String, val memorySize: Long)
internal fun MetalAdapter.dispose() {
disposeAdapter(ptr)
}
private external fun chooseAdapter(adapterPriority: Int): Long
private external fun disposeAdapter(adapter: Long)
private external fun getAdapterName(adapter: Long): String
private external fun getAdapterMemorySize(adapter: Long): Long
\ No newline at end of file
package org.jetbrains.skiko
internal interface CloseScope {
fun <T : AutoCloseable> T.autoClose(): T
}
/**
* Scope that takes care of closing resources inside this scope.
* It is convenient method to avoid nested blocks of code such as:
* ```
* openResource("resource1").use { resource1 ->
* openResource("resource2").use { resource2 ->
*
* }
* }
* ```
* Instead, we can write:
* ```
* autoCloseScope {
* val resource1 = openResource("resource1").autoClose()
* val resource2 = openResource("resource2").autoClose()
* }
* ```
*/
internal fun autoCloseScope(body: CloseScope.() -> Unit) {
val resources = mutableListOf<AutoCloseable>()
val scope = object : CloseScope {
override fun <T : AutoCloseable> T.autoClose(): T {
resources.add(this)
return this
}
}
try {
scope.body()
} finally {
resources.asReversed().forEach {
it.close()
}
}
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.jetbrains.skia.*
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.redrawer.RedrawerManager
import java.awt.Color
import java.awt.Component
import java.awt.event.*
......@@ -14,9 +18,6 @@ import javax.swing.JPanel
import javax.swing.SwingUtilities
import javax.swing.SwingUtilities.isEventDispatchThread
import javax.swing.UIManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
actual open class SkiaLayer internal constructor(
externalAccessibleFactory: ((Component) -> Accessible)? = null,
......@@ -297,15 +298,24 @@ actual open class SkiaLayer internal constructor(
@Volatile
private var isDisposed = false
internal var redrawer: Redrawer? = null
private val fallbackRenderApiQueue = SkikoProperties.fallbackRenderApiQueue(properties.renderApi).toMutableList()
private var renderApi_ = fallbackRenderApiQueue[0]
private val redrawerManager = RedrawerManager<Redrawer>(properties.renderApi) { renderApi, oldRedrawer ->
oldRedrawer?.dispose()
val newRedrawer = renderFactory.createRedrawer(this, renderApi, analytics, properties)
newRedrawer.syncSize()
newRedrawer
}
internal val redrawer: Redrawer?
get() = redrawerManager.redrawer
actual var renderApi: GraphicsApi
get() = renderApi_
private set(value) {
this.renderApi_ = value
get() = redrawerManager.renderApi
set(value) {
redrawerManager.forceRenderApi(value)
notifyChange(PropertyKind.Renderer)
}
val renderInfo: String
get() = if (redrawer == null)
"SkiaLayer isn't initialized yet"
......@@ -317,34 +327,11 @@ actual open class SkiaLayer internal constructor(
private var pictureRecorder: PictureRecorder? = null
private val pictureLock = Any()
private fun findNextWorkingRenderApi() {
var thrown: Boolean
do {
thrown = false
try {
renderApi = fallbackRenderApiQueue.removeAt(0)
redrawer?.dispose()
redrawer = renderFactory.createRedrawer(this, renderApi, analytics, properties)
redrawer?.syncSize()
} catch (e: RenderException) {
Logger.warn(e) { "Fallback to next API" }
thrown = true
}
} while (thrown && fallbackRenderApiQueue.isNotEmpty())
if (thrown && fallbackRenderApiQueue.isEmpty()) {
throw RenderException("Cannot fallback to any render API")
}
}
private fun init(recreation: Boolean = false) {
isDisposed = false
backedLayer.init()
pictureRecorder = PictureRecorder()
if (recreation) {
fallbackRenderApiQueue.add(0, renderApi)
}
findNextWorkingRenderApi()
redrawerManager.findNextWorkingRenderApi(recreation)
isInited = true
}
......@@ -366,7 +353,7 @@ actual open class SkiaLayer internal constructor(
if (isInited && !isDisposed) {
// we should dispose redrawer first (to cancel `draw` in rendering thread)
redrawer?.dispose()
redrawer = null
redrawerManager.dispose()
picture?.instance?.close()
picture = null
pictureRecorder?.close()
......@@ -553,7 +540,7 @@ actual open class SkiaLayer internal constructor(
// clipping
for (component in clipComponents) {
canvas.clipRectBy(component)
canvas.clipRectBy(component, contentScale)
}
try {
......@@ -584,7 +571,7 @@ actual open class SkiaLayer internal constructor(
} catch (e: RenderException) {
if (!isDisposed) {
Logger.warn(e) { "Exception in draw scope" }
findNextWorkingRenderApi()
redrawerManager.findNextWorkingRenderApi()
redrawer?.redrawImmediately()
}
}
......@@ -625,20 +612,6 @@ actual open class SkiaLayer internal constructor(
}
}
private fun Canvas.clipRectBy(rectangle: ClipRectangle) {
val dpi = contentScale
clipRect(
Rect.makeLTRB(
rectangle.x * dpi,
rectangle.y * dpi,
(rectangle.x + rectangle.width) * dpi,
(rectangle.y + rectangle.height) * dpi
),
ClipMode.DIFFERENCE,
true
)
}
private fun roundSize(value: Int): Int {
var rounded = value * contentScale
val diff = rounded - rounded.toInt()
......@@ -685,6 +658,19 @@ internal fun defaultFPSCounter(
)
}
internal fun Canvas.clipRectBy(rectangle: ClipRectangle, scale: Float) {
clipRect(
Rect.makeLTRB(
rectangle.x * scale,
rectangle.y * scale,
(rectangle.x + rectangle.width) * scale,
(rectangle.y + rectangle.height) * scale
),
ClipMode.DIFFERENCE,
true
)
}
// InputEvent is abstract, so we wrap to match modality.
actual typealias SkikoGesturePlatformEvent = Any
actual typealias SkikoPlatformInputEvent = Any
......
......@@ -2,6 +2,7 @@ package org.jetbrains.skiko.context
import org.jetbrains.skia.*
import org.jetbrains.skiko.Logger
import org.jetbrains.skiko.MetalAdapter
import org.jetbrains.skiko.RenderException
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.redrawer.MetalDevice
......@@ -17,7 +18,7 @@ import org.jetbrains.skiko.redrawer.MetalDevice
internal class MetalContextHandler(
layer: SkiaLayer,
private val device: MetalDevice,
private val adapterInfo: AdapterInfo? = null
private val adapter: MetalAdapter
) : JvmContextHandler(layer) {
override fun initContext(): Boolean {
try {
......@@ -69,11 +70,8 @@ internal class MetalContextHandler(
override fun rendererInfo(): String {
return super.rendererInfo() +
if (adapterInfo != null) {
"Video card: ${adapterInfo.adapterName}\n" + "Total VRAM: ${adapterInfo.adapterMemorySize / 1024 / 1024} MB\n"
} else {
""
}
"Video card: ${adapter.name}\n" +
"Total VRAM: ${adapter.memorySize / 1024 / 1024} MB\n"
}
private fun makeRenderTarget(width: Int, height: Int) = BackendRenderTarget(
......@@ -89,6 +87,4 @@ internal class MetalContextHandler(
private external fun makeMetalContext(device: Long): Long
private external fun makeMetalRenderTarget(device: Long, width: Int, height: Int): Long
private external fun finishFrame(device: Long)
data class AdapterInfo(val adapterName: String, val adapterMemorySize: Long)
}
......@@ -24,6 +24,10 @@ internal value class MetalDevice(val ptr: Long)
/**
* Provides a way to request draws on Skia canvas created in [layer] bounds using Metal GPU acceleration.
*
* This [MetalRedrawer] draws content on-screen for maximum efficiency,
* but it may prevent for using it in embedded components (such as interop with Swing).
* For off-screen implementation see [MetalOffScreenRedrawer]
*
* Content to draw is provided by [SkiaLayer.draw].
*
* @see MetalContextHandler
......@@ -57,17 +61,15 @@ internal class MetalRedrawer(
return currentDevice
}
private val adapter = chooseMetalAdapter(properties.adapterPriority)
init {
val adapter = chooseAdapter(properties.adapterPriority.ordinal)
val adapterName = getAdapterName(adapter)
val adapterMemorySize = getAdapterMemorySize(adapter)
onDeviceChosen(adapterName)
onDeviceChosen(adapter.name)
val initDevice = layer.backedLayer.useDrawingSurfacePlatformInfo {
MetalDevice(createMetalDevice(layer.windowHandle, layer.transparency, adapter, it))
MetalDevice(createMetalDevice(layer.windowHandle, layer.transparency, adapter.ptr, it))
}
_device = initDevice
contextHandler =
MetalContextHandler(layer, initDevice, MetalContextHandler.AdapterInfo(adapterName, adapterMemorySize))
contextHandler = MetalContextHandler(layer, initDevice, adapter)
setVSyncEnabled(initDevice.ptr, properties.isVsyncEnabled)
}
......@@ -90,6 +92,7 @@ internal class MetalRedrawer(
frameDispatcher.cancel()
contextHandler.dispose()
disposeDevice(device.ptr)
adapter.dispose()
_device = null
super.dispose()
}
......@@ -166,16 +169,13 @@ internal class MetalRedrawer(
setLayerVisible(device.ptr, isVisible)
}
private external fun chooseAdapter(adapterPriority: Int): Long
private external fun createMetalDevice(window:Long, transparency: Boolean, adapter: Long, platformInfo: Long): Long
private external fun createMetalDevice(window: Long, transparency: Boolean, adapter: Long, platformInfo: Long): Long
private external fun disposeDevice(device: Long)
private external fun resizeLayers(device: Long, x: Int, y: Int, width: Int, height: Int)
private external fun setLayerVisible(device: Long, isVisible: Boolean)
private external fun setContentScale(device: Long, contentScale: Float)
private external fun setVSyncEnabled(device: Long, enabled: Boolean)
private external fun isOccluded(window: Long): Boolean
private external fun getAdapterName(adapter: Long): String
private external fun getAdapterMemorySize(adapter: Long): Long
private external fun startRendering(): Long
private external fun endRendering(handle: Long)
}
package org.jetbrains.skiko.redrawer
import org.jetbrains.skiko.GraphicsApi
import org.jetbrains.skiko.Logger
import org.jetbrains.skiko.RenderException
import org.jetbrains.skiko.SkikoProperties
internal class RedrawerManager<R>(
defaultRenderApi: GraphicsApi,
private val redrawerFactory: (renderApi: GraphicsApi, oldRedrawer: R?) -> R
) {
private var _redrawer: R? = null
private val fallbackRenderApiQueue = SkikoProperties.fallbackRenderApiQueue(defaultRenderApi).toMutableList()
private var _renderApi = fallbackRenderApiQueue[0]
val redrawer: R?
get() = _redrawer
val renderApi: GraphicsApi
get() = _renderApi
fun findNextWorkingRenderApi(recreation: Boolean = false) {
if (recreation) {
fallbackRenderApiQueue.add(0, renderApi)
}
var thrown: Boolean
do {
thrown = false
try {
_renderApi = fallbackRenderApiQueue.removeAt(0)
_redrawer = redrawerFactory(_renderApi, redrawer)
} catch (e: RenderException) {
Logger.warn(e) { "Fallback to next API" }
thrown = true
}
} while (thrown && fallbackRenderApiQueue.isNotEmpty())
if (thrown) {
throw RenderException("Cannot fallback to any render API")
}
}
fun forceRenderApi(renderApi: GraphicsApi) {
_renderApi = renderApi
}
fun dispose() {
_redrawer = null
}
}
\ No newline at end of file
package org.jetbrains.skiko.swing
import org.jetbrains.skia.*
import org.jetbrains.skiko.*
import java.awt.Graphics2D
/**
* Provides a way to draw on Skia canvas rendered off-screen with Metal GPU acceleration and then pass it to [java.awt.Graphics2D].
* It provides better interoperability with Swing, but it is less efficient than on-screen rendering.
*
* For now, it uses drawing to [java.awt.image.BufferedImage] that cause VRAM <-> RAM memory transfer and so increased CPU usage.
*
* Content to draw is provided by [SkikoView].
*
* For on-screen rendering see [org.jetbrains.skiko.redrawer.MetalRedrawer].
*
* @see SwingRedrawerBase
* @see SwingOffscreenDrawer
*/
internal class MetalSwingRedrawer(
swingLayerProperties: SwingLayerProperties,
private val skikoView: SkikoView,
analytics: SkiaLayerAnalytics
) : SwingRedrawerBase(swingLayerProperties, analytics, GraphicsApi.METAL) {
companion object {
init {
Library.load()
}
}
private val adapter: MetalAdapter = chooseMetalAdapter(swingLayerProperties.adapterPriority).also {
onDeviceChosen(it.name)
}
private val context: DirectContext = makeMetalContext()
init {
onContextInit()
}
private val swingOffscreenDrawer = SwingOffscreenDrawer(swingLayerProperties)
override fun dispose() {
adapter.dispose()
super.dispose()
}
override fun onRender(g: Graphics2D, width: Int, height: Int, nanoTime: Long) = autoCloseScope {
val renderTarget = makeRenderTarget(width, height).autoClose()
val surface = Surface.makeFromBackendRenderTarget(
context,
renderTarget,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
)?.autoClose() ?: throw RenderException("Cannot create surface")
val canvas = surface.canvas
canvas.clear(Color.TRANSPARENT)
skikoView.onRender(canvas, width, height, nanoTime)
flush(surface, g)
}
private fun flush(surface: Surface, g: Graphics2D) {
surface.flushAndSubmit(syncCpu = true)
val width = surface.width
val height = surface.height
val storage = Bitmap()
storage.setImageInfo(ImageInfo.makeN32Premul(width, height))
storage.allocPixels()
// TODO: it copies pixels from GPU to CPU, so it is really slow
surface.readPixels(storage, 0, 0)
val bytes = storage.readPixels(storage.imageInfo, (width * 4), 0, 0)
if (bytes != null) {
swingOffscreenDrawer.draw(g, bytes, width, height)
}
}
override fun rendererInfo(): String {
return super.rendererInfo() +
"Video card: ${adapter.name}\n" +
"Total VRAM: ${adapter.memorySize / 1024 / 1024} MB\n"
}
private fun makeRenderTarget(width: Int, height: Int) = BackendRenderTarget(
makeMetalRenderTargetOffScreen(adapter.ptr, width, height)
)
private fun makeMetalContext(): DirectContext = DirectContext(
makeMetalContext(adapter.ptr)
)
private external fun makeMetalContext(adapter: Long): Long
private external fun makeMetalRenderTargetOffScreen(adapter: Long, width: Int, height: Int): Long
}
\ No newline at end of file
package org.jetbrains.skiko.swing
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.*
import org.jetbrains.skiko.redrawer.RedrawerManager
import java.awt.Graphics2D
import java.awt.GraphicsConfiguration
import javax.accessibility.Accessible
import javax.swing.JComponent
import javax.swing.SwingUtilities.isEventDispatchThread
/**
* Swing component that draws content provided by [skikoView] with GPU acceleration using Skia engine.
*
* Drawn content can be clipped by providing [ClipRectangle] to [clipComponents].
*
* This component can be used for better interop with Swing,
* so all Swing functionality like z-ordering, double-buffering etc. will be taken into account during rendering.
*
* But if no interop with Swing is needed, it is better to use [SkiaLayer] instead.
*/
@Suppress("unused") // used in Compose Multiplatform
@ExperimentalSkikoApi
open class SkiaSwingLayer(
skikoView: SkikoView,
analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty,
) : JComponent() {
internal companion object {
init {
Library.load()
}
}
private val properties = SkiaLayerProperties()
private var isInitialized = false
@Volatile
private var isDisposed = false
val clipComponents: MutableList<ClipRectangle> get() = mutableListOf()
private val skikoViewWithClipping = object : SkikoView by skikoView {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
val scale = graphicsConfiguration.defaultTransform.scaleX.toFloat()
// clipping
for (component in clipComponents) {
canvas.clipRectBy(component, scale)
}
skikoView.onRender(canvas, width, height, nanoTime)
}
}
private val swingLayerProperties = object : SwingLayerProperties {
override val width: Int
get() = this@SkiaSwingLayer.width
override val height: Int
get() = this@SkiaSwingLayer.height
override val graphicsConfiguration: GraphicsConfiguration
get() = this@SkiaSwingLayer.graphicsConfiguration
override val adapterPriority: GpuPriority
get() = this@SkiaSwingLayer.properties.adapterPriority
}
private val redrawerManager = RedrawerManager<SwingRedrawer>(properties.renderApi) { renderApi, oldRedrawer ->
oldRedrawer?.dispose()
createSwingRedrawer(swingLayerProperties, skikoViewWithClipping, renderApi, analytics)
}
private val redrawer: SwingRedrawer?
get() = redrawerManager.redrawer
val renderApi: GraphicsApi
get() = redrawerManager.renderApi
init {
isOpaque = false
layout = null
}
override fun removeNotify() {
Logger.debug { "SkiaSwingLayer.awt#removeNotify $this" }
dispose()
super.removeNotify()
}
override fun addNotify() {
Logger.debug { "SkiaSwingLayer.awt#addNotify $this" }
super.addNotify()
init(isInitialized)
}
private fun init(recreation: Boolean = false) {
isDisposed = false
redrawerManager.findNextWorkingRenderApi(recreation)
isInitialized = true
}
fun dispose() {
check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
if (isInitialized && !isDisposed) {
// we should dispose redrawer first (to cancel `draw` in rendering thread)
redrawer?.dispose()
redrawerManager.dispose()
isDisposed = true
}
}
override fun paint(g: java.awt.Graphics) {
try {
redrawer?.redraw(g as Graphics2D)
} catch (e: RenderException) {
if (!isDisposed) {
Logger.warn(e) { "Exception in draw scope" }
redrawerManager.findNextWorkingRenderApi()
repaint()
}
}
}
fun requestNativeFocusOnAccessible(accessible: Accessible?) {
// TODO: support accessibility
}
}
package org.jetbrains.skiko.swing
import org.jetbrains.skia.*
import org.jetbrains.skiko.GraphicsApi
import org.jetbrains.skiko.SkiaLayerAnalytics
import org.jetbrains.skiko.SkikoView
import org.jetbrains.skiko.autoCloseScope
import java.awt.Graphics2D
/**
* Provides a way to draw on Skia canvas using software rendering without GPU acceleration and then draw it on [java.awt.Graphics2D].
*
* Content to draw is provided by [SkikoView].
*
* @see SwingRedrawerBase
* @see SwingOffscreenDrawer
*/
internal class SoftwareSwingRedrawer(
swingLayerProperties: SwingLayerProperties,
private val skikoView: SkikoView,
analytics: SkiaLayerAnalytics
) : SwingRedrawerBase(
swingLayerProperties,
analytics,
GraphicsApi.SOFTWARE_FAST
) {
init {
onDeviceChosen("Software")
}
private val swingOffscreenDrawer = SwingOffscreenDrawer(swingLayerProperties)
private val storage = Bitmap()
init {
onContextInit()
}
override fun dispose() {
super.dispose()
storage.close()
}
override fun onRender(g: Graphics2D, width: Int, height: Int, nanoTime: Long) = autoCloseScope {
if (storage.width != width || storage.height != height) {
storage.allocPixelsFlags(ImageInfo.makeS32(width, height, ColorAlphaType.PREMUL), false)
}
val canvas = Canvas(storage, SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)).autoClose()
canvas.clear(Color.TRANSPARENT)
skikoView.onRender(canvas, width, height, nanoTime)
flush(g)
}
private fun flush(g: Graphics2D) {
val width = storage.width
val height = storage.height
val bytes = storage.readPixels(storage.imageInfo, (width * 4), 0, 0)
if (bytes != null) {
swingOffscreenDrawer.draw(g, bytes, width, height)
}
}
}
\ No newline at end of file
package org.jetbrains.skiko.swing
import org.jetbrains.skiko.GpuPriority
import java.awt.GraphicsConfiguration
internal interface SwingLayerProperties {
val width: Int
val height: Int
val graphicsConfiguration: GraphicsConfiguration
val adapterPriority: GpuPriority
}
internal val SwingLayerProperties.scale: Float get() = graphicsConfiguration.defaultTransform.scaleX.toFloat()
\ No newline at end of file
package org.jetbrains.skiko.swing
import java.awt.*
import java.awt.geom.AffineTransform
import java.awt.image.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.IntBuffer
import kotlin.math.*
// TODO: extract this code to a library and share it with JCEF implementation in IntelliJ
// since this code is mostly taken from intellij repository with some small changes
internal class SwingOffscreenDrawer(
private val swingLayerProperties: SwingLayerProperties
) {
@Volatile
private var volatileImage: VolatileImage? = null
/**
* Draws rendered image that is represented by [bytes] on [g]
*
* @param g graphics where rendered picture given in [bytes] should be drawn
* @param bytes bytes of rendered picture in little endian order
* @param width width of rendered picture in real pixels
* @param height height of rendered picture in real pixels
*/
fun draw(g: Graphics2D, bytes: ByteArray, width: Int, height: Int) {
val dirtyRectangles = listOf(
Rectangle(0, 0, width, height)
)
val image = createImageFromBytes(bytes, width, height, dirtyRectangles)
var vi = volatileImage
do {
if (vi == null || vi.width != swingLayerProperties.width || vi.height != swingLayerProperties.height) {
vi = createVolatileImage(image)
}
drawVolatileImage(vi, image)
when (vi.validate(swingLayerProperties.graphicsConfiguration)) {
VolatileImage.IMAGE_RESTORED -> drawVolatileImage(vi, image)
VolatileImage.IMAGE_INCOMPATIBLE -> vi = createVolatileImage(image)
}
g.drawImage(vi, 0, 0, null)
} while (vi!!.contentsLost())
volatileImage = vi
}
private fun createImageFromBytes(
bytes: ByteArray,
width: Int,
height: Int,
dirtyRectangles: List<Rectangle>
): BufferedImage {
val src = ByteBuffer.wrap(bytes)
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE)
val dstData = (image.raster.dataBuffer as DataBufferInt).data
val srcData: IntBuffer = src.order(ByteOrder.LITTLE_ENDIAN).asIntBuffer()
for (rect in dirtyRectangles) {
if (rect.width < image.width) {
for (line in rect.y until rect.y + rect.height) {
val offset: Int = line * image.width + rect.x
srcData.position(offset)[dstData, offset, min(
rect.width.toDouble(),
(src.capacity() - offset).toDouble()
).toInt()]
}
} else { // optimized for a buffer wide dirty rect
val offset: Int = rect.y * image.width
srcData.position(offset)[dstData, offset, min(
(rect.height * image.width).toDouble(),
(src.capacity() - offset).toDouble()
).toInt()]
}
}
return image
}
private fun createVolatileImage(image: BufferedImage): VolatileImage {
val vi = swingLayerProperties.graphicsConfiguration.createCompatibleVolatileImage(
swingLayerProperties.width,
swingLayerProperties.height,
Transparency.TRANSLUCENT
)
drawVolatileImage(vi, image)
return vi
}
private fun drawVolatileImage(vi: VolatileImage, image: BufferedImage) {
val g = vi.graphics.create() as Graphics2D
try {
g.background = Color(0, 0, 0, 0)
g.composite = AlphaComposite.Src
g.clearRect(0, 0, swingLayerProperties.width, swingLayerProperties.height)
drawImage(g, image)
} finally {
g.dispose()
}
}
private fun drawImage(
g: Graphics,
image: Image,
x: Int = 0,
y: Int = 0,
dw: Int = -1,
dh: Int = -1,
sourceBounds: Rectangle? = null,
op: BufferedImageOp? = null,
observer: ImageObserver? = null
) {
val hasDestinationSize = dw >= 0 && dh >= 0
doDrawHiDpi(
userWidth = swingLayerProperties.width,
userHeight = swingLayerProperties.height,
g = g,
scale = swingLayerProperties.scale.toDouble(),
dx = x,
dy = y,
dw = dw,
dh = dh,
hasDestinationSize = hasDestinationSize,
op = op,
image = image,
srcBounds = sourceBounds,
observer = observer
)
}
@Suppress("NAME_SHADOWING")
private fun doDrawHiDpi(
userWidth: Int,
userHeight: Int,
g: Graphics,
scale: Double,
dx: Int,
dy: Int,
dw: Int,
dh: Int,
hasDestinationSize: Boolean,
op: BufferedImageOp?,
image: Image,
srcBounds: Rectangle?,
observer: ImageObserver?
) {
var g1 = g
var scale1 = scale
var dx1 = dx
var dy1 = dy
var delta = 0.0
// Calculate the delta based on the image size. The bigger the size - the smaller the delta.
val maxSize = max(userWidth, userHeight)
if (maxSize < Int.MAX_VALUE / 2) {
var dotAccuracy = 1
var pow: Double
while (maxSize > 10.0.pow(dotAccuracy.toDouble()).also { pow = it }) {
dotAccuracy++
}
delta = 1 / pow
}
val tx = (g1 as Graphics2D).transform
var invG: Graphics2D? = null
if ((tx.type and AffineTransform.TYPE_MASK_ROTATION) == 0 &&
abs(scale1 - tx.scaleX) <= delta
) {
scale1 = tx.scaleX
// The image has the same original scale as the graphics scale. However, the real image
// scale - userSize/realSize - can suffer from inaccuracy due to the image user size
// rounding to int (userSize = (int)realSize/originalImageScale). This may case quality
// loss if the image is drawn via Graphics.drawImage(image, <srcRect>, <dstRect>)
// due to scaling in Graphics. To avoid that, the image should be drawn directly via
// Graphics.drawImage(image, 0, 0) on the unscaled Graphics.
val gScaleX = tx.scaleX
val gScaleY = tx.scaleY
tx.scale(1 / gScaleX, 1 / gScaleY)
tx.translate(dx1 * gScaleX, dy1 * gScaleY)
dy1 = 0
dx1 = 0
invG = g1.create() as Graphics2D
g1 = invG
invG.transform = tx
}
try {
var dw = dw
var dh = dh
if (invG != null && hasDestinationSize) {
dw = scaleSize(dw, scale1)
dh = scaleSize(dh, scale1)
}
doDraw(
op = op,
image = image,
invG = invG,
hasDestinationSize = hasDestinationSize,
dw = dw,
dh = dh,
sourceBounds = srcBounds,
userWidth = userWidth,
userHeight = userHeight,
g = g1,
dx = dx1,
dy = dy1,
observer = observer,
scale = scale1
)
} finally {
invG?.dispose()
}
}
private fun scaleSize(size: Int, scale: Double) = (size * scale).roundToInt()
@Suppress("NAME_SHADOWING")
private fun doDraw(
op: BufferedImageOp?,
image: Image,
invG: Graphics2D?,
hasDestinationSize: Boolean,
dw: Int,
dh: Int,
sourceBounds: Rectangle?,
userWidth: Int,
userHeight: Int,
g: Graphics,
dx: Int,
dy: Int,
observer: ImageObserver?,
scale: Double
) {
var image = image
if (op != null && image is BufferedImage) {
image = op.filter(image, null)
}
when {
sourceBounds != null -> {
fun size(size: Int) = scaleSize(size, scale)
val sx = size(sourceBounds.x)
val sy = size(sourceBounds.y)
val sw = if (sourceBounds.width >= 0) size(sourceBounds.width) else size(userWidth) - sx
val sh = if (sourceBounds.height >= 0) size(sourceBounds.height) else size(userHeight) - sy
var dw = dw
var dh = dh
if (!hasDestinationSize) {
dw = size(userWidth)
dh = size(userHeight)
}
g.drawImage(/* img = */ image,
/* dx1 = */ dx, /* dy1 = */ dy, /* dx2 = */ dx + dw, /* dy2 = */ dy + dh,
/* sx1 = */ sx, /* sy1 = */ sy, /* sx2 = */ sx + sw, /* sy2 = */ sy + sh,
/* observer = */ observer
)
}
hasDestinationSize -> {
g.drawImage(image, dx, dy, dw, dh, observer)
}
invG == null -> {
g.drawImage(image, dx, dy, userWidth, userHeight, observer)
}
else -> {
g.drawImage(image, dx, dy, observer)
}
}
}
}
\ No newline at end of file
package org.jetbrains.skiko.swing
import org.jetbrains.skiko.*
import java.awt.Graphics2D
/**
* Provides an interface for requesting content to be drawn on a [java.awt.Graphics2D].
*
* See [org.jetbrains.skiko.redrawer.Redrawer] redrawer for on-screen rendering
*/
internal interface SwingRedrawer {
/**
* Should be called when [SwingRedrawer] no longer needed to free native resources
*/
fun dispose()
/**
* Draw content synchronously on given [java.awt.Graphics2D].
* Content will be drawn off-screen using Skia engine and then passed to [java.awt.Graphics2D]
*/
fun redraw(g: Graphics2D)
}
/**
* Creates a [SwingRedrawer] that will draw content provided by [skikoView]
*/
internal fun createSwingRedrawer(
swingLayerProperties: SwingLayerProperties,
skikoView: SkikoView,
renderApi: GraphicsApi,
analytics: SkiaLayerAnalytics,
): SwingRedrawer {
return when (hostOs) {
OS.MacOS -> when (renderApi) {
GraphicsApi.SOFTWARE_COMPAT, GraphicsApi.SOFTWARE_FAST -> SoftwareSwingRedrawer(
swingLayerProperties,
skikoView,
analytics
)
else -> MetalSwingRedrawer(swingLayerProperties, skikoView, analytics)
}
else -> SoftwareSwingRedrawer(swingLayerProperties, skikoView, analytics)
}
}
\ No newline at end of file
package org.jetbrains.skiko.swing
import org.jetbrains.skia.*
import org.jetbrains.skiko.*
import org.jetbrains.skiko.SkiaLayerAnalytics.DeviceAnalytics
import java.awt.Graphics2D
import java.util.concurrent.CancellationException
import javax.swing.SwingUtilities
/**
* Provides a base implementation of drawing [SkikoView] content on [java.awt.Graphics2D]
*
* Each [redraw] request is handled in a following way:
* 1. For the first request initialize native GPU context using [createDirectContext]
* 2. Create [org.jetbrains.skia.Canvas] where content should be drawn using [initCanvas]
* 3. Acquire drawing "commands" using [SkikoView]
* 4. Flush these commands on [java.awt.Graphics2D] using [flush]
*
* All the steps are performed synchronously on EDT.
*/
@OptIn(ExperimentalSkikoApi::class)
internal abstract class SwingRedrawerBase(
private val swingLayerProperties: SwingLayerProperties,
private val analytics: SkiaLayerAnalytics,
private val graphicsApi: GraphicsApi
) : SwingRedrawer {
private var isFirstFrameRendered = false
private val rendererAnalytics = analytics.renderer(Version.skiko, hostOs, graphicsApi)
private var deviceAnalytics: DeviceAnalytics? = null
private var isDisposed = false
init {
rendererAnalytics.init()
}
protected abstract fun onRender(g: Graphics2D, width: Int, height: Int, nanoTime: Long)
override fun dispose() {
require(!isDisposed) { "$javaClass is disposed" }
isDisposed = true
}
final override fun redraw(g: Graphics2D) {
require(!isDisposed) { "$javaClass is disposed" }
inDrawScope {
val scale = swingLayerProperties.scale
val width = (swingLayerProperties.width * scale).toInt().coerceAtLeast(0)
val height = (swingLayerProperties.height * scale).toInt().coerceAtLeast(0)
onRender(g, width, height, System.nanoTime())
}
}
/**
* Should be called when the device name is known as early, as possible.
*/
protected fun onDeviceChosen(deviceName: String?) {
require(!isDisposed) { "$javaClass is disposed" }
require(deviceAnalytics == null) { "deviceAnalytics is not null" }
rendererAnalytics.deviceChosen()
deviceAnalytics = analytics.device(Version.skiko, hostOs, graphicsApi, deviceName)
deviceAnalytics?.init()
}
protected open fun rendererInfo(): String {
return "GraphicsApi: ${graphicsApi}\n" +
"OS: ${hostOs.id} ${hostArch.id}\n"
}
protected fun onContextInit() {
require(!isDisposed) { "$javaClass is disposed" }
requireNotNull(deviceAnalytics) { "deviceAnalytics is not null. Call onDeviceChosen after choosing the drawing device" }
if (System.getProperty("skiko.hardwareInfo.enabled") == "true") {
Logger.info { "Renderer info:\n ${rendererInfo()}" }
}
deviceAnalytics?.contextInit()
}
private inline fun inDrawScope(body: () -> Unit) {
check(SwingUtilities.isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
requireNotNull(deviceAnalytics) { "deviceAnalytics is not null. Call onDeviceChosen after choosing the drawing device" }
if (!isDisposed) {
if (!isFirstFrameRendered) {
deviceAnalytics?.beforeFirstFrameRender()
}
try {
body()
} catch (e: CancellationException) {
// ignore
}
if (!isFirstFrameRendered && !isDisposed) {
deviceAnalytics?.afterFirstFrameRender()
}
isFirstFrameRendered = true
}
}
}
\ No newline at end of file
#ifdef SK_METAL
#import <jawt.h>
#import <jawt_md.h>
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#import <GrBackendSurface.h>
#import <GrDirectContext.h>
#import <mtl/GrMtlBackendContext.h>
#import <mtl/GrMtlTypes.h>
#import "MetalDevice.h"
#define MuxGraphicsCard 7
#define kOpen 0
#define kGetMuxState 3
#define kDriverClassName "AppleGraphicsControl"
#define AdpapterPriorityAuto 0
#define AdpapterPriorityIntegrated 1
#define AdpapterPriorityDiscrete 2
extern "C"
{
BOOL isUsingIntegratedGPU() {
kern_return_t kernResult = 0;
io_iterator_t iterator = IO_OBJECT_NULL;
io_service_t service = IO_OBJECT_NULL;
kernResult = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(kDriverClassName), &iterator);
assert(kernResult == KERN_SUCCESS);
service = IOIteratorNext(iterator);
io_connect_t switcherConnect;
kernResult = IOServiceOpen(service, mach_task_self(), 0, &switcherConnect);
if (kernResult != KERN_SUCCESS) return 0;
kernResult = IOConnectCallScalarMethod(switcherConnect, kOpen, NULL, 0, NULL, NULL);
if (kernResult != KERN_SUCCESS) return 0;
uint64_t output;
uint32_t outputCount = 1;
uint64_t scalarI_64[2] = { 1, MuxGraphicsCard };
kernResult = IOConnectCallScalarMethod(switcherConnect,
kGetMuxState,
scalarI_64,
2,
&output,
&outputCount);
if (kernResult != KERN_SUCCESS) return 0;
return output != 0;
}
id<MTLDevice> MTLCreateIntegratedDevice(int adapterPriority) {
BOOL isIntegratedGPU = NO;
if (adapterPriority == AdpapterPriorityAuto) {
isIntegratedGPU = isUsingIntegratedGPU();
} else if (adapterPriority == AdpapterPriorityIntegrated) {
isIntegratedGPU = YES;
}
id<MTLDevice> gpu = nil;
if (isIntegratedGPU) {
NSArray<id<MTLDevice>> *devices = MTLCopyAllDevices();
for (id<MTLDevice> device in devices) {
if (device.isLowPower) {
gpu = device;
break;
}
}
}
if (gpu == nil) {
gpu = MTLCreateSystemDefaultDevice();
}
return gpu;
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_MetalApiKt_chooseAdapter(
JNIEnv *env, jobject obj, jlong adapterPriority)
{
@autoreleasepool {
id<MTLDevice> adapter = MTLCreateIntegratedDevice(adapterPriority);
return (jlong) (__bridge_retained void *) adapter;
}
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_MetalApiKt_disposeAdapter(
JNIEnv *env, jobject obj, jlong adapterPtr)
{
@autoreleasepool {
id<MTLDevice> adapter = (__bridge_transfer id<MTLDevice>) (void *) adapterPtr;
}
}
JNIEXPORT jstring JNICALL Java_org_jetbrains_skiko_MetalApiKt_getAdapterName(
JNIEnv *env, jobject obj, jlong adapterPtr)
{
@autoreleasepool {
id<MTLDevice> adapter = (__bridge id<MTLDevice>) (void *) adapterPtr;
const char *currentAdapterName = [[adapter name] cStringUsingEncoding:NSASCIIStringEncoding];
return env->NewStringUTF(currentAdapterName);
}
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_MetalApiKt_getAdapterMemorySize(
JNIEnv *env, jobject obj, jlong adapterPtr)
{
@autoreleasepool {
id<MTLDevice> adapter = (__bridge id<MTLDevice>) (void *) adapterPtr;
uint64_t totalMemory = [adapter recommendedMaxWorkingSetSize];
return (jlong)totalMemory;
}
}
} // extern C
#endif
......@@ -15,14 +15,6 @@
#import "MetalDevice.h"
#define MuxGraphicsCard 7
#define kOpen 0
#define kGetMuxState 3
#define kDriverClassName "AppleGraphicsControl"
#define AdpapterPriorityAuto 0
#define AdpapterPriorityIntegrated 1
#define AdpapterPriorityDiscrete 2
@implementation AWTMetalLayer
- (id)init
......@@ -77,77 +69,11 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_endRender
objc_autoreleasePoolPop((void*)handle);
}
BOOL isUsingIntegratedGPU() {
kern_return_t kernResult = 0;
io_iterator_t iterator = IO_OBJECT_NULL;
io_service_t service = IO_OBJECT_NULL;
kernResult = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(kDriverClassName), &iterator);
assert(kernResult == KERN_SUCCESS);
service = IOIteratorNext(iterator);
io_connect_t switcherConnect;
kernResult = IOServiceOpen(service, mach_task_self(), 0, &switcherConnect);
if (kernResult != KERN_SUCCESS) return 0;
kernResult = IOConnectCallScalarMethod(switcherConnect, kOpen, NULL, 0, NULL, NULL);
if (kernResult != KERN_SUCCESS) return 0;
uint64_t output;
uint32_t outputCount = 1;
uint64_t scalarI_64[2] = { 1, MuxGraphicsCard };
kernResult = IOConnectCallScalarMethod(switcherConnect,
kGetMuxState,
scalarI_64,
2,
&output,
&outputCount);
if (kernResult != KERN_SUCCESS) return 0;
return output != 0;
}
id<MTLDevice> MTLCreateIntegratedDevice(int adapterPriority) {
BOOL isIntegratedGPU = NO;
if (adapterPriority == AdpapterPriorityAuto) {
isIntegratedGPU = isUsingIntegratedGPU();
} else if (adapterPriority == AdpapterPriorityIntegrated) {
isIntegratedGPU = YES;
}
id<MTLDevice> gpu = nil;
if (isIntegratedGPU) {
NSArray<id<MTLDevice>> *devices = MTLCopyAllDevices();
for (id<MTLDevice> device in devices) {
if (device.isLowPower) {
gpu = device;
break;
}
}
}
if (gpu == nil) {
gpu = MTLCreateSystemDefaultDevice();
}
return gpu;
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_chooseAdapter(
JNIEnv *env, jobject redrawer, jlong adapterPriority)
{
@autoreleasepool {
id<MTLDevice> adapter = MTLCreateIntegratedDevice(adapterPriority);
return (jlong) (__bridge_retained void *) adapter;
}
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_createMetalDevice(
JNIEnv *env, jobject redrawer, jlong windowPtr, jboolean transparency, jlong adapterPtr, jlong platformInfoPtr)
{
@autoreleasepool {
id<MTLDevice> adapter = (__bridge_transfer id<MTLDevice>) (void *) adapterPtr;
id<MTLDevice> adapter = (__bridge id<MTLDevice>) (void *) adapterPtr;
MetalDevice *device = [MetalDevice new];
......@@ -261,26 +187,6 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_disposeDe
}
}
JNIEXPORT jstring JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_getAdapterName(
JNIEnv *env, jobject redrawer, jlong adapterPtr)
{
@autoreleasepool {
id<MTLDevice> adapter = (__bridge id<MTLDevice>) (void *) adapterPtr;
const char *currentAdapterName = [[adapter name] cStringUsingEncoding:NSASCIIStringEncoding];
return env->NewStringUTF(currentAdapterName);
}
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_getAdapterMemorySize(
JNIEnv *env, jobject redrawer, jlong adapterPtr)
{
@autoreleasepool {
id<MTLDevice> adapter = (__bridge id<MTLDevice>) (void *) adapterPtr;
uint64_t totalMemory = [adapter recommendedMaxWorkingSetSize];
return (jlong)totalMemory;
}
}
JNIEXPORT jboolean JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_isOccluded(
JNIEnv *env, jobject redrawer, jlong windowPtr) {
@autoreleasepool {
......
#ifdef SK_METAL
#import <jawt.h>
#import <jawt_md.h>
#import <QuartzCore/CAMetalLayer.h>
#import <Metal/Metal.h>
#import <GrDirectContext.h>
#import <mtl/GrMtlBackendContext.h>
#import <mtl/GrMtlTypes.h>
#import "MetalDevice.h"
extern "C"
{
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_swing_MetalSwingRedrawer_makeMetalContext(
JNIEnv *env, jobject contextHandler, jlong adapterPtr) {
@autoreleasepool {
id <MTLDevice> adapter = (__bridge id <MTLDevice>) (void *) adapterPtr;
GrMtlBackendContext backendContext = {};
backendContext.fDevice.retain((__bridge GrMTLHandle) adapter);
id <MTLCommandQueue> fQueue = [adapter newCommandQueue];
backendContext.fQueue.retain((__bridge GrMTLHandle) fQueue);
return (jlong) GrDirectContext::MakeMetal(backendContext).release();
}
}
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_swing_MetalSwingRedrawer_makeMetalRenderTargetOffScreen(
JNIEnv *env, jobject contextHandler, jlong adapterPtr, jint width, jint height) {
@autoreleasepool {
id <MTLDevice> adapter = (__bridge id <MTLDevice>) (void *) adapterPtr;
MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:width height:height mipmapped:NO];
// TODO: use double buffer
id <MTLTexture> metalTexture = [adapter newTextureWithDescriptor:textureDescriptor];
GrMtlTextureInfo info;
info.fTexture.retain((__bridge GrMTLHandle) metalTexture);
GrBackendRenderTarget *renderTarget = NULL;
renderTarget = new GrBackendRenderTarget(width, height, 0, info);
return (jlong) renderTarget;
}
}
} // extern C
#endif
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment