diff --git a/skiko/buildSrc/src/main/kotlin/tasks/configuration/CommonTasksConfiguration.kt b/skiko/buildSrc/src/main/kotlin/tasks/configuration/CommonTasksConfiguration.kt index 160b317c0..f6e566e25 100644 --- a/skiko/buildSrc/src/main/kotlin/tasks/configuration/CommonTasksConfiguration.kt +++ b/skiko/buildSrc/src/main/kotlin/tasks/configuration/CommonTasksConfiguration.kt @@ -90,7 +90,8 @@ fun skiaPreprocessorFlags(os: OS, buildType: SkiaBuildType): Array { "-DWIN32_LEAN_AND_MEAN", "-DNOMINMAX", "-DSK_GAMMA_APPLY_TO_A8", - "-DSK_DIRECT3D" + "-DSK_DIRECT3D", + "-DSK_ANGLE" ) OS.Linux -> listOf( "-DSK_BUILD_FOR_LINUX", diff --git a/skiko/buildSrc/src/main/kotlin/tasks/configuration/JvmTasksConfiguration.kt b/skiko/buildSrc/src/main/kotlin/tasks/configuration/JvmTasksConfiguration.kt index 31bcea778..804d3df5e 100644 --- a/skiko/buildSrc/src/main/kotlin/tasks/configuration/JvmTasksConfiguration.kt +++ b/skiko/buildSrc/src/main/kotlin/tasks/configuration/JvmTasksConfiguration.kt @@ -59,7 +59,8 @@ fun SkikoProjectContext.createCompileJvmBindingsTask( ) sourceRoots.set(srcDirs) if (targetOs != OS.Android) includeHeadersNonRecursive(jdkHome.resolve("include")) - includeHeadersNonRecursive(skiaHeadersDirs(skiaJvmBindingsDir.get())) + val skiaDir = skiaJvmBindingsDir.get() + includeHeadersNonRecursive(skiaHeadersDirs(skiaDir)) val projectDir = project.projectDir includeHeadersNonRecursive(projectDir.resolve("src/awtMain/cpp/include")) includeHeadersNonRecursive(projectDir.resolve("src/jvmMain/cpp/common")) @@ -98,6 +99,9 @@ fun SkikoProjectContext.createCompileJvmBindingsTask( OS.Windows -> { includeHeadersNonRecursive(windowsSdkPaths.includeDirs) includeHeadersNonRecursive(jdkHome.resolve("include/win32")) + includeHeadersNonRecursive(skiaDir.resolve("third_party/externals/angle2/include")) + includeHeadersNonRecursive(skiaDir.resolve("include/gpu")) + includeHeadersNonRecursive(skiaDir.resolve("src/gpu")) val targetArgs = if (targetArch == Arch.Arm64) arrayOf("/clang:--target=arm64-windows") else arrayOf() osFlags = arrayOf( "/nologo", @@ -106,10 +110,6 @@ fun SkikoProjectContext.createCompileJvmBindingsTask( "/GR-", // no-RTTI. "/FS", // Due to an error when building in Teamcity. https://docs.microsoft.com/en-us/cpp/build/reference/fs-force-synchronous-pdb-writes *targetArgs, - // LATER. Angle rendering arguments: - // "-I$skiaDir/third_party/externals/angle2/include", - // "-I$skiaDir/src/gpu", - // "-DSK_ANGLE", ) } OS.Android -> { @@ -309,6 +309,7 @@ fun SkikoProjectContext.createLinkJvmBindings( "ole32.lib", "Propsys.lib", "shcore.lib", + "Shlwapi.lib", "user32.lib", ) ) diff --git a/skiko/src/awtMain/cpp/windows/AngleRedrawer.cc b/skiko/src/awtMain/cpp/windows/AngleRedrawer.cc new file mode 100644 index 000000000..3aa179fa3 --- /dev/null +++ b/skiko/src/awtMain/cpp/windows/AngleRedrawer.cc @@ -0,0 +1,283 @@ +#ifdef SK_ANGLE + +#include +#include +#include +#include "jni_helpers.h" +#include "exceptions_handler.h" +#include "ganesh/GrBackendSurface.h" +#include "ganesh/GrDirectContext.h" +#include "ganesh/gl/GrGLBackendSurface.h" +#include "ganesh/gl/GrGLDirectContext.h" +#include "SkSurface.h" +#include +#include +#include "ganesh/gl/GrGLAssembleInterface.h" +#include "ganesh/gl/GrGLDefines.h" +#include +#include "window_util.h" + +void throwAngleException(JNIEnv *env, const char * function, const char * msg) { + char fullMsg[1024]; + snprintf(fullMsg, sizeof(fullMsg) - 1, "Native exception in [%s], message: %s", function, msg); + + static jclass cls = static_cast(env->NewGlobalRef(env->FindClass("org/jetbrains/skiko/redrawer/AngleRedrawerException"))); + static jmethodID init = env->GetMethodID(cls, "", "(Ljava/lang/String;)V"); + + jthrowable throwable = (jthrowable) env->NewObject(cls, init, env->NewStringUTF(fullMsg)); + env->Throw(throwable); +} + +class AngleDevice +{ +public: + HWND window; + HDC device; + EGLDisplay display = EGL_NO_DISPLAY; + EGLContext context = EGL_NO_CONTEXT; + EGLSurface surface = EGL_NO_SURFACE; + EGLConfig surfaceConfig; + sk_sp backendContext; + ~AngleDevice() + { + backendContext.reset(nullptr); + if (EGL_NO_CONTEXT != context) + { + eglDestroyContext(display, context); + } + if (EGL_NO_SURFACE != surface) + { + eglDestroySurface(display, surface); + } + if (EGL_NO_DISPLAY != display) + { + eglTerminate(display); + } + } +}; + +EGLDisplay getAngleEGLDisplay(HDC hdc) +{ + PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT; + eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC) eglGetProcAddress("eglGetPlatformDisplayEXT"); + // We expect ANGLE to support this extension + if (!eglGetPlatformDisplayEXT) + { + return EGL_NO_DISPLAY; + } + static constexpr EGLint attribs[] = { + // We currently only support D3D11 ANGLE. + EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, + EGL_NONE, EGL_NONE + }; + return eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, hdc, attribs); +} + +bool initAngleSurface(JNIEnv *env, AngleDevice *angleDevice, EGLint width, EGLint height) +{ + const EGLint surfaceAttribs[] = { + EGL_FIXED_SIZE_ANGLE, EGL_TRUE, + EGL_WIDTH, width, + EGL_HEIGHT, height, + EGL_NONE, EGL_NONE + }; + if (EGL_NO_SURFACE != angleDevice->surface) + { + eglDestroySurface(angleDevice->display, angleDevice->surface); + } + angleDevice->surface = eglCreateWindowSurface(angleDevice->display, angleDevice->surfaceConfig, angleDevice->window, surfaceAttribs); + if (EGL_NO_SURFACE == angleDevice->surface) + { + return false; + } + if (!eglMakeCurrent(angleDevice->display, angleDevice->surface, angleDevice->surface, angleDevice->context)) + { + throwAngleException(env, __FUNCTION__, "Could not make context current!"); + return false; + } + // For vsync we will use dwmFlush instead of swapInterval, see WindowsOpenGLRedrawer.kt + eglSwapInterval(angleDevice->display, 0); + return true; +} + +extern "C" +{ + JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_createAngleDevice( + JNIEnv *env, jobject redrawer, jlong platformInfoPtr, jboolean transparency) + { + __try + { + JAWT_Win32DrawingSurfaceInfo *dsi_win = fromJavaPointer(platformInfoPtr); + HWND hwnd = dsi_win->hwnd; + HDC hdc = GetDC(hwnd); + + if (transparency) + { + enableTransparentWindow(hwnd); + } + + AngleDevice *angleDevice = new AngleDevice(); + angleDevice->window = hwnd; + angleDevice->device = hdc; + angleDevice->display = getAngleEGLDisplay(angleDevice->device); + if (EGL_NO_DISPLAY == angleDevice->display) + { + throwAngleException(env, __FUNCTION__, "Could not get display!"); + return (jlong) 0; + } + + EGLint majorVersion; + EGLint minorVersion; + if (!eglInitialize(angleDevice->display, &majorVersion, &minorVersion)) + { + throwAngleException(env, __FUNCTION__, "Could not initialize display!"); + return (jlong) 0; + } + + static constexpr int fSampleCount = 1; + static constexpr int sampleBuffers = fSampleCount > 1 ? 1 : 0; + static constexpr int eglSampleCnt = fSampleCount > 1 ? fSampleCount : 0; + static constexpr EGLint configAttribs[] = { + // We currently only support ES3. + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_SAMPLE_BUFFERS, sampleBuffers, + EGL_SAMPLES, eglSampleCnt, + EGL_NONE, EGL_NONE + }; + + EGLint numConfigs; + if (!eglChooseConfig(angleDevice->display, configAttribs, &angleDevice->surfaceConfig, 1, &numConfigs)) + { + throwAngleException(env, __FUNCTION__, "Could not create choose config!"); + return (jlong) 0; + } + + // We currently only support ES3. + static constexpr EGLint contextAttribs[] = { + EGL_CONTEXT_MAJOR_VERSION, 3, + EGL_CONTEXT_MINOR_VERSION, 0, + EGL_NONE, EGL_NONE + }; + angleDevice->context = eglCreateContext(angleDevice->display, angleDevice->surfaceConfig, nullptr, contextAttribs); + if (EGL_NO_CONTEXT == angleDevice->context) + { + throwAngleException(env, __FUNCTION__, "Could not create context!"); + return (jlong) 0; + } + + // initial surface + if (!initAngleSurface(env, angleDevice, 0, 0)) + { + throwAngleException(env, __FUNCTION__, "Could not create surface!"); + return (jlong) 0; + } + + sk_sp glInterface(GrGLMakeAssembledInterface( + nullptr, + [](void *ctx, const char name[]) -> GrGLFuncPtr { return eglGetProcAddress(name); })); + + angleDevice->backendContext = glInterface; + + return toJavaPointer(angleDevice); + } + __except(EXCEPTION_EXECUTE_HANDLER) + { + auto code = GetExceptionCode(); + throwJavaRenderExceptionByExceptionCode(env, __FUNCTION__, code); + } + return (jlong) 0; + } + + JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeCurrent( + JNIEnv *env, jobject redrawer, jlong devicePtr) + { + AngleDevice *angleDevice = fromJavaPointer(devicePtr); + if (!eglMakeCurrent(angleDevice->display, angleDevice->surface, angleDevice->surface, angleDevice->context)) + { + throwAngleException(env, __FUNCTION__, "Could not make context current!"); + } + } + + JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeAngleContext( + JNIEnv *env, jobject redrawer, jlong devicePtr) + { + AngleDevice *angleDevice = fromJavaPointer(devicePtr); + sk_sp backendContext = angleDevice->backendContext; + return toJavaPointer(GrDirectContexts::MakeGL(backendContext).release()); + } + + JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeAngleRenderTarget( + JNIEnv *env, jobject redrawer, jlong devicePtr, jint width, jint height) + { + __try + { + AngleDevice *angleDevice = fromJavaPointer(devicePtr); + + if (!initAngleSurface(env, angleDevice, width, height)) + { + throwAngleException(env, __FUNCTION__, "Could not create surface!"); + return (jlong) 0; + } + + angleDevice->backendContext->fFunctions.fViewport(0, 0, width, height); + + GrGLint buffer; + angleDevice->backendContext->fFunctions.fGetIntegerv(GR_GL_FRAMEBUFFER_BINDING, &buffer); + + GrGLFramebufferInfo glInfo = { static_cast(buffer), GR_GL_RGBA8 }; + GrBackendRenderTarget renderTarget = GrBackendRenderTargets::MakeGL(width, + height, + 0, + 8, + glInfo); + GrBackendRenderTarget *pRenderTarget = new GrBackendRenderTarget(renderTarget); + + return toJavaPointer(pRenderTarget); + } + __except(EXCEPTION_EXECUTE_HANDLER) + { + auto code = GetExceptionCode(); + throwJavaRenderExceptionByExceptionCode(env, __FUNCTION__, code); + } + return (jlong) 0; + } + + JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_finishFrame( + JNIEnv *env, jobject redrawer, jlong devicePtr) + { + __try + { + AngleDevice *angleDevice = fromJavaPointer(devicePtr); + if (!eglSwapBuffers(angleDevice->display, angleDevice->surface)) + { + throwAngleException(env, __FUNCTION__, "Could not complete eglSwapBuffers."); + } + angleDevice->backendContext->fFunctions.fFinish(); + } + __except(EXCEPTION_EXECUTE_HANDLER) + { + auto code = GetExceptionCode(); + throwJavaRenderExceptionByExceptionCode(env, __FUNCTION__, code); + } + } + + JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_dwmFlush( + JNIEnv *env, jobject redrawer) + { + DwmFlush(); + } + + JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_disposeDevice( + JNIEnv *env, jobject redrawer, jlong devicePtr) + { + AngleDevice *angleDevice = fromJavaPointer(devicePtr); + eglMakeCurrent(angleDevice->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + delete angleDevice; + } +} // end extern C + +#endif diff --git a/skiko/src/awtMain/cpp/windows/AngleSupport.cc b/skiko/src/awtMain/cpp/windows/AngleSupport.cc new file mode 100644 index 000000000..d3cfbe71c --- /dev/null +++ b/skiko/src/awtMain/cpp/windows/AngleSupport.cc @@ -0,0 +1,103 @@ +#ifdef SK_ANGLE + +#include +#include +#define GL_GLES_PROTOTYPES 0 +#define EGL_EGL_PROTOTYPES 0 +#include +#include +#include "exceptions_handler.h" + +#define THROW_IF_NULL(action) \ + do { \ + auto __result { action }; \ + if (0 == __result) { \ + auto __code = GetLastError(); \ + throwJavaRenderExceptionByErrorCode(env, __FUNCTION__, __code); \ + return; \ + } \ + } while ((void)0, 0) + +static HINSTANCE AngleEGLLibrary = nullptr; + +extern "C" { + EGLBoolean EGLAPIENTRY eglInitialize (EGLDisplay dpy, EGLint *major, EGLint *minor) { + static auto eglInitialize = (PFNEGLINITIALIZEPROC) GetProcAddress(AngleEGLLibrary, "eglInitialize"); + return eglInitialize(dpy, major, minor); + } + + EGLBoolean EGLAPIENTRY eglTerminate (EGLDisplay dpy) { + static auto eglTerminate = (PFNEGLTERMINATEPROC) GetProcAddress(AngleEGLLibrary, "eglTerminate"); + return eglTerminate(dpy); + } + + EGLBoolean EGLAPIENTRY eglMakeCurrent (EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) { + static auto eglMakeCurrent = (PFNEGLMAKECURRENTPROC) GetProcAddress(AngleEGLLibrary, "eglMakeCurrent"); + return eglMakeCurrent(dpy, draw, read, ctx); + } + + EGLBoolean EGLAPIENTRY eglChooseConfig (EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config) { + static auto eglChooseConfig = (PFNEGLCHOOSECONFIGPROC) GetProcAddress(AngleEGLLibrary, "eglChooseConfig"); + return eglChooseConfig(dpy, attrib_list, configs, config_size, num_config); + } + + EGLContext EGLAPIENTRY eglCreateContext (EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list) { + static auto eglCreateContext = (PFNEGLCREATECONTEXTPROC) GetProcAddress(AngleEGLLibrary, "eglCreateContext"); + return eglCreateContext(dpy, config, share_context, attrib_list); + } + + EGLBoolean EGLAPIENTRY eglDestroyContext (EGLDisplay dpy, EGLContext ctx) { + static auto eglDestroyContext = (PFNEGLDESTROYCONTEXTPROC) GetProcAddress(AngleEGLLibrary, "eglDestroyContext"); + return eglDestroyContext(dpy, ctx); + } + + EGLSurface EGLAPIENTRY eglCreateWindowSurface (EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list) { + static auto eglCreateWindowSurface = (PFNEGLCREATEWINDOWSURFACEPROC) GetProcAddress(AngleEGLLibrary, "eglCreateWindowSurface"); + return eglCreateWindowSurface(dpy, config, win, attrib_list); + } + + EGLBoolean EGLAPIENTRY eglDestroySurface (EGLDisplay dpy, EGLSurface surface) { + static auto eglDestroySurface = (PFNEGLDESTROYSURFACEPROC) GetProcAddress(AngleEGLLibrary, "eglDestroySurface"); + return eglDestroySurface(dpy, surface); + } + + EGLBoolean EGLAPIENTRY eglSwapInterval (EGLDisplay dpy, EGLint interval) { + static auto eglSwapInterval = (PFNEGLSWAPINTERVALPROC) GetProcAddress(AngleEGLLibrary, "eglSwapInterval"); + return eglSwapInterval(dpy, interval); + } + + EGLBoolean EGLAPIENTRY eglSwapBuffers (EGLDisplay dpy, EGLSurface surface) { + static auto eglSwapBuffers = (PFNEGLSWAPBUFFERSPROC) GetProcAddress(AngleEGLLibrary, "eglSwapBuffers"); + return eglSwapBuffers(dpy, surface); + } + + __eglMustCastToProperFunctionPointerType EGLAPIENTRY eglGetProcAddress (const char *procname) { + static auto eglGetProcAddress = (PFNEGLGETPROCADDRESSPROC) GetProcAddress(AngleEGLLibrary, "eglGetProcAddress"); + return eglGetProcAddress(procname); + } + + JNIEXPORT jstring JNICALL Java_org_jetbrains_skiko_AngleApi_glGetString(JNIEnv *env, jobject object, jint value) + { + static auto glGetString = (PFNGLGETSTRINGPROC) eglGetProcAddress("glGetString"); + const char *content = reinterpret_cast(glGetString(value)); + return env->NewStringUTF(content); + } + + JNIEXPORT void JNICALL Java_org_jetbrains_skiko_AngleSupport_1jvmKt_loadAngleLibraryWindows(JNIEnv *env, jobject obj) { + TCHAR basePath[MAX_PATH] = TEXT(""); + TCHAR libEGL[MAX_PATH] = TEXT(""); + TCHAR libGLESv2[MAX_PATH] = TEXT(""); + HMODULE hmodule = nullptr; + THROW_IF_NULL(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPTSTR) &Java_org_jetbrains_skiko_AngleSupport_1jvmKt_loadAngleLibraryWindows, + &hmodule)); + THROW_IF_NULL(GetModuleFileName(hmodule, basePath, sizeof(basePath))); + THROW_IF_NULL(PathRemoveFileSpec(basePath)); + THROW_IF_NULL(PathCombine(libEGL, basePath, TEXT("libEGL.dll"))); + THROW_IF_NULL(PathCombine(libGLESv2, basePath, TEXT("libGLESv2.dll"))); + THROW_IF_NULL(AngleEGLLibrary = LoadLibrary(libEGL)); + THROW_IF_NULL(LoadLibrary(libGLESv2)); + } +} + +#endif diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/Actuals.awt.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/Actuals.awt.kt index c50307876..05d8f39d5 100644 --- a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/Actuals.awt.kt +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/Actuals.awt.kt @@ -31,6 +31,7 @@ internal actual fun makeDefaultRenderFactory(): RenderFactory = GraphicsApi.SOFTWARE_COMPAT -> SoftwareRedrawer(layer, analytics, properties) GraphicsApi.SOFTWARE_FAST -> WindowsSoftwareRedrawer(layer, analytics, properties) GraphicsApi.OPENGL -> WindowsOpenGLRedrawer(layer, analytics, properties) + GraphicsApi.ANGLE -> AngleRedrawer(layer, analytics, properties) else -> Direct3DRedrawer(layer, analytics, properties) } OS.Linux -> when (renderApi) { diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/context/AngleContextHandler.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/context/AngleContextHandler.kt new file mode 100644 index 000000000..cbb1fd862 --- /dev/null +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/context/AngleContextHandler.kt @@ -0,0 +1,72 @@ +package org.jetbrains.skiko.context + +import org.jetbrains.skia.* +import org.jetbrains.skiko.AngleApi +import org.jetbrains.skiko.Logger +import org.jetbrains.skiko.RenderException +import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.redrawer.AngleRedrawer + +internal class AngleContextHandler(layer: SkiaLayer) : JvmContextHandler(layer) { + private val angleRedrawer: AngleRedrawer + get() = layer.redrawer!! as AngleRedrawer + + override fun initContext(): Boolean { + try { + if (context == null) { + context = angleRedrawer.makeContext() + if (System.getProperty("skiko.hardwareInfo.enabled") == "true") { + Logger.info { "Renderer info:\n ${rendererInfo()}" } + } + } + } catch (e: Exception) { + Logger.warn(e) { "Failed to create Skia ANGLE context!" } + return false + } + return true + } + + private var currentWidth = 0 + private var currentHeight = 0 + private fun isSizeChanged(width: Int, height: Int): Boolean { + if (width != currentWidth || height != currentHeight) { + currentWidth = width + currentHeight = height + return true + } + return false + } + + override fun initCanvas() { + val context = context ?: return + val scale = layer.contentScale + + val w = (layer.width * scale).toInt().coerceAtLeast(0) + val h = (layer.height * scale).toInt().coerceAtLeast(0) + + if (isSizeChanged(w, h) || surface == null) { + disposeCanvas() + context.flush() + + renderTarget = angleRedrawer.makeRenderTarget(w, h) + surface = Surface.makeFromBackendRenderTarget( + context, + renderTarget!!, + SurfaceOrigin.BOTTOM_LEFT, + SurfaceColorFormat.RGBA_8888, + ColorSpace.sRGB, + SurfaceProps(pixelGeometry = layer.pixelGeometry) + ) ?: throw RenderException("Cannot create surface") + } + + canvas = surface!!.canvas + } + + override fun rendererInfo(): String { + return super.rendererInfo() + + "Vendor: ${AngleApi.glGetString(AngleApi.GL_VENDOR)}\n" + + "Model: ${AngleApi.glGetString(AngleApi.GL_RENDERER)}\n" + + "Version: ${AngleApi.glGetString(AngleApi.GL_VERSION)}\n" + // "Total VRAM: ${AngleApi.glGetIntegerv(AngleApi.GL_TOTAL_MEMORY) / 1024} MB\n" + } +} diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/AngleRedrawer.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/AngleRedrawer.kt new file mode 100644 index 000000000..05896b1a9 --- /dev/null +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/AngleRedrawer.kt @@ -0,0 +1,117 @@ +package org.jetbrains.skiko.redrawer + +import kotlinx.coroutines.withContext +import org.jetbrains.skia.BackendRenderTarget +import org.jetbrains.skia.DirectContext +import org.jetbrains.skiko.* +import org.jetbrains.skiko.context.AngleContextHandler + +internal class AngleRedrawer( + private val layer: SkiaLayer, + analytics: SkiaLayerAnalytics, + private val properties: SkiaLayerProperties +) : AWTRedrawer(layer, analytics, GraphicsApi.ANGLE) { + init { + loadAngleLibrary() + } + + private val contextHandler = AngleContextHandler(layer) + override val renderInfo: String get() = contextHandler.rendererInfo() + + private var drawLock = Any() + + private var device: Long = 0L + get() { + if (field == 0L) { + throw RenderException("ANGLE device is not initialized or already disposed") + } + return field + } + + private val frameDispatcher = FrameDispatcher(MainUIDispatcher) { + if (layer.isShowing) { + update(System.nanoTime()) + draw() + if (properties.isVsyncEnabled) { + withContext(dispatcherToBlockOn) { + dwmFlush() + } + } + } + } + + private val adapterName get() = AngleApi.glGetString(AngleApi.GL_RENDERER) + + init { + device = layer.backedLayer.useDrawingSurfacePlatformInfo { platformInfo -> + createAngleDevice(platformInfo, layer.transparency).takeIf { it != 0L } + ?: throw RenderException("Failed to create ANGLE device.") + } + adapterName.let { adapterName -> + if (adapterName == null || !isVideoCardSupported(GraphicsApi.ANGLE, hostOs, adapterName)) { + throw RenderException("Cannot create ANGLE redrawer.") + } + onDeviceChosen(adapterName) + } + onContextInit() + } + + override fun dispose() = synchronized(drawLock) { + frameDispatcher.cancel() + makeCurrent(device) + contextHandler.dispose() + disposeDevice(device) + device = 0L + super.dispose() + } + + override fun needRedraw() { + check(!isDisposed) { "ANGLE redrawer is disposed" } + frameDispatcher.scheduleFrame() + } + + override fun redrawImmediately() { + check(!isDisposed) { "ANGLE redrawer is disposed" } + inDrawScope { + update(System.nanoTime()) + drawAndSwap() + if (SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) { + dwmFlush() + } + } + } + + private fun draw() { + inDrawScope(::drawAndSwap) + } + + private fun drawAndSwap() = synchronized(drawLock) { + if (isDisposed) { + return + } + makeCurrent(device) + contextHandler.draw() + finishFrame(device) + } + + fun makeContext() = DirectContext( + makeAngleContext(device).takeIf { it != 0L } + ?: throw RenderException("Failed to make GL context.") + ) + + fun makeRenderTarget(width: Int, height: Int) = BackendRenderTarget( + makeAngleRenderTarget(device, width, height).takeIf { it != 0L } + ?: throw RenderException("Failed to make ANGLE render target.") + ) +} + +private external fun createAngleDevice(platformInfo: Long, transparency: Boolean): Long +private external fun makeCurrent(device: Long) +private external fun makeAngleContext(device: Long): Long +private external fun makeAngleRenderTarget(device: Long, width: Int, height: Int): Long +private external fun finishFrame(device: Long) +private external fun dwmFlush() +private external fun disposeDevice(device: Long) + +@Suppress("unused") +class AngleRedrawerException(message: String) : RuntimeException(message) diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skiko/AngleSupport.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/AngleSupport.kt new file mode 100644 index 000000000..5079657e3 --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/AngleSupport.kt @@ -0,0 +1,14 @@ +package org.jetbrains.skiko + +/** + * Load OpenGL library into memory. + * + * Should be called before any OpenGL operation. + * + * The current implementation loads OpenGl lazily on Windows, and at app startup on Linux/macOs, native and web targets + * + * Some Windows machines don't have OpenGL (usually CI machines), so we'll fallback to other renderers on them. + * + * @throws RenderException if OpenGL library can't be loaded. + */ +internal expect fun loadAngleLibrary() \ No newline at end of file diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt index ae45461bc..3253e58ff 100644 --- a/skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt @@ -17,6 +17,7 @@ enum class GraphicsApi { OPENGL, DIRECT3D, + ANGLE, VULKAN, METAL, WEBGL diff --git a/skiko/src/jsWasmMain/kotlin/org/jetbrains/skiko/Actuals.js.kt b/skiko/src/jsWasmMain/kotlin/org/jetbrains/skiko/Actuals.js.kt index 60762df0e..ca02f3c4a 100644 --- a/skiko/src/jsWasmMain/kotlin/org/jetbrains/skiko/Actuals.js.kt +++ b/skiko/src/jsWasmMain/kotlin/org/jetbrains/skiko/Actuals.js.kt @@ -35,4 +35,8 @@ internal actual fun getCursorById(id: PredefinedCursorsId): Cursor = internal actual fun loadOpenGLLibrary() { // Nothing to do here +} + +internal actual fun loadAngleLibrary() { + // Nothing to do here } \ No newline at end of file diff --git a/skiko/src/jvmMain/cpp/common/stubs.cc b/skiko/src/jvmMain/cpp/common/stubs.cc index 4e04ab336..2405039d7 100644 --- a/skiko/src/jvmMain/cpp/common/stubs.cc +++ b/skiko/src/jvmMain/cpp/common/stubs.cc @@ -221,4 +221,59 @@ JNIEXPORT jboolean JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_isOcc skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_MetalRedrawer_isOccluded"); return false; } -#endif \ No newline at end of file +#endif + + +#ifndef SK_ANGLE +JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_createAngleDevice( + JNIEnv *env, jobject redrawer, jlong platformInfoPtr, jboolean transparency) +{ + skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_createAngleDevice"); + return 0; +} + +JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeCurrent( + JNIEnv *env, jobject redrawer, jlong devicePtr) +{ + skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeCurrent"); +} + +JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeAngleContext( + JNIEnv *env, jobject redrawer, jlong devicePtr) +{ + skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeAngleContext"); + return 0; +} + +JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeAngleRenderTarget( + JNIEnv *env, jobject redrawer, jlong devicePtr, jint width, jint height) +{ + skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_makeAngleRenderTarget"); + return 0; +} + +JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_finishFrame( + JNIEnv *env, jobject redrawer, jlong devicePtr) +{ + skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_finishFrame"); +} + +JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_dwmFlush( + JNIEnv *env, jobject redrawer) +{ + skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_dwmFlush"); +} + +JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_disposeDevice( + JNIEnv *env, jobject redrawer, jlong devicePtr) +{ + skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_AngleRedrawerKt_disposeDevice"); +} + +JNIEXPORT jstring JNICALL Java_org_jetbrains_skiko_AngleApi_glGetString( + JNIEnv *env, jobject object, jint value) +{ + skikoUnimplemented("Java_org_jetbrains_skiko_AngleApi_glGetString"); + return 0; +} +#endif diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/AngleApi.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/AngleApi.kt new file mode 100644 index 000000000..c34e23239 --- /dev/null +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/AngleApi.kt @@ -0,0 +1,11 @@ +package org.jetbrains.skiko + +internal object AngleApi { + // OpenGL constants + val GL_VENDOR = 0x1F00 + val GL_RENDERER = 0x1F01 + val GL_VERSION = 0x1F02 + + // OpenGL functions + external fun glGetString(value: Int): String? +} diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/AngleSupport.jvm.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/AngleSupport.jvm.kt new file mode 100644 index 000000000..aa0080c3a --- /dev/null +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/AngleSupport.jvm.kt @@ -0,0 +1,26 @@ +package org.jetbrains.skiko + +import org.jetbrains.skia.impl.Library + +private external fun loadAngleLibraryWindows() + +private var isLoaded = false + +@Synchronized +internal actual fun loadAngleLibrary() { + if (!isLoaded) { + when { + hostOs.isWindows -> { + Library.staticLoad() + try { + loadAngleLibraryWindows() + } + catch (e: Exception) { + throw RenderException("Failed to load ANGLE library: ${e}") + } + } + else -> Unit + } + isLoaded = true + } +} \ No newline at end of file diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt index 4b912d1b6..715ed0e18 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt @@ -107,10 +107,15 @@ object SkikoProperties { "SOFTWARE_COMPAT" -> return GraphicsApi.SOFTWARE_COMPAT "SOFTWARE_FAST", "DIRECT_SOFTWARE" -> return GraphicsApi.SOFTWARE_FAST "SOFTWARE" -> return if (hostOs == OS.MacOS) GraphicsApi.SOFTWARE_COMPAT else GraphicsApi.SOFTWARE_FAST - "OPENGL" -> + "OPENGL" -> { // Skia isn't properly tested on OpenGL and Windows ARM (https://groups.google.com/g/skia-discuss/c/McoclAhLpvg?pli=1) return if (hostOs != OS.Windows || hostArch != Arch.Arm64) GraphicsApi.OPENGL else throw Exception("$hostOs-$hostArch does not support OpenGL rendering API.") + } + "ANGLE" -> { + return if (hostOs == OS.Windows) GraphicsApi.ANGLE + else throw Exception("$hostOs does not support ANGLE rendering API.") + } "DIRECT3D" -> { return if (hostOs == OS.Windows) GraphicsApi.DIRECT3D else throw Exception("$hostOs does not support DirectX rendering API.") @@ -127,7 +132,7 @@ object SkikoProperties { when(hostOs) { OS.MacOS -> return GraphicsApi.METAL OS.Linux -> return GraphicsApi.OPENGL - OS.Windows -> return GraphicsApi.DIRECT3D + OS.Windows -> return GraphicsApi.ANGLE OS.Android -> return GraphicsApi.OPENGL OS.JS, OS.Ios, OS.Tvos, OS.Unknown -> TODO("commonize me") } @@ -139,8 +144,8 @@ object SkikoProperties { OS.MacOS -> listOf(GraphicsApi.METAL, GraphicsApi.SOFTWARE_COMPAT) OS.Windows -> when (hostArch) { // Skia isn't properly tested on OpenGL and Windows ARM (https://groups.google.com/g/skia-discuss/c/McoclAhLpvg?pli=1) - Arch.Arm64 -> listOf(GraphicsApi.DIRECT3D, GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT) - else -> listOf(GraphicsApi.DIRECT3D, GraphicsApi.OPENGL, GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT) + Arch.Arm64 -> listOf(GraphicsApi.ANGLE, GraphicsApi.DIRECT3D, GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT) + else -> listOf(GraphicsApi.ANGLE, GraphicsApi.DIRECT3D, GraphicsApi.OPENGL, GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT) } OS.Android -> return listOf(GraphicsApi.OPENGL) OS.JS, OS.Ios, OS.Tvos, OS.Unknown -> TODO("commonize me") diff --git a/skiko/src/nativeMain/kotlin/org/jetbrains/skiko/Actuals.native.kt b/skiko/src/nativeMain/kotlin/org/jetbrains/skiko/Actuals.native.kt index a02e07c70..acaff9b14 100644 --- a/skiko/src/nativeMain/kotlin/org/jetbrains/skiko/Actuals.native.kt +++ b/skiko/src/nativeMain/kotlin/org/jetbrains/skiko/Actuals.native.kt @@ -3,4 +3,8 @@ package org.jetbrains.skiko internal actual inline fun maybeSynchronized(lock: Any, block: () -> R): R = block() -actual fun currentNanoTime(): Long = kotlin.system.getTimeNanos() \ No newline at end of file +actual fun currentNanoTime(): Long = kotlin.system.getTimeNanos() + +internal actual fun loadAngleLibrary() { + // Nothing to do here +} \ No newline at end of file