diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6cb44003f..dddabbcac9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ checkstyle = "13.3.0" jacoco = "0.8.12" lwjgl3 = "3.4.1" angle = "2026-05-09" -libjglios = "0.2" +libjglios = "0.3" saferalloc = "0.0.8" nifty = "1.4.3" spotbugs = "4.9.8" diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java index 7e61c51fa7..9a7a53cd7a 100644 --- a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java +++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java @@ -225,6 +225,11 @@ public void reshape(int width, int height) { app.reshape(width, height); } + @Override + public void reshape(int logicalWidth, int logicalHeight, int framebufferWidth, int framebufferHeight) { + app.reshape(logicalWidth, logicalHeight, framebufferWidth, framebufferHeight); + } + @Override public void rescale(float x, float y) { app.rescale(x, y); diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput14.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput14.java index 859ef595cd..9e76171dfc 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput14.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput14.java @@ -145,7 +145,9 @@ public void loadSettings(AppSettings settings) { if (inputHandler.getView().getWidth() != 0 && inputHandler.getView().getHeight() != 0) { scaleX = settings.getWidth() / (float)inputHandler.getView().getWidth(); scaleY = settings.getHeight() / (float)inputHandler.getView().getHeight(); - currentMouseState.setStartPosition(inputHandler.getView().getWidth()/2, inputHandler.getView().getHeight()/2); + currentMouseState.setStartPosition( + getJmeX(inputHandler.getView().getWidth() / 2f), + getJmeY(inputHandler.getView().getHeight() / 2f)); } if (logger.isLoggable(Level.FINE)) { diff --git a/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java b/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java index 259ad8b248..4b27f4decb 100644 --- a/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java +++ b/jme3-android/src/main/java/com/jme3/system/android/OGLESContext.java @@ -37,13 +37,10 @@ import android.content.DialogInterface; import android.content.pm.ConfigurationInfo; import android.graphics.PixelFormat; -import android.graphics.Rect; import android.opengl.GLSurfaceView; import android.os.Build; import android.text.InputType; import android.view.Gravity; -import android.view.SurfaceHolder; -import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.widget.EditText; @@ -58,6 +55,7 @@ import com.jme3.input.controls.SoftTextDialogInputListener; import com.jme3.input.dummy.DummyKeyInput; import com.jme3.material.Material; +import com.jme3.math.Vector2f; import com.jme3.renderer.Caps; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; @@ -68,6 +66,7 @@ import com.jme3.texture.FrameBuffer.FrameBufferTarget; import com.jme3.texture.Image; import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.texture.image.ColorSpace; import com.jme3.ui.Picture; @@ -99,6 +98,12 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex protected AndroidInputHandler androidInput; protected long minFrameDuration = 0; // No FPS cap protected long lastUpdateTime = 0; + private int logicalWidth = 1; + private int logicalHeight = 1; + private int framebufferWidth = 1; + private int framebufferHeight = 1; + private final Vector2f displayScale = new Vector2f(1f, 1f); + private float appliedDisplayScaleMode = Float.NaN; private Application application; private Material blitMaterial; private Picture blitGeometry; @@ -452,16 +457,62 @@ public void onSurfaceChanged(GL10 gl, int width, int height) { new Object[] { width, height } ); } - // update the application settings with the new resolution - settings.setResolution(width, height); - // Reload settings in androidInput so the correct touch event scaling can be - // calculated in case the surface resolution is different than the view. - androidInput.loadSettings(settings); + framebufferWidth = Math.max(width, 1); + framebufferHeight = Math.max(height, 1); + updateDisplayScaleMetrics(); // if the application has already been initialized (ie renderable is set) // then call reshape so the app can adjust to the new resolution. if (renderable.get()) { logger.log(Level.FINE, "App already initialized, calling reshape"); - listener.reshape(width, height); + listener.reshape(logicalWidth, logicalHeight, getRenderFramebufferWidth(), getRenderFramebufferHeight()); + listener.rescale(displayScale.x, displayScale.y); + } + } + + private float getAndroidDisplayDensity() { + if (androidInput != null && androidInput.getView() != null + && androidInput.getView().getResources() != null) { + android.util.DisplayMetrics metrics = androidInput.getView().getResources().getDisplayMetrics(); + if (metrics != null) { + if (metrics.density > 0f) { + return metrics.density; + } + if (metrics.densityDpi > 0) { + return metrics.densityDpi / 160f; + } + } + } + return 1f; + } + + private void updateDisplayScaleMetrics() { + float density = DisplayScaleUtils.sanitizeScale(getAndroidDisplayDensity()); + displayScale.set(density, density); + appliedDisplayScaleMode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isNativePixelsMode(appliedDisplayScaleMode)) { + logicalWidth = framebufferWidth; + logicalHeight = framebufferHeight; + } else { + logicalWidth = Math.max(Math.round(framebufferWidth / density), 1); + logicalHeight = Math.max(Math.round(framebufferHeight / density), 1); + } + settings.setResolution(logicalWidth, logicalHeight); + // Reload settings in androidInput so the correct touch event scaling can be + // calculated in case the surface resolution is different than the view. + if (androidInput != null) { + androidInput.loadSettings(settings); + } + } + + private void applyDisplayScaleModeIfNeeded() { + if (Float.compare(settings.getDisplayScaleMode(), appliedDisplayScaleMode) == 0) { + return; + } + + updateDisplayScaleMetrics(); + if (renderable.get()) { + listener.reshape(logicalWidth, logicalHeight, getRenderFramebufferWidth(), getRenderFramebufferHeight()); + listener.rescale(displayScale.x, displayScale.y); } } @@ -475,8 +526,13 @@ public void onDrawFrame(GL10 gl) { if (!renderable.get()) { if (created.get()) { + applyDisplayScaleModeIfNeeded(); logger.fine("GL Surface is setup, initializing application"); listener.initialize(); + if (framebufferWidth > 0 && framebufferHeight > 0) { + listener.reshape(logicalWidth, logicalHeight, getRenderFramebufferWidth(), getRenderFramebufferHeight()); + listener.rescale(displayScale.x, displayScale.y); + } renderable.set(true); } } else { @@ -484,6 +540,7 @@ public void onDrawFrame(GL10 gl) { throw new IllegalStateException("onDrawFrame without create"); } + applyDisplayScaleModeIfNeeded(); if (!renderFrameWithBlitSrgbConversion()) { listener.update(); } @@ -549,12 +606,43 @@ private boolean useBlitSrgbConversion() { return settings.isGammaCorrection() && application != null; } + private boolean useBlitFrameBuffer() { + float mode = settings.getDisplayScaleMode(); + return application != null && (useBlitSrgbConversion() + || DisplayScaleUtils.isDisabledMode(mode) || DisplayScaleUtils.isEmulatedScaleMode(mode)); + } + + private int getRenderFramebufferWidth() { + float mode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isDisabledMode(mode)) { + return Math.max(logicalWidth, 1); + } + if (DisplayScaleUtils.isEmulatedScaleMode(mode)) { + return Math.max(Math.round(framebufferWidth * mode), 1); + } + return Math.max(framebufferWidth, 1); + } + + private int getRenderFramebufferHeight() { + float mode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isDisabledMode(mode)) { + return Math.max(logicalHeight, 1); + } + if (DisplayScaleUtils.isEmulatedScaleMode(mode)) { + return Math.max(Math.round(framebufferHeight * mode), 1); + } + return Math.max(framebufferHeight, 1); + } + private int getLinearFrameBufferSampleCount() { int samples = Math.max(settings.getSamples(), 1); - if (samples > 1 && renderer != null && !renderer.getCaps().contains(Caps.TextureMultisample)) { + if (samples > 1 && renderer != null + && (!renderer.getCaps().contains(Caps.TextureMultisample) + || !renderer.getCaps().contains(Caps.OpenGL32))) { if (!multisampleTextureWarningIssued) { - logger.warning("sRGB blit conversion requires multisampled textures for MSAA. " - + "Falling back to a single-sample linear framebuffer."); + logger.log(Level.WARNING, + "Display scale blit requested {0}x MSAA, but this backend cannot sample multisample textures for the blit path. Falling back to a single-sample linear framebuffer.", + samples); multisampleTextureWarningIssued = true; } return 1; @@ -563,13 +651,13 @@ private int getLinearFrameBufferSampleCount() { } private void rebuildLinearFrameBufferIfNeeded() { - if (!useBlitSrgbConversion()) { + if (!useBlitFrameBuffer()) { destroyLinearFrameBufferResources(); return; } - int width = Math.max(settings.getWidth(), 1); - int height = Math.max(settings.getHeight(), 1); + int width = getRenderFramebufferWidth(); + int height = getRenderFramebufferHeight(); int samples = getLinearFrameBufferSampleCount(); if (linearFrameBuffer != null && linearFrameBuffer.getWidth() == width @@ -585,6 +673,8 @@ private void rebuildLinearFrameBufferIfNeeded() { Texture2D colorTexture = new Texture2D( new Image(getLinearFrameBufferColorFormat(), width, height, null, ColorSpace.Linear)); + colorTexture.setMagFilter(Texture.MagFilter.Bilinear); + colorTexture.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); if (samples > 1) { colorTexture.getImage().setMultiSamples(samples); } @@ -610,7 +700,7 @@ private Format getLinearFrameBufferColorFormat() { } private boolean ensureBlitResources() { - if (!useBlitSrgbConversion()) { + if (!useBlitFrameBuffer()) { return false; } @@ -622,10 +712,10 @@ private boolean ensureBlitResources() { if (blitMaterial == null) { blitMaterial = new Material(assetManager, BLIT_MATERIAL); - blitMaterial.setBoolean("Srgb", true); blitMaterial.getAdditionalRenderState().setDepthTest(false); blitMaterial.getAdditionalRenderState().setDepthWrite(false); } + blitMaterial.setBoolean("Srgb", useBlitSrgbConversion()); if (blitGeometry == null) { blitGeometry = new Picture("Linear to sRGB Blit"); @@ -667,7 +757,7 @@ private void destroyLinearFrameBufferResources() { } private boolean renderFrameWithBlitSrgbConversion() { - if (!useBlitSrgbConversion()) { + if (!useBlitFrameBuffer()) { return false; } @@ -699,8 +789,8 @@ private boolean renderFrameWithBlitSrgbConversion() { Camera previousCamera = renderManager.getCurrentCamera(); try { renderer.setFrameBuffer(null); - int blitWidth = linearFrameBuffer.getWidth(); - int blitHeight = linearFrameBuffer.getHeight(); + int blitWidth = Math.max(getFramebufferWidth(), 1); + int blitHeight = Math.max(getFramebufferHeight(), 1); if (blitCamera.getWidth() != blitWidth || blitCamera.getHeight() != blitHeight) { blitCamera.resize(blitWidth, blitHeight, true); } @@ -811,16 +901,9 @@ public void onClick(DialogInterface dialog, int whichButton) { ); } - /** - * Returns the height of the input surface. - * - * @return the height (in pixels) - */ @Override public int getFramebufferHeight() { - Rect rect = getSurfaceFrame(); - int result = rect.height(); - return result; + return framebufferHeight; } /** @@ -830,9 +913,7 @@ public int getFramebufferHeight() { */ @Override public int getFramebufferWidth() { - Rect rect = getSurfaceFrame(); - int result = rect.width(); - return result; + return framebufferWidth; } /** @@ -855,19 +936,6 @@ public int getWindowYPosition() { throw new UnsupportedOperationException("not implemented yet"); } - /** - * Retrieves the dimensions of the input surface. Note: do not modify the - * returned object. - * - * @return the dimensions (in pixels, left and top are 0) - */ - private Rect getSurfaceFrame() { - SurfaceView view = (SurfaceView) androidInput.getView(); - SurfaceHolder holder = view.getHolder(); - Rect result = holder.getSurfaceFrame(); - return result; - } - @Override public Displays getDisplays() { // TODO Auto-generated method stub diff --git a/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java b/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java index 1e0a75daf7..5d482c8719 100644 --- a/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java +++ b/jme3-core/src/main/java/com/jme3/app/LegacyApplication.java @@ -593,8 +593,13 @@ public void startCanvas(boolean waitFor) { */ @Override public void reshape(int w, int h) { + reshape(w, h, w, h); + } + + @Override + public void reshape(int logicalWidth, int logicalHeight, int framebufferWidth, int framebufferHeight) { if (renderManager != null) { - renderManager.notifyReshape(w, h); + renderManager.notifyReshape(logicalWidth, logicalHeight, framebufferWidth, framebufferHeight); } } diff --git a/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java b/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java index c0fc96d4cf..06665d1bf2 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderManager.java @@ -110,10 +110,12 @@ public class RenderManager { private final ArrayList postViewPorts = new ArrayList<>(); private final HashMap, PipelineContext> contexts = new HashMap<>(); private final LinkedList usedContexts = new LinkedList<>(); - private final LinkedList> usedPipelines = new LinkedList<>(); + private final LinkedList> usedPipelines = new LinkedList<>(); private RenderPipeline defaultPipeline = new ForwardPipeline(); - private Camera prevCam = null; - private Material forcedMaterial = null; + private Camera prevCam = null; + private int prevViewportWidth = -1; + private int prevViewportHeight = -1; + private Material forcedMaterial = null; private String forcedTechnique = null; private RenderState forcedRenderState = null; private final SafeArrayList forcedOverrides = new SafeArrayList<>(MatParamOverride.class); @@ -472,10 +474,11 @@ public ViewPort createPostView(String viewName, Camera cam) { return vp; } - private void notifyReshape(ViewPort vp, int w, int h) { - List processors = vp.getProcessors(); - for (SceneProcessor proc : processors) { - if (!proc.isInitialized()) { + private void notifyReshape(ViewPort vp, int w, int h) { + vp.setRenderTargetSize(w, h); + List processors = vp.getProcessors(); + for (SceneProcessor proc : processors) { + if (!proc.isInitialized()) { proc.initialize(this, vp); } else { proc.reshape(vp, w, h); @@ -502,33 +505,55 @@ private void notifyRescale(ViewPort vp, float x, float y) { * @param w the new width (in pixels) * @param h the new height (in pixels) */ - public void notifyReshape(int w, int h) { - for (ViewPort vp : preViewPorts) { - if (vp.getOutputFrameBuffer() == null) { - Camera cam = vp.getCamera(); - cam.resize(w, h, true); - } - notifyReshape(vp, w, h); - } - for (ViewPort vp : viewPorts) { - if (vp.getOutputFrameBuffer() == null) { - Camera cam = vp.getCamera(); - cam.resize(w, h, true); - } - notifyReshape(vp, w, h); - } - for (ViewPort vp : postViewPorts) { - if (vp.getOutputFrameBuffer() == null) { - Camera cam = vp.getCamera(); - cam.resize(w, h, true); - } - notifyReshape(vp, w, h); - } - } - - /** - * Internal use only. - * Updates the scale of all on-screen ViewPorts + public void notifyReshape(int w, int h) { + notifyReshape(w, h, w, h); + } + + /** + * Internal use only. + * Updates logical on-screen camera sizes while keeping the physical size of + * the default framebuffer available for viewport and post-processing work. + * + * @param logicalWidth the logical application width + * @param logicalHeight the logical application height + * @param framebufferWidth the physical default framebuffer width + * @param framebufferHeight the physical default framebuffer height + */ + public void notifyReshape(int logicalWidth, int logicalHeight, int framebufferWidth, int framebufferHeight) { + prevCam = null; + int surfaceWidth = Math.max(framebufferWidth, 1); + int surfaceHeight = Math.max(framebufferHeight, 1); + reshapeViewPorts(preViewPorts, logicalWidth, logicalHeight, surfaceWidth, surfaceHeight); + reshapeViewPorts(viewPorts, logicalWidth, logicalHeight, surfaceWidth, surfaceHeight); + reshapeViewPorts(postViewPorts, logicalWidth, logicalHeight, surfaceWidth, surfaceHeight); + } + + private void reshapeViewPorts(List viewPorts, int logicalWidth, int logicalHeight, + int surfaceWidth, int surfaceHeight) { + for (ViewPort vp : viewPorts) { + FrameBuffer frameBuffer = vp.getOutputFrameBuffer(); + if (frameBuffer == null) { + Camera cam = vp.getCamera(); + cam.resize(logicalWidth, logicalHeight, true); + notifyReshape(vp, surfaceWidth, surfaceHeight); + } else { + notifyReshape(vp, getFrameBufferWidth(frameBuffer, logicalWidth), + getFrameBufferHeight(frameBuffer, logicalHeight)); + } + } + } + + private int getFrameBufferWidth(FrameBuffer frameBuffer, int fallbackWidth) { + return frameBuffer.getWidth() > 0 ? frameBuffer.getWidth() : fallbackWidth; + } + + private int getFrameBufferHeight(FrameBuffer frameBuffer, int fallbackHeight) { + return frameBuffer.getHeight() > 0 ? frameBuffer.getHeight() : fallbackHeight; + } + + /** + * Internal use only. + * Updates the scale of all on-screen ViewPorts * * @param x the new horizontal scale * @param y the new vertical scale @@ -1237,11 +1262,11 @@ public void renderViewPortQueues(ViewPort vp, boolean flush) { prof.vpStep(VpStep.RenderBucket, vp, Bucket.Gui); } renderer.setDepthRange(0, 0); - setCamera(cam, true); - rq.renderQueue(Bucket.Gui, this, cam, flush); - setCamera(cam, false); - depthRangeChanged = true; - } + setCamera(cam, true, vp.getRenderTargetWidth(), vp.getRenderTargetHeight()); + rq.renderQueue(Bucket.Gui, this, cam, flush); + setCamera(cam, false, vp.getRenderTargetWidth(), vp.getRenderTargetHeight()); + depthRangeChanged = true; + } // restore range to default if (depthRangeChanged) { @@ -1272,20 +1297,23 @@ public void renderTranslucentQueue(ViewPort vp) { } } - private void setViewPort(Camera cam) { - // this will make sure to clearReservations viewport only if needed - if (cam != prevCam || cam.isViewportChanged()) { - int viewX = (int) (cam.getViewPortLeft() * cam.getWidth()); - int viewY = (int) (cam.getViewPortBottom() * cam.getHeight()); - int viewX2 = (int) (cam.getViewPortRight() * cam.getWidth()); - int viewY2 = (int) (cam.getViewPortTop() * cam.getHeight()); - int viewWidth = viewX2 - viewX; - int viewHeight = viewY2 - viewY; - uniformBindingManager.setViewPort(viewX, viewY, viewWidth, viewHeight); + private void setViewPort(Camera cam, int surfaceWidth, int surfaceHeight) { + // this will make sure to clearReservations viewport only if needed + if (cam != prevCam || cam.isViewportChanged() + || surfaceWidth != prevViewportWidth || surfaceHeight != prevViewportHeight) { + int viewX = (int) (cam.getViewPortLeft() * surfaceWidth); + int viewY = (int) (cam.getViewPortBottom() * surfaceHeight); + int viewX2 = (int) (cam.getViewPortRight() * surfaceWidth); + int viewY2 = (int) (cam.getViewPortTop() * surfaceHeight); + int viewWidth = viewX2 - viewX; + int viewHeight = viewY2 - viewY; + uniformBindingManager.setViewPort(viewX, viewY, viewWidth, viewHeight); renderer.setViewPort(viewX, viewY, viewWidth, viewHeight); - renderer.setClipRect(viewX, viewY, viewWidth, viewHeight); - cam.clearViewportChanged(); - prevCam = cam; + renderer.setClipRect(viewX, viewY, viewWidth, viewHeight); + cam.clearViewportChanged(); + prevCam = cam; + prevViewportWidth = surfaceWidth; + prevViewportHeight = surfaceHeight; // float translateX = viewWidth == viewX ? 0 : -(viewWidth + viewX) / (viewWidth - viewX); // float translateY = viewHeight == viewY ? 0 : -(viewHeight + viewY) / (viewHeight - viewY); @@ -1328,14 +1356,18 @@ private void setViewProjection(Camera cam, boolean ortho) { * @param ortho True if to use orthographic projection (for GUI rendering), * false if to use the camera's view and projection matrices. */ - public void setCamera(Camera cam, boolean ortho) { - // Tell the light filter which camera to use for filtering. - if (lightFilter != null) { - lightFilter.setCamera(cam); - } - setViewPort(cam); - setViewProjection(cam, ortho); - } + public void setCamera(Camera cam, boolean ortho) { + setCamera(cam, ortho, cam.getWidth(), cam.getHeight()); + } + + private void setCamera(Camera cam, boolean ortho, int targetWidth, int targetHeight) { + // Tell the light filter which camera to use for filtering. + if (lightFilter != null) { + lightFilter.setCamera(cam); + } + setViewPort(cam, targetWidth, targetHeight); + setViewProjection(cam, ortho); + } /** * Draws the viewport but without notifying {@link SceneProcessor scene @@ -1345,9 +1377,9 @@ public void setCamera(Camera cam, boolean ortho) { * * @see #renderViewPort(com.jme3.renderer.ViewPort, float) */ - public void renderViewPortRaw(ViewPort vp) { - setCamera(vp.getCamera(), false); - List scenes = vp.getScenes(); + public void renderViewPortRaw(ViewPort vp) { + setCamera(vp.getCamera(), false, vp.getRenderTargetWidth(), vp.getRenderTargetHeight()); + List scenes = vp.getScenes(); for (int i = scenes.size() - 1; i >= 0; i--) { renderScene(scenes.get(i), vp); } @@ -1360,10 +1392,10 @@ public void renderViewPortRaw(ViewPort vp) { * * @param vp The ViewPort to apply. */ - public void applyViewPort(ViewPort vp) { - renderer.setFrameBuffer(vp.getOutputFrameBuffer()); - setCamera(vp.getCamera(), false); - if (vp.isClearDepth() || vp.isClearColor() || vp.isClearStencil()) { + public void applyViewPort(ViewPort vp) { + renderer.setFrameBuffer(vp.getOutputFrameBuffer()); + setCamera(vp.getCamera(), false, vp.getRenderTargetWidth(), vp.getRenderTargetHeight()); + if (vp.isClearDepth() || vp.isClearColor() || vp.isClearStencil()) { if (vp.isClearColor()) { renderer.setBackgroundColor(vp.getBackgroundColor()); } diff --git a/jme3-core/src/main/java/com/jme3/renderer/ViewPort.java b/jme3-core/src/main/java/com/jme3/renderer/ViewPort.java index b1fcfed1b5..523f0ee7c4 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/ViewPort.java +++ b/jme3-core/src/main/java/com/jme3/renderer/ViewPort.java @@ -94,6 +94,8 @@ public class ViewPort { * FrameBuffer for output. */ protected FrameBuffer out = null; + protected int renderTargetWidth; + protected int renderTargetHeight; /** * Color applied when the color buffer is cleared. @@ -310,6 +312,41 @@ public void setOutputFrameBuffer(FrameBuffer out) { this.out = out; } + void setRenderTargetSize(int width, int height) { + renderTargetWidth = Math.max(width, 1); + renderTargetHeight = Math.max(height, 1); + } + + /** + * Returns the width of this ViewPort's current render target. + * + * @return the width in pixels + */ + public int getRenderTargetWidth() { + if (out != null && out.getWidth() > 0) { + return out.getWidth(); + } + if (renderTargetWidth > 0) { + return renderTargetWidth; + } + return cam.getWidth(); + } + + /** + * Returns the height of this ViewPort's current render target. + * + * @return the height in pixels + */ + public int getRenderTargetHeight() { + if (out != null && out.getHeight() > 0) { + return out.getHeight(); + } + if (renderTargetHeight > 0) { + return renderTargetHeight; + } + return cam.getHeight(); + } + /** * Returns the camera which renders the attached scenes. * diff --git a/jme3-core/src/main/java/com/jme3/system/AppSettings.java b/jme3-core/src/main/java/com/jme3/system/AppSettings.java index b39b115f2a..053f6b022d 100644 --- a/jme3-core/src/main/java/com/jme3/system/AppSettings.java +++ b/jme3-core/src/main/java/com/jme3/system/AppSettings.java @@ -62,6 +62,24 @@ public final class AppSettings extends HashMap { private static final AppSettings defaults = new AppSettings(false); + /** + * Do not request display scaling support on platforms where the backend can + * opt out. + */ + public static final float DISPLAY_SCALE_DISABLED = 0f; + + /** + * Request the native-density framebuffer and expose physical framebuffer + * pixels as the application coordinate system. + */ + public static final float DISPLAY_SCALE_NATIVE_PIXELS = -1f; + + /** + * Request the native-density framebuffer while keeping cameras, GUI, + * picking, and input in DPI-aware logical coordinates. + */ + public static final float DISPLAY_SCALE_DPI_AWARE = 1f; + /** * Use LWJGL as the display system and force using the OpenGL2.0 renderer. *

@@ -1512,20 +1530,67 @@ public void setGraphicsTrace(boolean trace) { putBoolean("GraphicsTrace", trace); } + /** + * Returns the active display scale mode. + *

+ * If {@link #setDisplayScaleMode(float)} was not called, this method + * returns {@link #DISPLAY_SCALE_DISABLED}. + *

+ * Values greater than {@link #DISPLAY_SCALE_DPI_AWARE} mean emulated display + * scaling. For example, {@code 1.5f} keeps DPI-aware application coordinates + * while rendering on-screen content to an intermediate framebuffer sized + * {@code physicalFramebufferSize * 1.5}. + * + * @return the active display scale mode + */ + public float getDisplayScaleMode() { + Object mode = get("DisplayScaleMode"); + if (mode instanceof Float) { + return DisplayScaleUtils.normalizeDisplayScaleMode((Float) mode); + } + return DISPLAY_SCALE_DISABLED; + } + + /** + * Sets the display scale mode. New applications should use this API + * instead of {@link #setUseRetinaFrameBuffer(boolean)}. + *

+ * Use {@link #DISPLAY_SCALE_DISABLED}, + * {@link #DISPLAY_SCALE_NATIVE_PIXELS}, or + * {@link #DISPLAY_SCALE_DPI_AWARE} for built-in modes. Values below + * {@code 1.0}, except {@link #DISPLAY_SCALE_NATIVE_PIXELS}, are normalized + * to {@link #DISPLAY_SCALE_DISABLED}. Values above + * {@link #DISPLAY_SCALE_DPI_AWARE} enable emulated display scaling above the + * physical framebuffer size. + * + * @param mode the desired mode + */ + public void setDisplayScaleMode(float mode) { + if (!Float.isFinite(mode)) { + throw new IllegalArgumentException("DisplayScaleMode must be finite."); + } + putFloat("DisplayScaleMode", DisplayScaleUtils.normalizeDisplayScaleMode(mode)); + } + /** * Determine whether to use full resolution framebuffers on Retina displays. * * @return whether to use full resolution framebuffers on Retina displays. + * @deprecated use {@link #getDisplayScaleMode()} instead. */ + @Deprecated public boolean isUseRetinaFrameBuffer() { return getBoolean("UseRetinaFrameBuffer"); } /** - * Specifies whether to use full resolution framebuffers on Retina displays. This is ignored on other platforms. + * Specifies whether to use full resolution framebuffers on Retina/high-DPI + * displays where the backend supports requesting them. * * @param useRetinaFrameBuffer whether to use full resolution framebuffers on Retina displays. + * @deprecated use {@link #setDisplayScaleMode(float)} instead. */ + @Deprecated public void setUseRetinaFrameBuffer(boolean useRetinaFrameBuffer) { putBoolean("UseRetinaFrameBuffer", useRetinaFrameBuffer); } diff --git a/jme3-core/src/main/java/com/jme3/system/DisplayScaleUtils.java b/jme3-core/src/main/java/com/jme3/system/DisplayScaleUtils.java new file mode 100644 index 0000000000..2fb214f809 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/system/DisplayScaleUtils.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.system; + +/** + * Shared calculations for the split logical/physical display scale model. + */ +public final class DisplayScaleUtils { + + private DisplayScaleUtils() { + } + + /** + * Resolves the logical application size for a window-backed context. + * + * @param mode the active display scale mode + * @param windowWidth the platform window coordinate width + * @param windowHeight the platform window coordinate height + * @param framebufferWidth the physical framebuffer width + * @param framebufferHeight the physical framebuffer height + * @param displayScaleX the platform content/display scale on X + * @param displayScaleY the platform content/display scale on Y + * @return an array containing logical width and logical height + */ + public static int[] resolveLogicalSize(float mode, int windowWidth, int windowHeight, + int framebufferWidth, int framebufferHeight, float displayScaleX, float displayScaleY) { + if (!isDpiAwareMode(mode)) { + return new int[] { Math.max(framebufferWidth, 1), Math.max(framebufferHeight, 1) }; + } + + int safeWindowWidth = Math.max(windowWidth, 1); + int safeWindowHeight = Math.max(windowHeight, 1); + int safeFramebufferWidth = Math.max(framebufferWidth, 1); + int safeFramebufferHeight = Math.max(framebufferHeight, 1); + float scaleX = sanitizeScale(displayScaleX); + float scaleY = sanitizeScale(displayScaleY); + + int scaledWidth = Math.max(Math.round(safeFramebufferWidth / scaleX), 1); + int scaledHeight = Math.max(Math.round(safeFramebufferHeight / scaleY), 1); + + if (approximatelyEqual(scaledWidth, safeWindowWidth) && approximatelyEqual(scaledHeight, safeWindowHeight)) { + return new int[] { safeWindowWidth, safeWindowHeight }; + } + + return new int[] { scaledWidth, scaledHeight }; + } + + public static float sanitizeScale(float value) { + return Float.isFinite(value) && value > 0f ? value : 1f; + } + + public static float normalizeDisplayScaleMode(float mode) { + if (!Float.isFinite(mode)) { + return AppSettings.DISPLAY_SCALE_DISABLED; + } + if (mode == AppSettings.DISPLAY_SCALE_NATIVE_PIXELS) { + return AppSettings.DISPLAY_SCALE_NATIVE_PIXELS; + } + if (mode < AppSettings.DISPLAY_SCALE_DPI_AWARE) { + return AppSettings.DISPLAY_SCALE_DISABLED; + } + return mode; + } + + public static boolean isNativePixelsMode(float mode) { + return mode == AppSettings.DISPLAY_SCALE_NATIVE_PIXELS; + } + + public static boolean isDpiAwareMode(float mode) { + return mode >= AppSettings.DISPLAY_SCALE_DPI_AWARE; + } + + public static boolean isEmulatedScaleMode(float mode) { + return mode > AppSettings.DISPLAY_SCALE_DPI_AWARE; + } + + public static boolean isDisabledMode(float mode) { + return !isNativePixelsMode(mode) && !isDpiAwareMode(mode); + } + + public static boolean requestsHighDensityFramebuffer(float mode) { + return isNativePixelsMode(mode) || isDpiAwareMode(mode); + } + + /** + * Converts a native window-coordinate X value to jME input coordinates. + * + * @param nativeX the platform coordinate + * @param targetWidth the target jME coordinate width + * @param nativeWidth the platform coordinate width + * @return the jME X coordinate + */ + public static int toInputX(float nativeX, int targetWidth, int nativeWidth) { + return Math.round(nativeX * Math.max(targetWidth, 1) / Math.max(nativeWidth, 1)); + } + + /** + * Converts a native top-origin Y value to jME bottom-origin input + * coordinates. + * + * @param nativeY the platform coordinate + * @param targetHeight the target jME coordinate height + * @param nativeHeight the platform coordinate height + * @return the jME Y coordinate + */ + public static int toInputY(float nativeY, int targetHeight, int nativeHeight) { + int safeTargetHeight = Math.max(targetHeight, 1); + return Math.round(safeTargetHeight - (nativeY * safeTargetHeight / Math.max(nativeHeight, 1))); + } + + private static boolean approximatelyEqual(int a, int b) { + return Math.abs(a - b) <= 1; + } +} diff --git a/jme3-core/src/main/java/com/jme3/system/SystemListener.java b/jme3-core/src/main/java/com/jme3/system/SystemListener.java index f6145c5783..a1c32b0e1c 100644 --- a/jme3-core/src/main/java/com/jme3/system/SystemListener.java +++ b/jme3-core/src/main/java/com/jme3/system/SystemListener.java @@ -46,11 +46,24 @@ public interface SystemListener { /** * Called to notify the application that the resolution has changed. - * @param width the new width of the display (in pixels, ≥0) - * @param height the new height of the display (in pixels, ≥0) + * @param width the new logical width of the display (≥0) + * @param height the new logical height of the display (≥0) */ public void reshape(int width, int height); + /** + * Called to notify the application that logical application size and + * physical framebuffer size changed independently. + * + * @param logicalWidth the width used by cameras, GUI, picking, and input + * @param logicalHeight the height used by cameras, GUI, picking, and input + * @param framebufferWidth the physical framebuffer width in pixels + * @param framebufferHeight the physical framebuffer height in pixels + */ + public default void reshape(int logicalWidth, int logicalHeight, int framebufferWidth, int framebufferHeight) { + reshape(logicalWidth, logicalHeight); + } + /** * Called to notify the application that the scale has changed. * @param x the new horizontal scale of the display diff --git a/jme3-core/src/test/java/com/jme3/system/DisplayScaleUtilsTest.java b/jme3-core/src/test/java/com/jme3/system/DisplayScaleUtilsTest.java new file mode 100644 index 0000000000..507a207784 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/system/DisplayScaleUtilsTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.system; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DisplayScaleUtilsTest { + + @Test + void logicalModeKeepsWindowCoordinatesWhenTheyMatchDisplayScale() { + int[] size = DisplayScaleUtils.resolveLogicalSize( + AppSettings.DISPLAY_SCALE_DPI_AWARE, 1280, 720, 2560, 1440, 2f, 2f); + + assertArrayEquals(new int[] {1280, 720}, size); + } + + @Test + void nativePixelsUsesFramebufferSizeAsLogicalSize() { + int[] size = DisplayScaleUtils.resolveLogicalSize( + AppSettings.DISPLAY_SCALE_NATIVE_PIXELS, 1280, 720, 2560, 1440, 2f, 2f); + + assertArrayEquals(new int[] {2560, 1440}, size); + } + + @Test + void disabledUsesFramebufferSizeAsLogicalSize() { + int[] size = DisplayScaleUtils.resolveLogicalSize( + AppSettings.DISPLAY_SCALE_DISABLED, 1280, 720, 2560, 1440, 2f, 2f); + + assertArrayEquals(new int[] {2560, 1440}, size); + } + + @Test + void inputConversionCanTargetLogicalOrPhysicalCoordinates() { + assertEquals(640, DisplayScaleUtils.toInputX(640, 1280, 1280)); + assertEquals(360, DisplayScaleUtils.toInputY(360, 720, 720)); + assertEquals(1280, DisplayScaleUtils.toInputX(640, 2560, 1280)); + assertEquals(720, DisplayScaleUtils.toInputY(360, 1440, 720)); + } +} diff --git a/jme3-examples/src/main/java/jme3test/app/TestDisplayScaling.java b/jme3-examples/src/main/java/jme3test/app/TestDisplayScaling.java new file mode 100644 index 0000000000..16a2495c93 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/app/TestDisplayScaling.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package jme3test.app; + +import com.jme3.app.SimpleApplication; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListenerAdapter; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.input.event.TouchEvent; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Box; +import com.jme3.scene.shape.Quad; +import com.jme3.scene.debug.Grid; +import com.jme3.system.AppSettings; + + +public class TestDisplayScaling extends SimpleApplication implements ActionListener { + + private static final String NEXT_SCALE_MODE = "NextScaleMode"; + private static final float SUPERSAMPLING = 2f; + + private BitmapText infoText; + private Geometry cube; + private Geometry testPanel; + private BitmapText testText; + private Node qualityTarget; + private float requestedMode = Float.NaN; + + public static void main(String[] args) { + TestDisplayScaling app = new TestDisplayScaling(); + AppSettings settings = new AppSettings(true); + settings.setWindowSize(1280, 720); + settings.setDisplayScaleMode(AppSettings.DISPLAY_SCALE_DISABLED); + app.setSettings(settings); + app.start(); + } + + @Override + public void simpleInitApp() { + flyCam.setEnabled(false); + inputManager.addMapping("ScaleDisabled", new KeyTrigger(KeyInput.KEY_1)); + inputManager.addMapping("ScaleNativePixels", new KeyTrigger(KeyInput.KEY_2)); + inputManager.addMapping("ScaleDpiAware", new KeyTrigger(KeyInput.KEY_3)); + inputManager.addMapping("ScaleSS", new KeyTrigger(KeyInput.KEY_4)); + inputManager.addMapping(NEXT_SCALE_MODE, new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + inputManager.addListener(this, "ScaleDisabled", "ScaleNativePixels", "ScaleDpiAware", "ScaleSS", + NEXT_SCALE_MODE); + inputManager.addRawInputListener(new RawInputListenerAdapter() { + @Override + public void onTouchEvent(TouchEvent evt) { + if (evt.getType() == TouchEvent.Type.DOWN) { + requestCycleMode(); + } + } + }); + + viewPort.setBackgroundColor(new ColorRGBA(0.08f, 0.09f, 0.11f, 1f)); + cam.setLocation(new Vector3f(0f, 0f, 6f)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + + Geometry fineGrid = new Geometry("Fine Edge Quality Grid", new Grid(121, 121, 0.05f)); + fineGrid.setMaterial(createMaterial(new ColorRGBA(0.75f, 0.78f, 0.84f, 1f))); + fineGrid.setLocalTranslation(-3f, -3f, -1.35f); + fineGrid.rotate(FastMath.HALF_PI, 0f, FastMath.QUARTER_PI); + rootNode.attachChild(fineGrid); + + cube = new Geometry("High DPI Cube", new Box(1f, 1f, 1f)); + cube.setMaterial(createMaterial(new ColorRGBA(0.1f, 0.65f, 1f, 1f))); + rootNode.attachChild(cube); + + testPanel = new Geometry("Logical GUI Test Panel", new Quad(180f, 80f)); + testPanel.setMaterial(createMaterial(new ColorRGBA(1f, 0.72f, 0.18f, 1f))); + testPanel.setLocalTranslation(40f, 40f, 0f); + guiNode.attachChild(testPanel); + + testText = new BitmapText(guiFont); + testText.setText("TEST"); + testText.setSize(36f); + testText.setColor(ColorRGBA.Black); + testText.setLocalTranslation(60f, 98f, 1f); + guiNode.attachChild(testText); + + createQualityTarget(); + + infoText = new BitmapText(guiFont); + infoText.setSize(18f); + infoText.setLocalTranslation(20f, cam.getHeight() - 20f, 0f); + guiNode.attachChild(infoText); + } + + @Override + public void simpleUpdate(float tpf) { + if (!Float.isNaN(requestedMode)) { + setMode(requestedMode); + return; + } + + cube.rotate(tpf * 0.45f, tpf * 0.7f, 0f); + Vector2f cursor = inputManager.getCursorPosition(); + infoText.setText("Mode: " + getDisplayScaleModeName(settings.getDisplayScaleMode()) + + "\nLogical: " + cam.getWidth() + " x " + cam.getHeight() + + "\nRender target: " + viewPort.getRenderTargetWidth() + " x " + viewPort.getRenderTargetHeight() + + "\nDrawable: " + context.getFramebufferWidth() + " x " + context.getFramebufferHeight() + + "\nMouse: " + Math.round(cursor.x) + ", " + Math.round(cursor.y) + + "\nKeys: 1 disabled, 2 native pixels, 3 DPI aware, 4 supersampling 2x" + + "\nClick/touch: cycle mode"); + infoText.setLocalTranslation(20f, cam.getHeight() - 20f, 0f); + qualityTarget.setLocalTranslation(cam.getWidth() - 390f, 40f, 0f); + } + + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (!isPressed) { + return; + } + if ("ScaleDisabled".equals(name)) { + requestMode(AppSettings.DISPLAY_SCALE_DISABLED); + } else if ("ScaleNativePixels".equals(name)) { + requestMode(AppSettings.DISPLAY_SCALE_NATIVE_PIXELS); + } else if ("ScaleDpiAware".equals(name)) { + requestMode(AppSettings.DISPLAY_SCALE_DPI_AWARE); + } else if ("ScaleSS".equals(name)) { + requestMode(SUPERSAMPLING); + } else if (NEXT_SCALE_MODE.equals(name)) { + requestCycleMode(); + } + } + + private void requestCycleMode() { + float currentMode = settings.getDisplayScaleMode(); + if (currentMode == AppSettings.DISPLAY_SCALE_DISABLED) { + requestMode(AppSettings.DISPLAY_SCALE_NATIVE_PIXELS); + } else if (currentMode == AppSettings.DISPLAY_SCALE_NATIVE_PIXELS) { + requestMode(AppSettings.DISPLAY_SCALE_DPI_AWARE); + } else if (currentMode == AppSettings.DISPLAY_SCALE_DPI_AWARE) { + requestMode(SUPERSAMPLING); + } else { + requestMode(AppSettings.DISPLAY_SCALE_DISABLED); + } + } + + private void requestMode(float mode) { + requestedMode = mode; + } + + private String getDisplayScaleModeName(float mode) { + if (mode == AppSettings.DISPLAY_SCALE_DISABLED) { + return "DISABLED"; + } else if (mode == AppSettings.DISPLAY_SCALE_NATIVE_PIXELS) { + return "NATIVE_PIXELS"; + } else if (mode == AppSettings.DISPLAY_SCALE_DPI_AWARE) { + return "DPI_AWARE"; + } + return "SUPERSAMPLING " + mode + "x"; + } + + private void setMode(float mode) { + requestedMode = Float.NaN; + settings.setDisplayScaleMode(mode); + restart(); + } + + private void createQualityTarget() { + qualityTarget = new Node("High DPI Quality Target"); + guiNode.attachChild(qualityTarget); + + Geometry background = new Geometry("Quality Target Background", new Quad(350f, 150f)); + background.setMaterial(createMaterial(new ColorRGBA(0.02f, 0.025f, 0.03f, 1f))); + qualityTarget.attachChild(background); + + Material white = createMaterial(ColorRGBA.White); + Material gray = createMaterial(new ColorRGBA(0.45f, 0.48f, 0.52f, 1f)); + + for (int x = 20; x < 180; x += 2) { + Geometry stripe = new Geometry("One Pixel Stripe", new Quad(1f, 56f)); + stripe.setMaterial(white); + stripe.setLocalTranslation(x, 72f, 1f); + qualityTarget.attachChild(stripe); + } + + for (int i = 0; i < 18; i++) { + Geometry diagonal = new Geometry("Subtle Diagonal Edge", new Quad(155f, 1f)); + diagonal.setMaterial(i % 2 == 0 ? white : gray); + diagonal.setLocalTranslation(190f, 20f + i * 6f, 1f); + diagonal.rotate(0f, 0f, 0.08f * i); + qualityTarget.attachChild(diagonal); + } + + BitmapText label = new BitmapText(guiFont); + label.setText("1px stripes and tiny text"); + label.setSize(10f); + label.setLocalTranslation(20f, 142f, 1f); + qualityTarget.attachChild(label); + + BitmapText tinyText = new BitmapText(guiFont); + tinyText.setText("abcdef0123456789 ABCDEF"); + tinyText.setSize(8f); + tinyText.setLocalTranslation(20f, 58f, 1f); + qualityTarget.attachChild(tinyText); + } + + private Material createMaterial(ColorRGBA color) { + Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setColor("Color", color); + return material; + } +} diff --git a/jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java b/jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java index e0ba22f276..d0cbfc1f74 100644 --- a/jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java +++ b/jme3-ios/src/main/java/com/jme3/system/ios/IGLESContext.java @@ -41,6 +41,7 @@ import com.jme3.input.ios.IosInputHandler; import com.jme3.input.ios.IosJoyInput; import com.jme3.material.Material; +import com.jme3.math.Vector2f; import com.jme3.renderer.Caps; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; @@ -51,6 +52,7 @@ import com.jme3.texture.FrameBuffer.FrameBufferTarget; import com.jme3.texture.Image; import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.texture.image.ColorSpace; import com.jme3.ui.Picture; @@ -73,6 +75,10 @@ public class IGLESContext implements JmeContext { protected boolean autoFlush = true; protected int framebufferWidth; protected int framebufferHeight; + protected int logicalWidth = 1; + protected int logicalHeight = 1; + private final Vector2f displayScale = new Vector2f(1f, 1f); + private float appliedDisplayScaleMode = Float.NaN; /* * >= OpenGL ES 2.0 (iOS) @@ -239,7 +245,7 @@ public void create(boolean waitFor) { listener.initialize(); renderable.set(true); if (framebufferWidth > 0 && framebufferHeight > 0) { - listener.reshape(framebufferWidth, framebufferHeight); + listener.reshape(logicalWidth, logicalHeight, getRenderFramebufferWidth(), getRenderFramebufferHeight()); } if (waitFor) { @@ -259,6 +265,7 @@ public void runFrame() { throw new IllegalStateException("Unable to make iOS EGL context current: " + LibJGLIOSEglBridge.lastError()); } updateFramebufferSizeFromLibJGLIOS(); + applyDisplayScaleModeIfNeeded(); if (!renderFrameWithBlitSrgbConversion()) { listener.update(); } @@ -273,6 +280,9 @@ public void runFrame() { public void resizeFramebuffer(int width, int height) { int oldWidth = framebufferWidth; int oldHeight = framebufferHeight; + int oldLogicalWidth = logicalWidth; + int oldLogicalHeight = logicalHeight; + boolean displayScaleModeChanged = Float.compare(settings.getDisplayScaleMode(), appliedDisplayScaleMode) != 0; if (width > 0) { framebufferWidth = width; } @@ -280,22 +290,71 @@ public void resizeFramebuffer(int width, int height) { framebufferHeight = height; } if (framebufferWidth > 0 && framebufferHeight > 0) { - settings.setResolution(framebufferWidth, framebufferHeight); - settings.setWindowSize(framebufferWidth, framebufferHeight); + updateLogicalSize(); + settings.setResolution(logicalWidth, logicalHeight); + settings.setWindowSize(logicalWidth, logicalHeight); } if (input != null) { - input.setFramebufferSize(framebufferWidth, framebufferHeight); + input.setFramebufferSize(logicalWidth, logicalHeight); } - if (framebufferWidth != oldWidth || framebufferHeight != oldHeight) { + if (framebufferWidth != oldWidth || framebufferHeight != oldHeight + || logicalWidth != oldLogicalWidth || logicalHeight != oldLogicalHeight + || displayScaleModeChanged) { linearFrameBufferDirty = true; if (renderable.get() && listener != null) { logger.log(Level.FINE, "iOS framebuffer resized, width: {0} height: {1}", new Object[]{framebufferWidth, framebufferHeight}); - listener.reshape(framebufferWidth, framebufferHeight); + listener.reshape(logicalWidth, logicalHeight, getRenderFramebufferWidth(), getRenderFramebufferHeight()); + listener.rescale(displayScale.x, displayScale.y); } } } + private void updateLogicalSize() { + float scale = DisplayScaleUtils.sanitizeScale(LibJGLIOSEglBridge.displayScale()); + displayScale.set(scale, scale); + + appliedDisplayScaleMode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isNativePixelsMode(appliedDisplayScaleMode)) { + logicalWidth = Math.max(framebufferWidth, 1); + logicalHeight = Math.max(framebufferHeight, 1); + return; + } + + int windowWidth = LibJGLIOSEglBridge.windowWidth(); + int windowHeight = LibJGLIOSEglBridge.windowHeight(); + if (windowWidth <= 0) { + windowWidth = Math.max(Math.round(framebufferWidth / scale), 1); + } + if (windowHeight <= 0) { + windowHeight = Math.max(Math.round(framebufferHeight / scale), 1); + } + + int[] logicalSize = DisplayScaleUtils.resolveLogicalSize(AppSettings.DISPLAY_SCALE_DPI_AWARE, + windowWidth, windowHeight, + framebufferWidth, framebufferHeight, displayScale.x, displayScale.y); + logicalWidth = logicalSize[0]; + logicalHeight = logicalSize[1]; + } + + private void applyDisplayScaleModeIfNeeded() { + if (Float.compare(settings.getDisplayScaleMode(), appliedDisplayScaleMode) == 0) { + return; + } + + updateLogicalSize(); + settings.setResolution(logicalWidth, logicalHeight); + settings.setWindowSize(logicalWidth, logicalHeight); + if (input != null) { + input.setFramebufferSize(logicalWidth, logicalHeight); + } + linearFrameBufferDirty = true; + if (renderable.get() && listener != null) { + listener.reshape(logicalWidth, logicalHeight, getRenderFramebufferWidth(), getRenderFramebufferHeight()); + listener.rescale(displayScale.x, displayScale.y); + } + } + private void updateFramebufferSizeFromLibJGLIOS() { int width = LibJGLIOSEglBridge.framebufferWidth(); int height = LibJGLIOSEglBridge.framebufferHeight(); @@ -304,6 +363,7 @@ private void updateFramebufferSizeFromLibJGLIOS() { } else if (framebufferWidth <= 0 || framebufferHeight <= 0) { framebufferWidth = Math.max(settings.getWidth(), 1); framebufferHeight = Math.max(settings.getHeight(), 1); + updateLogicalSize(); } } @@ -351,12 +411,43 @@ private boolean useBlitSrgbConversion() { return settings.isGammaCorrection() && application != null; } + private boolean useBlitFrameBuffer() { + float mode = settings.getDisplayScaleMode(); + return application != null && (useBlitSrgbConversion() + || DisplayScaleUtils.isDisabledMode(mode) || DisplayScaleUtils.isEmulatedScaleMode(mode)); + } + + private int getRenderFramebufferWidth() { + float mode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isDisabledMode(mode)) { + return Math.max(logicalWidth, 1); + } + if (DisplayScaleUtils.isEmulatedScaleMode(mode)) { + return Math.max(Math.round(framebufferWidth * mode), 1); + } + return Math.max(framebufferWidth, 1); + } + + private int getRenderFramebufferHeight() { + float mode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isDisabledMode(mode)) { + return Math.max(logicalHeight, 1); + } + if (DisplayScaleUtils.isEmulatedScaleMode(mode)) { + return Math.max(Math.round(framebufferHeight * mode), 1); + } + return Math.max(framebufferHeight, 1); + } + private int getLinearFrameBufferSampleCount() { int samples = Math.max(settings.getSamples(), 1); - if (samples > 1 && renderer != null && !renderer.getCaps().contains(Caps.TextureMultisample)) { + if (samples > 1 && renderer != null + && (!renderer.getCaps().contains(Caps.TextureMultisample) + || !renderer.getCaps().contains(Caps.OpenGL32))) { if (!multisampleTextureWarningIssued) { - logger.warning("iOS sRGB blit conversion requires multisampled textures for MSAA. " - + "Falling back to a single-sample linear framebuffer."); + logger.log(Level.WARNING, + "Display scale blit requested {0}x MSAA, but this backend cannot sample multisample textures for the blit path. Falling back to a single-sample linear framebuffer.", + samples); multisampleTextureWarningIssued = true; } return 1; @@ -365,13 +456,13 @@ private int getLinearFrameBufferSampleCount() { } private void rebuildLinearFrameBufferIfNeeded() { - if (!useBlitSrgbConversion()) { + if (!useBlitFrameBuffer()) { destroyLinearFrameBufferResources(); return; } - int width = Math.max(settings.getWidth(), 1); - int height = Math.max(settings.getHeight(), 1); + int width = getRenderFramebufferWidth(); + int height = getRenderFramebufferHeight(); int samples = getLinearFrameBufferSampleCount(); if (linearFrameBuffer != null && linearFrameBuffer.getWidth() == width @@ -387,6 +478,8 @@ private void rebuildLinearFrameBufferIfNeeded() { Texture2D colorTexture = new Texture2D( new Image(getLinearFrameBufferColorFormat(), width, height, null, ColorSpace.Linear)); + colorTexture.setMagFilter(Texture.MagFilter.Bilinear); + colorTexture.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); if (samples > 1) { colorTexture.getImage().setMultiSamples(samples); } @@ -412,7 +505,7 @@ private Format getLinearFrameBufferColorFormat() { } private boolean ensureBlitResources() { - if (!useBlitSrgbConversion()) { + if (!useBlitFrameBuffer()) { return false; } @@ -423,10 +516,10 @@ private boolean ensureBlitResources() { if (blitMaterial == null) { blitMaterial = new Material(application.getAssetManager(), BLIT_MATERIAL); - blitMaterial.setBoolean("Srgb", true); blitMaterial.getAdditionalRenderState().setDepthTest(false); blitMaterial.getAdditionalRenderState().setDepthWrite(false); } + blitMaterial.setBoolean("Srgb", useBlitSrgbConversion()); if (blitGeometry == null) { blitGeometry = new Picture("Linear to sRGB Blit"); @@ -468,7 +561,7 @@ private void destroyLinearFrameBufferResources() { } private boolean renderFrameWithBlitSrgbConversion() { - if (!useBlitSrgbConversion()) { + if (!useBlitFrameBuffer()) { return false; } @@ -521,11 +614,6 @@ private boolean renderFrameWithBlitSrgbConversion() { return true; } - /** - * Returns the height of the framebuffer. - * - * @throws UnsupportedOperationException - */ @Override public int getFramebufferHeight() { updateFramebufferSizeFromLibJGLIOS(); diff --git a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlMouseInput.java b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlMouseInput.java index d5c10e5b07..2370974e1d 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlMouseInput.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/input/lwjgl/SdlMouseInput.java @@ -37,6 +37,7 @@ import com.jme3.input.event.MouseButtonEvent; import com.jme3.input.event.MouseMotionEvent; import com.jme3.math.Vector2f; +import com.jme3.system.AppSettings; import com.jme3.system.lwjgl.LwjglWindow; import com.jme3.util.BufferUtils; import java.nio.ByteBuffer; @@ -86,6 +87,8 @@ public class SdlMouseInput implements MouseInput { private int mouseWheel; private int currentWidth; private int currentHeight; + private float windowCoordWidth = 1f; + private float windowCoordHeight = 1f; private boolean cursorVisible = true; private boolean x11WarpGrabMode; @@ -142,8 +145,6 @@ public void onSDLEvent(SDL_Event event) { return; } ignoreNextX11WarpEvent = false; - float logicalWidth = currentWidth / Math.max(inputScale.x, 1f); - float logicalHeight = currentHeight / Math.max(inputScale.y, 1f); int centerX = currentWidth / 2; int centerY = currentHeight / 2; xDelta = Math.round(event.motion().xrel() * inputScale.x); @@ -164,8 +165,8 @@ public void onSDLEvent(SDL_Event event) { x = mouseX; y = mouseY; } else { - x = toPixelX(event.motion().x()); - y = toPixelY(event.motion().y()); + x = toInputX(event.motion().x()); + y = toInputY(event.motion().y()); xDelta = x - mouseX; yDelta = y - mouseY; mouseX = x; @@ -202,8 +203,8 @@ public void onSDLEvent(SDL_Event event) { return; } refreshWindowMetrics(); - mouseX = toPixelX(event.button().x()); - mouseY = toPixelY(event.button().y()); + mouseX = toInputX(event.button().x()); + mouseY = toInputY(event.button().y()); int button = Byte.toUnsignedInt(event.button().button()); MouseButtonEvent mouseButtonEvent = @@ -214,9 +215,12 @@ public void onSDLEvent(SDL_Event event) { } private void refreshWindowMetrics() { - currentWidth = Math.max(context.getFramebufferWidth(), 1); - currentHeight = Math.max(context.getFramebufferHeight(), 1); - context.getWindowContentScale(inputScale); + AppSettings settings = context.getSettings(); + currentWidth = Math.max(settings.getWidth(), 1); + currentHeight = Math.max(settings.getHeight(), 1); + context.getMouseInputScale(inputScale); + windowCoordWidth = currentWidth / Math.max(inputScale.x, 0.0001f); + windowCoordHeight = currentHeight / Math.max(inputScale.y, 0.0001f); } private void initCurrentMousePosition() { @@ -224,16 +228,16 @@ private void initCurrentMousePosition() { FloatBuffer x = stack.callocFloat(1); FloatBuffer y = stack.callocFloat(1); SDL_GetMouseState(x, y); - mouseX = toPixelX(x.get(0)); - mouseY = toPixelY(y.get(0)); + mouseX = toInputX(x.get(0)); + mouseY = toInputY(y.get(0)); } } - private int toPixelX(float x) { + private int toInputX(float x) { return Math.round(x * inputScale.x); } - private int toPixelY(float y) { + private int toInputY(float y) { return Math.round(currentHeight - (y * inputScale.y)); } @@ -352,9 +356,7 @@ private boolean isX11Backend() { private void warpMouseToWindowCenter() { refreshWindowMetrics(); - float logicalWidth = currentWidth / Math.max(inputScale.x, 1f); - float logicalHeight = currentHeight / Math.max(inputScale.y, 1f); - SDL_WarpMouseInWindow(context.getWindowHandle(), logicalWidth * 0.5f, logicalHeight * 0.5f); + SDL_WarpMouseInWindow(context.getWindowHandle(), windowCoordWidth * 0.5f, windowCoordHeight * 0.5f); } private void centerVisibleCursor() { @@ -370,10 +372,8 @@ private void syncMouseToWindowCenter() { private boolean isNearWindowCenter(float x, float y) { refreshWindowMetrics(); - float logicalWidth = currentWidth / Math.max(inputScale.x, 1f); - float logicalHeight = currentHeight / Math.max(inputScale.y, 1f); - return Math.abs(x - (logicalWidth * 0.5f)) <= 1.5f - && Math.abs(y - (logicalHeight * 0.5f)) <= 1.5f; + return Math.abs(x - (windowCoordWidth * 0.5f)) <= 1.5f + && Math.abs(y - (windowCoordHeight * 0.5f)) <= 1.5f; } @Override diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java index bac1c4fc66..3e286dd112 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java @@ -38,7 +38,6 @@ import com.jme3.input.awt.AwtKeyInput; import com.jme3.input.awt.AwtMouseInput; import com.jme3.input.lwjgl.SdlJoystickInput; -import com.jme3.math.Vector2f; import com.jme3.system.AppSettings; import com.jme3.system.Displays; import com.jme3.system.JmeCanvasContext; @@ -52,12 +51,10 @@ import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; -import java.awt.GraphicsConfiguration; import java.awt.Toolkit; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; -import java.awt.geom.AffineTransform; import javax.swing.SwingUtilities; @@ -412,8 +409,6 @@ public Graphics getGraphics() { /** Notify if there is a change in canvas dimensions. */ private final AtomicBoolean needResize = new AtomicBoolean(false); - /** Notify if there are changes to the canvas scales. */ - private final AtomicBoolean needRescale = new AtomicBoolean(false); /** * Flag that uses the context to check if it is initialized or not, this prevents @@ -424,11 +419,6 @@ public Graphics getGraphics() { /** lock-object. */ private final Object lock = new Object(); - /** Scale of the component in {@code x} */ - private float xScale = 1; - /** Scale of the component in {@code y} */ - private float yScale = 1; - /** Framebuffer width. */ private int framebufferWidth = 1; /** Framebuffer height. */ @@ -534,11 +524,7 @@ public void run() { while (true) { if (needResize.getAndSet(false)) { settings.setResolution(framebufferWidth, framebufferHeight); - listener.reshape(framebufferWidth, framebufferHeight); - } - - if (needRescale.getAndSet(false)) { - listener.rescale(xScale, yScale); + listener.reshape(framebufferWidth, framebufferHeight, framebufferWidth, framebufferHeight); } synchronized (lock) { @@ -638,7 +624,9 @@ protected void runLoop() { throw new IllegalStateException(); } - listener.update(); + if (!renderFrameWithBlitFramebuffer()) { + listener.update(); + } // Subclasses just call GLObjectManager. Clean up objects here. // It is safe ... for now. @@ -838,9 +826,15 @@ public JoyInput getJoyInput() { @Override protected void showWindow() { } /** (non-Javadoc) */ @Override protected void setWindowIcon(final AppSettings settings) { } - /**(non-Javadoc) */ - @Override public Vector2f getWindowContentScale(Vector2f store) { - return store == null ? new Vector2f() : store; + + @Override + protected int getRenderFramebufferWidth() { + return Math.max(framebufferWidth, 1); + } + + @Override + protected int getRenderFramebufferHeight() { + return Math.max(framebufferHeight, 1); } /** @@ -849,29 +843,14 @@ public JoyInput getJoyInput() { @Override protected void updateSizes() { synchronized (lock) { - GraphicsConfiguration gc = canvas.getGraphicsConfiguration(); - if (gc == null) { - return; - } - - AffineTransform at = gc.getDefaultTransform(); - float sx = (float) at.getScaleX(), - sy = (float) at.getScaleY(); - - int fw = (int) (canvas.getWidth() * sx); - int fh = (int) (canvas.getHeight() * sy); + int fw = canvas.getWidth(); + int fh = canvas.getHeight(); if (fw != framebufferWidth || fh != framebufferHeight) { framebufferWidth = Math.max(fw, 1); framebufferHeight = Math.max(fh, 1); needResize.set(true); } - - if (xScale != sx || yScale != sy) { - xScale = sx; - yScale = sy; - needRescale.set(true); - } } } diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java index b78d39952c..d80b133901 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglContext.java @@ -206,7 +206,6 @@ protected void initContextFirstTime() { private void initContext(boolean first) { final String renderer = settings.getRenderer(); - boolean auxFramebufferSrgb = useAuxFramebufferSrgb(); if (first) { GL gl; @@ -286,26 +285,9 @@ private void initContext(boolean first) { } this.renderer.initialize(); - boolean isSrgbFb = false; - if (settings.isGammaCorrection() && !auxFramebufferSrgb) { - isSrgbFb = isDefaultFramebufferSrgb(); - - if (!isSrgbFb && settings.isGammaCorrection()) { - if (enableAuxFramebufferSrgbFallback()) { - auxFramebufferSrgb = useAuxFramebufferSrgb(); - if (auxFramebufferSrgb) { - logger.warning( - "sRGB framebuffer not supported by the backend platform, using auxiliary sRGB framebuffer"); - } - } - if (!auxFramebufferSrgb) { - logger.warning( - "sRGB framebuffer not supported by the backend platform, disabling gamma correction"); - } - } - } + - this.renderer.setMainFrameBufferSrgb(auxFramebufferSrgb ? false : isSrgbFb); + this.renderer.setMainFrameBufferSrgb(false); this.renderer.setLinearizeSrgbImages(settings.isGammaCorrection()); if (first) { @@ -341,14 +323,6 @@ protected boolean isDefaultFramebufferSrgb() { return false; } - protected boolean useAuxFramebufferSrgb() { - return false; - } - - protected boolean enableAuxFramebufferSrgbFallback() { - return false; - } - public void internalDestroy() { renderer = null; timer = null; diff --git a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java index edfa3f9418..dbad655331 100644 --- a/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java +++ b/jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java @@ -45,8 +45,10 @@ import com.jme3.renderer.Caps; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; +import com.jme3.renderer.opengl.GLRenderer; import com.jme3.system.AppSettings; import com.jme3.system.Displays; +import com.jme3.system.DisplayScaleUtils; import com.jme3.system.JmeContext; import com.jme3.system.JmeSystem; import com.jme3.system.NanoTimer; @@ -57,12 +59,12 @@ import com.jme3.texture.FrameBuffer.FrameBufferTarget; import com.jme3.texture.Image; import com.jme3.texture.Image.Format; +import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.texture.image.ColorSpace; import com.jme3.ui.Picture; import com.jme3.util.BufferUtils; import com.jme3.util.SafeArrayList; -import com.jme3.renderer.opengl.GLRenderer; import java.awt.Graphics2D; import java.awt.image.BufferedImage; @@ -209,19 +211,27 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable { protected boolean wasActive = false; protected boolean autoFlush = true; protected boolean allowSwapBuffers = false; - private boolean auxFramebufferSrgbFallback; // state maintained by updateSizes() + private int logicalWidth = 1; + private int logicalHeight = 1; + private int windowWidth = 1; + private int windowHeight = 1; + private int framebufferWidth = 1; + private int framebufferHeight = 1; private int oldFramebufferWidth; private int oldFramebufferHeight; + private int oldLogicalWidth; + private int oldLogicalHeight; private final Vector2f oldScale = new Vector2f(1, 1); - private Material auxFramebufferBlitMaterial; - private Picture auxFramebufferBlitGeometry; - private final Camera auxFramebufferBlitCamera = new Camera(1, 1); - private FrameBuffer auxFramebuffer; - private Texture2D auxFramebufferColorTexture; - private boolean auxFramebufferDirty; - private boolean auxFramebufferTextureMultisampleWarningIssued; + private final Vector2f displayScale = new Vector2f(1, 1); + private Material blitMaterial; + private Picture blitGeometry; + private final Camera blitCamera = new Camera(1, 1); + private FrameBuffer blitFramebuffer; + private Texture2D blitColorTexture; + private boolean blitFramebufferDirty; + private boolean blitFramebufferTextureMultisampleWarningIssued; public LwjglWindow(final JmeContext.Type type) { if (!SUPPORTED_TYPES.contains(type)) { @@ -274,7 +284,6 @@ public void restart() { protected void createContext(final AppSettings settings) { disableNvidiaThreadedOptimizations(); useAngle = AppSettings.ANGLE_GLES3.equals(settings.getRenderer()); - auxFramebufferSrgbFallback = false; configureVideoDriverHints(settings); configureAngleHints(settings); @@ -317,7 +326,7 @@ protected void createContext(final AppSettings settings) { if (settings.isResizable()) { windowFlags |= SDL_WINDOW_RESIZABLE; } - if (settings.isUseRetinaFrameBuffer()) { + if (DisplayScaleUtils.requestsHighDensityFramebuffer(settings.getDisplayScaleMode())) { windowFlags |= SDL_WINDOW_HIGH_PIXEL_DENSITY; } if (settings.isFullscreen()) { @@ -420,14 +429,8 @@ private long createWindow(AppSettings settings, int width, int height, long flag } String initialError = SDL_GetError(); - if (canUseAuxFramebufferSrgb()) { - auxFramebufferSrgbFallback = true; - } - LOGGER.log(Level.WARNING, - auxFramebufferSrgbFallback - ? "Unable to create an sRGB-capable SDL window, retrying with the auxiliary sRGB framebuffer: {0}" - : "Unable to create an sRGB-capable SDL window, retrying with a linear default framebuffer: {0}", + "Unable to create an sRGB-capable SDL window, retrying with a linear default framebuffer and shader sRGB conversion: {0}", initialError); SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, 0); @@ -470,12 +473,10 @@ private boolean configureGLAttributes(AppSettings settings) { SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, settings.getSamples() > 0 ? 1 : 0); SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, Math.max(settings.getSamples(), 0)); - boolean srgbFramebufferRequested = settings.isGammaCorrection() && !useAuxFramebufferSrgb(); - if (srgbFramebufferRequested) { - if (!SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, 1)) { - throw new IllegalStateException("SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, 1) failed: " - + SDL_GetError()); - } + boolean srgbFramebufferRequested = false; + if (!SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, 0)) { + throw new IllegalStateException("SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE) failed: " + + SDL_GetError()); } if (settings.getBitsPerPixel() == 24) { @@ -503,8 +504,8 @@ private void updateSizes(boolean notifyListener) { return; } - int windowWidth = Math.max(winW.get(0), 16); - int windowHeight = Math.max(winH.get(0), 16); + windowWidth = Math.max(winW.get(0), 16); + windowHeight = Math.max(winH.get(0), 16); if (settings.getWindowWidth() != windowWidth || settings.getWindowHeight() != windowHeight) { settings.setWindowSize(windowWidth, windowHeight); if (notifyListener) { @@ -520,29 +521,48 @@ private void updateSizes(boolean notifyListener) { return; } - int framebufferWidth = Math.max(fbW.get(0), 16); - int framebufferHeight = Math.max(fbH.get(0), 16); + framebufferWidth = Math.max(fbW.get(0), 16); + framebufferHeight = Math.max(fbH.get(0), 16); + updateScaleState(windowWidth, windowHeight, framebufferWidth, framebufferHeight); + + float mode = settings.getDisplayScaleMode(); + int[] logicalSize = DisplayScaleUtils.resolveLogicalSize(mode, windowWidth, windowHeight, + framebufferWidth, framebufferHeight, displayScale.x, displayScale.y); + logicalWidth = logicalSize[0]; + logicalHeight = logicalSize[1]; + if (!notifyListener) { - settings.setResolution(framebufferWidth, framebufferHeight); + settings.setResolution(logicalWidth, logicalHeight); return; } - if (framebufferWidth != oldFramebufferWidth || framebufferHeight != oldFramebufferHeight) { - settings.setResolution(framebufferWidth, framebufferHeight); - listener.reshape(framebufferWidth, framebufferHeight); + if (logicalWidth != oldLogicalWidth || logicalHeight != oldLogicalHeight + || framebufferWidth != oldFramebufferWidth || framebufferHeight != oldFramebufferHeight) { + settings.setResolution(logicalWidth, logicalHeight); + listener.reshape(logicalWidth, logicalHeight, getRenderFramebufferWidth(), getRenderFramebufferHeight()); + oldLogicalWidth = logicalWidth; + oldLogicalHeight = logicalHeight; oldFramebufferWidth = framebufferWidth; oldFramebufferHeight = framebufferHeight; } - float xScale = (float) framebufferWidth / windowWidth; - float yScale = (float) framebufferHeight / windowHeight; - if (oldScale.x != xScale || oldScale.y != yScale) { - listener.rescale(xScale, yScale); - oldScale.set(xScale, yScale); + if (oldScale.x != displayScale.x || oldScale.y != displayScale.y) { + listener.rescale(displayScale.x, displayScale.y); + oldScale.set(displayScale); } } } + private void updateScaleState(int windowWidth, int windowHeight, int framebufferWidth, int framebufferHeight) { + float density = DisplayScaleUtils.sanitizeScale(SDL_GetWindowPixelDensity(window)); + float scale = SDL_GetWindowDisplayScale(window); + if (!Float.isFinite(scale) || scale <= 0f) { + scale = density; + } + scale = DisplayScaleUtils.sanitizeScale(scale); + displayScale.set(scale, scale); + } + protected void showWindow() { SDL_ShowWindow(window); } @@ -598,7 +618,7 @@ private SDL_Surface imageToSdlSurface(BufferedImage image) { protected void destroyContext() { Exception failure = null; try { - destroyAuxFramebufferResources(); + destroyBlitFramebufferResources(); if (renderer != null) { renderer.cleanup(); } @@ -619,6 +639,8 @@ protected void destroyContext() { } oldFramebufferWidth = 0; oldFramebufferHeight = 0; + oldLogicalWidth = 0; + oldLogicalHeight = 0; oldScale.set(1, 1); } catch (Exception ex) { if (failure == null) { @@ -694,23 +716,28 @@ protected boolean initInThread() { return true; } - @Override - protected boolean useAuxFramebufferSrgb() { - return (useAngle || auxFramebufferSrgbFallback) && canUseAuxFramebufferSrgb(); + private boolean canUseBlitFramebuffer() { + return (type == Type.Display || type == Type.Canvas) && listener instanceof Application; } - @Override - protected boolean enableAuxFramebufferSrgbFallback() { - if (!canUseAuxFramebufferSrgb()) { - return false; + protected boolean useBlitFramebuffer() { + return canUseBlitFramebuffer(); + } + + protected int getRenderFramebufferWidth() { + float mode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isEmulatedScaleMode(mode)) { + return Math.max(Math.round(framebufferWidth * mode), 1); } - auxFramebufferSrgbFallback = true; - return true; + return Math.max(framebufferWidth, 1); } - private boolean canUseAuxFramebufferSrgb() { - return settings.isGammaCorrection() && (type == Type.Display || type == Type.Canvas) - && listener instanceof Application; + protected int getRenderFramebufferHeight() { + float mode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isEmulatedScaleMode(mode)) { + return Math.max(Math.round(framebufferHeight * mode), 1); + } + return Math.max(framebufferHeight, 1); } @Override @@ -735,42 +762,42 @@ private Application getApplicationListener() { return null; } - private int getAuxFramebufferSampleCount() { + private int getBlitFramebufferSampleCount() { int samples = Math.max(settings.getSamples(), 1); - if (samples > 1 && renderer != null && !renderer.getCaps().contains(Caps.TextureMultisample)) { - if (!auxFramebufferTextureMultisampleWarningIssued) { - LOGGER.warning( - "AuxFramebuffer sRGB blit requires multisampled textures for MSAA. Falling back to a single-sample auxiliary framebuffer."); - auxFramebufferTextureMultisampleWarningIssued = true; + if (samples > 1 && renderer != null + && (!renderer.getCaps().contains(Caps.TextureMultisample) + || !renderer.getCaps().contains(Caps.OpenGL32))) { + if (!blitFramebufferTextureMultisampleWarningIssued) { + LOGGER.log(Level.WARNING, + "Blit framebuffer requested {0}x MSAA, but this backend cannot sample multisample textures for the blit path. Falling back to a single-sample blit framebuffer.", + samples); + blitFramebufferTextureMultisampleWarningIssued = true; } return 1; } return samples; } - private void rebuildAuxFramebufferIfNeeded() { - if (!useAuxFramebufferSrgb()) { - destroyAuxFramebufferResources(); - return; - } - - int width = Math.max(settings.getWidth(), 1); - int height = Math.max(settings.getHeight(), 1); - int samples = getAuxFramebufferSampleCount(); + private void rebuildBlitFramebufferIfNeeded() { + int width = getRenderFramebufferWidth(); + int height = getRenderFramebufferHeight(); + int samples = getBlitFramebufferSampleCount(); - if (auxFramebuffer != null && auxFramebuffer.getWidth() == width - && auxFramebuffer.getHeight() == height && auxFramebuffer.getSamples() == samples) { + if (blitFramebuffer != null && blitFramebuffer.getWidth() == width + && blitFramebuffer.getHeight() == height && blitFramebuffer.getSamples() == samples) { return; } - destroyAuxFramebuffer(); + destroyBlitFramebuffer(); FrameBuffer frameBuffer = new FrameBuffer(width, height, samples); - frameBuffer.setName("LWJGL3 AuxFramebuffer"); + frameBuffer.setName("LWJGL3 Blit FrameBuffer"); frameBuffer.setSrgb(false); Texture2D colorTexture = new Texture2D( new Image(Format.RGBA16F, width, height, null, ColorSpace.Linear)); + colorTexture.setMagFilter(Texture.MagFilter.Bilinear); + colorTexture.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); if (samples > 1) { colorTexture.getImage().setMultiSamples(samples); } @@ -781,13 +808,13 @@ private void rebuildAuxFramebufferIfNeeded() { .newTarget(settings.getStencilBits() > 0 ? Format.Depth24Stencil8 : Format.Depth)); } - auxFramebufferColorTexture = colorTexture; - auxFramebuffer = frameBuffer; - auxFramebufferDirty = true; + blitColorTexture = colorTexture; + blitFramebuffer = frameBuffer; + blitFramebufferDirty = true; } - private boolean ensureAuxFramebufferBlitResources() { - if (!useAuxFramebufferSrgb()) { + private boolean ensureBlitResources() { + if (!useBlitFramebuffer()) { return false; } @@ -802,54 +829,58 @@ private boolean ensureAuxFramebufferBlitResources() { return false; } - if (auxFramebufferBlitMaterial == null) { - auxFramebufferBlitMaterial = new Material(assetManager, BLIT_MATERIAL); - auxFramebufferBlitMaterial.setBoolean("Srgb", true); - auxFramebufferBlitMaterial.getAdditionalRenderState().setDepthTest(false); - auxFramebufferBlitMaterial.getAdditionalRenderState().setDepthWrite(false); + if (blitMaterial == null) { + blitMaterial = new Material(assetManager, BLIT_MATERIAL); + blitMaterial.getAdditionalRenderState().setDepthTest(false); + blitMaterial.getAdditionalRenderState().setDepthWrite(false); } + blitMaterial.setBoolean("Srgb", useBlitFramebufferShaderSrgbConversion()); - if (auxFramebufferBlitGeometry == null) { - auxFramebufferBlitGeometry = new Picture("AuxFramebuffer Blit"); - auxFramebufferBlitGeometry.setWidth(1f); - auxFramebufferBlitGeometry.setHeight(1f); - auxFramebufferBlitGeometry.setMaterial(auxFramebufferBlitMaterial); + if (blitGeometry == null) { + blitGeometry = new Picture("Blit FrameBuffer"); + blitGeometry.setWidth(1f); + blitGeometry.setHeight(1f); + blitGeometry.setMaterial(blitMaterial); } - if (auxFramebufferDirty && auxFramebufferColorTexture != null) { - auxFramebufferBlitMaterial.setTexture("Texture", auxFramebufferColorTexture); - if (auxFramebuffer != null && auxFramebuffer.getSamples() > 1) { - auxFramebufferBlitMaterial.setInt("NumSamples", auxFramebuffer.getSamples()); + if (blitFramebufferDirty && blitColorTexture != null) { + blitMaterial.setTexture("Texture", blitColorTexture); + if (blitFramebuffer != null && blitFramebuffer.getSamples() > 1) { + blitMaterial.setInt("NumSamples", blitFramebuffer.getSamples()); } else { - auxFramebufferBlitMaterial.clearParam("NumSamples"); + blitMaterial.clearParam("NumSamples"); } - auxFramebufferDirty = false; + blitFramebufferDirty = false; } return true; } - private void destroyAuxFramebuffer() { - if (auxFramebuffer != null) { - auxFramebuffer.dispose(); - auxFramebuffer = null; + private boolean useBlitFramebufferShaderSrgbConversion() { + return settings.isGammaCorrection() && renderer != null && !renderer.isMainFrameBufferSrgb(); + } + + private void destroyBlitFramebuffer() { + if (blitFramebuffer != null) { + blitFramebuffer.dispose(); + blitFramebuffer = null; } - if (auxFramebufferColorTexture != null && auxFramebufferColorTexture.getImage() != null) { - auxFramebufferColorTexture.getImage().dispose(); + if (blitColorTexture != null && blitColorTexture.getImage() != null) { + blitColorTexture.getImage().dispose(); } - auxFramebufferColorTexture = null; - auxFramebufferDirty = true; + blitColorTexture = null; + blitFramebufferDirty = true; } - private void destroyAuxFramebufferResources() { - destroyAuxFramebuffer(); - auxFramebufferBlitMaterial = null; - auxFramebufferBlitGeometry = null; - auxFramebufferTextureMultisampleWarningIssued = false; + private void destroyBlitFramebufferResources() { + destroyBlitFramebuffer(); + blitMaterial = null; + blitGeometry = null; + blitFramebufferTextureMultisampleWarningIssued = false; } - private boolean renderFrameWithAuxFramebuffer() { - if (!(renderer instanceof GLRenderer) || !useAuxFramebufferSrgb()) { + protected boolean renderFrameWithBlitFramebuffer() { + if (!(renderer instanceof GLRenderer) || !useBlitFramebuffer()) { return false; } @@ -858,8 +889,8 @@ private boolean renderFrameWithAuxFramebuffer() { return false; } - rebuildAuxFramebufferIfNeeded(); - if (auxFramebuffer == null || !ensureAuxFramebufferBlitResources()) { + rebuildBlitFramebufferIfNeeded(); + if (blitFramebuffer == null || !ensureBlitResources()) { return false; } @@ -867,11 +898,11 @@ private boolean renderFrameWithAuxFramebuffer() { RenderManager renderManager = getApplicationListener().getRenderManager(); FrameBuffer restoreMainFramebuffer = previousMainFramebuffer; - glRenderer.setMainFrameBufferOverride(auxFramebuffer); + glRenderer.setMainFrameBufferOverride(blitFramebuffer); try { listener.update(); FrameBuffer currentMainFramebuffer = renderer.getCurrentFrameBuffer(); - if (currentMainFramebuffer != auxFramebuffer) { + if (currentMainFramebuffer != blitFramebuffer) { restoreMainFramebuffer = currentMainFramebuffer; } } finally { @@ -882,19 +913,19 @@ private boolean renderFrameWithAuxFramebuffer() { Camera previousCamera = renderManager.getCurrentCamera(); try { glRenderer.setFrameBuffer(null); - int blitWidth = auxFramebuffer.getWidth(); - int blitHeight = auxFramebuffer.getHeight(); - if (auxFramebufferBlitCamera.getWidth() != blitWidth - || auxFramebufferBlitCamera.getHeight() != blitHeight) { - auxFramebufferBlitCamera.resize(blitWidth, blitHeight, true); + int blitWidth = Math.max(getFramebufferWidth(), 1); + int blitHeight = Math.max(getFramebufferHeight(), 1); + if (blitCamera.getWidth() != blitWidth + || blitCamera.getHeight() != blitHeight) { + blitCamera.resize(blitWidth, blitHeight, true); } - renderManager.setCamera(auxFramebufferBlitCamera, true); - if (auxFramebufferBlitGeometry.getWidth() != blitWidth || auxFramebufferBlitGeometry.getHeight() != blitHeight) { - auxFramebufferBlitGeometry.setWidth(blitWidth); - auxFramebufferBlitGeometry.setHeight(blitHeight); + renderManager.setCamera(blitCamera, true); + if (blitGeometry.getWidth() != blitWidth || blitGeometry.getHeight() != blitHeight) { + blitGeometry.setWidth(blitWidth); + blitGeometry.setHeight(blitHeight); } - auxFramebufferBlitGeometry.updateGeometricState(); - renderManager.renderGeometry(auxFramebufferBlitGeometry); + blitGeometry.updateGeometricState(); + renderManager.renderGeometry(blitGeometry); } finally { glRenderer.setMainFrameBufferOverride(restoreMainFramebuffer); if (previousCamera != null) { @@ -913,7 +944,7 @@ protected void runLoop() { throw new IllegalStateException(); } - if (!renderFrameWithAuxFramebuffer()) { + if (!renderFrameWithBlitFramebuffer()) { listener.update(); } @@ -1146,50 +1177,27 @@ public int getWindowId() { return windowId; } - public Vector2f getWindowContentScale(Vector2f store) { + public Vector2f getMouseInputScale(Vector2f store) { if (store == null) { store = new Vector2f(); } - - try (MemoryStack stack = MemoryStack.stackPush()) { - IntBuffer fbW = stack.mallocInt(1); - IntBuffer fbH = stack.mallocInt(1); - IntBuffer winW = stack.mallocInt(1); - IntBuffer winH = stack.mallocInt(1); - - SDL_GetWindowSizeInPixels(window, fbW, fbH); - SDL_GetWindowSize(window, winW, winH); - - float wx = Math.max(winW.get(0), 1); - float wy = Math.max(winH.get(0), 1); - store.set(fbW.get(0) / wx, fbH.get(0) / wy); + float mode = settings.getDisplayScaleMode(); + if (DisplayScaleUtils.isDpiAwareMode(mode)) { + return store.set((float) logicalWidth / Math.max(windowWidth, 1), + (float) logicalHeight / Math.max(windowHeight, 1)); } - - return store; + return store.set((float) framebufferWidth / Math.max(windowWidth, 1), + (float) framebufferHeight / Math.max(windowHeight, 1)); } @Override public int getFramebufferHeight() { - try (MemoryStack stack = MemoryStack.stackPush()) { - IntBuffer w = stack.mallocInt(1); - IntBuffer h = stack.mallocInt(1); - if (!SDL_GetWindowSizeInPixels(window, w, h)) { - return 0; - } - return h.get(0); - } + return framebufferHeight; } @Override public int getFramebufferWidth() { - try (MemoryStack stack = MemoryStack.stackPush()) { - IntBuffer w = stack.mallocInt(1); - IntBuffer h = stack.mallocInt(1); - if (!SDL_GetWindowSizeInPixels(window, w, h)) { - return 0; - } - return w.get(0); - } + return framebufferWidth; } @Override