Skip to content
This repository has been archived by the owner on Aug 27, 2022. It is now read-only.

8258754: Gracefully fallback to the OpenGL rendering pipeline if Metal rendering pipeline initialization fails #147

Closed
wants to merge 10 commits into from
57 changes: 53 additions & 4 deletions src/java.desktop/macosx/classes/sun/awt/CGraphicsDevice.java
Expand Up @@ -56,7 +56,10 @@ public final class CGraphicsDevice extends GraphicsDevice
private volatile Rectangle bounds;
private volatile int scale;

private final GraphicsConfiguration config;
private GraphicsConfiguration config;
private static boolean metalPipelineEnabled = false;
private static boolean oglPipelineEnabled = false;


private static AWTPermission fullScreenExclusivePermission;

Expand All @@ -65,9 +68,51 @@ public final class CGraphicsDevice extends GraphicsDevice

public CGraphicsDevice(final int displayID) {
this.displayID = displayID;
config = MacOSFlags.isMetalEnabled() ?
MTLGraphicsConfig.getConfig(this, displayID, 0) :
CGLGraphicsConfig.getConfig(this);

if (MacOSFlags.isMetalEnabled()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to use metal and OGL at the same time on different devices? I think it is better to move these checks to the CGraphicsEnvironment.makeScreenDevice(), similar Win32GraphicsEnvironment.makeScreenDevice()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why someone wants to use different pipelines on different devices? and should we support it?
On Windows we do that as a fallback to overcome a limitation of WGL - that's what I could infer from code in W32GraphicsDevice.getDefaultConfiguration()

I doubt whether we can add these checks in CGraphicsEnvironment.makeScreenDevice() - as we do not have specific MTLGraphicsDevice and CGLGraphicsDevice classes. That's the reason this method is currently unsupported.

Win32GraphicsEnvironment.makeScreenDevice() - creates either D3DGraphicsDevice or Win32GraphicsDevice based on whether isD3DEnabled.
Win32GraphicsDevice in turn uses either WGLGraphicsConfig or Win32GraphicsConfig based on whether OGL is enabled.

I felt CGraphicsDevice (similar to Win32GraphicsDevice) can differentiate between metal or OGL. Hence, I have added checks here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is unsupported then why you should check that for every device creation? What happens if one device was created using metal and then another device will use OGL?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have moved the Metal framework availability check to be done only once now.
There is still a check if MTLGraphicsConfig.getConfig() fails by any chance.

CGraphicsDevice contains GraphicsConfiguration - which is created in the constructor - It can be either MTLGraphicsConfiguration or OpenGLConfiguration. This check needs to be done while creating the config.
For Win32GraphicsDevice - the config is not created in the constructor, but at a later stage in getDefaultConfiguration(). So only the place of checking for OGL or falling back to GDI is different, but the concept is the same.

The whole idea of one device using metal and another device using OGL is hypothetical. The condition check cannot pass for one device and fail for another.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it makes no sense to even consider using different pipelines on different devices. The change you made to do the check once seems best.

aghaisas marked this conversation as resolved.
Show resolved Hide resolved
// Try to create MTLGraphicsConfig, if it fails, try to create CGLGraphicsConfig as a fallback
this.config = MTLGraphicsConfig.getConfig(this, displayID, 0);

if (this.config != null) {
metalPipelineEnabled = true;
} else {
// Try falling back to OpenGL pipeline
if (MacOSFlags.isMetalVerbose()) {
System.out.println("Metal rendering pipeline initialization failed. Using OpenGL rendering pipeline.");
}

this.config = CGLGraphicsConfig.getConfig(this);

if (this.config != null) {
oglPipelineEnabled = true;
}
}
} else {
// Try to create CGLGraphicsConfig, if it fails, try to create MTLGraphicsConfig as a fallback
this.config = CGLGraphicsConfig.getConfig(this);

if (this.config != null) {
oglPipelineEnabled = true;
} else {
// Try falling back to Metal pipeline
if (MacOSFlags.isOGLVerbose()) {
System.out.println("OpenGL rendering pipeline initialization failed. Using Metal rendering pipeline.");
}

this.config = MTLGraphicsConfig.getConfig(this, displayID, 0);

if (this.config != null) {
metalPipelineEnabled = true;
}
}
}

if (!metalPipelineEnabled && !oglPipelineEnabled) {
// This indicates fallback to other rendering pipeline also failed.
// Should never reach here
throw new InternalError("Error - unable to initialize any rendering pipeline.");
}

// initializes default device state, might be redundant step since we
// call "displayChanged()" later anyway, but we do not want to leave the
// device in an inconsistent state after construction
Expand Down Expand Up @@ -269,6 +314,10 @@ public DisplayMode[] getDisplayModes() {
return nativeGetDisplayModes(displayID);
}

public static boolean usingMetalPipeline() {
return metalPipelineEnabled;
}

private void initScaleFactor() {
if (SunGraphicsEnvironment.isUIScaleEnabled()) {
double debugScale = SunGraphicsEnvironment.getDebugScale();
Expand Down
Expand Up @@ -27,7 +27,7 @@

import sun.awt.image.SunVolatileImage;
import sun.awt.image.VolatileSurfaceManager;
import sun.java2d.macos.MacOSFlags;
import sun.awt.CGraphicsDevice;
import sun.java2d.metal.MTLVolatileSurfaceManager;
import sun.java2d.opengl.CGLVolatileSurfaceManager;

Expand All @@ -51,7 +51,7 @@ public class MacosxSurfaceManagerFactory extends SurfaceManagerFactory {
public VolatileSurfaceManager createVolatileManager(SunVolatileImage vImg,
Object context)
{
return MacOSFlags.isMetalEnabled() ? new MTLVolatileSurfaceManager(vImg, context) :
return CGraphicsDevice.usingMetalPipeline() ? new MTLVolatileSurfaceManager(vImg, context) :
new CGLVolatileSurfaceManager(vImg, context);
}
}
98 changes: 90 additions & 8 deletions src/java.desktop/macosx/classes/sun/java2d/macos/MacOSFlags.java
Expand Up @@ -26,6 +26,9 @@
package sun.java2d.macos;

import java.security.PrivilegedAction;
import sun.java2d.metal.MTLGraphicsConfig;
import sun.java2d.opengl.CGLGraphicsConfig;


public class MacOSFlags {

Expand All @@ -35,7 +38,12 @@ public class MacOSFlags {
* metalEnabled: usage: "-Dsun.java2d.metal=[true|false]"
*/

private static boolean oglEnabled;
private static boolean oglVerbose;
private static boolean metalEnabled;
private static boolean metalVerbose;

private enum PropertyState {ENABLED, DISABLED, UNSPECIFIED};

static {
initJavaFlags();
Expand All @@ -44,43 +52,117 @@ public class MacOSFlags {

private static native boolean initNativeFlags();

private static boolean getBooleanProp(String p, boolean defaultVal) {
private static PropertyState getBooleanProp(String p, PropertyState defaultVal) {
String propString = System.getProperty(p);
boolean returnVal = defaultVal;
PropertyState returnVal = defaultVal;
aghaisas marked this conversation as resolved.
Show resolved Hide resolved
if (propString != null) {
if (propString.equals("true") ||
propString.equals("t") ||
propString.equals("True") ||
propString.equals("T") ||
propString.equals("")) // having the prop name alone
{ // is equivalent to true
returnVal = true;
returnVal = PropertyState.ENABLED;
} else if (propString.equals("false") ||
propString.equals("f") ||
propString.equals("False") ||
propString.equals("F"))
{
returnVal = false;
returnVal = PropertyState.DISABLED;
}
}
return returnVal;
}


private static boolean getPropertySet(String p) {
private static boolean isBooleanPropTrueVerbose(String p) {
String propString = System.getProperty(p);
aghaisas marked this conversation as resolved.
Show resolved Hide resolved
return (propString != null) ? true : false;
if (propString != null) {
if (propString.equals("True") ||
propString.equals("T"))
{
return true;
}
}
return false;
}

private static void initJavaFlags() {
java.security.AccessController.doPrivileged(
(PrivilegedAction<Object>) () -> {
metalEnabled = getBooleanProp("sun.java2d.metal", false);
PropertyState oglState = getBooleanProp("sun.java2d.opengl", PropertyState.UNSPECIFIED);
PropertyState metalState = getBooleanProp("sun.java2d.metal", PropertyState.UNSPECIFIED);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right here we could add
if (metalState == PropertyState.UNSPECIFIED && oglState == PropertyState.UNSPECIFIED) {
// change this line to set metalState to toggle the default pipeline.
oglState = PropertyState.ENABLED;
}
This makes it easier to set the default

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion.
We need to handle other invalid combinations here as well - I will update the code.

// Handle invalid combinations to use the default rendering pipeline
// Current default rendering pipeline is OpenGL
// (The default can be changed to Metal in future just by toggling two states in this if condition block)
if ((oglState == PropertyState.UNSPECIFIED && metalState == PropertyState.UNSPECIFIED) ||
(oglState == PropertyState.DISABLED && metalState == PropertyState.DISABLED) ||
(oglState == PropertyState.ENABLED && metalState == PropertyState.ENABLED)) {
oglState = PropertyState.ENABLED; // Enable default pipeline
metalState = PropertyState.DISABLED; // Disable non-default pipeline
}

if (metalState == PropertyState.UNSPECIFIED) {
if (oglState == PropertyState.DISABLED) {
oglEnabled = false;
metalEnabled = true;
} else {
oglEnabled = true;
metalEnabled = false;
}
} else if (metalState == PropertyState.ENABLED) {
oglEnabled = false;
metalEnabled = true;
} else if (metalState == PropertyState.DISABLED) {
oglEnabled = true;
metalEnabled = false;
}

oglVerbose = isBooleanPropTrueVerbose("sun.java2d.opengl");
metalVerbose = isBooleanPropTrueVerbose("sun.java2d.metal");

if (oglEnabled && !metalEnabled) {
// Check whether OGL is available
if (!CGLGraphicsConfig.isCGLAvailable()) {
if (oglVerbose) {
System.out.println("Could not enable OpenGL pipeline (CGL not available)");
}
oglEnabled = false;
metalEnabled = MTLGraphicsConfig.isMetalAvailable();
}
} else if (metalEnabled && !oglEnabled) {
// Check whether Metal framework is available
if (!MTLGraphicsConfig.isMetalAvailable()) {
if (metalVerbose) {
System.out.println("Could not enable Metal pipeline (Metal framework not available)");
}
metalEnabled = false;
oglEnabled = CGLGraphicsConfig.isCGLAvailable();
}
}

// At this point one of the rendering pipeline must be enabled.
if (!metalEnabled && !oglEnabled) {
throw new InternalError("Error - unable to initialize any rendering pipeline.");
}

return null;
});
}

public static boolean isMetalEnabled() {
return metalEnabled;
}

public static boolean isMetalVerbose() {
return metalVerbose;
}

public static boolean isOGLEnabled() {
return oglEnabled;
}

public static boolean isOGLVerbose() {
return oglVerbose;
}
}
Expand Up @@ -78,7 +78,8 @@ public final class MTLGraphicsConfig extends CGraphicsConfig
private final Object disposerReferent = new Object();
private final int maxTextureSize;

private static native boolean initMTL();
private static native boolean isMetalFrameworkAvailable();
private static native boolean tryLoadMetalLibrary(int displayID, String shaderLib);
private static native long getMTLConfigInfo(int displayID, String mtlShadersLib);

/**
Expand All @@ -88,7 +89,7 @@ public final class MTLGraphicsConfig extends CGraphicsConfig
private static native int nativeGetMaxTextureSize();

static {
mtlAvailable = initMTL();
mtlAvailable = isMetalFrameworkAvailable();
}

private MTLGraphicsConfig(CGraphicsDevice device, int pixfmt,
Expand Down Expand Up @@ -126,6 +127,10 @@ public static MTLGraphicsConfig getConfig(CGraphicsDevice device,
return null;
}

if (!tryLoadMetalLibrary(displayID, mtlShadersLib)) {
return null;
}

long cfginfo = 0;
int textureSize = 0;
final String[] ids = new String[1];
Expand Down Expand Up @@ -164,6 +169,10 @@ public static MTLGraphicsConfig getConfig(CGraphicsDevice device,
return new MTLGraphicsConfig(device, pixfmt, cfginfo, textureSize, caps);
}

public static boolean isMetalAvailable() {
return mtlAvailable;
}

/**
* Returns true if the provided capability bit is present for this config.
* See MTLContext.java for a list of supported capabilities.
Expand Down
Expand Up @@ -68,13 +68,13 @@

import com.sun.java.swing.SwingUtilities3;
import sun.awt.AWTAccessor;
import sun.awt.CGraphicsDevice;
import sun.awt.PaintEventDispatcher;
import sun.awt.RepaintArea;
import sun.awt.SunToolkit;
import sun.awt.event.IgnorePaintEvent;
import sun.awt.image.SunVolatileImage;
import sun.java2d.SunGraphics2D;
import sun.java2d.macos.MacOSFlags;
import sun.java2d.metal.MTLRenderQueue;
import sun.java2d.opengl.OGLRenderQueue;
import sun.java2d.pipe.Region;
Expand Down Expand Up @@ -1417,7 +1417,7 @@ protected final void paintPeer(final Graphics g) {
}

protected static final void flushOnscreenGraphics(){
RenderQueue rq = MacOSFlags.isMetalEnabled() ?
RenderQueue rq = CGraphicsDevice.usingMetalPipeline() ?
MTLRenderQueue.getInstance() : OGLRenderQueue.getInstance();
rq.lock();
try {
Expand Down
Expand Up @@ -28,8 +28,8 @@
import java.awt.*;
import java.awt.event.FocusEvent;

import sun.awt.CGraphicsDevice;
import sun.java2d.SurfaceData;
import sun.java2d.macos.MacOSFlags;
import sun.java2d.metal.MTLLayer;
import sun.java2d.opengl.CGLLayer;
import sun.lwawt.LWWindowPeer;
Expand All @@ -56,7 +56,7 @@ public class CPlatformEmbeddedFrame implements PlatformWindow {
@Override // PlatformWindow
public void initialize(Window target, final LWWindowPeer peer, PlatformWindow owner) {
this.peer = peer;
if (MacOSFlags.isMetalEnabled()) {
if ( CGraphicsDevice.usingMetalPipeline()) {
this.windowLayer = new MTLLayer(peer);
} else {
this.windowLayer = new CGLLayer(peer);
Expand All @@ -71,7 +71,7 @@ public LWWindowPeer getPeer() {

@Override
public long getLayerPtr() {
if (MacOSFlags.isMetalEnabled()) {
if (CGraphicsDevice.usingMetalPipeline()) {
return ((MTLLayer)windowLayer).getPointer();
} else {
return ((CGLLayer)windowLayer).getPointer();
Expand All @@ -80,7 +80,7 @@ public long getLayerPtr() {

@Override
public void dispose() {
if (MacOSFlags.isMetalEnabled()) {
if ( CGraphicsDevice.usingMetalPipeline()) {
((MTLLayer)windowLayer).dispose();
} else {
((CGLLayer)windowLayer).dispose();
Expand Down Expand Up @@ -115,7 +115,7 @@ public FontMetrics getFontMetrics(Font f) {

@Override
public SurfaceData getScreenSurface() {
if (MacOSFlags.isMetalEnabled()) {
if ( CGraphicsDevice.usingMetalPipeline()) {
return ((MTLLayer)windowLayer).getSurfaceData();
} else {
return ((CGLLayer)windowLayer).getSurfaceData();
Expand All @@ -124,7 +124,7 @@ public SurfaceData getScreenSurface() {

@Override
public SurfaceData replaceSurfaceData() {
if (MacOSFlags.isMetalEnabled()) {
if ( CGraphicsDevice.usingMetalPipeline()) {
return ((MTLLayer)windowLayer).replaceSurfaceData();
} else {
return ((CGLLayer)windowLayer).replaceSurfaceData();
Expand Down