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 package SkiaAwtSample
import org.jetbrains.skiko.*
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skia.paragraph.* import org.jetbrains.skia.paragraph.*
import org.jetbrains.skiko.*
import kotlin.math.PI
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin 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 typeface = Typeface.makeFromFile("fonts/JetBrainsMono-Regular.ttf")
private val font = Font(typeface, 13f).apply { private val font = Font(typeface, 13f).apply {
edging = FontEdging.SUBPIXEL_ANTI_ALIAS edging = FontEdging.SUBPIXEL_ANTI_ALIAS
...@@ -84,7 +86,7 @@ class ClocksAwt(private val layer: SkiaLayer) : SkikoView { ...@@ -84,7 +86,7 @@ class ClocksAwt(private val layer: SkiaLayer) : SkikoView {
} }
val paragraph = ParagraphBuilder(style, fontCollection) val paragraph = ParagraphBuilder(style, fontCollection)
.pushStyle(TextStyle().setColor(0xFF000000.toInt())) .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() .popStyle()
.build() .build()
paragraph.layout(Float.POSITIVE_INFINITY) paragraph.layout(Float.POSITIVE_INFINITY)
...@@ -93,8 +95,9 @@ class ClocksAwt(private val layer: SkiaLayer) : SkikoView { ...@@ -93,8 +95,9 @@ class ClocksAwt(private val layer: SkiaLayer) : SkikoView {
// Alpha layers test // Alpha layers test
val rectW = 100f val rectW = 100f
val rectH = 100f val rectH = 100f
val left = (width / layer.contentScale - rectW) / 2f val scale = scaleProvider()
val top = (height / layer.contentScale - rectH) / 2f val left = (width / scale - rectW) / 2f
val top = (height / scale - rectH) / 2f
val pictureRecorder = PictureRecorder() val pictureRecorder = PictureRecorder()
val pictureCanvas = pictureRecorder.beginRecording( val pictureCanvas = pictureRecorder.beginRecording(
Rect.makeLTRB(left, top, left + rectW, top + rectH) 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 package SkiaAwtSample
import org.jetbrains.skiko.GenericSkikoView
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Color import java.awt.Color
import java.awt.Dimension import java.awt.Dimension
import java.awt.event.ComponentAdapter import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import javax.swing.* import javax.swing.*
import org.jetbrains.skiko.GenericSkikoView
fun makeButton( fun makeButton(
text: String, text: String,
...@@ -57,7 +57,8 @@ fun swingSkia() = SwingUtilities.invokeLater { ...@@ -57,7 +57,8 @@ fun swingSkia() = SwingUtilities.invokeLater {
), ),
BorderLayout.EAST BorderLayout.EAST
) )
window.contentPane.add( val southButtonsPanel = JPanel(BorderLayout())
southButtonsPanel.add(
makeButton( makeButton(
text = "New Window", text = "New Window",
action = { action = {
...@@ -66,6 +67,16 @@ fun swingSkia() = SwingUtilities.invokeLater { ...@@ -66,6 +67,16 @@ fun swingSkia() = SwingUtilities.invokeLater {
), ),
BorderLayout.SOUTH 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.contentPane.add(panel, BorderLayout.CENTER)
window.setSize(800, 600) 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 package org.jetbrains.skiko
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.redrawer.RedrawerManager
import java.awt.Color import java.awt.Color
import java.awt.Component import java.awt.Component
import java.awt.event.* import java.awt.event.*
...@@ -14,9 +18,6 @@ import javax.swing.JPanel ...@@ -14,9 +18,6 @@ import javax.swing.JPanel
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
import javax.swing.UIManager import javax.swing.UIManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
actual open class SkiaLayer internal constructor( actual open class SkiaLayer internal constructor(
externalAccessibleFactory: ((Component) -> Accessible)? = null, externalAccessibleFactory: ((Component) -> Accessible)? = null,
...@@ -297,15 +298,24 @@ actual open class SkiaLayer internal constructor( ...@@ -297,15 +298,24 @@ actual open class SkiaLayer internal constructor(
@Volatile @Volatile
private var isDisposed = false private var isDisposed = false
internal var redrawer: Redrawer? = null
private val fallbackRenderApiQueue = SkikoProperties.fallbackRenderApiQueue(properties.renderApi).toMutableList() private val redrawerManager = RedrawerManager<Redrawer>(properties.renderApi) { renderApi, oldRedrawer ->
private var renderApi_ = fallbackRenderApiQueue[0] oldRedrawer?.dispose()
val newRedrawer = renderFactory.createRedrawer(this, renderApi, analytics, properties)
newRedrawer.syncSize()
newRedrawer
}
internal val redrawer: Redrawer?
get() = redrawerManager.redrawer
actual var renderApi: GraphicsApi actual var renderApi: GraphicsApi
get() = renderApi_ get() = redrawerManager.renderApi
private set(value) { set(value) {
this.renderApi_ = value redrawerManager.forceRenderApi(value)
notifyChange(PropertyKind.Renderer) notifyChange(PropertyKind.Renderer)
} }
val renderInfo: String val renderInfo: String
get() = if (redrawer == null) get() = if (redrawer == null)
"SkiaLayer isn't initialized yet" "SkiaLayer isn't initialized yet"
...@@ -317,34 +327,11 @@ actual open class SkiaLayer internal constructor( ...@@ -317,34 +327,11 @@ actual open class SkiaLayer internal constructor(
private var pictureRecorder: PictureRecorder? = null private var pictureRecorder: PictureRecorder? = null
private val pictureLock = Any() 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) { private fun init(recreation: Boolean = false) {
isDisposed = false isDisposed = false
backedLayer.init() backedLayer.init()
pictureRecorder = PictureRecorder() pictureRecorder = PictureRecorder()
if (recreation) { redrawerManager.findNextWorkingRenderApi(recreation)
fallbackRenderApiQueue.add(0, renderApi)
}
findNextWorkingRenderApi()
isInited = true isInited = true
} }
...@@ -366,7 +353,7 @@ actual open class SkiaLayer internal constructor( ...@@ -366,7 +353,7 @@ actual open class SkiaLayer internal constructor(
if (isInited && !isDisposed) { if (isInited && !isDisposed) {
// we should dispose redrawer first (to cancel `draw` in rendering thread) // we should dispose redrawer first (to cancel `draw` in rendering thread)
redrawer?.dispose() redrawer?.dispose()
redrawer = null redrawerManager.dispose()
picture?.instance?.close() picture?.instance?.close()
picture = null picture = null
pictureRecorder?.close() pictureRecorder?.close()
...@@ -553,7 +540,7 @@ actual open class SkiaLayer internal constructor( ...@@ -553,7 +540,7 @@ actual open class SkiaLayer internal constructor(
// clipping // clipping
for (component in clipComponents) { for (component in clipComponents) {
canvas.clipRectBy(component) canvas.clipRectBy(component, contentScale)
} }
try { try {
...@@ -584,7 +571,7 @@ actual open class SkiaLayer internal constructor( ...@@ -584,7 +571,7 @@ actual open class SkiaLayer internal constructor(
} catch (e: RenderException) { } catch (e: RenderException) {
if (!isDisposed) { if (!isDisposed) {
Logger.warn(e) { "Exception in draw scope" } Logger.warn(e) { "Exception in draw scope" }
findNextWorkingRenderApi() redrawerManager.findNextWorkingRenderApi()
redrawer?.redrawImmediately() redrawer?.redrawImmediately()
} }
} }
...@@ -625,20 +612,6 @@ actual open class SkiaLayer internal constructor( ...@@ -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 { private fun roundSize(value: Int): Int {
var rounded = value * contentScale var rounded = value * contentScale
val diff = rounded - rounded.toInt() val diff = rounded - rounded.toInt()
...@@ -685,6 +658,19 @@ internal fun defaultFPSCounter( ...@@ -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. // InputEvent is abstract, so we wrap to match modality.
actual typealias SkikoGesturePlatformEvent = Any actual typealias SkikoGesturePlatformEvent = Any
actual typealias SkikoPlatformInputEvent = Any actual typealias SkikoPlatformInputEvent = Any
......
...@@ -2,6 +2,7 @@ package org.jetbrains.skiko.context ...@@ -2,6 +2,7 @@ package org.jetbrains.skiko.context
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skiko.Logger import org.jetbrains.skiko.Logger
import org.jetbrains.skiko.MetalAdapter
import org.jetbrains.skiko.RenderException import org.jetbrains.skiko.RenderException
import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.redrawer.MetalDevice import org.jetbrains.skiko.redrawer.MetalDevice
...@@ -17,7 +18,7 @@ import org.jetbrains.skiko.redrawer.MetalDevice ...@@ -17,7 +18,7 @@ import org.jetbrains.skiko.redrawer.MetalDevice
internal class MetalContextHandler( internal class MetalContextHandler(
layer: SkiaLayer, layer: SkiaLayer,
private val device: MetalDevice, private val device: MetalDevice,
private val adapterInfo: AdapterInfo? = null private val adapter: MetalAdapter
) : JvmContextHandler(layer) { ) : JvmContextHandler(layer) {
override fun initContext(): Boolean { override fun initContext(): Boolean {
try { try {
...@@ -69,11 +70,8 @@ internal class MetalContextHandler( ...@@ -69,11 +70,8 @@ internal class MetalContextHandler(
override fun rendererInfo(): String { override fun rendererInfo(): String {
return super.rendererInfo() + return super.rendererInfo() +
if (adapterInfo != null) { "Video card: ${adapter.name}\n" +
"Video card: ${adapterInfo.adapterName}\n" + "Total VRAM: ${adapterInfo.adapterMemorySize / 1024 / 1024} MB\n" "Total VRAM: ${adapter.memorySize / 1024 / 1024} MB\n"
} else {
""
}
} }
private fun makeRenderTarget(width: Int, height: Int) = BackendRenderTarget( private fun makeRenderTarget(width: Int, height: Int) = BackendRenderTarget(
...@@ -89,6 +87,4 @@ internal class MetalContextHandler( ...@@ -89,6 +87,4 @@ internal class MetalContextHandler(
private external fun makeMetalContext(device: Long): Long private external fun makeMetalContext(device: Long): Long
private external fun makeMetalRenderTarget(device: Long, width: Int, height: Int): Long private external fun makeMetalRenderTarget(device: Long, width: Int, height: Int): Long
private external fun finishFrame(device: 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) ...@@ -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. * 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]. * Content to draw is provided by [SkiaLayer.draw].
* *
* @see MetalContextHandler * @see MetalContextHandler
...@@ -57,17 +61,15 @@ internal class MetalRedrawer( ...@@ -57,17 +61,15 @@ internal class MetalRedrawer(
return currentDevice return currentDevice
} }
private val adapter = chooseMetalAdapter(properties.adapterPriority)
init { init {
val adapter = chooseAdapter(properties.adapterPriority.ordinal) onDeviceChosen(adapter.name)
val adapterName = getAdapterName(adapter)
val adapterMemorySize = getAdapterMemorySize(adapter)
onDeviceChosen(adapterName)
val initDevice = layer.backedLayer.useDrawingSurfacePlatformInfo { val initDevice = layer.backedLayer.useDrawingSurfacePlatformInfo {
MetalDevice(createMetalDevice(layer.windowHandle, layer.transparency, adapter, it)) MetalDevice(createMetalDevice(layer.windowHandle, layer.transparency, adapter.ptr, it))
} }
_device = initDevice _device = initDevice
contextHandler = contextHandler = MetalContextHandler(layer, initDevice, adapter)
MetalContextHandler(layer, initDevice, MetalContextHandler.AdapterInfo(adapterName, adapterMemorySize))
setVSyncEnabled(initDevice.ptr, properties.isVsyncEnabled) setVSyncEnabled(initDevice.ptr, properties.isVsyncEnabled)
} }
...@@ -90,6 +92,7 @@ internal class MetalRedrawer( ...@@ -90,6 +92,7 @@ internal class MetalRedrawer(
frameDispatcher.cancel() frameDispatcher.cancel()
contextHandler.dispose() contextHandler.dispose()
disposeDevice(device.ptr) disposeDevice(device.ptr)
adapter.dispose()
_device = null _device = null
super.dispose() super.dispose()
} }
...@@ -166,16 +169,13 @@ internal class MetalRedrawer( ...@@ -166,16 +169,13 @@ internal class MetalRedrawer(
setLayerVisible(device.ptr, isVisible) 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 disposeDevice(device: Long)
private external fun resizeLayers(device: Long, x: Int, y: Int, width: Int, height: Int) 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 setLayerVisible(device: Long, isVisible: Boolean)
private external fun setContentScale(device: Long, contentScale: Float) private external fun setContentScale(device: Long, contentScale: Float)
private external fun setVSyncEnabled(device: Long, enabled: Boolean) private external fun setVSyncEnabled(device: Long, enabled: Boolean)
private external fun isOccluded(window: Long): 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 startRendering(): Long
private external fun endRendering(handle: 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 @@ ...@@ -15,14 +15,6 @@
#import "MetalDevice.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
@implementation AWTMetalLayer @implementation AWTMetalLayer
- (id)init - (id)init
...@@ -77,77 +69,11 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_endRender ...@@ -77,77 +69,11 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_endRender
objc_autoreleasePoolPop((void*)handle); 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( JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_createMetalDevice(
JNIEnv *env, jobject redrawer, jlong windowPtr, jboolean transparency, jlong adapterPtr, jlong platformInfoPtr) JNIEnv *env, jobject redrawer, jlong windowPtr, jboolean transparency, jlong adapterPtr, jlong platformInfoPtr)
{ {
@autoreleasepool { @autoreleasepool {
id<MTLDevice> adapter = (__bridge_transfer id<MTLDevice>) (void *) adapterPtr; id<MTLDevice> adapter = (__bridge id<MTLDevice>) (void *) adapterPtr;
MetalDevice *device = [MetalDevice new]; MetalDevice *device = [MetalDevice new];
...@@ -261,26 +187,6 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_disposeDe ...@@ -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( JNIEXPORT jboolean JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_isOccluded(
JNIEnv *env, jobject redrawer, jlong windowPtr) { JNIEnv *env, jobject redrawer, jlong windowPtr) {
@autoreleasepool { @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