From 8fb157357cbd754f25fa2cc3df33aee7e7bcf9f3 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 10 May 2026 04:25:25 +0300 Subject: [PATCH] iOS: add ios.metal.colorSpace build hint for the Metal renderer Issue #4908 surfaces that Metal's hard-coded sRGB layer colour space is not the right pick for every app (wide-gamut content, custom DeviceRGB pipelines, or apps that prefer the system default). METALView.m now selects the CAMetalLayer colorspace from a compile-time CN1_METAL_COLORSPACE_* define and falls back to sRGB so existing builds are unchanged. IPhoneBuilder reads the new ios.metal.colorSpace hint (values: sRGB, displayP3, deviceRGB, linearSRGB, extendedSRGB, extendedLinearSRGB, none) and rewrites a placeholder line in CN1ES2compat.h with the matching define when ios.metal=true. The same change is mirrored in the build server's IPhoneBuilder; the developer guide gets a new section under Working with iOS describing each value. Co-Authored-By: Claude Opus 4.7 (1M context) --- Ports/iOSPort/nativeSources/CN1ES2compat.h | 11 ++++++ Ports/iOSPort/nativeSources/METALView.m | 31 ++++++++++++++--- .../developer-guide/Working-With-iOS.asciidoc | 34 +++++++++++++++++++ .../com/codename1/builders/IPhoneBuilder.java | 33 ++++++++++++++++++ 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/Ports/iOSPort/nativeSources/CN1ES2compat.h b/Ports/iOSPort/nativeSources/CN1ES2compat.h index 557e9e214e..2894937b43 100644 --- a/Ports/iOSPort/nativeSources/CN1ES2compat.h +++ b/Ports/iOSPort/nativeSources/CN1ES2compat.h @@ -25,6 +25,17 @@ // activated and the OpenGL ES 2 backend stays linked but unused. See // Ports/iOSPort/METAL_PORT_STATUS.md for the migration plan. //#define CN1_USE_METAL +// IPhoneBuilder.java replaces the line below with one of: +// #define CN1_METAL_COLORSPACE_SRGB +// #define CN1_METAL_COLORSPACE_DISPLAY_P3 +// #define CN1_METAL_COLORSPACE_DEVICE_RGB +// #define CN1_METAL_COLORSPACE_LINEAR_SRGB +// #define CN1_METAL_COLORSPACE_EXTENDED_SRGB +// #define CN1_METAL_COLORSPACE_EXTENDED_LINEAR_SRGB +// #define CN1_METAL_COLORSPACE_NONE +// based on the `ios.metal.colorSpace` build hint. METALView.m falls back +// to sRGB when none of these are defined. +//#define CN1_METAL_COLORSPACE_PLACEHOLDER #define USE_ES2 1 enum CN1GLenum { CN1_GL_ALPHA_TEXTURE, diff --git a/Ports/iOSPort/nativeSources/METALView.m b/Ports/iOSPort/nativeSources/METALView.m index 8828b82096..675a345f53 100644 --- a/Ports/iOSPort/nativeSources/METALView.m +++ b/Ports/iOSPort/nativeSources/METALView.m @@ -138,16 +138,37 @@ - (id)initWithCoder:(NSCoder*)coder metalLayer.opaque = TRUE; metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; metalLayer.framebufferOnly = YES; - // sRGB colourspace so colours match the GL path's CAEAGLLayer - // output. Without this, CG-rasterised images and gradients - // (DeviceRGB-tagged in their CGBitmapContext) display slightly - // brighter on Metal because the layer treats their bytes as - // linear-RGB instead of sRGB-encoded. + // Colour space for the Metal layer. Default is sRGB so colours + // match the GL path's CAEAGLLayer output: without it, CG-rasterised + // images and gradients (DeviceRGB-tagged in their CGBitmapContext) + // display slightly brighter on Metal because the layer treats + // their bytes as linear-RGB instead of sRGB-encoded. + // + // The build hint `ios.metal.colorSpace` selects the value (see + // IPhoneBuilder, which injects one of the CN1_METAL_COLORSPACE_* + // defines below). Set the hint to "none" to leave the layer's + // colorspace property untouched (system default). +#if defined(CN1_METAL_COLORSPACE_NONE) + // Skip setting metalLayer.colorspace entirely. +#else + #if defined(CN1_METAL_COLORSPACE_DISPLAY_P3) + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceDisplayP3); + #elif defined(CN1_METAL_COLORSPACE_DEVICE_RGB) + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + #elif defined(CN1_METAL_COLORSPACE_LINEAR_SRGB) + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB); + #elif defined(CN1_METAL_COLORSPACE_EXTENDED_SRGB) + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB); + #elif defined(CN1_METAL_COLORSPACE_EXTENDED_LINEAR_SRGB) + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearSRGB); + #else CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + #endif if (cs != NULL) { metalLayer.colorspace = cs; CGColorSpaceRelease(cs); } +#endif // Cap drawable pool to 3 so the GPU has at most one render in // flight while CPU prepares the next two. Higher counts trade // smoothness for latency and memory; 3 is the iOS default for diff --git a/docs/developer-guide/Working-With-iOS.asciidoc b/docs/developer-guide/Working-With-iOS.asciidoc index 964c9354af..d2041cf435 100644 --- a/docs/developer-guide/Working-With-iOS.asciidoc +++ b/docs/developer-guide/Working-With-iOS.asciidoc @@ -177,6 +177,40 @@ NOTE: This is supported for pro users as part of the crash protection feature. To take advantage of that capability use the build hint `ios.testFlight=true` and then submit the app to the store for beta testing. Make sure to use a release build target. +=== Choosing a colour space for the Metal renderer + +When the Metal rendering backend is enabled with `ios.metal=true`, the `CAMetalLayer` is configured with the sRGB colour space by default. This matches the behaviour of the legacy OpenGL ES 2 backend on `CAEAGLLayer`: CG-rasterised images and gradients (which are tagged `DeviceRGB` in their `CGBitmapContext`) end up displayed with the same brightness on both backends. + +For most apps the default is the right choice. Apps that need a different colour profile -- for example, wide-gamut artwork that should be displayed in Display P3, or a strictly device-RGB pipeline that matches a custom rendering toolchain -- can override the choice with the `ios.metal.colorSpace` build hint: + +[cols="1,3"] +|=== +| Value | Effect + +| `sRGB` (default) +| Sets `metalLayer.colorspace` to `kCGColorSpaceSRGB`. Use this unless you have a specific reason not to. + +| `displayP3` +| Sets `metalLayer.colorspace` to `kCGColorSpaceDisplayP3`. Use for wide-gamut content on devices with a P3 display. + +| `deviceRGB` +| Calls `CGColorSpaceCreateDeviceRGB()`. Skips sRGB gamma encoding -- raw bytes are written to the layer. + +| `linearSRGB` +| Sets `metalLayer.colorspace` to `kCGColorSpaceLinearSRGB` for linear (non-gamma-encoded) sRGB output. + +| `extendedSRGB` +| Sets `metalLayer.colorspace` to `kCGColorSpaceExtendedSRGB` (extended-range sRGB, allows values outside [0,1]). + +| `extendedLinearSRGB` +| Sets `metalLayer.colorspace` to `kCGColorSpaceExtendedLinearSRGB`. + +| `none` +| Leaves `metalLayer.colorspace` unset, so Metal uses the system default for the device. +|=== + +NOTE: The hint only takes effect when `ios.metal=true`. With the OpenGL ES 2 backend the layer is `CAEAGLLayer` and the colour space is fixed by the system. + === Accessing insecure URL's Due to security exploits Apple blocked some access to insecure URL's which means that http code that worked before could stop working for you on iOS 9+. This is a good move, you should use https and avoid http as much as possible but that's sometimes impractical when working with an internal or debug environment. diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java index 0d54cf4952..a9cf3f3a32 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -710,6 +710,8 @@ public void usesClassMethod(String cls, String method) { try { File CN1ES2compat = new File(buildinRes, "CN1ES2compat.h"); replaceInFile(CN1ES2compat, "//#define CN1_USE_METAL", "#define CN1_USE_METAL"); + String colorSpaceDefine = resolveMetalColorSpaceDefine(request.getArg("ios.metal.colorSpace", "sRGB")); + replaceInFile(CN1ES2compat, "//#define CN1_METAL_COLORSPACE_PLACEHOLDER", colorSpaceDefine); copy(new File(buildinRes, "MainWindowMETAL.xib"), new File(buildinRes, "MainWindow.xib")); copy(new File(buildinRes, "CodenameOne_METALViewController.xib"), new File(buildinRes, "CodenameOne_GLViewController.xib")); } catch (Exception ex) { @@ -3100,6 +3102,37 @@ private String[] getStubCompileSourceTarget(String javacPath) { return new String[]{source, target}; } + private String resolveMetalColorSpaceDefine(String hint) { + String value = hint == null ? "sRGB" : hint.trim(); + if (value.length() == 0) { + value = "sRGB"; + } + String key = value.toLowerCase().replace("-", "").replace("_", ""); + if (key.equals("srgb")) { + return "#define CN1_METAL_COLORSPACE_SRGB"; + } + if (key.equals("displayp3") || key.equals("p3")) { + return "#define CN1_METAL_COLORSPACE_DISPLAY_P3"; + } + if (key.equals("devicergb") || key.equals("device")) { + return "#define CN1_METAL_COLORSPACE_DEVICE_RGB"; + } + if (key.equals("linearsrgb") || key.equals("linear")) { + return "#define CN1_METAL_COLORSPACE_LINEAR_SRGB"; + } + if (key.equals("extendedsrgb")) { + return "#define CN1_METAL_COLORSPACE_EXTENDED_SRGB"; + } + if (key.equals("extendedlinearsrgb")) { + return "#define CN1_METAL_COLORSPACE_EXTENDED_LINEAR_SRGB"; + } + if (key.equals("none") || key.equals("default") || key.equals("system")) { + return "#define CN1_METAL_COLORSPACE_NONE"; + } + log("Unknown ios.metal.colorSpace value: " + hint + " - falling back to sRGB"); + return "#define CN1_METAL_COLORSPACE_SRGB"; + } + private String resolveXcodebuild() { String explicitXcodebuild = System.getenv("XCODEBUILD"); if (explicitXcodebuild != null && explicitXcodebuild.length() > 0) {