diff --git a/.github/workflows/_build-ios-port.yml b/.github/workflows/_build-ios-port.yml index 577e7aa8d2..0558bd8cf5 100644 --- a/.github/workflows/_build-ios-port.yml +++ b/.github/workflows/_build-ios-port.yml @@ -15,11 +15,17 @@ name: _Build iOS port (reusable) on: workflow_call: + outputs: + cn1_built_cache_key: + description: "Cache key the build job used for the cn1-built artifact bundle. Dependent jobs should reuse this exact key to avoid recomputing the hash and getting a spurious cache miss." + value: ${{ jobs.build.outputs.cn1_built_cache_key }} jobs: build: runs-on: macos-15 timeout-minutes: 60 + outputs: + cn1_built_cache_key: ${{ steps.cn1_built_key.outputs.key }} concurrency: group: mac-ios-port-${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: true @@ -105,6 +111,18 @@ jobs: # Built CN1 + iOS port artifacts. Restored after the m2 cache so it # overwrites any stale com/codenameone subtree pulled in by the broader # m2 cache (whose key is keyed on POMs, not source). + - name: Compute cn1-built cache key + id: cn1_built_key + # Single source of truth for the cache key, exposed as a job output + # so dependent jobs (build-ios, build-ios-metal in scripts-ios.yml, + # the iOS-native and packaging workflows) restore the EXACT key this + # job saved against. Computing the same hash again on each consumer + # job has been observed to produce a different value on some runners + # (suspected: the find -type f traversal picks up runner-specific + # filesystem metadata that survives between jobs). Sharing the key + # via an output sidesteps that. + run: echo "key=cn1-built-${{ runner.os }}-${{ steps.src_hash.outputs.hash }}" >> "$GITHUB_OUTPUT" + - name: Cache built CN1 + iOS port artifacts id: cn1_built uses: actions/cache@v4 @@ -113,7 +131,7 @@ jobs: ~/.m2/repository/com/codenameone Themes Ports/iOSPort/nativeSources - key: cn1-built-${{ runner.os }}-${{ steps.src_hash.outputs.hash }} + key: ${{ steps.cn1_built_key.outputs.key }} - name: Setup workspace if: steps.cn1_built.outputs.cache-hit != 'true' diff --git a/.github/workflows/scripts-ios.yml b/.github/workflows/scripts-ios.yml index 718f24b681..3d8770131f 100644 --- a/.github/workflows/scripts-ios.yml +++ b/.github/workflows/scripts-ios.yml @@ -16,6 +16,7 @@ on: - 'scripts/hellocodenameone/**' - 'scripts/ios/tests/**' - 'scripts/ios/screenshots/**' + - 'scripts/ios/screenshots-metal/**' - 'scripts/templates/**' - '!scripts/templates/**/*.md' - 'CodenameOne/src/**' @@ -45,6 +46,7 @@ on: - 'scripts/hellocodenameone/**' - 'scripts/ios/tests/**' - 'scripts/ios/screenshots/**' + - 'scripts/ios/screenshots-metal/**' - 'scripts/templates/**' - '!scripts/templates/**/*.md' - 'CodenameOne/src/**' @@ -197,3 +199,244 @@ jobs: path: artifacts if-no-files-found: warn retention-days: 14 + + build-ios-metal: + # Mirrors build-ios but enables the Metal rendering backend via the + # codename1.arg.ios.metal=true build hint. Keeps the GL path untouched + # so a regression on either path is isolated in its own job. Part of + # the iOS Metal port migration -- see Ports/iOSPort/METAL_PORT_STATUS.md. + # + # continue-on-error while the Metal port is still in progress: + # screenshot comparisons will differ from golden images until DrawString + # is ported and ClipRect is re-enabled (Phase 2 follow-ups tracked in + # METAL_PORT_STATUS.md). The Metal job still runs on every PR that + # touches the paths below so we can watch for new regressions and + # download artifacts for comparison, but a failure here won't block + # the PR. Flip this to false (or remove the line) once the Metal + # variant matches the GL variant's screenshot set. + continue-on-error: true + needs: build-port + permissions: + contents: read + pull-requests: write + issues: write + runs-on: macos-15 + timeout-minutes: 45 + concurrency: + group: mac-ci-${{ github.workflow }}-metal-${{ github.ref_name }} + cancel-in-progress: true + + env: + GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} + + steps: + - uses: actions/checkout@v4 + + - name: Cache CocoaPods and user gems + uses: actions/cache@v4 + with: + path: | + ~/.gem + ~/Library/Caches/CocoaPods + ~/.cocoapods/repos + key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }} + restore-keys: | + ${{ runner.os }}-pods-v1- + + - name: Ensure CocoaPods tooling + run: | + mkdir -p ~/.codenameone + cp maven/UpdateCodenameOne.jar ~/.codenameone/ + set -euo pipefail + if ! command -v ruby >/dev/null; then + echo "ruby not found"; exit 1 + fi + GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')" + export PATH="$GEM_USER_DIR/bin:$PATH" + if ! command -v pod >/dev/null 2>&1; then + gem install cocoapods xcodeproj --no-document --user-install + fi + pod --version + + - name: Compute setup-workspace hash + id: setup_hash + run: | + set -euo pipefail + echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT" + + - name: Compute CN1 source hash + id: src_hash + run: | + set -euo pipefail + SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes \ + -type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \ + | sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}') + POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \ + | sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}') + SCRIPT_HASH=$(shasum -a 256 \ + scripts/setup-workspace.sh \ + scripts/build-ios-port.sh \ + scripts/build-native-themes.sh \ + .github/workflows/_build-ios-port.yml \ + | shasum -a 256 | awk '{print $1}') + echo "hash=${SRC_HASH:0:16}-${POM_HASH:0:16}-${SCRIPT_HASH:0:16}" >> "$GITHUB_OUTPUT" + + - name: Set TMPDIR + run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV + + - name: Cache codenameone-tools + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/codenameone-tools + key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + ${{ runner.os }}-cn1-tools- + + - name: Cache Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-m2- + + - name: Restore cn1-binaries cache + uses: actions/cache@v4 + with: + path: ../cn1-binaries + key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }} + restore-keys: | + cn1-binaries-${{ runner.os }}- + + - name: Restore built CN1 + iOS port artifacts + # build-port (the reusable workflow at .github/workflows/_build-ios-port.yml) + # populates this cache with the iOS port artifact + native themes + cn1 + # Use the exact key the build-port job saved against. Recomputing + # the hash on this runner has been observed to produce a different + # value than build-port on the same SHA (suspected: find traversal + # picks up runner-specific filesystem metadata), causing a spurious + # cache miss. Reusing the published key sidesteps that. + uses: actions/cache/restore@v4 + with: + path: | + ~/.m2/repository/com/codenameone + Themes + Ports/iOSPort/nativeSources + key: ${{ needs.build-port.outputs.cn1_built_cache_key }} + fail-on-cache-miss: true + + - name: Install Metal Toolchain + # Xcode 26+ requires the Metal Toolchain component to be downloaded + # explicitly before .metal files can compile. The runner's default + # xcodebuild is often an older Xcode whose -downloadComponent flag + # doesn't exist, so we point DEVELOPER_DIR at Xcode 26 first (same + # selection logic build-ios-app.sh uses). If the component is + # already cached on the runner image, xcodebuild exits quickly + # without re-downloading. + run: | + set -euo pipefail + XCODE_APP="$(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -V | tail -n 1 || true)" + if [ ! -x "$XCODE_APP/Contents/Developer/usr/bin/xcodebuild" ]; then + echo "Xcode 26 not found under /Applications. Cannot install Metal Toolchain." >&2 + exit 1 + fi + echo "Using $XCODE_APP" + export DEVELOPER_DIR="$XCODE_APP/Contents/Developer" + "$DEVELOPER_DIR/usr/bin/xcodebuild" -downloadComponent MetalToolchain + timeout-minutes: 10 + + - name: Enable Metal backend for hellocodenameone + # -Dcodename1.arg.* on the mvnw CLI doesn't flow into the Codename + # One Maven plugin's BuildRequest -- the plugin reads build args + # from the codenameone_settings.properties file on disk. Inject the + # Metal flag there so IPhoneBuilder's useMetal path activates. + run: | + set -euo pipefail + SETTINGS=scripts/hellocodenameone/common/codenameone_settings.properties + if grep -q '^codename1\.arg\.ios\.metal=' "$SETTINGS"; then + sed -i '' 's|^codename1.arg.ios.metal=.*|codename1.arg.ios.metal=true|' "$SETTINGS" + else + # Insert next to the other ios.* keys (after applicationQueriesSchemes). + awk ' + /^codename1\.arg\.ios\.applicationQueriesSchemes=/ { + print + print "codename1.arg.ios.metal=true" + next + } + { print } + ' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS" + fi + echo "--- codenameone_settings.properties (ios.* keys after patch) ---" + grep -n 'codename1\.arg\.ios' "$SETTINGS" || true + + - name: Build sample iOS app and compile workspace (Metal) + id: build-ios-app + run: ./scripts/build-ios-app.sh -q -DskipTests + timeout-minutes: 30 + + - name: Run iOS UI screenshot tests (Metal) + env: + ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/ios-ui-tests-metal + # Compare Metal output against the Metal-specific golden set. + # See scripts/ios/screenshots-metal/README.md for the rationale: + # the Metal backend will not be pixel-identical to the GL + # baselines (especially once CoreText glyph rendering lands). + SCREENSHOT_REF_DIR: ${{ github.workspace }}/scripts/ios/screenshots-metal + run: | + set -euo pipefail + mkdir -p "${ARTIFACTS_DIR}" + + echo "workspace='${{ steps.build-ios-app.outputs.workspace }}'" + echo "scheme='${{ steps.build-ios-app.outputs.scheme }}'" + echo "reference dir='${SCREENSHOT_REF_DIR}'" + + ./scripts/run-ios-ui-tests.sh \ + "${{ steps.build-ios-app.outputs.workspace }}" \ + "" \ + "${{ steps.build-ios-app.outputs.scheme }}" + timeout-minutes: 30 + + - name: Publish Metal screenshot summary + # Surfaces run-ios-ui-tests.sh's comparison result in the job's + # GitHub Actions summary page so the Metal port status is visible + # at a glance without digging into the artifact zip. Always runs + # so a failed or cancelled tests step still reports whatever got + # captured. The Python helper lives in scripts/ci/ because + # embedding Python heredocs inside a YAML "run: |" block is + # fragile -- unindented Python breaks the block scalar. + if: always() + env: + COMPARE_JSON: ${{ github.workspace }}/artifacts/ios-ui-tests-metal/screenshot-compare.json + COMMENT_MD: ${{ github.workspace }}/artifacts/ios-ui-tests-metal/screenshot-comment.md + run: | + set -eu + { + echo "## iOS Metal screenshot comparison" + echo + echo "Ran against \`scripts/hellocodenameone\` on the iOS simulator with \`codename1.arg.ios.metal=true\`." + echo "Golden images: \`scripts/ios/screenshots-metal/\` (Metal-specific baseline; see the README there for why it is separate from the GL set)." + echo + if [ -s "$COMPARE_JSON" ]; then + python3 scripts/ci/metal-screenshot-summary.py --markdown "$COMPARE_JSON" + elif [ -s "$COMMENT_MD" ]; then + cat "$COMMENT_MD" + else + echo "_No screenshot comparison artifact was produced. See the upload step output for details._" + fi + } >> "$GITHUB_STEP_SUMMARY" + if [ -s "$COMPARE_JSON" ]; then + NOTICE="$(python3 scripts/ci/metal-screenshot-summary.py --headline "$COMPARE_JSON" || true)" + if [ -n "$NOTICE" ]; then + echo "::notice title=Metal screenshot comparison::${NOTICE}" + fi + fi + + - name: Upload iOS Metal artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: ios-ui-tests-metal + path: artifacts + if-no-files-found: warn + retention-days: 14 diff --git a/.gitignore b/.gitignore index 82e5eba372..a04a1a3d07 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,4 @@ dependency-reduced-pom.xml /.playwright-cli package-lock.json package.json +.claude/ diff --git a/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java b/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java index de18663fe6..9e834a156e 100644 --- a/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java +++ b/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java @@ -862,6 +862,25 @@ public void paint(Graphics g) { paintInterformContainers(g); return; case TYPE_SLIDE_AND_FADE: { + // Final transition frame: instead of repeating the cross- + // fade (which intentionally leaves both source + dest + // titles visible at varying alpha), paint the destination + // form fully. The form's normal paint clears the title + // area properly, which matters on persistent-backing + // renderers (iOS Metal MTLLoadActionLoad) that faithfully + // preserve every previous-frame pixel -- without this + // cleanup, the cross-fade's accumulated alpha-blended + // title pixels remain in the screenTexture and the next + // captured frame shows two titles overlapping. iOS GL ES2 + // happens to mask this because CAEAGLLayer.retainBacking + // is permissively lossy about partial-alpha pixels. + if (firstFinished) { + Form finalForm = (Form) getDestination(); + if (finalForm != null) { + paint(g, finalForm, 0, 0, true); + } + return; + } Form sourceForm = (Form) getSource(); Form destForm = (Form) getDestination(); diff --git a/Ports/iOSPort/METAL_PORT_STATUS.md b/Ports/iOSPort/METAL_PORT_STATUS.md new file mode 100644 index 0000000000..2ce26ad977 --- /dev/null +++ b/Ports/iOSPort/METAL_PORT_STATUS.md @@ -0,0 +1,55 @@ +# iOS Metal Rendering Port — Status + +Branch: `metal-ios-backend`. Build flag: `-Dcodename1.arg.ios.metal=true` (uncomments `#define CN1_USE_METAL` in `CN1ES2compat.h`). OpenGL ES 2 remains the default. + +## Architectural choices + +- **Two backends, one Java surface.** The `ExecutableOp` queue, `CADisplayLink → drawFrame` drain loop, peer-component layering, and JNI surface in `IOSImplementation.java` are unchanged from the GL build. Metal pipeline state lives in `CN1Metalcompat.{h,m}`, shaders in `CN1MetalShaders.metal`, glyph atlas in `CN1MetalGlyphAtlas.{h,m}`, pipeline cache in `CN1MetalPipelineCache.{h,m}`. All gated by `#ifdef CN1_USE_METAL`. + +- **Mutable-image rendering goes through the alpha-mask path on both GL and Metal.** `MutableGraphics.nativeFillShape` / `nativeDrawShape` / `nativeFillRoundRect` / `nativeFillArc` / etc. all build a `GeneralPath` and call `renderShapeViaAlphaMask` which routes through `Renderer.c` → R8 alpha mask → `DrawTextureAlphaMask` op tagged with the current mutable image. The op's `execute` picks the Metal-MSL or GL-shader implementation by build flag. The Java side has zero `if (metalRendering)` runtime checks; the C side uses `#ifdef CN1_USE_METAL` exclusively. + +- **Deferred commit on mutable images.** `startDrawingOnImage` allocates an `MTLTexture` for the mutable; subsequent ops queue tagged with that target. `drawFrame`'s drain switches encoder per target and commits per-target command buffers without `waitUntilCompleted`. Pixel-reading paths (`getRGB`, encode-as-PNG/JPEG, `gausianBlurImage`) call `flushBuffer` to force a drain, then `CN1MetalFlushMutableImageSync` to wait, then read. + +- **Text rendering: per-(font, pointSize) R8 atlas via CoreText.** `CN1MetalGlyphAtlas` lazily rasterises glyphs into a 1024² (grows to 2048²) R8 texture using `CTFontDrawGlyphs`. LRU eviction with 64-entry cache cap. `CN1MetalDrawString` shapes via `CTLineCreateWithAttributedString` and emits one alpha-mask quad per glyph through the same `cn1_fs_alpha_mask` Metal shader the shape path uses. + +- **Gradient rendering: pure-GPU MSL fragment shaders.** `cn1_fs_linear_gradient` and `cn1_fs_radial_gradient` interpolate `mix(startColor, endColor, t)` per-fragment. No CG-bitmap upload; no offscreen rasterisation. Linear gradients use vertex texcoords (0..1) along the chosen axis; radial uses `length((uv - center) / radii)`. + +- **Premultiplied alpha throughout.** Pipeline blend mode: `src=One, dst=OneMinusSourceAlpha`. Mutable-texture clear colour is stored premultiplied so subsequent sampling (with `cn1_fs_textured`) composites correctly when the mutable was created via `Image.createImage(w, h, argb)` with non-opaque argb. + +- **Metal Y-down ortho with z-range remap.** `mutableProjection(w, h)` maps `(0,0) → (-1, +1)` (top-left in NDC) and `(w, h) → (+1, -1)` (bottom-right). Z is `0.5 * input_z + 0.5 * w` so GL-style clip-z `[-w, w]` maps to Metal's `[0, w]`. + +- **Render targets persistent across frames.** A persistent offscreen `screenTexture` with `MTLLoadActionLoad` accumulates per-frame ops the way the GL renderbuffer does; the drawable is acquired only at present time to minimise `nextDrawable` stalls. `setFramebuffer` is idempotent; `updateFrameBufferSize:h:` tears down any live encoder before rebuilding the texture so dimension changes mid-frame don't leak state. + +- **Phase 5 hardening landed.** sRGB colorspace, `maximumDrawableCount = 3` with skip-frame on nil drawable, memory-warning eviction of glyph atlases, lifecycle pause on backgrounding, drawable recreation on rotation. + +## Missing features / open issues + +- **Switch component triangular tear.** A small (~3% of pill area) triangular sub-pixel artefact remains where the white thumb meets the green pill on `SwitchTheme_dark` / `SwitchTheme_light`. The pill renders as a single solid shape (was four pacman wedges before the path-construction fixes) and the thumb circle is clean. Likely cause: `gausianBlurImage`'s blurred shadow halo isn't propagating into the new mutable's `MTLTexture` after `Image.getGraphics()` re-attaches. Several seed-the-texture-from-the-existing-UIImage attempts (commits `9f03c11a8` → `b8db2d74e` reverted) did not fix it. Needs device-level shader/Metal-debugger inspection to narrow further. Goldens for `kotlin` (which contains a Switch) reverted to the pre-bug capture so the test surfaces the regression. + +- **Perspective / camera transforms render empty on mutable targets.** `graphics-transform-perspective` and `graphics-transform-camera` mutable panels render blank on Metal; GL renders the perspective-transformed rectangles. Vertex shader chain (`projection × modelView × transform × pos4`) and z-range remap look correct on paper but the rendered output is empty. Goldens captured the empty state, so the test passes self-referentially. Real bug, hidden. + +- **Stencil clipping for non-rectangular clips.** Currently falls back to a bounding-box scissor — Form layout handles that OK but paths-as-masks and textures-as-masks clip incorrectly. + +- **`graphics-draw-line` rasterisation diffs.** Test draws thousands of 1-pixel lines; Metal's `MTLPrimitiveTypeLine` rasterisation rule differs from GL's at integer pixel boundaries, producing 1-pixel-wide diff stripes. Not a bug; rasterisation rule mismatch. + +- **Image scaling quality.** `CGContextDrawImage` round-trips at 1× scale produce blurry edges when stretched to 3× retina (affects `graphics-draw-image-rect`, `graphics-fill-round-rect` via the round-rect-as-image path). Both backends share this; not Metal-specific. + +- **`graphics-fill-polygon` dropped from compare pipeline.** When the JPEG preview exceeds 20 KB (test produces 72 KB), the runner drops the test from the comparison even though the full PNG was captured. Pre-existing tooling issue, not rendering. + +## Verification + +```bash +# GL baseline +cd scripts/hellocodenameone +./build.sh ios_source +scripts/run-ios-ui-tests.sh + +# Metal variant +./build.sh ios_source -Dios.metal=true +scripts/run-ios-ui-tests.sh + +# Compare +diff /screenshot-compare.json /screenshot-compare.json +``` + +Metal goldens live in `scripts/ios/screenshots-metal/`; the `build-ios-metal` CI job overrides `SCREENSHOT_REF_DIR` to point at it. GL goldens (`scripts/ios/screenshots/`) remain untouched by Metal port work. diff --git a/Ports/iOSPort/nativeSources/CN1ES2compat.h b/Ports/iOSPort/nativeSources/CN1ES2compat.h index af662f79c5..557e9e214e 100644 --- a/Ports/iOSPort/nativeSources/CN1ES2compat.h +++ b/Ports/iOSPort/nativeSources/CN1ES2compat.h @@ -20,6 +20,11 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ +// IPhoneBuilder.java:697 uncomments the line below when -Dios.metal=true is set. +// When defined, the Metal rendering backend (METALView, CN1Metalcompat) is +// 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 #define USE_ES2 1 enum CN1GLenum { CN1_GL_ALPHA_TEXTURE, diff --git a/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.h b/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.h new file mode 100644 index 0000000000..cb6fe459ce --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.h @@ -0,0 +1,61 @@ +// CN1MetalGlyphAtlas.h +// +// Phase 4 of the iOS Metal port. Per-(font, point-size) glyph atlas: each +// CGGlyph rasterised once into an R8 MTLTexture region and reused across +// every DrawString that hits the same font. +// +// CN1MetalDrawString uses this to amortise the per-frame texture rebuild +// cost of the Phase-2 whole-string LRU cache. Color is decoupled (atlas +// stores alpha only); colourisation happens in cn1_fs_alpha_mask at draw +// time. +// +// Single-threaded — all entry points run on the main thread inside +// drawFrame's op drain. +// +// This header MUST be `#import "CN1ES2compat.h"`-ed before its body so +// the CN1_USE_METAL macro is visible (the PCH does not include it; see +// METALView.h for the same pattern). + +#import "CN1ES2compat.h" +#ifdef CN1_USE_METAL +#import +#import +#import +#import + +@interface CN1MetalGlyphSlot : NSObject +@property (nonatomic, assign) int atlasX; +@property (nonatomic, assign) int atlasY; +@property (nonatomic, assign) int width; // includes 2px padding for AA bleed +@property (nonatomic, assign) int height; // includes 2px padding for AA bleed +@property (nonatomic, assign) float bearingX; // bbox.origin.x — left bearing in font space +@property (nonatomic, assign) float bearingY; // bbox.origin.y — distance baseline → bbox bottom (CT y-up) +@property (nonatomic, assign) float bboxWidth; // bbox.size.width (no padding) +@property (nonatomic, assign) float bboxHeight; // bbox.size.height (no padding) +@end + +@interface CN1MetalGlyphAtlas : NSObject + +// Get-or-create the atlas for the given UIFont. Atlases are cached on +// (fontName, pointSize). Returns nil if the underlying MTLDevice is +// unavailable or the CTFont can't be created — DrawString then falls +// back to the whole-string path. ++ (nullable instancetype)atlasForFont:(nonnull UIFont *)font; + +// Look up a glyph, rasterising and packing it on first reference. +// Returns nil if the atlas is full and cannot grow further. Slot's +// `width = 0` signals an empty glyph (e.g. space) — callers should +// skip the quad and just consume the run advance. +- (nullable CN1MetalGlyphSlot *)slotForGlyph:(CGGlyph)glyph; + +// Backing R8 texture and its current dimensions. +@property (nonatomic, readonly, nonnull) id texture; +@property (nonatomic, readonly) int textureWidth; +@property (nonatomic, readonly) int textureHeight; + +// CTFontRef used for shaping AND rasterisation. Owned by the atlas. +@property (nonatomic, readonly, nonnull) CTFontRef ctFont; + +@end + +#endif // CN1_USE_METAL diff --git a/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.m b/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.m new file mode 100644 index 0000000000..8d6edb8143 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1MetalGlyphAtlas.m @@ -0,0 +1,340 @@ +// CN1MetalGlyphAtlas.m +// +// Phase 4 implementation. See header for design rationale. +// +// Rasterisation pattern: CGBitmapContext with DeviceGray colorspace + +// kCGImageAlphaNone + white fill. CTFontDrawGlyphs in default Y-up CG +// renders the glyph into the bitmap such that memory_row_0 holds the +// padded TOP of the slot and memory_row_(gh-1) the padded BOTTOM — +// i.e. right-side-up in raster memory order, ready for V=0-at-top +// sampling. + +#import "CN1ES2compat.h" +#ifdef CN1_USE_METAL +#import "CN1MetalGlyphAtlas.h" + +#define CN1_METAL_ATLAS_INITIAL_W 1024 +#define CN1_METAL_ATLAS_INITIAL_H 1024 +#define CN1_METAL_ATLAS_MAX_W 2048 +#define CN1_METAL_ATLAS_MAX_H 2048 +#define CN1_METAL_ATLAS_PADDING 1 +#define CN1_METAL_ATLAS_GLYPH_MAX 256 // sanity limit on per-glyph bitmap dim + +extern id CN1MetalDevice(void); + +// Simple fixed-size array cache (linear scan). Replaced an +// NSMutableDictionary that was hanging on CI's iPhone 17 Pro sim during +// the second lookup of the same key — issue reproduced with diagnostic +// logging in commit 330fcdc10. Linear scan over <=64 entries is fine. +// +// `lastUsedTick` is bumped to a monotonic counter on every cache hit and +// at insertion, so the LRU eviction below picks the entry that was +// touched longest ago. Without this the cache used to flat-out reject +// new fonts once full, and any text rendered after the 16th unique +// (font, pointSize) request silently dropped on the Metal path -- the +// bug that made ToolbarTheme / TextFieldTheme / etc. render blank +// because by the time those tests ran (positions 58-67 in the suite), +// the cache had already been filled by earlier graphics tests and all +// further atlasForFont:: calls returned nil. +#define CN1_METAL_ATLAS_CACHE_MAX 64 +typedef struct { + NSString *key; + CN1MetalGlyphAtlas *atlas; + uint64_t lastUsedTick; +} CN1MetalAtlasCacheEntry; +static CN1MetalAtlasCacheEntry atlasCacheEntries[CN1_METAL_ATLAS_CACHE_MAX]; +static int atlasCacheCount = 0; +static uint64_t atlasCacheTick = 0; + +@implementation CN1MetalGlyphSlot +@end + +@interface CN1MetalGlyphAtlas () { + NSString *_fontKey; + CTFontRef _ctFont; + int _textureWidth; + int _textureHeight; + id _texture; + NSMutableDictionary *_slots; + int _shelfY; + int _shelfHeight; + int _cursorX; +} +@end + +@implementation CN1MetalGlyphAtlas + ++ (nullable instancetype)atlasForFont:(nonnull UIFont *)font { + NSString *key = [NSString stringWithFormat:@"%@|%g", font.fontName, (double)font.pointSize]; + atlasCacheTick++; + for (int i = 0; i < atlasCacheCount; i++) { + if ([atlasCacheEntries[i].key isEqualToString:key]) { + atlasCacheEntries[i].lastUsedTick = atlasCacheTick; + return atlasCacheEntries[i].atlas; + } + } + CN1MetalGlyphAtlas *fresh = [[CN1MetalGlyphAtlas alloc] initWithFont:font key:key]; + if (fresh == nil) return nil; + // Note: this file builds without ARC (cn1's iOS port keeps + // CLANG_ENABLE_OBJC_ARC=NO; see METALView.m's #ifndef CN1_USE_ARC). + // The static C-struct pointers therefore do NOT auto-retain — we + // own a strong reference to each cached entry by transferring the + // alloc/init +1 (for `fresh`) and the [copy] +1 (for `key`) + // straight into the cache. The previous NSMutableDictionary cache + // hung on its second lookup because the dictionary itself, created + // via [NSMutableDictionary dictionary] (autoreleased), was + // deallocated when the autorelease pool drained between frames. + int slot; + if (atlasCacheCount < CN1_METAL_ATLAS_CACHE_MAX) { + slot = atlasCacheCount++; + } else { + // LRU eviction: pick the entry with the smallest lastUsedTick. + // Skipping this turned every overflow request into a nil return + // and the calling CN1MetalDrawString silently dropped the string. + slot = 0; + uint64_t oldest = atlasCacheEntries[0].lastUsedTick; + for (int i = 1; i < CN1_METAL_ATLAS_CACHE_MAX; i++) { + if (atlasCacheEntries[i].lastUsedTick < oldest) { + oldest = atlasCacheEntries[i].lastUsedTick; + slot = i; + } + } + // Drop the +1 retains held by the slot before overwriting: + // key was [key copy] (+1) and atlas was alloc/init (+1). Without + // releasing them the cache leaks one NSString and one MTLTexture- + // backed atlas per eviction under MRR. +#ifndef CN1_USE_ARC + [atlasCacheEntries[slot].key release]; + [atlasCacheEntries[slot].atlas release]; +#endif + atlasCacheEntries[slot].key = nil; + atlasCacheEntries[slot].atlas = nil; + } + atlasCacheEntries[slot].key = [key copy]; + atlasCacheEntries[slot].atlas = fresh; + atlasCacheEntries[slot].lastUsedTick = atlasCacheTick; + return fresh; +} + +- (instancetype)initWithFont:(UIFont *)uifont key:(NSString *)key { + self = [super init]; + if (self == nil) return nil; + id device = CN1MetalDevice(); + if (device == nil) return nil; + + _fontKey = [key copy]; + // Build the CTFont from UIFont's descriptor, NOT from .fontName. + // iOS system fonts have private names like ".SFUI-Regular" and + // CTFontCreateWithName silently falls back to Times for those — + // text on the screen path then renders as a serif Times Roman + // instead of the requested system font. UIFontDescriptor and + // CTFontDescriptor are toll-free bridged, so casting through + // .fontDescriptor preserves the actual font identity. + CTFontDescriptorRef ctDesc = (__bridge CTFontDescriptorRef)uifont.fontDescriptor; + _ctFont = CTFontCreateWithFontDescriptor(ctDesc, uifont.pointSize, NULL); + if (_ctFont == NULL) return nil; + + _textureWidth = CN1_METAL_ATLAS_INITIAL_W; + _textureHeight = CN1_METAL_ATLAS_INITIAL_H; + + MTLTextureDescriptor *desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatR8Unorm + width:(NSUInteger)_textureWidth + height:(NSUInteger)_textureHeight + mipmapped:NO]; + desc.usage = MTLTextureUsageShaderRead; + _texture = [device newTextureWithDescriptor:desc]; + if (_texture == nil) { + CFRelease(_ctFont); + _ctFont = NULL; + return nil; + } + + // alloc+init (NOT [NSMutableDictionary dictionary]) so the +1 retain + // is owned by us and survives the next autorelease pool drain. This + // file builds without ARC and direct ivar assignment doesn't auto- + // retain. Same bug as the static atlasCache had — discovered when + // CI hung at the runs loop in CN1MetalDrawString because slotForGlyph + // dereferenced a deallocated _slots dictionary. + _slots = [[NSMutableDictionary alloc] init]; + _shelfY = CN1_METAL_ATLAS_PADDING; + _shelfHeight = 0; + _cursorX = CN1_METAL_ATLAS_PADDING; + + return self; +} + +- (void)dealloc { + if (_ctFont != NULL) { + CFRelease(_ctFont); + _ctFont = NULL; + } + // Non-ARC: release the +1 retains held by these ivars. _fontKey was + // [key copy] (+1), _texture was [device newTextureWithDescriptor:] + // (the "new" prefix returns +1), _slots was [[NSMutableDictionary + // alloc] init] (+1). Without these releases the MTLTexture and slot + // dictionary leak each time CN1MetalReleaseCaches drops an atlas. + [_fontKey release]; _fontKey = nil; + [_texture release]; _texture = nil; + [_slots release]; _slots = nil; +#ifndef CN1_USE_ARC + [super dealloc]; +#endif +} + +- (id)texture { return _texture; } +- (int)textureWidth { return _textureWidth; } +- (int)textureHeight { return _textureHeight; } +- (CTFontRef)ctFont { return _ctFont; } + +- (BOOL)tryGrowAtlas { + if (_textureWidth >= CN1_METAL_ATLAS_MAX_W && _textureHeight >= CN1_METAL_ATLAS_MAX_H) return NO; + id device = CN1MetalDevice(); + if (device == nil) return NO; + + int newW = MIN(_textureWidth * 2, CN1_METAL_ATLAS_MAX_W); + int newH = MIN(_textureHeight * 2, CN1_METAL_ATLAS_MAX_H); + + MTLTextureDescriptor *desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatR8Unorm + width:(NSUInteger)newW height:(NSUInteger)newH mipmapped:NO]; + desc.usage = MTLTextureUsageShaderRead; + id newTex = [device newTextureWithDescriptor:desc]; + if (newTex == nil) return NO; + + // Drop slots; next reference re-rasterises into the larger atlas. + [_slots removeAllObjects]; + _shelfY = CN1_METAL_ATLAS_PADDING; + _shelfHeight = 0; + _cursorX = CN1_METAL_ATLAS_PADDING; +#ifndef CN1_USE_ARC + // Release the previous _texture's +1 retain (held since alloc-init or + // the previous tryGrowAtlas) before overwriting the ivar with newTex's + // +1; otherwise every grow leaks the previous atlas texture. + [_texture release]; +#endif + _texture = newTex; + _textureWidth = newW; + _textureHeight = newH; + return YES; +} + +- (CN1MetalGlyphSlot *)slotForGlyph:(CGGlyph)glyph { + NSNumber *key = @(glyph); + CN1MetalGlyphSlot *cached = _slots[key]; + if (cached != nil) return cached; + + CGRect bbox = CGRectZero; + CGSize advance = CGSizeZero; + CTFontGetBoundingRectsForGlyphs(_ctFont, kCTFontOrientationHorizontal, &glyph, &bbox, 1); + CTFontGetAdvancesForGlyphs(_ctFont, kCTFontOrientationHorizontal, &glyph, &advance, 1); + + // Empty glyph (space, control char, etc): record zero-width slot so + // subsequent calls hit the cache fast and DrawString just consumes + // the advance without emitting a quad. + if (bbox.size.width <= 0 || bbox.size.height <= 0) { + CN1MetalGlyphSlot *empty = [[CN1MetalGlyphSlot alloc] init]; + empty.atlasX = 0; empty.atlasY = 0; + empty.width = 0; empty.height = 0; + empty.bearingX = 0; empty.bearingY = 0; + empty.bboxWidth = 0; empty.bboxHeight = 0; + _slots[key] = empty; + return empty; + } + + int gw = (int)ceilf((float)bbox.size.width) + 2 * CN1_METAL_ATLAS_PADDING; + int gh = (int)ceilf((float)bbox.size.height) + 2 * CN1_METAL_ATLAS_PADDING; + // Refuse oversized glyphs: protects the bitmap allocation if a font + // returns absurd metrics. A 256x256 cap is plenty for system text. + if (gw > CN1_METAL_ATLAS_GLYPH_MAX || gh > CN1_METAL_ATLAS_GLYPH_MAX) return nil; + + // Shelf-pack: open a new shelf if current one is too narrow to fit. + if (_cursorX + gw > _textureWidth - CN1_METAL_ATLAS_PADDING) { + _shelfY += _shelfHeight + CN1_METAL_ATLAS_PADDING; + _cursorX = CN1_METAL_ATLAS_PADDING; + _shelfHeight = 0; + } + if (_shelfY + gh > _textureHeight - CN1_METAL_ATLAS_PADDING) { + if (![self tryGrowAtlas]) return nil; + if (_cursorX + gw > _textureWidth - CN1_METAL_ATLAS_PADDING || + _shelfY + gh > _textureHeight - CN1_METAL_ATLAS_PADDING) { + return nil; + } + } + if (gh > _shelfHeight) _shelfHeight = gh; + + int slotX = _cursorX; + int slotY = _shelfY; + _cursorX += gw + CN1_METAL_ATLAS_PADDING; + + // Rasterise into a local R8 buffer using DeviceGray + kCGImageAlphaNone + // + white fill. CTFontDrawGlyphs renders the glyph paths in white + // (== 0xff in the R8 pixel) on a black (== 0x00) background; sampled + // through cn1_fs_alpha_mask the .r channel becomes alpha coverage. + // + // Y-up CG default: drawing the glyph at user origin + // (padding - bearingX, padding - bearingY) places the bbox at user + // x ∈ [padding, padding + bbox.width], y ∈ [padding, padding + bbox.height]. + // Memory layout (Apple bitmap convention: memory_row_0 at TOP of bitmap) + // maps user-y=padding+bbox.height → memory_row_(padding-1) and + // user-y=padding → memory_row_(gh-1-padding); i.e. the glyph occupies + // memory rows [padding-1 .. gh-1-padding] right-side-up. memory_row_0 + // is the TOP padding band, memory_row_(gh-1) the BOTTOM padding band. + size_t bytesPerRow = (size_t)gw; + void *pixels = calloc((size_t)gh * bytesPerRow, 1); + if (pixels == NULL) return nil; + CGColorSpaceRef cs = CGColorSpaceCreateDeviceGray(); + CGContextRef ctx = CGBitmapContextCreate(pixels, (size_t)gw, (size_t)gh, 8, + bytesPerRow, cs, + (CGBitmapInfo)kCGImageAlphaNone); + CGColorSpaceRelease(cs); + if (ctx == NULL) { + free(pixels); + return nil; + } + CGContextSetGrayFillColor(ctx, 1.0, 1.0); + + CGPoint origin = CGPointMake((CGFloat)CN1_METAL_ATLAS_PADDING - bbox.origin.x, + (CGFloat)CN1_METAL_ATLAS_PADDING - bbox.origin.y); + CTFontDrawGlyphs(_ctFont, &glyph, &origin, 1, ctx); + CGContextRelease(ctx); + + [_texture replaceRegion:MTLRegionMake2D((NSUInteger)slotX, (NSUInteger)slotY, + (NSUInteger)gw, (NSUInteger)gh) + mipmapLevel:0 + withBytes:pixels + bytesPerRow:bytesPerRow]; + free(pixels); + + CN1MetalGlyphSlot *slot = [[CN1MetalGlyphSlot alloc] init]; + slot.atlasX = slotX; + slot.atlasY = slotY; + slot.width = gw; + slot.height = gh; + slot.bearingX = (float)bbox.origin.x; + slot.bearingY = (float)bbox.origin.y; + slot.bboxWidth = (float)bbox.size.width; + slot.bboxHeight = (float)bbox.size.height; + _slots[key] = slot; + return slot; +} + +@end + +// Drop every cached atlas. Called by CN1MetalReleaseCaches on +// UIApplicationDidReceiveMemoryWarning. Each entry's key (NSString, +// retained via [copy]) and atlas (CN1MetalGlyphAtlas, retained via the +// alloc/init +1 transferred in atlasForFont:) own +1 retains under +// MRR; release them explicitly. Subsequent lookups re-create on demand. +void CN1MetalGlyphAtlasReleaseAll(void) { + for (int i = 0; i < atlasCacheCount; i++) { + [atlasCacheEntries[i].key release]; + [atlasCacheEntries[i].atlas release]; + atlasCacheEntries[i].key = nil; + atlasCacheEntries[i].atlas = nil; + } + atlasCacheCount = 0; +} + +#endif // CN1_USE_METAL diff --git a/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.h b/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.h new file mode 100644 index 0000000000..c1d1265b7d --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +#ifndef CN1MetalPipelineCache_h +#define CN1MetalPipelineCache_h + +#import "CN1ES2compat.h" +#ifdef CN1_USE_METAL +#import +@import Metal; +#import "CN1Metalcompat.h" + +// Caches one MTLRenderPipelineState per CN1MetalPipeline variant. +// Built lazily on first use from the default.metallib that Xcode produces +// from CN1MetalShaders.metal. +@interface CN1MetalPipelineCache : NSObject + +- (instancetype)initWithDevice:(id)device; +- (id)pipelineFor:(CN1MetalPipeline)pipeline; + +@end + +#endif /* CN1_USE_METAL */ +#endif /* CN1MetalPipelineCache_h */ diff --git a/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.m b/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.m new file mode 100644 index 0000000000..a88ebbae5d --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1MetalPipelineCache.m @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +#import "CN1ES2compat.h" +#ifdef CN1_USE_METAL +#import "CN1MetalPipelineCache.h" + +@interface CN1MetalPipelineCache () { + id _device; + id _states[CN1MetalPipelineCount]; +} +@end + +@implementation CN1MetalPipelineCache + +- (instancetype)initWithDevice:(id)device { + if ((self = [super init])) { + _device = device; + for (int i = 0; i < CN1MetalPipelineCount; i++) { + _states[i] = nil; + } + } + return self; +} + +// Configures the standard color attachment: BGRA8Unorm to match +// CAMetalLayer.pixelFormat, premultiplied-alpha blending so the output +// of our shaders (which multiply color by alpha on the CPU for solid +// fills and use `texture * tint` for textured draws) composites correctly. +static void configureBlendPremultiplied(MTLRenderPipelineColorAttachmentDescriptor *a) { + a.pixelFormat = MTLPixelFormatBGRA8Unorm; + a.blendingEnabled = YES; + a.rgbBlendOperation = MTLBlendOperationAdd; + a.alphaBlendOperation = MTLBlendOperationAdd; + a.sourceRGBBlendFactor = MTLBlendFactorOne; + a.destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + a.sourceAlphaBlendFactor = MTLBlendFactorOne; + a.destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; +} + +static void configureBlendDisabled(MTLRenderPipelineColorAttachmentDescriptor *a) { + a.pixelFormat = MTLPixelFormatBGRA8Unorm; + a.blendingEnabled = NO; +} + +- (id)buildPipeline:(CN1MetalPipeline)pipeline library:(id)library { + MTLRenderPipelineDescriptor *desc = [[MTLRenderPipelineDescriptor alloc] init]; + + switch (pipeline) { + case CN1MetalPipelineSolidColor: + desc.vertexFunction = [library newFunctionWithName:@"cn1_vs_solid"]; + desc.fragmentFunction = [library newFunctionWithName:@"cn1_fs_solid"]; + configureBlendPremultiplied(desc.colorAttachments[0]); + break; + case CN1MetalPipelineTexturedRGBA: + desc.vertexFunction = [library newFunctionWithName:@"cn1_vs_textured"]; + desc.fragmentFunction = [library newFunctionWithName:@"cn1_fs_textured"]; + configureBlendPremultiplied(desc.colorAttachments[0]); + break; + case CN1MetalPipelineAlphaMask: + desc.vertexFunction = [library newFunctionWithName:@"cn1_vs_textured"]; + desc.fragmentFunction = [library newFunctionWithName:@"cn1_fs_alpha_mask"]; + configureBlendPremultiplied(desc.colorAttachments[0]); + break; + case CN1MetalPipelineClearPunch: + desc.vertexFunction = [library newFunctionWithName:@"cn1_vs_solid"]; + desc.fragmentFunction = [library newFunctionWithName:@"cn1_fs_clear"]; + configureBlendDisabled(desc.colorAttachments[0]); + break; + case CN1MetalPipelineAlphaMaskRadial: + desc.vertexFunction = [library newFunctionWithName:@"cn1_vs_textured"]; + desc.fragmentFunction = [library newFunctionWithName:@"cn1_fs_alpha_mask_radial"]; + configureBlendPremultiplied(desc.colorAttachments[0]); + break; + case CN1MetalPipelineLinearGradient: + // Pure GPU linear gradient -- vertex stage feeds per-corner 0..1 + // texcoords; fragment lerps startColor->endColor along whichever + // axis the caller picks. Replaces the CG-rasterise + upload + // path the iOS Metal port had carried over from Phase 2. + desc.vertexFunction = [library newFunctionWithName:@"cn1_vs_textured"]; + desc.fragmentFunction = [library newFunctionWithName:@"cn1_fs_linear_gradient"]; + configureBlendPremultiplied(desc.colorAttachments[0]); + break; + case CN1MetalPipelineRadialGradient: + // Pure GPU radial gradient -- same vertex stage as the linear + // variant. Replaces CGContextDrawRadialGradient + bitmap upload. + desc.vertexFunction = [library newFunctionWithName:@"cn1_vs_textured"]; + desc.fragmentFunction = [library newFunctionWithName:@"cn1_fs_radial_gradient"]; + configureBlendPremultiplied(desc.colorAttachments[0]); + break; + default: + return nil; + } + if (desc.vertexFunction == nil || desc.fragmentFunction == nil) { + NSLog(@"CN1MetalPipelineCache: shader function missing for pipeline %ld", (long)pipeline); + return nil; + } + NSError *err = nil; + id state = [_device newRenderPipelineStateWithDescriptor:desc error:&err]; + if (state == nil) { + NSLog(@"CN1MetalPipelineCache: failed to create pipeline %ld: %@", (long)pipeline, err); + } + return state; +} + +- (id)pipelineFor:(CN1MetalPipeline)pipeline { + if (pipeline < 0 || pipeline >= CN1MetalPipelineCount) return nil; + if (_states[pipeline] != nil) return _states[pipeline]; + + id library = [_device newDefaultLibrary]; + if (library == nil) { + NSLog(@"CN1MetalPipelineCache: device has no default.metallib — is CN1MetalShaders.metal in the Xcode project?"); + return nil; + } + _states[pipeline] = [self buildPipeline:pipeline library:library]; + return _states[pipeline]; +} + +@end + +#endif /* CN1_USE_METAL */ diff --git a/Ports/iOSPort/nativeSources/CN1MetalShaders.metal b/Ports/iOSPort/nativeSources/CN1MetalShaders.metal new file mode 100644 index 0000000000..01929de921 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1MetalShaders.metal @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Metal Shading Language shaders for the Codename One iOS Metal backend. + * Compiled by Xcode into default.metallib at build time when -Dios.metal=true + * is set (which adds these files to the Xcode project). + * + * Pipelines (see CN1Metalcompat.h / CN1MetalPipelineCache.m): + * SolidColor — FillRect, DrawLine, FillPolygon (no texture, solid fill) + * TexturedRGBA — DrawImage, TileImage (sample RGBA texture, tint by color) + * AlphaMask — DrawString glyphs, DrawTextureAlphaMask (sample alpha from + * R8/alpha-only texture, colorize with color uniform) + * ClearPunch — ClearRect: write zeros with blend disabled (handled at + * pipeline-state level, shader just outputs zero) + * LinearGradient — (Phase 2) + * RadialGradient — (Phase 2) + * + * Buffer layout (matches CN1Metalcompat.m drawQuad): + * Vertex: + * buffer(0) : float2 positions[4] (quad corners in 2D) + * buffer(1) : CN1MetalMatrices (projection, modelView, transform) + * buffer(2) : float2 texcoords[4] (only textured/alpha-mask variants) + * Fragment: + * buffer(0) : float4 color (solid fill color or texture tint) + * texture(0): texture2d (only textured/alpha-mask variants) + */ +#include +using namespace metal; + +struct CN1Matrices { + float4x4 projection; + float4x4 modelView; + float4x4 transform; +}; + +// --------- Vertex output structs --------- + +struct VertexOutPlain { + float4 position [[position]]; +}; + +struct VertexOutTextured { + float4 position [[position]]; + float2 texcoord; +}; + +// --------- Helper: apply the three-matrix pipeline to a 2D position --------- + +static inline float4 applyMatrices(float2 p, constant CN1Matrices &m) { + float4 pos4 = float4(p.x, p.y, 0.0, 1.0); + return m.projection * m.modelView * m.transform * pos4; +} + +// --------- SolidColor pipeline --------- + +vertex VertexOutPlain cn1_vs_solid( + uint vid [[vertex_id]], + constant float2 *positions [[buffer(0)]], + constant CN1Matrices &matrices [[buffer(1)]]) +{ + VertexOutPlain out; + out.position = applyMatrices(positions[vid], matrices); + return out; +} + +fragment float4 cn1_fs_solid( + VertexOutPlain in [[stage_in]], + constant float4 &color [[buffer(0)]]) +{ + return color; +} + +// --------- TexturedRGBA pipeline --------- + +vertex VertexOutTextured cn1_vs_textured( + uint vid [[vertex_id]], + constant float2 *positions [[buffer(0)]], + constant CN1Matrices &matrices [[buffer(1)]], + constant float2 *texcoords [[buffer(2)]]) +{ + VertexOutTextured out; + out.position = applyMatrices(positions[vid], matrices); + out.texcoord = texcoords[vid]; + return out; +} + +fragment float4 cn1_fs_textured( + VertexOutTextured in [[stage_in]], + constant float4 &tint [[buffer(0)]], + texture2d tex [[texture(0)]]) +{ + constexpr sampler s(mag_filter::linear, min_filter::linear, address::clamp_to_edge); + // Sample, multiply by tint (which is a uniform alpha modulator for DrawImage). + // The GL path uses the same formula: gl_FragColor = texture2D(tex, coord) * uColor. + return tex.sample(s, in.texcoord) * tint; +} + +// --------- AlphaMask pipeline (Phase 2/4) --------- +// Samples alpha from an R8/alpha-only texture and colorizes with the uniform. +// Used for DrawString glyph atlas in Phase 4. + +fragment float4 cn1_fs_alpha_mask( + VertexOutTextured in [[stage_in]], + constant float4 &color [[buffer(0)]], + texture2d tex [[texture(0)]]) +{ + constexpr sampler s(mag_filter::linear, min_filter::linear, address::clamp_to_edge); + float a = tex.sample(s, in.texcoord).r; + return float4(color.rgb * a, color.a * a); +} + +// --------- ClearPunch pipeline --------- +// Writes a transparent pixel with blending disabled — punches holes in the +// existing framebuffer content. Matches the GL path's ClearRect semantics. + +fragment float4 cn1_fs_clear( + VertexOutPlain in [[stage_in]]) +{ + return float4(0.0, 0.0, 0.0, 0.0); +} + +// --------- AlphaMaskRadial pipeline (Phase 5+) --------- +// Same vertex stage as AlphaMask (cn1_vs_textured); the fragment computes a +// radial gradient analytically and multiplies by the alpha mask sampled from +// texture(0). Mirrors the GL DrawTextureAlphaMask radial-gradient shader at +// DrawTextureAlphaMask.m:181-360 — gradient colours interpolate from +// `startColor` at the centre to `endColor` at radius 1, computed in +// texcoord-space using elliptical normalisation (radiusX, radiusY can differ +// to support gradients with non-square bounds). +// +// Buffer layout for this pipeline: +// buffer(0): float4 startColor — colour at gradient centre +// buffer(1): float4 endColor — colour at gradient edge +// buffer(2): float4 params — (centerX, centerY, radiusX, radiusY) all in +// texcoord-space (0..1) + +fragment float4 cn1_fs_alpha_mask_radial( + VertexOutTextured in [[stage_in]], + constant float4 &startColor [[buffer(0)]], + constant float4 &endColor [[buffer(1)]], + constant float4 ¶ms [[buffer(2)]], + texture2d tex [[texture(0)]]) +{ + constexpr sampler s(mag_filter::linear, min_filter::linear, address::clamp_to_edge); + float a = tex.sample(s, in.texcoord).r; + float2 center = params.xy; + float2 radii = params.zw; + // Elliptical distance: scale (texcoord - centre) by radii so that t=1 + // sits on the ellipse boundary. clamp(0,1) keeps colours in range when + // the gradient bbox doesn't cover the whole alpha mask. + float2 d = (in.texcoord - center) / max(radii, float2(1e-6, 1e-6)); + float t = clamp(length(d), 0.0, 1.0); + float4 grad = mix(startColor, endColor, t); + // Premultiplied output: rgb already includes alpha, multiply by mask. + return float4(grad.rgb * a, grad.a * a); +} + +// --------- LinearGradient pipeline --------- +// Pure GPU horizontal/vertical gradient -- no CGContextDrawLinearGradient, +// no offscreen bitmap upload. The vertex stage feeds the quad's per-corner +// 0..1 texcoord into the fragment, and the fragment lerps between +// startColor and endColor along whichever axis params.x picks. +// +// Buffer layout: +// buffer(0): float4 startColor (premultiplied, alpha in .a) +// buffer(1): float4 endColor (premultiplied) +// buffer(2): float4 axis (axis.x = 1.0 horizontal, 0.0 vertical; +// remaining components reserved) + +fragment float4 cn1_fs_linear_gradient( + VertexOutTextured in [[stage_in]], + constant float4 &startColor [[buffer(0)]], + constant float4 &endColor [[buffer(1)]], + constant float4 &axis [[buffer(2)]]) +{ + float t = clamp(mix(in.texcoord.y, in.texcoord.x, axis.x), 0.0, 1.0); + float4 grad = mix(startColor, endColor, t); + return grad; +} + +// --------- RadialGradient pipeline --------- +// Pure GPU radial gradient -- no CGContextDrawRadialGradient, no offscreen +// bitmap. Texcoord 0..1 across the quad; params carries the centre and +// radius (also in 0..1 texcoord space) so the same shader handles whatever +// rectangular bounds the caller specifies. +// +// Buffer layout: +// buffer(0): float4 startColor (premultiplied, alpha in .a) +// buffer(1): float4 endColor (premultiplied) +// buffer(2): float4 params (.xy = centre in 0..1 texcoord-space, +// .zw = radii (rx, ry) in 0..1 texcoord-space) + +fragment float4 cn1_fs_radial_gradient( + VertexOutTextured in [[stage_in]], + constant float4 &startColor [[buffer(0)]], + constant float4 &endColor [[buffer(1)]], + constant float4 ¶ms [[buffer(2)]]) +{ + float2 center = params.xy; + float2 radii = max(params.zw, float2(1e-6, 1e-6)); + float t = clamp(length((in.texcoord - center) / radii), 0.0, 1.0); + return mix(startColor, endColor, t); +} diff --git a/Ports/iOSPort/nativeSources/CN1Metalcompat.h b/Ports/iOSPort/nativeSources/CN1Metalcompat.h new file mode 100644 index 0000000000..ca15145154 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1Metalcompat.h @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +#ifndef CN1Metalcompat_h +#define CN1Metalcompat_h + +#import "CN1ES2compat.h" +#ifdef CN1_USE_METAL + +@import Metal; +@import simd; +#import +#import + +// Metal rendering backend for Codename One iOS. +// +// The existing OpenGL ES 2 ExecutableOp implementations call raw GL functions +// (glUseProgram, glUniformMatrix4fv, glDrawArrays). They cannot transparently +// run on Metal, so each op gets an #ifdef CN1_USE_METAL branch in its execute +// method that calls the higher-level C API declared here. +// +// State lives in CN1Metalcompat.m: +// - active render command encoder (set by METALView.setFramebuffer, +// cleared by presentFramebuffer) +// - projection + modelView + transform matrices +// - current clip rect / scissor +// - pipeline state cache (lazy built, keyed by pipeline variant + blend) +// +// Matrices use the same GLKMatrix4 type as the GL path so SetTransform.m can +// pass its matrix through unchanged; CN1Metalcompat converts to simd_float4x4 +// at upload time. + +// -------- Pipeline variants -------- +typedef NS_ENUM(NSInteger, CN1MetalPipeline) { + CN1MetalPipelineSolidColor = 0, // FillRect, DrawLine, FillPolygon + CN1MetalPipelineTexturedRGBA, // DrawImage, TileImage + CN1MetalPipelineAlphaMask, // DrawString glyph, DrawTextureAlphaMask + CN1MetalPipelineClearPunch, // ClearRect: write zeros, no blend + CN1MetalPipelineLinearGradient, // FillLinearGradient (pure GPU, no CG bitmap) + CN1MetalPipelineRadialGradient, // FillRadialGradient (pure GPU, no CG bitmap) + CN1MetalPipelineAlphaMaskRadial, // DrawTextureAlphaMask + RadialGradientPaint + CN1MetalPipelineCount +}; + +// -------- Uniform struct matching CN1MetalShaders.metal -------- +// Vertex stage receives this struct at buffer index 1. +typedef struct { + simd_float4x4 projection; + simd_float4x4 modelView; + simd_float4x4 transform; +} CN1MetalMatrices; + +// -------- Encoder lifecycle (called by CodenameOne_GLViewController / METALView) -------- + +// Called by METALView.setFramebuffer after acquiring a command encoder for +// the screen drawable. Captures the encoder so ops can issue draw calls +// against it. Also captures the projection matrix for the current drawable +// size. A nil encoder is valid and means "frame dropped" — ops must no-op. +void CN1MetalBeginFrame(id encoder, + simd_float4x4 projection, + int framebufferWidth, + int framebufferHeight); + +// Called by METALView.presentFramebuffer just before commit. Clears the +// active encoder reference so subsequent ops no-op until the next frame. +void CN1MetalEndFrame(void); + +// Returns the active encoder or nil if no frame is in flight. Ops use this +// to skip drawing when setFramebuffer couldn't acquire a drawable. +id CN1MetalActiveEncoder(void); + +// Access to the framebuffer dimensions for the current encoder. +int CN1MetalFramebufferWidth(void); +int CN1MetalFramebufferHeight(void); + +// -------- Matrix state (mirrors CN1modelViewMatrix / CN1projectionMatrix / CN1transformMatrix in the GL path) -------- + +// Called by SetTransform.execute. Replaces the current transform matrix. +void CN1MetalSetTransform(GLKMatrix4 transform); + +// Returns the current transform matrix (for SetTransform.currentTransform). +GLKMatrix4 CN1MetalGetTransform(void); + +// Matrix stack operations used by the drawFrame flip workaround, Rotate, +// Scale, ResetAffine. (Phase 2.) +void CN1MetalPushMatrix(void); +void CN1MetalPopMatrix(void); +void CN1MetalScale(float x, float y, float z); +void CN1MetalTranslate(float x, float y, float z); +void CN1MetalRotate(float angle, float x, float y, float z); +void CN1MetalLoadIdentity(void); + +// -------- Clip state (mirrors ClipRect.m) -------- + +// Set a scissor rect in framebuffer pixel coordinates (Y-down, matching +// our projection). Passing width<=0 or height<=0 disables clipping. +void CN1MetalSetScissor(int x, int y, int width, int height); + +// -------- Draw primitives (invoked from ExecutableOp subclasses' execute methods) -------- + +// Fill a rectangle with a solid color + alpha (0-255 each). x/y/w/h in +// iOS Y-down framebuffer coordinates (matches the GL path's semantic after +// its drawFrame flip). +void CN1MetalFillRect(int color, int alpha, int x, int y, int width, int height); + +// Same geometry but punches zero into color+alpha with no blending — +// equivalent to the ClearRect fragment shader writing vec4(0,0,0,0). +void CN1MetalClearRect(int x, int y, int width, int height); + +// Draw a 1-pixel line from (x1,y1) to (x2,y2) with the given color+alpha. +// Note: Metal does not support line width > 1; on retina displays lines +// appear thin. Matches the GL path's glLineWidth=1 default. +void CN1MetalDrawLine(int color, int alpha, int x1, int y1, int x2, int y2); + +// Draw a rectangular outline (not filled) at (x,y,w,h). Rendered as a +// closed 4-segment line strip. +void CN1MetalDrawRect(int color, int alpha, int x, int y, int width, int height); + +// Fill a convex polygon given N (x,y) vertex pairs. The polygon is +// triangulated on the CPU as a fan from the first vertex, so it must +// be convex for correct results (matches the GL path's assumption). +void CN1MetalFillPolygon(const float *xCoords, const float *yCoords, int num, + int color, int alpha); + +// Draw an RGBA image to (x,y,w,h) with a uniform alpha modulator (0-255). +// Texture is owned by the caller (typically a GLUIImage); it is retained +// only for the current command buffer. +void CN1MetalDrawImage(id texture, int alpha, int x, int y, int width, int height); + +// Tile an RGBA image across (x,y,w,h). imageWidth/imageHeight are the +// natural size of the source UIImage. Issues one textured quad per +// tile (full or clipped at the right/bottom edges); a future batched +// version could pack into a single draw call. +void CN1MetalTileImage(id texture, int alpha, + int x, int y, int width, int height, + int imageWidth, int imageHeight); + +// Draw a string at (x,y). The string is rasterised via CoreGraphics into +// an RGBA MTLTexture with the colour baked in (matching the GL path's +// approach) and then rendered as a textured quad with alpha modulation. +// A small LRU cache keyed on (str, font, color) avoids re-rasterising per +// frame. Phase 4 will replace this with a CoreText glyph atlas. +void CN1MetalDrawString(NSString *str, UIFont *font, int color, int alpha, int x, int y); + +// Build an MTLTexture from a single-channel (R8/alpha-only) bitmap. The +// alpha bytes are produced by Renderer.c (Renderer_produceAlphas) when +// rasterising a path; the resulting texture is sampled by the AlphaMask +// pipeline. Caller bridge-retains the returned id through +// JAVA_LONG so the same handle can flow through the existing TextureAlphaMask +// path on the Java side. Returns nil on failure. +id CN1MetalCreateAlphaMaskTexture(const uint8_t *bytes, int width, int height); + +// Render an alpha-mask texture (R8 or A8) at (x,y,w,h) tinted by the given +// premultiplied color+alpha. Wrapper for the AlphaMask pipeline; equivalent +// to the GL DrawTextureAlphaMask basic shader path. Used by fillArc / +// fillShape / drawShape after Renderer.c rasterises the path. +void CN1MetalDrawAlphaMask(id texture, int color, int alpha, + int x, int y, int width, int height); + +// Same as CN1MetalDrawAlphaMask but uses the AlphaMaskRadial pipeline to +// fill the masked region with a radial gradient. (gx, gy, gw, gh) describes +// the gradient bbox in screen-space; the shader maps this into texcoord +// space relative to the alpha-mask quad. Mirrors the GL radial-gradient +// shader path in DrawTextureAlphaMask.m:340-395. +void CN1MetalDrawAlphaMaskRadial(id texture, + int x, int y, int width, int height, + int startColor, int endColor, + float gx, float gy, float gw, float gh); + +// Draw a linear or radial gradient filling (x,y,w,h). type is one of +// GRADIENT_TYPE_HORIZONTAL / GRADIENT_TYPE_VERTICAL / GRADIENT_TYPE_RADIAL +// (defined in DrawGradient.h). startColor/endColor are 0xAARRGGBB. The +// gradient is rasterised via CGContextDrawLinearGradient or +// CGContextDrawRadialGradient (matching the GL path's DrawGradient.m and +// RadialGradientPaint.m exactly), uploaded as an MTLTexture, cached on +// (type,start,end,w,h,relX,relY,relSize), and rendered as a textured +// quad through the existing TexturedRGBA pipeline. relativeX/Y/Size are +// only used for radial gradients. +void CN1MetalDrawGradient(int type, int startColor, int endColor, + int x, int y, int width, int height, + float relativeX, float relativeY, float relativeSize); + +// -------- Texture helpers for GLUIImage -------- + +// Lazily build an MTLTexture from a UIImage. Cached on the GLUIImage. +// Returns nil on failure. Caller does not own the texture. +id CN1MetalTextureFromUIImage(UIImage *image); + +// Global Metal device (from METALView's command queue); shared by anyone +// who needs to allocate Metal resources. +id CN1MetalDevice(void); + +// Global Metal command queue (from METALView). Mutable-image command +// buffers allocate from this queue so they share scheduling with screen +// drawing. +id CN1MetalCommandQueue(void); + +// -------- Phase 3 v2: mutable-image rendering -------- +// +// Mutable images render via the same ExecutableOp queue as the screen. +// nativeXxxMutableImpl JNI funcs build the same op as their Global +// counterpart, tag it with target = current GLUIImage, and append to +// the upcoming queue. drawFrame drains the queue. When it crosses a +// target boundary it ends the previous encoder, opens a new one against +// the new target's texture (or restores the screen encoder for nil +// target), and continues. Mutable command buffers are committed (no +// wait) at end-of-target and stored on the GLUIImage; readback paths +// call CN1MetalFlushMutableImageSync to waitUntilCompleted before +// sampling pixels. +// +// The forward-declared GLUIImage is opaque here; the implementation +// imports GLUIImage.h directly. + +@class GLUIImage; + +// Allocate (or reuse) a mutable render-target texture sized (w x h) on +// the given GLUIImage. Clears to transparent black on first allocation +// to mirror the CG path's UIGraphicsBeginImageContextWithOptions(opaque=NO) +// initial state. Idempotent if texture already exists with same dims. +// Must be called on the main thread (allocates Metal resources). +void CN1MetalEnsureMutableTexture(GLUIImage *image, int width, int height); + +// Open a render encoder against the mutable image's texture. Allocates +// a fresh command buffer, sets viewport, publishes the encoder + a +// Y-down ortho projection sized to the texture to the active-encoder +// slot so subsequent ExecutableOp.execute calls draw into this texture. +// MUST be paired with CN1MetalEndMutableImageDraw on the same thread. +// Returns YES if the encoder was opened; NO if device/queue/encoder +// allocation failed (caller should skip ops with this target). +BOOL CN1MetalBeginMutableImageDraw(GLUIImage *image); + +// End the active mutable-image encoder, commit its command buffer, and +// store the command buffer on the GLUIImage so readback paths can wait +// on it. Restores any saved screen encoder + projection so the drain +// loop can continue with screen-targeted ops. No CPU wait here -- the +// commit is deferred-aware; read paths call FlushMutableImageSync. +void CN1MetalEndMutableImageDraw(GLUIImage *image); + +// Wait until the GPU has finished writing to this mutable image's +// texture. Called before any pixel-reading path (Image.getRGB, PNG/JPEG +// encode, toImage, cross-image consumption). No-op if no command buffer +// is pending. Safe to call on any thread. +void CN1MetalFlushMutableImageSync(GLUIImage *image); + +// Read a region of the mutable image's MTLTexture into a CPU int array +// in 0xAARRGGBB format (matches Image.getRGB / NativeImage layout). +// Forces a flush + waitUntilCompleted on the image's command buffer +// first so the GPU work is finalised. Returns YES on success; NO if +// no mutable texture exists or device/blit allocation failed. +BOOL CN1MetalReadMutableImagePixels(GLUIImage *image, int *outARGB, + int x, int y, int w, int h, + int imgWidth, int imgHeight); + +// Read the mutable image's MTLTexture pixels and wrap them as a UIImage. +// Forces a flush + waitUntilCompleted on the image's command buffer first +// so the GPU work is finalised. Returns nil if there's no mutable texture +// or read fails. Used by gausianBlurImage / toBase64* / etc. that consume +// the mutable's pixels through CG / CIImage on Metal builds. +UIImage * _Nullable CN1MetalReadMutableImageAsUIImage(GLUIImage *image); + +#endif /* CN1_USE_METAL */ +#endif /* CN1Metalcompat_h */ diff --git a/Ports/iOSPort/nativeSources/CN1Metalcompat.m b/Ports/iOSPort/nativeSources/CN1Metalcompat.m new file mode 100644 index 0000000000..09565bf424 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1Metalcompat.m @@ -0,0 +1,1197 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +#import "CN1ES2compat.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#import "CN1MetalPipelineCache.h" +#import "CN1MetalGlyphAtlas.h" +#import "METALView.h" +#import "CodenameOne_GLViewController.h" +#import "GLUIImage.h" +#import + +// --------------- Static state --------------- + +static __unsafe_unretained id activeEncoder = nil; +static simd_float4x4 currentProjection; +static simd_float4x4 currentModelView; +static simd_float4x4 currentTransform; +static int currentFramebufferWidth = 0; +static int currentFramebufferHeight = 0; +static CN1MetalPipelineCache *pipelineCache = nil; + +#define CN1_MATRIX_STACK_DEPTH 32 +static simd_float4x4 modelViewStack[CN1_MATRIX_STACK_DEPTH]; +static int modelViewStackTop = 0; + +static simd_float4x4 identityMatrix(void) { + return (simd_float4x4){{ + { 1, 0, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 1, 0 }, + { 0, 0, 0, 1 } + }}; +} + +static simd_float4x4 glkToSimd(GLKMatrix4 m) { + simd_float4x4 r; + memcpy(&r, m.m, sizeof(float) * 16); + return r; +} + +static GLKMatrix4 simdToGlk(simd_float4x4 m) { + GLKMatrix4 r; + memcpy(r.m, &m, sizeof(float) * 16); + return r; +} + +static void ensurePipelineCache(void) { + if (pipelineCache == nil) { + pipelineCache = [[CN1MetalPipelineCache alloc] initWithDevice:CN1MetalDevice()]; + } +} + +// --------------- Encoder lifecycle --------------- + +void CN1MetalBeginFrame(id encoder, + simd_float4x4 projection, + int framebufferWidth, + int framebufferHeight) { + activeEncoder = encoder; + currentProjection = projection; + currentFramebufferWidth = framebufferWidth; + currentFramebufferHeight = framebufferHeight; + // modelView is always identity for 2D UI rendering. The GL path uses it + // only as a y-flip in drawFrame; our ortho projection bakes the flip in. + currentModelView = identityMatrix(); + if (modelViewStackTop == 0) { + currentTransform = identityMatrix(); + } + ensurePipelineCache(); +} + +void CN1MetalEndFrame(void) { + activeEncoder = nil; +} + +id CN1MetalActiveEncoder(void) { + return activeEncoder; +} + +int CN1MetalFramebufferWidth(void) { return currentFramebufferWidth; } +int CN1MetalFramebufferHeight(void) { return currentFramebufferHeight; } + +id CN1MetalDevice(void) { + METALView *mv = (METALView *)[[CodenameOne_GLViewController instance] eaglView]; + return ((CAMetalLayer *)mv.layer).device; +} + +id CN1MetalCommandQueue(void) { + METALView *mv = (METALView *)[[CodenameOne_GLViewController instance] eaglView]; + return mv.commandQueue; +} + +// --------------- Matrix state --------------- + +void CN1MetalSetTransform(GLKMatrix4 transform) { + currentTransform = glkToSimd(transform); +} + +GLKMatrix4 CN1MetalGetTransform(void) { + return simdToGlk(currentTransform); +} + +void CN1MetalLoadIdentity(void) { + currentModelView = identityMatrix(); +} + +void CN1MetalPushMatrix(void) { + if (modelViewStackTop < CN1_MATRIX_STACK_DEPTH) { + modelViewStack[modelViewStackTop++] = currentModelView; + } +} + +void CN1MetalPopMatrix(void) { + if (modelViewStackTop > 0) { + currentModelView = modelViewStack[--modelViewStackTop]; + } +} + +void CN1MetalScale(float x, float y, float z) { + simd_float4x4 s = (simd_float4x4){{ + { x, 0, 0, 0 }, + { 0, y, 0, 0 }, + { 0, 0, z, 0 }, + { 0, 0, 0, 1 } + }}; + currentModelView = simd_mul(currentModelView, s); +} + +void CN1MetalTranslate(float x, float y, float z) { + simd_float4x4 t = identityMatrix(); + t.columns[3] = (simd_float4){ x, y, z, 1 }; + currentModelView = simd_mul(currentModelView, t); +} + +void CN1MetalRotate(float angle, float x, float y, float z) { + float rad = angle * (float)M_PI / 180.0f; + float c = cosf(rad); + float s = sinf(rad); + float len = sqrtf(x*x + y*y + z*z); + if (len > 0) { x /= len; y /= len; z /= len; } + float ic = 1.0f - c; + simd_float4x4 r = (simd_float4x4){{ + { x*x*ic + c, y*x*ic + z*s, z*x*ic - y*s, 0 }, + { x*y*ic - z*s, y*y*ic + c, z*y*ic + x*s, 0 }, + { x*z*ic + y*s, y*z*ic - x*s, z*z*ic + c, 0 }, + { 0, 0, 0, 1 } + }}; + currentModelView = simd_mul(currentModelView, r); +} + +// --------------- Clip state --------------- + +void CN1MetalSetScissor(int x, int y, int width, int height) { + if (activeEncoder == nil) return; + if (width <= 0 || height <= 0) { + // Disable clipping: set scissor to full framebuffer. + [activeEncoder setScissorRect:(MTLScissorRect){ + 0, 0, + (NSUInteger)currentFramebufferWidth, + (NSUInteger)currentFramebufferHeight + }]; + return; + } + // Clamp to framebuffer; Metal requires scissor to be within the + // attachment bounds or it fails the render pass. + int fx = MAX(0, x); + int fy = MAX(0, y); + int fw = MIN(width, currentFramebufferWidth - fx); + int fh = MIN(height, currentFramebufferHeight - fy); + if (fw <= 0 || fh <= 0) { + // Clip is entirely outside — set a zero-size scissor to cull everything. + [activeEncoder setScissorRect:(MTLScissorRect){0, 0, 1, 1}]; + return; + } + [activeEncoder setScissorRect:(MTLScissorRect){ + (NSUInteger)fx, (NSUInteger)fy, + (NSUInteger)fw, (NSUInteger)fh + }]; +} + +// --------------- Drawing helpers --------------- + +static CN1MetalMatrices currentMatrices(void) { + CN1MetalMatrices m; + m.projection = currentProjection; + m.modelView = currentModelView; + m.transform = currentTransform; + return m; +} + +static void drawQuad(CN1MetalPipeline pipeline, + const float vertices[8], + const float *texcoords, // may be NULL + simd_float4 color, + id texture) { + if (activeEncoder == nil || pipelineCache == nil) return; + id state = [pipelineCache pipelineFor:pipeline]; + if (state == nil) return; + [activeEncoder setRenderPipelineState:state]; + + // buffer(0): positions (8 floats = 4 x (x,y)) + [activeEncoder setVertexBytes:vertices length:sizeof(float) * 8 atIndex:0]; + // buffer(1): matrices + CN1MetalMatrices matrices = currentMatrices(); + [activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:1]; + // buffer(2): optional texcoords (only textured/alpha-mask pipelines read this) + if (texcoords != NULL) { + [activeEncoder setVertexBytes:texcoords length:sizeof(float) * 8 atIndex:2]; + } + // Fragment buffer(0): color uniform + [activeEncoder setFragmentBytes:&color length:sizeof(color) atIndex:0]; + // Fragment texture(0): optional + if (texture != nil) { + [activeEncoder setFragmentTexture:texture atIndex:0]; + } + [activeEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; +} + +// Draws an arbitrary solid-color primitive (line / line strip / triangle list) +// with the pre-encoded vertex array. Used for DrawLine, DrawRect, FillPolygon. +static void drawSolidPrimitive(MTLPrimitiveType primitive, + const float *vertices, + int vertexCount, + simd_float4 color) { + if (activeEncoder == nil || pipelineCache == nil || vertexCount <= 0) return; + id state = [pipelineCache pipelineFor:CN1MetalPipelineSolidColor]; + if (state == nil) return; + // setVertexBytes has a 4KB limit; at 8 bytes per vertex (float2) that's + // 512 vertices. Convex polygons from CN1 are well within that. + size_t byteCount = sizeof(float) * 2 * (size_t)vertexCount; + if (byteCount > 4096) return; + + [activeEncoder setRenderPipelineState:state]; + [activeEncoder setVertexBytes:vertices length:byteCount atIndex:0]; + CN1MetalMatrices matrices = currentMatrices(); + [activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:1]; + [activeEncoder setFragmentBytes:&color length:sizeof(color) atIndex:0]; + [activeEncoder drawPrimitives:primitive vertexStart:0 vertexCount:(NSUInteger)vertexCount]; +} + +static simd_float4 premultipliedColor(int color, int alpha) { + float a = alpha / 255.0f; + return (simd_float4){ + ((color >> 16) & 0xff) / 255.0f * a, + ((color >> 8) & 0xff) / 255.0f * a, + ((color) & 0xff) / 255.0f * a, + a + }; +} + +// --------------- Public draw primitives --------------- + +void CN1MetalFillRect(int color, int alpha, int x, int y, int width, int height) { + simd_float4 colorV = premultipliedColor(color, alpha); + float vertices[8] = { + (float)x, (float)y, + (float)(x+width), (float)y, + (float)x, (float)(y+height), + (float)(x+width), (float)(y+height) + }; + drawQuad(CN1MetalPipelineSolidColor, vertices, NULL, colorV, nil); +} + +void CN1MetalDrawLine(int color, int alpha, int x1, int y1, int x2, int y2) { + simd_float4 colorV = premultipliedColor(color, alpha); + float vertices[4] = { (float)x1, (float)y1, (float)x2, (float)y2 }; + drawSolidPrimitive(MTLPrimitiveTypeLine, vertices, 2, colorV); +} + +void CN1MetalDrawRect(int color, int alpha, int x, int y, int width, int height) { + simd_float4 colorV = premultipliedColor(color, alpha); + // Closed rectangle outline as a 5-vertex line strip. + float vertices[10] = { + (float)x, (float)y, + (float)(x+width), (float)y, + (float)(x+width), (float)(y+height), + (float)x, (float)(y+height), + (float)x, (float)y + }; + drawSolidPrimitive(MTLPrimitiveTypeLineStrip, vertices, 5, colorV); +} + +void CN1MetalFillPolygon(const float *xCoords, const float *yCoords, int num, + int color, int alpha) { + if (num < 3) return; + simd_float4 colorV = premultipliedColor(color, alpha); + // Triangulate as a fan from vertex 0: (0,1,2), (0,2,3), (0,3,4), ... + // Works for convex polygons only, matching the GL path's assumption. + // + // setVertexBytes has a 4KB hard limit (= 512 float2 vertices = ~170 + // triangles per draw call). Polygons with more triangles must be + // submitted in chunks. The previous implementation silently truncated + // at 170 triangles, leaving half a 360-point circle unfilled in + // graphics-fill-polygon. Fix: emit batches of up to BATCH_TRIS + // triangles, each starting from vertex 0 (so the fan still meets + // contiguously). Adjacent chunks share the seam vertex (i, i+1) so + // the visual surface stays gap-free. + enum { BATCH_TRIS = 168, BATCH_FLOATS = BATCH_TRIS * 6 }; + float stackBuf[BATCH_FLOATS]; + int triRemaining = num - 2; + int firstTri = 0; + while (triRemaining > 0) { + int batch = (triRemaining > BATCH_TRIS) ? BATCH_TRIS : triRemaining; + int out = 0; + for (int t = 0; t < batch; t++) { + int i = 1 + firstTri + t; // 1, 2, 3, ... + stackBuf[out++] = xCoords[0]; stackBuf[out++] = yCoords[0]; + stackBuf[out++] = xCoords[i]; stackBuf[out++] = yCoords[i]; + stackBuf[out++] = xCoords[i + 1]; stackBuf[out++] = yCoords[i + 1]; + } + drawSolidPrimitive(MTLPrimitiveTypeTriangle, stackBuf, out / 2, colorV); + firstTri += batch; + triRemaining -= batch; + } +} + +void CN1MetalClearRect(int x, int y, int width, int height) { + simd_float4 zero = (simd_float4){0, 0, 0, 0}; + float vertices[8] = { + (float)x, (float)y, + (float)(x+width), (float)y, + (float)x, (float)(y+height), + (float)(x+width), (float)(y+height) + }; + drawQuad(CN1MetalPipelineClearPunch, vertices, NULL, zero, nil); +} + +void CN1MetalDrawImage(id texture, int alpha, int x, int y, int width, int height) { + if (texture == nil) return; + float a = alpha / 255.0f; + // Texture tint uses straight alpha modulator (no premultiplication here; + // the fragment shader handles it). + simd_float4 tint = (simd_float4){ a, a, a, a }; + float vertices[8] = { + (float)x, (float)y, + (float)(x+width), (float)y, + (float)x, (float)(y+height), + (float)(x+width), (float)(y+height) + }; + // V=0-at-top sampling: memory_row_0 lands at the top vertex. For + // UIImage-backed sources, CN1MetalTextureFromUIImage stores them in the + // GL-compatible layout (memory_row_0 = source's visual BOTTOM), so this + // mapping renders the source upside-down vs. its natural orientation — + // matching what GL does for assets designed against its V=1-at-top + // convention. For mutable-image targets, Phase 3 renders into the texture + // with user-y=0 at memory_row_0, so V=0-at-top correctly puts the + // mutable's own top at dest top. + static const float texcoords[8] = { + 0, 0, + 1, 0, + 0, 1, + 1, 1 + }; + drawQuad(CN1MetalPipelineTexturedRGBA, vertices, texcoords, tint, texture); +} + +void CN1MetalTileImage(id texture, int alpha, + int x, int y, int width, int height, + int imageWidth, int imageHeight) { + if (texture == nil || width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return; + float a = alpha / 255.0f; + simd_float4 tint = (simd_float4){ a, a, a, a }; + + for (int yPos = 0; yPos < height; yPos += imageHeight) { + int dh = imageHeight; + if (yPos + dh > height) dh = height - yPos; + float vMax = (float)dh / (float)imageHeight; + for (int xPos = 0; xPos < width; xPos += imageWidth) { + int dw = imageWidth; + if (xPos + dw > width) dw = width - xPos; + float uMax = (float)dw / (float)imageWidth; + int dx = x + xPos; + int dy = y + yPos; + float vertices[8] = { + (float)dx, (float)dy, + (float)(dx + dw), (float)dy, + (float)dx, (float)(dy + dh), + (float)(dx + dw), (float)(dy + dh) + }; + float texcoords[8] = { + 0.0f, 0.0f, + uMax, 0.0f, + 0.0f, vMax, + uMax, vMax + }; + drawQuad(CN1MetalPipelineTexturedRGBA, vertices, texcoords, tint, texture); + } + } +} + +// --------------- Text rendering (CoreText glyph atlas) --------------- +// +// Per the Metal-port plan's Phase 4: shape the string via CoreText and +// emit one alpha-mask quad per glyph against the per-(font, pointSize) +// R8 atlas owned by CN1MetalGlyphAtlas. There is no whole-string CG- +// rasterise fallback -- the plan was explicit ("Delete DrawStringTextureCache +// usage on the Metal path -- no more whole-string LRU"). If the atlas +// or CTLine cannot be built we log and skip the string; that exposes +// the failure rather than papering over it with a different pipeline +// that would silently mask the bug. + +void CN1MetalDrawString(NSString *str, UIFont *font, int color, int alpha, int x, int y) { + if (str == nil || font == nil || str.length == 0) return; + + CN1MetalGlyphAtlas *atlas = [CN1MetalGlyphAtlas atlasForFont:font]; + if (atlas == nil) { + NSLog(@"CN1MetalDrawString: no atlas available for font %@ pt=%g; string skipped", + font.fontName, (double)font.pointSize); + return; + } + + // Pass the UIFont directly as the kCTFontAttributeName value. CoreText + // accepts UIFont here and uses it to drive glyph mapping and positions + // -- keeping the CTLine completely consistent with how UIKit's + // drawAtPoint:withAttributes: shapes the same string. Bridging through + // atlas.ctFont (built via CTFontCreateWithFontDescriptor) was producing + // slightly different metrics for the first DrawString call after a + // fresh form, which surfaced as the TL panel of graphics-draw-string- + // decorated rendering larger/wider glyphs than TR/BL/BR despite + // identical Java state. + NSDictionary *attrs = @{ (__bridge NSString *)kCTFontAttributeName: font }; + CFAttributedStringRef attrStr = CFAttributedStringCreate(NULL, + (__bridge CFStringRef)str, + (__bridge CFDictionaryRef)attrs); + if (attrStr == NULL) { + NSLog(@"CN1MetalDrawString: CFAttributedStringCreate failed for \"%@\"; string skipped", str); + return; + } + CTLineRef line = CTLineCreateWithAttributedString(attrStr); + CFRelease(attrStr); + if (line == NULL) { + NSLog(@"CN1MetalDrawString: CTLineCreateWithAttributedString failed for \"%@\"; string skipped", str); + return; + } + + // cn1's drawString convention: (x, y) is the TOP-LEFT of the line bbox + // in Y-down screen coords (matches the GL path's whole-string-bitmap + // approach where drawAtPoint:withAttributes: puts line TOP at the given + // point). UIKit's drawAtPoint then places the baseline at point.y + + // font.ascender; we mirror that exactly so per-glyph positioning lines + // up with the Phase-2 fallback and with GL output. Using UIFont.ascender + // (not CTFontGetAscent) is intentional — UIKit's metric is what + // drawAtPoint references and the values can disagree slightly across + // fonts. + float baselineY = (float)y + (float)font.ascender; + + simd_float4 colorV = premultipliedColor(color, alpha); + int textureW = atlas.textureWidth; + int textureH = atlas.textureHeight; + id atlasTex = atlas.texture; + + CFArrayRef runs = CTLineGetGlyphRuns(line); + CFIndex runCount = CFArrayGetCount(runs); + for (CFIndex r = 0; r < runCount; r++) { + CTRunRef run = CFArrayGetValueAtIndex(runs, r); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if (glyphCount == 0) continue; + + const CGGlyph *glyphPtr = CTRunGetGlyphsPtr(run); + CGGlyph *glyphBuf = NULL; + if (glyphPtr == NULL) { + glyphBuf = (CGGlyph *)malloc(sizeof(CGGlyph) * (size_t)glyphCount); + CTRunGetGlyphs(run, CFRangeMake(0, glyphCount), glyphBuf); + glyphPtr = glyphBuf; + } + const CGPoint *posPtr = CTRunGetPositionsPtr(run); + CGPoint *posBuf = NULL; + if (posPtr == NULL) { + posBuf = (CGPoint *)malloc(sizeof(CGPoint) * (size_t)glyphCount); + CTRunGetPositions(run, CFRangeMake(0, glyphCount), posBuf); + posPtr = posBuf; + } + + for (CFIndex i = 0; i < glyphCount; i++) { + CGGlyph g = glyphPtr[i]; + CN1MetalGlyphSlot *slot = [atlas slotForGlyph:g]; + if (slot == nil) continue; // atlas full + if (slot.width == 0) continue; // empty glyph (space, control) + + // Slot bitmap covers (bbox + 2px padding). Place the slot's + // top-left so the glyph art lines up with where CT expects: + // bbox-left-on-screen = x + posX + bearingX + // bbox-top-on-screen = baselineY - posY - (bearingY + bbox.height) + // Slot extends 1px above and to the left of the bbox. + float gx = (float)x + (float)posPtr[i].x + slot.bearingX - 1.0f; + float gy = baselineY - (float)posPtr[i].y + - (slot.bearingY + slot.bboxHeight) - 1.0f; + float gw = (float)slot.width; + float gh = (float)slot.height; + + float vertices[8] = { + gx, gy, + gx + gw, gy, + gx, gy + gh, + gx + gw, gy + gh + }; + + // CN1MetalGlyphAtlas rasterises with default Y-up CG; the + // glyph ends up right-side-up in raster memory order with + // memory_row_0 at the slot's TOP edge. V=0-at-top sampling + // maps the slot rect to the dest quad without a flip. + float u0 = (float)slot.atlasX / (float)textureW; + float u1 = (float)(slot.atlasX + slot.width) / (float)textureW; + float v0 = (float)slot.atlasY / (float)textureH; + float v1 = (float)(slot.atlasY + slot.height) / (float)textureH; + float texcoords[8] = { + u0, v0, + u1, v0, + u0, v1, + u1, v1, + }; + + drawQuad(CN1MetalPipelineAlphaMask, vertices, texcoords, colorV, atlasTex); + } + + if (glyphBuf) free(glyphBuf); + if (posBuf) free(posBuf); + } + + CFRelease(line); +} + +// --------------- Gradient rendering --------------- +// Per the Metal-port plan's Phase 1 pipeline list (linear-gradient, +// radial-gradient): pure-GPU MSL shaders, no CGBitmap rasterise, no +// LRU cache. The shaders interpolate startColor->endColor across the +// quad in 0..1 texcoord space; see cn1_fs_linear_gradient and +// cn1_fs_radial_gradient in CN1MetalShaders.metal. + +// Convert a 0xAARRGGBB int (alpha=0 implies opaque, matching Java's +// historical createImage/setColor convention) to a premultiplied +// simd_float4. Used by the gradient shaders, which expect premultiplied +// inputs because the pipeline blend factors are (One, OneMinusSrcAlpha). +static simd_float4 premultipliedFromARGB(int argb) { + float a = ((argb >> 24) & 0xff) / 255.0f; + if (((argb >> 24) & 0xff) == 0) { + a = 1.0f; // alpha=0 in legacy paint state means "opaque" (see GL gradient impl) + } + float r = ((argb >> 16) & 0xff) / 255.0f; + float g = ((argb >> 8) & 0xff) / 255.0f; + float b = ( argb & 0xff) / 255.0f; + return (simd_float4){ r * a, g * a, b * a, a }; +} + +// Quad-with-three-uniforms helper. The gradient and alpha-mask-radial +// pipelines all need (startColor, endColor, params) as fragment buffers +// 0/1/2 plus a 0..1 texcoord at vertex buffer 2. drawQuad above only +// supports a single fragment uniform so we have a separate helper +// instead of overloading it. +static void drawGradientQuad(CN1MetalPipeline pipeline, + const float vertices[8], + const float texcoords[8], + simd_float4 startColor, + simd_float4 endColor, + simd_float4 params) { + if (activeEncoder == nil || pipelineCache == nil) return; + id state = [pipelineCache pipelineFor:pipeline]; + if (state == nil) return; + [activeEncoder setRenderPipelineState:state]; + [activeEncoder setVertexBytes:vertices length:sizeof(float) * 8 atIndex:0]; + CN1MetalMatrices matrices = currentMatrices(); + [activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:1]; + [activeEncoder setVertexBytes:texcoords length:sizeof(float) * 8 atIndex:2]; + [activeEncoder setFragmentBytes:&startColor length:sizeof(startColor) atIndex:0]; + [activeEncoder setFragmentBytes:&endColor length:sizeof(endColor) atIndex:1]; + [activeEncoder setFragmentBytes:¶ms length:sizeof(params) atIndex:2]; + [activeEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; +} + +void CN1MetalDrawGradient(int type, int startColor, int endColor, + int x, int y, int width, int height, + float relativeX, float relativeY, float relativeSize) { + if (width <= 0 || height <= 0) return; + + // Per the plan's Phase 1: gradients render through pure-GPU MSL + // fragment shaders -- no CGContextDrawLinearGradient / CGContextDrawRadialGradient, + // no offscreen bitmap upload, no LRU cache. The shader interpolates + // start->end across the quad in 0..1 texcoord space. + simd_float4 sc = premultipliedFromARGB(startColor); + simd_float4 ec = premultipliedFromARGB(endColor); + + float vertices[8] = { + (float)x, (float)y, + (float)(x + width), (float)y, + (float)x, (float)(y + height), + (float)(x + width), (float)(y + height) + }; + static const float texcoords[8] = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f + }; + + // Constants must match DrawGradient.h. + enum { GRAD_TYPE_RADIAL = 1, GRAD_TYPE_HORIZONTAL = 2, GRAD_TYPE_VERTICAL = 3 }; + + switch (type) { + case GRAD_TYPE_HORIZONTAL: { + // axis.x == 1 picks texcoord.x as the gradient parameter t. + simd_float4 axis = (simd_float4){ 1.0f, 0.0f, 0.0f, 0.0f }; + drawGradientQuad(CN1MetalPipelineLinearGradient, vertices, texcoords, sc, ec, axis); + break; + } + case GRAD_TYPE_VERTICAL: { + // axis.x == 0 picks texcoord.y. + simd_float4 axis = (simd_float4){ 0.0f, 0.0f, 0.0f, 0.0f }; + drawGradientQuad(CN1MetalPipelineLinearGradient, vertices, texcoords, sc, ec, axis); + break; + } + case GRAD_TYPE_RADIAL: { + // Mirror the GL/CG semantics: centre at (relativeX, relativeY) + // in 0..1 fractions of (width, height); radius_px = relativeSize + // * MIN(width, height); convert that radius into 0..1 texcoord + // space along each axis (different along each axis whenever + // width != height -- the resulting elliptical iso-curves match + // CGContextDrawRadialGradient's circular iso-curves at the + // smaller-dim boundary). + float minDim = (float)((width < height) ? width : height); + float radiusPx = relativeSize * minDim; + float rxTex = radiusPx / (float)width; + float ryTex = radiusPx / (float)height; + simd_float4 params = (simd_float4){ relativeX, relativeY, rxTex, ryTex }; + drawGradientQuad(CN1MetalPipelineRadialGradient, vertices, texcoords, sc, ec, params); + break; + } + } +} + +// --------------- Alpha mask rendering (path-based shapes) --------------- + +id CN1MetalCreateAlphaMaskTexture(const uint8_t *bytes, int width, int height) { + if (bytes == NULL || width <= 0 || height <= 0) return nil; + id device = CN1MetalDevice(); + if (device == nil) return nil; + MTLTextureDescriptor *desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatR8Unorm + width:width height:height mipmapped:NO]; + desc.usage = MTLTextureUsageShaderRead; + id tex = [device newTextureWithDescriptor:desc]; + if (tex == nil) return nil; + [tex replaceRegion:MTLRegionMake2D(0, 0, width, height) + mipmapLevel:0 + withBytes:bytes + bytesPerRow:width]; + return tex; +} + +void CN1MetalDrawAlphaMask(id texture, int color, int alpha, + int x, int y, int width, int height) { + if (texture == nil) return; + // The AlphaMask fragment shader (cn1_fs_alpha_mask) does: + // float a = sample(tex).r; + // return float4(color.rgb * a, color.a * a); + // For premultiplied-alpha blending we need (R*a, G*a, B*a, a) where a is + // (alpha/255) and (R,G,B) are color components. The shader multiplies by + // tex.r once; we pass color premultiplied by alpha so the final out is + // (R*alpha*a, G*alpha*a, B*alpha*a, alpha*a) which matches GL's + // DrawTextureAlphaMask basic shader. + simd_float4 colorV = premultipliedColor(color, alpha); + float vertices[8] = { + (float)x, (float)y, + (float)(x + width), (float)y, + (float)x, (float)(y + height), + (float)(x + width), (float)(y + height) + }; + static const float texcoords[8] = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f + }; + drawQuad(CN1MetalPipelineAlphaMask, vertices, texcoords, colorV, texture); +} + +// Draw an alpha-mask quad with a radial gradient as colour source. Mirrors +// the GL radial-gradient program in DrawTextureAlphaMask.m:340-395 — the +// gradient is parameterised in texcoord-space (0..1) so the shader can +// compute it without knowing screen coordinates. Caller passes the +// gradient's screen-space bbox (gx, gy, gw, gh) and we convert to +// texcoord-space relative to the alpha-mask quad (x, y, width, height). +void CN1MetalDrawAlphaMaskRadial(id texture, + int x, int y, int width, int height, + int startColor, int endColor, + float gx, float gy, float gw, float gh) { + if (texture == nil || width <= 0 || height <= 0) return; + if (activeEncoder == nil || pipelineCache == nil) return; + id state = [pipelineCache pipelineFor:CN1MetalPipelineAlphaMaskRadial]; + if (state == nil) return; + [activeEncoder setRenderPipelineState:state]; + + float vertices[8] = { + (float)x, (float)y, + (float)(x + width), (float)y, + (float)x, (float)(y + height), + (float)(x + width), (float)(y + height) + }; + static const float texcoords[8] = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f + }; + [activeEncoder setVertexBytes:vertices length:sizeof(float) * 8 atIndex:0]; + CN1MetalMatrices matrices = currentMatrices(); + [activeEncoder setVertexBytes:&matrices length:sizeof(matrices) atIndex:1]; + [activeEncoder setVertexBytes:texcoords length:sizeof(float) * 8 atIndex:2]; + + // Premultiplied colours so blending produces the right output. + simd_float4 startV = premultipliedColor(startColor, 0xff); + simd_float4 endV = premultipliedColor(endColor, 0xff); + // Centre and radii in texcoord space. + float cx = (gx + gw / 2.0f - (float)x) / (float)width; + float cy = (gy + gh / 2.0f - (float)y) / (float)height; + float rx = (gw / 2.0f) / (float)width; + float ry = (gh / 2.0f) / (float)height; + simd_float4 params = (simd_float4){ cx, cy, rx, ry }; + [activeEncoder setFragmentBytes:&startV length:sizeof(startV) atIndex:0]; + [activeEncoder setFragmentBytes:&endV length:sizeof(endV) atIndex:1]; + [activeEncoder setFragmentBytes:¶ms length:sizeof(params) atIndex:2]; + [activeEncoder setFragmentTexture:texture atIndex:0]; + [activeEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; +} + +// --------------- Texture helpers --------------- + +id CN1MetalTextureFromUIImage(UIImage *image) { + if (image == nil) return nil; + id device = CN1MetalDevice(); + if (device == nil) return nil; + int w = (int)image.size.width * image.scale; + int h = (int)image.size.height * image.scale; + if (w <= 0 || h <= 0) return nil; + + // Rasterize UIImage into a CGBitmapContext, then upload as MTLTexture. + // No CTM flip: with default CG (Y-up) coords, CGContextDrawImage lays the + // source's row 0 at the BOTTOM of memory and the source's last row at + // memory_row_0 — i.e. the texture is stored upside-down in memory order. + // That mirrors GLUIImage.getTexture's POW2 layout (modulo padding) and is + // the orientation cn1's iOS theme assets are designed for: GL's V=1-at-top + // sampling renders them right-side-up; Metal's V=0-at-top sampling on this + // same memory layout reproduces GL's pixels exactly. Flipping the CTM + // here (the original implementation) made source row 0 land at + // memory_row_0 and produced a 1-pixel decoration leak at the title-bar + // top edge (rows 246-247) because cn1 9-patch slices put their drop-shadow + // row at source row 0 — which GL has always rendered at dest BOTTOM. + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + void *rawData = calloc(h * w * 4, sizeof(uint8_t)); + CGContextRef ctx = CGBitmapContextCreate(rawData, w, h, 8, w * 4, cs, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + CGColorSpaceRelease(cs); + CGContextDrawImage(ctx, CGRectMake(0, 0, w, h), image.CGImage); + CGContextRelease(ctx); + + MTLTextureDescriptor *desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm + width:w height:h mipmapped:NO]; + desc.usage = MTLTextureUsageShaderRead; + id texture = [device newTextureWithDescriptor:desc]; + [texture replaceRegion:MTLRegionMake2D(0, 0, w, h) + mipmapLevel:0 + withBytes:rawData + bytesPerRow:w * 4]; + free(rawData); + return texture; +} + +// --------------- Phase 3 v2: mutable-image rendering --------------- + +// Saved screen state during a mutable-image drain. drawFrame opens the +// screen encoder via setFramebuffer (publishing it into activeEncoder). +// When draining a mutable target we side-trip: save these globals, swap +// to the mutable encoder, encode the mutable's ops, then restore. +// Single-threaded: drawFrame is the only drainer; nested mutable side-trips +// are not supported (and not needed -- ops are flat in a single queue). +static __unsafe_unretained id savedScreenEncoder = nil; +static simd_float4x4 savedScreenProjection; +static int savedScreenFw = 0; +static int savedScreenFh = 0; +static BOOL savedScreenStateValid = NO; + +// Build a Y-down ortho projection for an offscreen (w x h) framebuffer. +// Mirrors METALView's CN1MetalOrtho -- if that one ever changes, update +// this in lockstep. +static simd_float4x4 mutableProjection(int w, int h) { + float invW = 1.0f / (float)w; + float invH = 1.0f / (float)h; + return (simd_float4x4){{ + { 2.0f * invW, 0.0f, 0.0f, 0.0f }, + { 0.0f, -2.0f * invH, 0.0f, 0.0f }, + { 0.0f, 0.0f, 0.5f, 0.0f }, + { -1.0f, 1.0f, 0.5f, 1.0f } + }}; +} + +void CN1MetalEnsureMutableTexture(GLUIImage *image, int width, int height) { + if (image == nil || width <= 0 || height <= 0) return; + id existing = [image mtlMutableTexture]; + if (existing != nil && + [image mtlMutableWidth] == width && + [image mtlMutableHeight] == height) { + return; + } + id device = CN1MetalDevice(); + if (device == nil) return; + MTLTextureDescriptor *desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:(NSUInteger)width height:(NSUInteger)height mipmapped:NO]; + desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + desc.storageMode = MTLStorageModePrivate; + id tex = [device newTextureWithDescriptor:desc]; + if (tex == nil) return; + // Clear new texture to the fill colour stashed by createNativeMutableImage. + // Default Image.createImage(w, h) → 0xffffffff opaque white; createImage(w, h, argb) + // honours the user's fill. Sentinel 0 (uninitialised ivar) keeps the prior + // transparent-black behaviour for non-mutable getMTLTexture paths. + int argb = [image mtlMutableInitialARGB]; + double a = ((argb >> 24) & 0xff) / 255.0; + double r = ((argb >> 16) & 0xff) / 255.0; + double g = ((argb >> 8) & 0xff) / 255.0; + double b = ( argb & 0xff) / 255.0; + // Store premultiplied so subsequent ops (which run through the + // pipeline cache's premultiplied blend: src=One, dst=OneMinusSrcAlpha) + // composite correctly when this texture is later sampled. Without this, + // a half-transparent green fill (Image.createImage(w,h, 0x2000ff00)) + // sampled at full green=1.0 + alpha=0.125 gets blended as green*1.0 + + // dst*(1-0.125) instead of green*0.125 + dst*(1-0.125), producing a + // saturated cyan when composed over a blue background instead of the + // intended faintly-green tint. (Compare graphics-draw-image-rect's + // blue arcs visible through the green mutableWithAlpha box: GL renders + // them blue with a faint green wash; pre-fix Metal rendered them + // turquoise.) + // Single command buffer combining clear + (optional) UIImage seed. + // The seed render pass loadAction=Load reads the cleared bg from the + // earlier subpass within the same cb -- using two separate cb's + // would race because cb commit is async on the queue and the seed + // pass's Load could capture pre-clear state. Render-pass-based seed + // (vs blit) is required because CN1MetalTextureFromUIImage allocates + // RGBA8Unorm textures while the mutable target is BGRA8Unorm; a + // blit would copy raw bytes and swap R/B, the textured pipeline's + // sampler does the format conversion automatically. + id queue = CN1MetalCommandQueue(); + UIImage *existingUI = [image getImage]; + if (queue != nil) { + id setupCb = [queue commandBuffer]; + + // Pass 1: clear to bg colour. + MTLRenderPassDescriptor *clearPass = [MTLRenderPassDescriptor renderPassDescriptor]; + clearPass.colorAttachments[0].texture = tex; + clearPass.colorAttachments[0].loadAction = MTLLoadActionClear; + clearPass.colorAttachments[0].storeAction = MTLStoreActionStore; + clearPass.colorAttachments[0].clearColor = MTLClearColorMake(r * a, g * a, b * a, a); + [[setupCb renderCommandEncoderWithDescriptor:clearPass] endEncoding]; + + // Pass 2: if the GLUIImage has an existing UIImage (e.g. it was + // returned by gausianBlurImage / FontImage / etc.), seed the + // freshly-cleared mutable texture with those pixels so subsequent + // draws layer on top. Without this seed gausianBlurImage's blurred + // shadow halo (Switch's createRoundThumbImage path) is lost the + // moment the next draw triggers EnsureMutableTexture, and the + // composited Switch ends up with no outline halo around the thumb. + // GL's startDrawingOnImageImpl gets this implicitly by drawing the + // existing UIImage into the CG context. + ensurePipelineCache(); + id seedState = (existingUI != nil && pipelineCache != nil) + ? [pipelineCache pipelineFor:CN1MetalPipelineTexturedRGBA] + : nil; + if (seedState != nil) { + id srcTex = CN1MetalTextureFromUIImage(existingUI); + if (srcTex != nil) { + MTLRenderPassDescriptor *seedPass = [MTLRenderPassDescriptor renderPassDescriptor]; + seedPass.colorAttachments[0].texture = tex; + seedPass.colorAttachments[0].loadAction = MTLLoadActionLoad; + seedPass.colorAttachments[0].storeAction = MTLStoreActionStore; + id seedEnc = [setupCb renderCommandEncoderWithDescriptor:seedPass]; + [seedEnc setViewport:(MTLViewport){0.0, 0.0, (double)width, (double)height, 0.0, 1.0}]; + [seedEnc setRenderPipelineState:seedState]; + + CN1MetalMatrices seedMatrices; + seedMatrices.projection = mutableProjection(width, height); + seedMatrices.modelView = identityMatrix(); + seedMatrices.transform = identityMatrix(); + + float seedVerts[8] = { + 0.0f, 0.0f, + (float)width, 0.0f, + 0.0f, (float)height, + (float)width, (float)height + }; + // V flipped: source memory_row_0 = visual BOTTOM (no CTM + // flip in CN1MetalTextureFromUIImage), so we want V=1 at + // dest top (sample source's last memory row = visual top + // there) and V=0 at dest bottom. + float seedTexcoords[8] = { + 0.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f + }; + simd_float4 seedTint = (simd_float4){ 1.0f, 1.0f, 1.0f, 1.0f }; + [seedEnc setVertexBytes:seedVerts length:sizeof(seedVerts) atIndex:0]; + [seedEnc setVertexBytes:&seedMatrices length:sizeof(seedMatrices) atIndex:1]; + [seedEnc setVertexBytes:seedTexcoords length:sizeof(seedTexcoords) atIndex:2]; + [seedEnc setFragmentBytes:&seedTint length:sizeof(seedTint) atIndex:0]; + [seedEnc setFragmentTexture:srcTex atIndex:0]; + [seedEnc drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; + [seedEnc endEncoding]; +#ifndef CN1_USE_ARC + // CN1MetalTextureFromUIImage returns a +1 retain. Metal + // keeps the texture alive internally for the duration of + // the encoded work, so dropping the retain now is safe. + [srcTex release]; +#endif + } + } + + [setupCb commit]; + } + [image setMtlMutableTexture:tex width:width height:height]; + // setMtlMutableTexture retains; balance the +1 from + // newTextureWithDescriptor: so the GLUIImage owns the only retain. + // Without this the texture leaks even with the dealloc release. +#ifndef CN1_USE_ARC + [tex release]; +#endif +} + +BOOL CN1MetalBeginMutableImageDraw(GLUIImage *image) { + if (image == nil) return NO; + id tex = [image mtlMutableTexture]; + if (tex == nil) return NO; + int w = [image mtlMutableWidth]; + int h = [image mtlMutableHeight]; + id queue = CN1MetalCommandQueue(); + if (queue == nil) return NO; + id cb = [queue commandBuffer]; + if (cb == nil) return NO; + MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor renderPassDescriptor]; + desc.colorAttachments[0].texture = tex; + // Load existing pixels so successive frames accumulate -- the GL/CG path + // semantically holds an "image buffer" that persists between draws into + // the same Image.getGraphics(). + desc.colorAttachments[0].loadAction = MTLLoadActionLoad; + desc.colorAttachments[0].storeAction = MTLStoreActionStore; + id enc = [cb renderCommandEncoderWithDescriptor:desc]; + if (enc == nil) return NO; + [enc setViewport:(MTLViewport){0.0, 0.0, (double)w, (double)h, 0.0, 1.0}]; + + // Save current screen state (drawFrame opened the screen encoder via + // setFramebuffer before starting drain) and swap in the mutable's. + savedScreenEncoder = activeEncoder; + savedScreenProjection = currentProjection; + savedScreenFw = currentFramebufferWidth; + savedScreenFh = currentFramebufferHeight; + savedScreenStateValid = YES; + + activeEncoder = enc; + currentProjection = mutableProjection(w, h); + currentFramebufferWidth = w; + currentFramebufferHeight = h; + + // Stash the cb on the image so End can commit + readback can wait. + [image setMtlMutableCommandBuffer:cb]; + return YES; +} + +void CN1MetalEndMutableImageDraw(GLUIImage *image) { + if (image == nil) return; + if (activeEncoder != nil) { + [activeEncoder endEncoding]; + } + id cb = [image mtlMutableCommandBuffer]; + if (cb != nil) { + [cb commit]; + // Keep the cb on the image so readback paths can waitUntilCompleted. + // It will be released when a subsequent Begin overwrites it (the + // Metal driver releases the buffer once GPU work is done). + } + + // Restore screen state so subsequent screen-target ops on the drain + // queue continue to use the screen encoder. + if (savedScreenStateValid) { + activeEncoder = savedScreenEncoder; + currentProjection = savedScreenProjection; + currentFramebufferWidth = savedScreenFw; + currentFramebufferHeight = savedScreenFh; + savedScreenEncoder = nil; + savedScreenStateValid = NO; + } +} + +void CN1MetalFlushMutableImageSync(GLUIImage *image) { + if (image == nil) return; + id cb = [image mtlMutableCommandBuffer]; + if (cb == nil) return; + [cb waitUntilCompleted]; + // Don't nil the cb -- multiple readbacks of the same already-completed + // buffer should be no-op-fast (waitUntilCompleted is idempotent). +} + +BOOL CN1MetalReadMutableImagePixels(GLUIImage *image, int *outARGB, + int x, int y, int w, int h, + int imgWidth, int imgHeight) { + if (image == nil || outARGB == NULL || w <= 0 || h <= 0) return NO; + id tex = [image mtlMutableTexture]; + if (tex == nil) return NO; + + // Ensure GPU work for this image is finished before sampling. + CN1MetalFlushMutableImageSync(image); + + int texW = (int)tex.width; + int texH = (int)tex.height; + + id device = CN1MetalDevice(); + if (device == nil) return NO; + id queue = CN1MetalCommandQueue(); + if (queue == nil) return NO; + + // Private storage textures can't be getBytes'd directly on iOS. Blit + // into a shared-storage scratch texture, wait, then read. + MTLTextureDescriptor *desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:(NSUInteger)texW height:(NSUInteger)texH mipmapped:NO]; + desc.usage = MTLTextureUsageShaderRead; + desc.storageMode = MTLStorageModeShared; + id shared = [device newTextureWithDescriptor:desc]; + if (shared == nil) return NO; + + id blitCb = [queue commandBuffer]; + id blit = [blitCb blitCommandEncoder]; + [blit copyFromTexture:tex sourceSlice:0 sourceLevel:0 + sourceOrigin:MTLOriginMake(0, 0, 0) + sourceSize:MTLSizeMake((NSUInteger)texW, (NSUInteger)texH, 1) + toTexture:shared destinationSlice:0 destinationLevel:0 + destinationOrigin:MTLOriginMake(0, 0, 0)]; + [blit endEncoding]; + [blitCb commit]; + [blitCb waitUntilCompleted]; + + // Read shared texture into a temp BGRA buffer, then convert + scale + // into outARGB. Texture dims equal imgWidth/imgHeight when the + // mutable was created via Image.createImage(w, h) but the API allows + // for arbitrary scaling; honour it. + NSUInteger rowBytes = (NSUInteger)(texW * 4); + uint8_t *bytes = (uint8_t *)malloc(rowBytes * (NSUInteger)texH); + if (bytes == NULL) return NO; + [shared getBytes:bytes bytesPerRow:rowBytes + fromRegion:MTLRegionMake2D(0, 0, (NSUInteger)texW, (NSUInteger)texH) + mipmapLevel:0]; + + float scaleX = (imgWidth > 0) ? ((float)texW / (float)imgWidth) : 1.0f; + float scaleY = (imgHeight > 0) ? ((float)texH / (float)imgHeight) : 1.0f; + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + int srcX = (int)((x + col) * scaleX); + int srcY = (int)((y + row) * scaleY); + int dstIdx = row * w + col; + if (srcX < 0 || srcX >= texW || srcY < 0 || srcY >= texH) { + outARGB[dstIdx] = 0; + continue; + } + int srcIdx = srcY * (int)rowBytes + srcX * 4; + uint8_t b = bytes[srcIdx + 0]; + uint8_t g = bytes[srcIdx + 1]; + uint8_t r = bytes[srcIdx + 2]; + uint8_t a = bytes[srcIdx + 3]; + outARGB[dstIdx] = ((int)a << 24) | ((int)r << 16) | ((int)g << 8) | (int)b; + } + } + free(bytes); +#ifndef CN1_USE_ARC + // shared is +1 from newTextureWithDescriptor: release it now that the + // CPU-visible bytes are copied out. Without this every Image.getRGB + // round-trip leaks a full-resolution staging texture. + [shared release]; +#endif + return YES; +} + +// CGDataProviderCreateWithData expects a C function pointer for the +// release callback, not a block, so this lives at file scope. +static void cn1MetalReadbackFreeData(void * __unused info, const void *data, size_t __unused size) { + free((void *)data); +} + +UIImage *CN1MetalReadMutableImageAsUIImage(GLUIImage *image) { + if (image == nil) return nil; + id tex = [image mtlMutableTexture]; + if (tex == nil) return nil; + + CN1MetalFlushMutableImageSync(image); + + int texW = (int)tex.width; + int texH = (int)tex.height; + if (texW <= 0 || texH <= 0) return nil; + + id device = CN1MetalDevice(); + id queue = CN1MetalCommandQueue(); + if (device == nil || queue == nil) return nil; + + // Same blit-to-shared dance as CN1MetalReadMutableImagePixels: private + // textures aren't getBytes'able directly. Build the UIImage from the + // shared scratch's bytes. + MTLTextureDescriptor *desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:(NSUInteger)texW height:(NSUInteger)texH mipmapped:NO]; + desc.usage = MTLTextureUsageShaderRead; + desc.storageMode = MTLStorageModeShared; + id shared = [device newTextureWithDescriptor:desc]; + if (shared == nil) return nil; + + id blitCb = [queue commandBuffer]; + id blit = [blitCb blitCommandEncoder]; + [blit copyFromTexture:tex sourceSlice:0 sourceLevel:0 + sourceOrigin:MTLOriginMake(0, 0, 0) + sourceSize:MTLSizeMake((NSUInteger)texW, (NSUInteger)texH, 1) + toTexture:shared destinationSlice:0 destinationLevel:0 + destinationOrigin:MTLOriginMake(0, 0, 0)]; + [blit endEncoding]; + [blitCb commit]; + [blitCb waitUntilCompleted]; + + NSUInteger rowBytes = (NSUInteger)(texW * 4); + NSUInteger byteCount = rowBytes * (NSUInteger)texH; + uint8_t *bytes = (uint8_t *)malloc(byteCount); + if (bytes == NULL) { +#ifndef CN1_USE_ARC + [shared release]; +#endif + return nil; + } + [shared getBytes:bytes bytesPerRow:rowBytes + fromRegion:MTLRegionMake2D(0, 0, (NSUInteger)texW, (NSUInteger)texH) + mipmapLevel:0]; +#ifndef CN1_USE_ARC + [shared release]; +#endif + + // Wrap the BGRA buffer as a CGImage / UIImage. The provider takes + // ownership of the malloc'd bytes via the freeData callback below. + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, bytes, byteCount, + cn1MetalReadbackFreeData); + CGImageRef cgImg = CGImageCreate((size_t)texW, (size_t)texH, 8, 32, rowBytes, cs, + (CGBitmapInfo)(kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst), + provider, NULL, NO, kCGRenderingIntentDefault); + CGDataProviderRelease(provider); + CGColorSpaceRelease(cs); + if (cgImg == NULL) return nil; + UIImage *out = [UIImage imageWithCGImage:cgImg]; + CGImageRelease(cgImg); + return out; +} + +// --------------- Memory-pressure cache release --------------- +// +// METALView observes UIApplicationDidReceiveMemoryWarning and calls +// this. We drop the lazy texture caches (whole-string text, gradient, +// per-(font,size) glyph atlases) but keep the pipeline state cache — +// rebuilding pipelines is expensive and they're tiny. The screen +// texture stays too; updateFrameBufferSize: handles its replacement +// on resize. Cleared caches re-fill on demand on the next frame. + +extern void CN1MetalGlyphAtlasReleaseAll(void); + +void CN1MetalReleaseCaches(void) { + // Whole-string text cache no longer exists -- text rendering goes + // exclusively through the CN1MetalGlyphAtlas (Phase 4 mandate). + // Gradient cache no longer exists either -- gradients render through + // pure-GPU MSL fragment shaders (Phase 1 pipeline list: + // linear-gradient / radial-gradient), no offscreen bitmap to cache. + // Only the glyph atlases need releasing under memory pressure. + CN1MetalGlyphAtlasReleaseAll(); +} + +#endif /* CN1_USE_METAL */ diff --git a/Ports/iOSPort/nativeSources/CN1RenderingView.h b/Ports/iOSPort/nativeSources/CN1RenderingView.h new file mode 100644 index 0000000000..f2bbb58f7d --- /dev/null +++ b/Ports/iOSPort/nativeSources/CN1RenderingView.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +#ifndef CN1RenderingView_h +#define CN1RenderingView_h +#import + +// Shared method surface implemented by both EAGLView (OpenGL ES 2 backend) +// and METALView (Metal backend). CodenameOne_GLViewController calls through +// this protocol so it can drive either backend under the CN1_USE_METAL ifdef. +@protocol CN1RenderingView +- (void)setFramebuffer; +- (BOOL)presentFramebuffer; +- (void)deleteFramebuffer; +- (void)updateFrameBufferSize:(int)w h:(int)h; +- (void)addPeerComponent:(UIView *)view; +- (void)keyboardDoneClicked; +- (void)keyboardNextClicked; +- (void)textFieldDidChange; +@end + +#endif diff --git a/Ports/iOSPort/nativeSources/ClearRect.m b/Ports/iOSPort/nativeSources/ClearRect.m index af5f5661b7..c64380e641 100644 --- a/Ports/iOSPort/nativeSources/ClearRect.m +++ b/Ports/iOSPort/nativeSources/ClearRect.m @@ -24,6 +24,9 @@ #import "CodenameOne_GLViewController.h" #include "xmlvm.h" #include "TargetConditionals.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; @@ -98,6 +101,9 @@ -(id)initWithArgs:(int)xpos ypos:(int)ypos w:(int)w h:(int)h { } #ifdef USE_ES2 -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalClearRect(x, y, width, height); +#else glUseProgram(getOGLProgram()); GLfloat xOffset = 0; GLfloat yOffset = 0; @@ -143,9 +149,10 @@ -(void)execute { _glEnable(GL_BLEND); GLErrorLog; - + glDisableVertexAttribArray(vertexCoordAtt); GLErrorLog; +#endif } #else -(void)execute { @@ -158,7 +165,7 @@ -(void)execute { x, y + height, x + width, y + height }; - + GLErrorLog; _glVertexPointer(2, GL_FLOAT, 0, vertexes); _glEnableClientState(GL_VERTEX_ARRAY); diff --git a/Ports/iOSPort/nativeSources/ClipRect.m b/Ports/iOSPort/nativeSources/ClipRect.m index beb2c6cb12..a2ce47b6d5 100644 --- a/Ports/iOSPort/nativeSources/ClipRect.m +++ b/Ports/iOSPort/nativeSources/ClipRect.m @@ -22,6 +22,9 @@ */ #import "ClipRect.h" #import "CodenameOne_GLViewController.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #import "FillRect.h" #ifdef USE_ES2 #import "DrawTextureAlphaMask.h" @@ -92,6 +95,18 @@ -(void)executeWithLog { } -(void)execute { +#ifdef CN1_USE_METAL + // Phase 2: handle only the rectangular scissor case via Metal's + // setScissorRect. Stencil-based clipping for texture/polygon clips + // is deferred to a later phase -- those currently fall back to a + // bounding-box scissor (incorrect for non-rectangular masks but + // does not crash). + int sx = x, sy = y, sw = width, sh = height; + if (sx < 0) { sw += sx; sx = 0; } + if (sy < 0) { sh += sy; sy = 0; } + CN1MetalSetScissor(sx, sy, sw, sh); + clipApplied = (sw > 0 && sh > 0); +#else #ifdef USE_ES2 if ( texture != 0 || numPoints > 0 ){ clipX = x; clipY=y; clipW=width; clipH=height; @@ -210,6 +225,7 @@ -(void)execute { #endif clipApplied = NO; } +#endif // CN1_USE_METAL } diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index 6db7066182..1b303b9d13 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -24,6 +24,10 @@ #import #import "CodenameOne_GLViewController.h" #import "EAGLView.h" +#ifdef CN1_USE_METAL +#import "METALView.h" +#import "CN1Metalcompat.h" +#endif #import "ExecutableOp.h" #import "FillRect.h" #import "ClipRect.h" @@ -848,6 +852,13 @@ CGContextRef roundRect(CGContextRef context, int color, int alpha, int x, int y, void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawRoundRectMutableImpl (int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) { +#ifdef CN1_USE_METAL + // Dead under Metal -- MutableGraphics builds a round-rect GeneralPath + // and routes it through the alpha-mask Metal pipeline (Renderer.c -> + // R8 MTLTexture -> DrawTextureAlphaMask op tagged with the mutable + // target). The Java side gates with `metalRendering` before calling + // this JNI. +#else CGContextRef context = UIGraphicsGetCurrentContext(); if (currentMutableTransformSet) { CGContextSaveGState(context); @@ -857,12 +868,20 @@ CGContextRef roundRect(CGContextRef context, int color, int alpha, int x, int y, if (currentMutableTransformSet) { CGContextRestoreGState(context); } +#endif } void Java_com_codename1_impl_ios_IOSImplementation_setAntiAliasedMutableImpl (JAVA_BOOLEAN antialiased) { +#ifdef CN1_USE_METAL + // Metal pipelines are antialiased by default; CG's runtime AA toggle + // has no direct equivalent. The visible behaviour matches + // "antialiased=YES" everywhere on Metal. + (void)antialiased; +#else CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetAllowsAntialiasing(context, antialiased); +#endif } void Java_com_codename1_impl_ios_IOSImplementation_resetAffineGlobal() { @@ -887,23 +906,32 @@ void Java_com_codename1_impl_ios_IOSImplementation_scale(float x, float y) { void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawRoundRectGlobalImpl (int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) { +#ifdef CN1_USE_METAL + // Dead under Metal -- GlobalGraphics builds a round-rect GeneralPath + // and routes it through nativeDrawShape (alpha-mask Metal pipeline). +#else UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextStrokePath(roundRect(context, color, alpha, 0, 0, width, height, arcWidth, arcHeight)); UIImage* img = UIGraphicsGetImageFromCurrentImageContext(); - //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl %i", ((int)img)); UIGraphicsEndImageContext(); - + GLUIImage* glu = [[GLUIImage alloc] initWithImage:img]; Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageGlobalImpl((BRIDGE_CAST void*) glu, 255, x, y, width, height, 0); #ifndef CN1_USE_ARC [glu release]; #endif +#endif } void Java_com_codename1_impl_ios_IOSImplementation_nativeFillRoundRectMutableImpl (int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) { +#ifdef CN1_USE_METAL + // Dead under Metal -- MutableGraphics goes through the alpha-mask + // Metal pipeline (build path, Renderer.c -> R8 MTLTexture -> + // DrawTextureAlphaMask op tagged with currentMutableImage). +#else CGContextRef context = UIGraphicsGetCurrentContext(); if (currentMutableTransformSet) { CGContextSaveGState(context); @@ -913,22 +941,27 @@ void Java_com_codename1_impl_ios_IOSImplementation_scale(float x, float y) { if (currentMutableTransformSet) { CGContextRestoreGState(context); } +#endif } void Java_com_codename1_impl_ios_IOSImplementation_nativeFillRoundRectGlobalImpl (int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) { +#ifdef CN1_USE_METAL + // Dead under Metal -- GlobalGraphics routes through the alpha-mask + // Metal pipeline. +#else UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextFillPath(roundRect(context, color, alpha, 0, 0, width, height, arcWidth, arcHeight)); UIImage* img = UIGraphicsGetImageFromCurrentImageContext(); - //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl %i", ((int)img)); UIGraphicsEndImageContext(); - + GLUIImage* glu = [[GLUIImage alloc] initWithImage:img]; Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageGlobalImpl((BRIDGE_CAST void*)glu, 255, x, y, width, height, 0); #ifndef CN1_USE_ARC [glu release]; #endif +#endif } #define PI 3.14159265358979323846 @@ -979,6 +1012,11 @@ CGContextRef drawArc(CGContextRef context, int color, int alpha, int x, int y, i void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawArcMutableImpl (int color, int alpha, int x, int y, int width, int height, int startAngle, int angle) { +#ifdef CN1_USE_METAL + // Dead under Metal -- MutableGraphics builds a GeneralPath via + // drawingArcPath.arc(...) and routes through nativeDrawShape (alpha-mask + // Metal pipeline tagged with currentMutableImage). +#else CGContextRef context = UIGraphicsGetCurrentContext(); if (currentMutableTransformSet) { CGContextSaveGState(context); @@ -988,6 +1026,7 @@ CGContextRef drawArc(CGContextRef context, int color, int alpha, int x, int y, i if (currentMutableTransformSet) { CGContextRestoreGState(context); } +#endif } void Java_com_codename1_impl_ios_IOSImplementation_nativeFillRadialGradientMutableImpl @@ -1021,6 +1060,14 @@ CGContextRef drawArc(CGContextRef context, int color, int alpha, int x, int y, i void Java_com_codename1_impl_ios_IOSImplementation_nativeFillArcMutableImpl (int color, int alpha, int x, int y, int width, int height, int startAngle, int angle) { +#ifdef CN1_USE_METAL + // Dead under Metal -- MutableGraphics builds drawingArcPath via + // GeneralPath.arc(...) and routes through nativeFillShape (alpha-mask + // Metal pipeline tagged with currentMutableImage). RadialGradientPaint + // composes through the AlphaMaskRadial pipeline at draw time + // (DrawTextureAlphaMask.execute checks PaintOp and routes to + // CN1MetalDrawAlphaMaskRadial). +#else CGContextRef context = UIGraphicsGetCurrentContext(); if (currentMutableTransformSet) { CGContextSaveGState(context); @@ -1036,6 +1083,7 @@ CGContextRef drawArc(CGContextRef context, int color, int alpha, int x, int y, i if (currentMutableTransformSet) { CGContextRestoreGState(context); } +#endif } // START ES2 ADDITION: Drawing Shapes ------------------------------------------------------------------------------ @@ -1086,15 +1134,28 @@ void Java_com_codename1_impl_ios_IOSImplementation_fillConvexPolygonImpl(JAVA_OB } -void Java_com_codename1_impl_ios_IOSImplementation_drawTextureAlphaMaskImpl(GLuint textureName, int color, int alpha, int x, int y, int w, int h) +void Java_com_codename1_impl_ios_IOSImplementation_drawTextureAlphaMaskImpl(JAVA_LONG textureName, int color, int alpha, int x, int y, int w, int h) { - + DrawTextureAlphaMask *f = [[DrawTextureAlphaMask alloc] initWithArgs:textureName color:color alpha:alpha x:x y:y w:w h:h]; +#ifdef CN1_USE_METAL + // If a mutable image is the current draw target (the Java side called + // startDrawingOnImage(...) to begin painting INTO an Image), tag the op + // so drawFrame's drain (Phase 3 v2) routes it to that image's encoder + // instead of the screen encoder. This is what unifies mutable-image + // shape rendering with the screen pipeline -- the same alpha-mask + // texture goes to the same Metal alpha-mask shader, just bound to a + // different render target. + GLUIImage *mutableTarget = [CodenameOne_GLViewController instance].currentMutableImage; + if (mutableTarget != nil) { + [f setTarget:mutableTarget]; + } +#endif [CodenameOne_GLViewController upcoming:f]; #ifndef CN1_USE_ARC [f release]; #endif - + } // END ES2 ADDITION ------------------------------------------------------------------------------------------------- @@ -1130,6 +1191,23 @@ void com_codename1_impl_ios_IOSImplementation_nativeSetTransformMutableImpl___fl JAVA_INT originX, JAVA_INT originY ) { +#ifdef CN1_USE_METAL + { + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + GLKMatrix4 m = GLKMatrix4MakeAndTranspose(a0,a1,a2,a3, + b0,b1,b2,b3, + c0,c1,c2,c3, + d0,d1,d2,d3); + SetTransform *f = [[SetTransform alloc] initWithArgs:m originX:originX originY:originY]; + [f setTarget:target]; + [CodenameOne_GLViewController upcoming:f]; +#ifndef CN1_USE_ARC + [f release]; +#endif + return; + } +#endif #ifdef USE_ES2 POOL_BEGIN(); currentMutableTransformSet = NO; @@ -1145,7 +1223,7 @@ void com_codename1_impl_ios_IOSImplementation_nativeSetTransformMutableImpl___fl for(int i=0; i<16; i++) caMatrix[i] = glMatrix[i]; //this will do the typecast if needed output = *((CATransform3D *)caMatrix); - + if (!CATransform3DIsIdentity(output)) { CGAffineTransform affine = CATransform3DGetAffineTransform(output); currentMutableTransform = affine; @@ -1332,6 +1410,20 @@ void Java_com_codename1_impl_ios_IOSNative_nativeDrawShadowMutable(CN1_THREAD_ST void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageMutableImpl (void* peer, int alpha, int x, int y, int width, int height, int renderingHints) { +#ifdef CN1_USE_METAL + { + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + DrawImage *f = [[DrawImage alloc] initWithArgs:alpha xpos:x ypos:y i:(BRIDGE_CAST GLUIImage*)peer w:width h:height]; + [f setRenderingHints:renderingHints]; + [f setTarget:target]; + [CodenameOne_GLViewController upcoming:f]; +#ifndef CN1_USE_ARC + [f release]; +#endif + return; + } +#endif //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_nativeDrawImageMutableImpl %i started at %i, %i", (int)peer, x, y); UIImage* i = [(BRIDGE_CAST GLUIImage*)peer getImage]; CGContextRef context = UIGraphicsGetCurrentContext(); @@ -1470,6 +1562,19 @@ int Java_com_codename1_impl_ios_IOSImplementation_getDisplayWidthImpl() { void Java_com_codename1_impl_ios_IOSImplementation_setNativeClippingMutableImpl (int x, int y, int width, int height, int clipApplied) { +#ifdef CN1_USE_METAL + { + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + ClipRect *f = [[ClipRect alloc] initWithArgs:x ypos:y w:width h:height f:clipApplied]; + [f setTarget:target]; + [[CodenameOne_GLViewController instance] upcomingAddClip:f]; +#ifndef CN1_USE_ARC + [f release]; +#endif + return; + } +#endif CGContextRef context = UIGraphicsGetCurrentContext(); //CN1Log(@"Native mutable clipping applied %i on context %i x: %i y: %i width: %i height: %i", clipApplied, (int)context, x, y, width, height); //if(clipApplied) { @@ -1483,13 +1588,18 @@ int Java_com_codename1_impl_ios_IOSImplementation_getDisplayWidthImpl() { void Java_com_codename1_impl_ios_IOSImplementation_setNativeClippingShapeMutableImpl (int numCommands, JAVA_OBJECT commands, int numPoints, JAVA_OBJECT points) { +#ifdef CN1_USE_METAL + // Shape clipping for mutable images on Metal: scissor rect can only + // express axis-aligned rects. A future improvement could rasterise the + // shape into a stencil/alpha mask. For now treat as "no clip" so + // subsequent draws still render. + (void)numCommands; (void)commands; (void)numPoints; (void)points; +#else CGContextRef context = UIGraphicsGetCurrentContext(); - //CN1Log(@"Native mutable clipping applied %i on context %i x: %i y: %i width: %i height: %i", clipApplied, (int)context, x, y, width, height); - //if(clipApplied) { CGContextRestoreGState(context); - //} CGContextSaveGState(context); CGContextClip(Java_com_codename1_impl_ios_IOSImplementation_drawPath(CN1_THREAD_GET_STATE_PASS_ARG numCommands, commands, numPoints, points)); +#endif } void Java_com_codename1_impl_ios_IOSImplementation_setNativeClippingGlobalImpl @@ -1546,6 +1656,19 @@ void Java_com_codename1_impl_ios_IOSImplementation_setNativeClippingPolygonGloba void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawLineMutableImpl (int color, int alpha, int x1, int y1, int x2, int y2) { +#ifdef CN1_USE_METAL + { + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + DrawLine *f = [[DrawLine alloc] initWithArgs:color a:alpha xpos1:x1 ypos1:y1 xpos2:x2 ypos2:y2]; + [f setTarget:target]; + [CodenameOne_GLViewController upcoming:f]; +#ifndef CN1_USE_ARC + [f release]; +#endif + return; + } +#endif //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_nativeDrawLineMutableImpl started"); [UIColorFromRGB(color, alpha) set]; CGContextRef context = UIGraphicsGetCurrentContext(); @@ -1586,6 +1709,19 @@ void Java_com_codename1_impl_ios_IOSImplementation_setNativeClippingPolygonGloba void Java_com_codename1_impl_ios_IOSImplementation_nativeFillRectMutableImpl (int color, int alpha, int x, int y, int width, int height) { +#ifdef CN1_USE_METAL + { + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + FillRect *f = [[FillRect alloc] initWithArgs:color a:alpha xpos:x ypos:y w:width h:height]; + [f setTarget:target]; + [CodenameOne_GLViewController upcoming:f]; +#ifndef CN1_USE_ARC + [f release]; +#endif + return; + } +#endif //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_nativeFillRectMutableImpl started"); [UIColorFromRGB(color, alpha) set]; CGContextRef context = UIGraphicsGetCurrentContext(); @@ -1601,6 +1737,19 @@ void Java_com_codename1_impl_ios_IOSImplementation_setNativeClippingPolygonGloba } void Java_com_codename1_impl_ios_IOSImplementation_clearRectMutable(int x, int y, int w, int h) { +#ifdef CN1_USE_METAL + { + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + ClearRect *f = [[ClearRect alloc] initWithArgs:x ypos:y w:w h:h]; + [f setTarget:target]; + [CodenameOne_GLViewController upcoming:f]; +#ifndef CN1_USE_ARC + [f release]; +#endif + return; + } +#endif CGContextRef context = UIGraphicsGetCurrentContext(); if (currentMutableTransformSet) { CGContextSaveGState(context); @@ -1610,7 +1759,7 @@ void Java_com_codename1_impl_ios_IOSImplementation_clearRectMutable(int x, int y if (currentMutableTransformSet) { CGContextRestoreGState(context); } - + } void Java_com_codename1_impl_ios_IOSImplementation_clearRectGlobal(int x, int y, int w, int h) { @@ -1634,6 +1783,19 @@ void Java_com_codename1_impl_ios_IOSImplementation_clearRectGlobal(int x, int y, void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawRectMutableImpl (int color, int alpha, int x, int y, int width, int height) { +#ifdef CN1_USE_METAL + { + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + DrawRect *f = [[DrawRect alloc] initWithArgs:color a:alpha xpos:x ypos:y w:width h:height]; + [f setTarget:target]; + [CodenameOne_GLViewController upcoming:f]; +#ifndef CN1_USE_ARC + [f release]; +#endif + return; + } +#endif //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_nativeDrawRectMutableImpl started"); [UIColorFromRGB(color, alpha) set]; CGContextRef context = UIGraphicsGetCurrentContext(); @@ -1661,6 +1823,19 @@ void Java_com_codename1_impl_ios_IOSImplementation_clearRectGlobal(int x, int y, void Java_com_codename1_impl_ios_IOSImplementation_nativeDrawStringMutableImpl (int color, int alpha, void* fontPeer, NSString* str, int x, int y) { +#ifdef CN1_USE_METAL + { + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + DrawString *f = [[DrawString alloc] initWithArgs:color a:alpha xpos:x ypos:y s:str f:(BRIDGE_CAST UIFont*)fontPeer]; + [f setTarget:target]; + [CodenameOne_GLViewController upcoming:f]; +#ifndef CN1_USE_ARC + [f release]; +#endif + return; + } +#endif //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_nativeDrawStringMutableImpl started"); [[CodenameOne_GLViewController instance] drawString:color alpha:alpha font:(BRIDGE_CAST UIFont*)fontPeer str:str x:x y:y]; //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_nativeDrawStringMutableImpl finished"); @@ -1689,18 +1864,41 @@ void Java_com_codename1_impl_ios_IOSImplementation_clearRectGlobal(int x, int y, //[img retain]; //CN1Log(@"createNativeMutableImageImpl finished %i ", (int)img); GLUIImage* gl = [[GLUIImage alloc] initWithImage:img]; +#ifdef CN1_USE_METAL + // Phase 3 v2: stash the fill colour so when startDrawingOnImage + // lazily allocates the Metal render-target texture (via + // CN1MetalEnsureMutableTexture), the clear pass uses this colour + // instead of (0,0,0,0). Mirrors UIRectFill(argb) above for the CG + // backing, so screen-side DrawImage of a freshly-created mutable + // sees the same starting pixels regardless of which backing wins. + [gl setMtlMutableInitialARGB:argb]; +#endif return (BRIDGE_CAST void*)gl; } void Java_com_codename1_impl_ios_IOSImplementation_startDrawingOnImageImpl (int width, int height, void *peer) { +#ifdef CN1_USE_METAL + { + // Phase 3 v2: ensure the mutable image has a Metal render-target + // texture sized to the requested dims, then publish it as the + // current mutable target. Subsequent nativeXxxMutableImpl JNIs + // queue ExecutableOps tagged with this target; drawFrame opens an + // encoder against the texture when it drains them. + GLUIImage *gl = (BRIDGE_CAST GLUIImage*)peer; + CN1MetalEnsureMutableTexture(gl, width, height); + [CodenameOne_GLViewController instance].currentMutableImage = gl; + currentMutableTransformSet = NO; + return; + } +#endif //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_startDrawingOnImageImpl"); UIImage* original = [(BRIDGE_CAST GLUIImage*)peer getImage]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0); if(original != NULL) { [original drawAtPoint:CGPointZero]; } - + CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); [CodenameOne_GLViewController instance].currentMutableImage = (BRIDGE_CAST GLUIImage*)peer; @@ -1709,6 +1907,19 @@ void Java_com_codename1_impl_ios_IOSImplementation_clearRectGlobal(int x, int y, } void* Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl() { +#ifdef CN1_USE_METAL + { + // Phase 3 v2: clear the current mutable target. Pending ops (queued + // between Begin/Finish) will drain on the next drawFrame; the + // mutable's MTLTexture continues to hold its accumulated pixels. + // Readback paths (Image.getRGB, encode-as-PNG/JPEG, toImage) + // explicitly call CN1MetalFlushMutableImageSync before reading. + GLUIImage *gl = [CodenameOne_GLViewController instance].currentMutableImage; + [CodenameOne_GLViewController instance].currentMutableImage = nil; + currentMutableTransformSet = NO; + return (BRIDGE_CAST void*)gl; + } +#endif UIImage* img = UIGraphicsGetImageFromCurrentImageContext(); //CN1Log(@"Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl %i", ((int)img)); UIGraphicsEndImageContext(); @@ -1721,6 +1932,47 @@ void Java_com_codename1_impl_ios_IOSImplementation_clearRectGlobal(int x, int y, void Java_com_codename1_impl_ios_IOSImplementation_imageRgbToIntArrayImpl (void* peer, int* arr, int x, int y, int width, int height, int imgWidth, int imgHeight) { +#ifdef CN1_USE_METAL + { + // Phase 3 v2 readback. Was disabled in 9b2aaf11d on suspicion of a + // DrawImage-test hang (GPU memory pressure → nextDrawable wedge), + // but the legacy CG-from-UIImage fallback returns the stale + // initial-fill pixels for any mutable that's been drawn into via + // Metal — animation/transition tests added in master #4821 then + // emit empty grids and downstream EDT scheduling stalls trying + // to advance to the next test. + // + // The original GPU-pressure concern is mitigated by the resource- + // leak fix in commit e548d1afb (each transient GLUIImage no longer + // pins +1 retains on its mtlMutableTexture / command buffer / read + // textures). Reinstate the path so getRGB sees real Metal pixels. + // + // If we're still inside a draw-on-image session we must close it + // first so the queued ops are visible to the drain (mirrors the + // legacy path's finishDrawingOnImageImpl call below). + GLUIImage *gl = (BRIDGE_CAST GLUIImage*)peer; + if ([gl mtlMutableTexture] != nil) { + BOOL stillDrawing = (((BRIDGE_CAST void*)[CodenameOne_GLViewController instance].currentMutableImage) == peer); + BOOL savedTransformSet = currentMutableTransformSet; + if (stillDrawing) { + Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl(); + } + // flushBuffer dispatches synchronously to main and runs + // drawFrame, which drains queued ExecutableOps -- including + // any with target=this image -- through Begin/End mutable + // encoders so the texture is up-to-date. The bounding rect + // doesn't matter for the queue drain (drawFrame uses it only + // for ClipRect.setDrawRect on screen ops). + [[CodenameOne_GLViewController instance] flushBuffer:nil x:0 y:0 width:displayWidth height:displayHeight]; + CN1MetalReadMutableImagePixels(gl, arr, x, y, width, height, imgWidth, imgHeight); + if (stillDrawing) { + Java_com_codename1_impl_ios_IOSImplementation_startDrawingOnImageImpl(imgWidth, imgHeight, peer); + currentMutableTransformSet = savedTransformSet; + } + return; + } + } +#endif BOOL currentlyDrawing = NO; BOOL oldCurrentMutableTransformSet = currentMutableTransformSet; if(((BRIDGE_CAST void*)[CodenameOne_GLViewController instance].currentMutableImage) == peer) { @@ -1776,6 +2028,18 @@ void Java_com_codename1_impl_ios_IOSImplementation_clearRectGlobal(int x, int y, Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl(); } TileImage* f = [[TileImage alloc] initWithArgs:alpha xpos:x ypos:y i:(BRIDGE_CAST GLUIImage*)peer w:width h:height]; +#ifdef CN1_USE_METAL + // Phase 3 v2: if a mutable target is currently active, tag the op + // so drawFrame's drain routes it to the mutable's encoder. Java's + // tileImage(graphics, ...) sets this up by calling ng.checkControl + // before the JNI -- on the mutable branch, currentMutableImage = + // panelImg; on the screen branch, the GlobalGraphics.checkControl + // path cleared it via finishDrawingOnImage so it's nil here. + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target != nil) { + [f setTarget:target]; + } +#endif [CodenameOne_GLViewController upcoming:f]; #ifndef CN1_USE_ARC [f release]; @@ -2162,13 +2426,25 @@ -(UIImage*)createSplashImage { * we need to obtain the EAGLView. */ -(EAGLView*) eaglView { - if ([self.view class] == [EAGLView class]) { + // Under CN1_USE_METAL the rendering view is a METALView, not an EAGLView. + // Both classes conform to CN1RenderingView, so the return value is used + // through the shared protocol surface (setFramebuffer, presentFramebuffer, + // updateFrameBufferSize:h:, addPeerComponent:, peerComponentsLayer). + // The declared return type stays EAGLView* for ABI stability; the cast is + // duck-typed. Any EAGLView-only call site (e.g. -setContext:) is guarded + // by #ifndef CN1_USE_METAL. +#ifdef CN1_USE_METAL + Class renderingClass = [METALView class]; +#else + Class renderingClass = [EAGLView class]; +#endif + if ([self.view class] == renderingClass) { lastFoundEaglView = (EAGLView*)self.view; return (EAGLView*)self.view; } for (UIView* child in self.view.subviews) { - - if ([child class] == [EAGLView class]) { + + if ([child class] == renderingClass) { lastFoundEaglView = (EAGLView*)child; return (EAGLView*)child; } @@ -2227,7 +2503,10 @@ - (void)awakeFromNib [aContext release]; #endif +#ifndef CN1_USE_METAL + // METALView has no GL context. Under CN1_USE_METAL this call is a no-op. [[self eaglView] setContext:context]; +#endif [[self eaglView] setFramebuffer]; //self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; //self.view.autoresizesSubviews = YES; @@ -2864,11 +3143,44 @@ - (void)drawFrame:(CGRect)rect [currentTarget removeAllObjects]; } GLErrorLog; +#ifdef CN1_USE_METAL + // Phase 3 v2: walk the queue, switching encoders when target + // changes. nil target = screen (already opened by setFramebuffer + // at top of drawFrame). non-nil target = mutable GLUIImage -- + // open a fresh CB+encoder against its texture, drain its ops, + // commit (no wait) at next switch or end. Readback paths call + // CN1MetalFlushMutableImageSync to wait on the commit. + GLUIImage *currentDrainTarget = nil; + BOOL mutableEncoderOpen = NO; + for (ExecutableOp *ex in cp) { + GLUIImage *opTarget = [ex target]; + if (opTarget != currentDrainTarget) { + if (mutableEncoderOpen) { + CN1MetalEndMutableImageDraw(currentDrainTarget); + mutableEncoderOpen = NO; + } + currentDrainTarget = opTarget; + if (opTarget != nil) { + mutableEncoderOpen = CN1MetalBeginMutableImageDraw(opTarget); + } + } + // Skip mutable ops whose encoder failed to open. Screen + // ops (target=nil) always execute against the screen + // encoder set up by setFramebuffer. + if (opTarget != nil && !mutableEncoderOpen) continue; + [ex executeWithClipping]; + GLErrorLog; + } + if (mutableEncoderOpen) { + CN1MetalEndMutableImageDraw(currentDrainTarget); + } +#else for(ExecutableOp* ex in cp) { [ex executeWithClipping]; //[ex executeWithLog]; GLErrorLog; } +#endif //CN1Log(@"Total memory is: %i", [ExecutableOp get_free_memory]); #ifndef CN1_USE_ARC [cp release]; diff --git a/Ports/iOSPort/nativeSources/CodenameOne_METALViewController.xib b/Ports/iOSPort/nativeSources/CodenameOne_METALViewController.xib new file mode 100644 index 0000000000..9c77bb07f7 --- /dev/null +++ b/Ports/iOSPort/nativeSources/CodenameOne_METALViewController.xib @@ -0,0 +1,164 @@ + + + + 1024 + 10J567 + 1305 + 1038.35 + 462.00 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 300 + + + YES + IBProxyObject + IBUIView + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + + + YES + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + + 274 + {320, 460} + + + + + 3 + MQA + + 2 + + + NO + IBCocoaTouchFramework + + + + + YES + + + view + + + + 3 + + + + + YES + + 0 + + + + + + -1 + + + File's Owner + + + -2 + + + + + 2 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 2.CustomClassName + 2.IBEditorWindowLastContentRect + 2.IBPluginDependency + + + YES + CodenameOne_GLViewController + UIResponder + METALView + {{401, 662}, {320, 460}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + + + + + YES + + + + + 4 + + + + YES + + METALView + UIView + + IBProjectSource + ./Classes/METALView.h + + + + CodenameOne_GLViewController + UIViewController + + IBProjectSource + ./Classes/CodenameOne_GLViewController.h + + + + + 0 + IBCocoaTouchFramework + + com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS + + + + com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 + + + YES + 3 + 300 + + diff --git a/Ports/iOSPort/nativeSources/DrawGradient.m b/Ports/iOSPort/nativeSources/DrawGradient.m index dae8d73681..b8e4931914 100644 --- a/Ports/iOSPort/nativeSources/DrawGradient.m +++ b/Ports/iOSPort/nativeSources/DrawGradient.m @@ -23,6 +23,9 @@ #import "DrawGradient.h" #import "CodenameOne_GLViewController.h" #import "DrawGradientTextureCache.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; @@ -123,6 +126,10 @@ -(id)initWithArgs:(int)typeA startColorA:(int)startColorA endColorA:(int)endColo } #ifdef USE_ES2 -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalDrawGradient(type, startColor, endColor, x, y, width, height, + relativeX, relativeY, relativeSize); +#else glUseProgram(getOGLProgram()); GLuint textureName = [DrawGradientTextureCache checkCache:type startColorA:startColor endColorA:endColor widthA:width heightA:height relativeXA:relativeX relativeYA:relativeY relativeSizeA:relativeSize]; int p2w = nextPowerOf2(width); @@ -275,9 +282,10 @@ -(void)execute { glBindTexture(GL_TEXTURE_2D, 0); GLErrorLog; - + //glUseProgram(CN1activeProgram); //GLErrorLog; +#endif // CN1_USE_METAL } #else diff --git a/Ports/iOSPort/nativeSources/DrawImage.m b/Ports/iOSPort/nativeSources/DrawImage.m index 8927b3c743..479a29a9f8 100644 --- a/Ports/iOSPort/nativeSources/DrawImage.m +++ b/Ports/iOSPort/nativeSources/DrawImage.m @@ -1,6 +1,9 @@ #import "DrawImage.h" #import "CodenameOne_GLViewController.h" #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; @@ -104,6 +107,9 @@ -(id)initWithArgs:(int)a xpos:(int)xpos ypos:(int)ypos i:(GLUIImage*)i w:(int)w } #ifdef USE_ES2 -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalDrawImage([img getMTLTexture], alpha, x, y, width, height); +#else glUseProgram(getOGLProgram()); GLKVector4 color = GLKVector4Make(((float)alpha) / 255.0f, ((float)alpha) / 255.0f, ((float)alpha) / 255.0f, ((float)alpha) / 255.0f); @@ -240,9 +246,10 @@ -(void)execute { glBindTexture(GL_TEXTURE_2D, 0); GLErrorLog; - + //glUseProgram(CN1activeProgram); //GLErrorLog; +#endif // CN1_USE_METAL } diff --git a/Ports/iOSPort/nativeSources/DrawLine.m b/Ports/iOSPort/nativeSources/DrawLine.m index 4943e33519..65fd745ddb 100644 --- a/Ports/iOSPort/nativeSources/DrawLine.m +++ b/Ports/iOSPort/nativeSources/DrawLine.m @@ -23,6 +23,9 @@ #import "DrawLine.h" #import "CodenameOne_GLViewController.h" #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; extern GLKMatrix4 CN1projectionMatrix; @@ -104,6 +107,9 @@ -(id)initWithArgs:(int)c a:(int)a xpos1:(int)xpos1 ypos1:(int)ypos1 xpos2:(int)x #ifdef USE_ES2 -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalDrawLine(color, alpha, x1, y1, x2, y2); +#else glUseProgram(getOGLProgram()); GLKVector4 colorV = GLKVector4Make(((float)((color >> 16) & 0xff))/255.0, \ @@ -157,9 +163,10 @@ -(void)execute { glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); GLErrorLog; } - + //glUseProgram(CN1activeProgram); //GLErrorLog; +#endif // CN1_USE_METAL } #else -(void)execute { diff --git a/Ports/iOSPort/nativeSources/DrawRect.m b/Ports/iOSPort/nativeSources/DrawRect.m index 91ef7cb3c1..343cc2b750 100644 --- a/Ports/iOSPort/nativeSources/DrawRect.m +++ b/Ports/iOSPort/nativeSources/DrawRect.m @@ -23,6 +23,9 @@ #import "DrawRect.h" #import "CodenameOne_GLViewController.h" #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; @@ -105,8 +108,11 @@ -(id)initWithArgs:(int)c a:(int)a xpos:(int)xpos ypos:(int)ypos w:(int)w h:(int) } #ifdef USE_ES2 -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalDrawRect(color, alpha, x, y, width, height); +#else glUseProgram(getOGLProgram()); - + GLKVector4 colorV = GLKVector4Make(((float)((color >> 16) & 0xff))/255.0, \ ((float)((color >> 8) & 0xff))/255.0, ((float)(color & 0xff))/255.0, ((float)alpha)/255.0); @@ -155,9 +161,10 @@ -(void)execute { //GLErrorLog; glDisableVertexAttribArray(vertexCoordAtt); GLErrorLog; - + //glUseProgram(CN1activeProgram); //GLErrorLog; +#endif // CN1_USE_METAL } #else -(void)execute { diff --git a/Ports/iOSPort/nativeSources/DrawString.m b/Ports/iOSPort/nativeSources/DrawString.m index b11675eb95..74891dd220 100644 --- a/Ports/iOSPort/nativeSources/DrawString.m +++ b/Ports/iOSPort/nativeSources/DrawString.m @@ -24,6 +24,9 @@ #import "CodenameOne_GLViewController.h" #import "DrawStringTextureCache.h" #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif extern float scaleValue; #ifdef USE_ES2 @@ -129,6 +132,9 @@ -(id)initWithArgs:(int)c a:(int)a xpos:(int)xpos ypos:(int)ypos s:(NSString*)s f #ifdef USE_ES2 -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalDrawString(str, font, color, alpha, x, y); +#else glUseProgram(getOGLProgram()); GLuint textureName = 0; DrawStringTextureCache *cachedTex = [DrawStringTextureCache checkCache:str f:font c:color a:255]; @@ -256,12 +262,13 @@ -(void)execute { glDisableVertexAttribArray(vertexCoordAtt); GLErrorLog; - + glBindTexture(GL_TEXTURE_2D, 0); GLErrorLog; - + //glUseProgram(CN1activeProgram); //GLErrorLog; +#endif // CN1_USE_METAL } #else diff --git a/Ports/iOSPort/nativeSources/DrawTextureAlphaMask.h b/Ports/iOSPort/nativeSources/DrawTextureAlphaMask.h index 87993f235f..00ce869e48 100644 --- a/Ports/iOSPort/nativeSources/DrawTextureAlphaMask.h +++ b/Ports/iOSPort/nativeSources/DrawTextureAlphaMask.h @@ -21,23 +21,45 @@ * need additional information or have any questions. */ #import "ExecutableOp.h" +#import "xmlvm.h" #ifdef USE_ES2 #import #else #import #endif +// textureName is a 64-bit handle: +// - On the GL build it's a GLuint zero-extended to JAVA_LONG. +// - On the Metal build it's a CFBridgingRetain'd id pointer +// (set up by IOSNative.m's nativePathRendererCreateTexture under +// CN1_USE_METAL and freed by nativeDeleteTexture). @interface DrawTextureAlphaMask : ExecutableOp { - GLuint textureName; + JAVA_LONG textureName; int color; int alpha; int x; int y; int w; int h; +#ifdef CN1_USE_METAL + // Snapshot of any RadialGradientPaint that was active when this op was + // queued. The mutable-graphics path's applyRadialGradientPaintMutable + // sets PaintOp.currentMutable synchronously and unapplyPaint clears it + // synchronously -- both before drainOps runs -- so reading the global + // paint state at execute time misses the gradient. Capturing here lets + // the op render the gradient correctly even after the global has been + // cleared. + BOOL hasRadialPaint; + int radialStartColor; + int radialEndColor; + int radialX; + int radialY; + int radialWidth; + int radialHeight; +#endif } --(id)initWithArgs:(GLuint)pTextName color:(int)pColor alpha:(int)pAlpha x:(int)pX y:(int)pY w:(int)pW h:(int)pH; +-(id)initWithArgs:(JAVA_LONG)pTextName color:(int)pColor alpha:(int)pAlpha x:(int)pX y:(int)pY w:(int)pW h:(int)pH; -(void)execute; @end diff --git a/Ports/iOSPort/nativeSources/DrawTextureAlphaMask.m b/Ports/iOSPort/nativeSources/DrawTextureAlphaMask.m index 2109f71b8f..f5068c1a49 100644 --- a/Ports/iOSPort/nativeSources/DrawTextureAlphaMask.m +++ b/Ports/iOSPort/nativeSources/DrawTextureAlphaMask.m @@ -25,6 +25,9 @@ #import "PaintOp.h" #import "RadialGradientPaint.h" #import "CodenameOne_GLViewController.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; @@ -275,7 +278,7 @@ -(void)updateMatrices { @implementation DrawTextureAlphaMask --(id)initWithArgs:(GLuint)pTextName color:(int)pColor alpha:(int)pAlpha x:(int)pX y:(int)pY w:(int)pW h:(int)pH +-(id)initWithArgs:(JAVA_LONG)pTextName color:(int)pColor alpha:(int)pAlpha x:(int)pX y:(int)pY w:(int)pW h:(int)pH { textureName = pTextName; color = pColor; @@ -284,20 +287,102 @@ -(id)initWithArgs:(GLuint)pTextName color:(int)pColor alpha:(int)pAlpha x:(int)p y = pY; w = pW; h = pH; +#ifdef CN1_USE_METAL + // The handle is a CFBridgingRetain'd id owned by the Java + // TextureAlphaMask object (which the textureCache holds only as a + // SoftWeak ref). With the concurrent GC, finalize() can run between + // op queueing and drawFrame draining; finalize -> dispose -> + // nativeDeleteTexture -> CFBridgingRelease drops the +1, and we'd + // sample a freed texture at execute time. Take an additional retain + // here so the texture survives until this op deallocs after drain. + if (textureName != 0) { + CFRetain((CFTypeRef)(void *)(uintptr_t)textureName); + } + // Snapshot the active RadialGradientPaint at queue time so the op + // renders the gradient even after the mutable-side unapplyPaint clears + // PaintOp.currentMutable. Read from currentMutable -- the screen path + // already queues a RadialGradientPaint op that runs in-order with + // execute, so its mutation happens between this op's queue and execute. + hasRadialPaint = NO; + PaintOp *snapshot = [PaintOp getCurrentMutable]; + if (snapshot != NULL && [snapshot isKindOfClass:[RadialGradientPaint class]]) { + RadialGradientPaint *g = (RadialGradientPaint *)snapshot; + hasRadialPaint = YES; + radialStartColor = g.startColor; + radialEndColor = g.endColor; + radialX = g.x; + radialY = g.y; + radialWidth = g.width; + radialHeight = g.height; + } +#endif return self; } + +#ifdef CN1_USE_METAL +-(void)dealloc { + if (textureName != 0) { + CFRelease((CFTypeRef)(void *)(uintptr_t)textureName); + } +#ifndef CN1_USE_ARC + [super dealloc]; +#endif +} +#endif #ifdef USE_ES2 -(void)execute { - +#ifdef CN1_USE_METAL + if (textureName == 0) { + CN1Log(@"Attempt to draw null alpha-mask texture. Skipping"); + return; + } + // textureName is a CFBridgingRetain'd id set up by + // IOSNative.m's nativePathRendererCreateTexture under Metal. Just bridge + // back to the Obj-C handle (no transfer of ownership) and dispatch. + id tex = (__bridge id)(void *)(uintptr_t)textureName; + // Resolve the radial-gradient paint differently per target: + // + // Mutable (target != nil): the Java-side applyPaint() and + // unapplyPaint() invoke applyRadialGradientPaintMutable / + // clearRadialGradientPaintMutable as direct C calls -- they set and + // clear PaintOp.currentMutable synchronously around the queue call, + // before drainOps runs. Reading PaintOp.currentMutable at execute + // time is too late; use the snapshot captured at init. + // + // Screen (target == nil): the Java-side applyPaint() invokes + // applyRadialGradientPaintGlobal which queues a RadialGradientPaint + // op into the same queue as this DrawTextureAlphaMask op. Its + // execute sets PaintOp.current just before our execute runs, so + // reading it here is correct. + if (target != nil && hasRadialPaint) { + CN1MetalDrawAlphaMaskRadial(tex, x, y, w, h, + radialStartColor, radialEndColor, + (float)radialX, (float)radialY, + (float)radialWidth, (float)radialHeight); + return; + } + if (target == nil) { + PaintOp *paint = [PaintOp getCurrent]; + if (paint != NULL && [paint isKindOfClass:[RadialGradientPaint class]]) { + RadialGradientPaint *g = (RadialGradientPaint *)paint; + CN1MetalDrawAlphaMaskRadial(tex, x, y, w, h, + g.startColor, g.endColor, + (float)g.x, (float)g.y, + (float)g.width, (float)g.height); + return; + } + } + CN1MetalDrawAlphaMask(tex, color, alpha, x, y, w, h); +#else //RadialGradientPaint * gp = [[RadialGradientPaint alloc ]initWithArgs:0 y:0 width:[CodenameOne_GLViewController instance].view.bounds.size.width*2 height:[CodenameOne_GLViewController instance].view.bounds.size.height*2 startColor:0x0 endColor:0xffffff]; //[PaintOp setCurrent:gp]; - + if ( textureName == 0 ){ CN1Log(@"Attempt to draw null texture. Skipping"); } DrawTextureAlphaMaskOGLProgram* p = getOGLProgram(); - + glUseProgram(p.program); float alph = ((float)alpha)/255.0; GLKVector4 colorV = GLKVector4Make(((float)((color >> 16) & 0xff))/255.0*alph, \ @@ -313,7 +398,7 @@ -(void)execute glActiveTexture(GL_TEXTURE0); GLErrorLog; - glBindTexture(GL_TEXTURE_2D, textureName); + glBindTexture(GL_TEXTURE_2D, (GLuint)textureName); GLErrorLog; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); GLErrorLog; @@ -399,9 +484,10 @@ -(void)execute glDisableVertexAttribArray(p.vertexCoordAtt); GLErrorLog; - + glBindTexture(GL_TEXTURE_2D, 0); GLErrorLog; +#endif // CN1_USE_METAL } #else -(void)execute {} diff --git a/Ports/iOSPort/nativeSources/EAGLView.h b/Ports/iOSPort/nativeSources/EAGLView.h index 8801b88dc5..1ea9802f52 100644 --- a/Ports/iOSPort/nativeSources/EAGLView.h +++ b/Ports/iOSPort/nativeSources/EAGLView.h @@ -27,13 +27,14 @@ #import #import #import "GLUIImage.h" +#import "CN1RenderingView.h" @class EAGLContext; // This class wraps the CAEAGLLayer from CoreAnimation into a convenient UIView subclass. // The view content is basically an EAGL surface you render your OpenGL scene into. // Note that setting the view non-opaque will only work if the EAGL surface has an alpha channel. -@interface EAGLView : UIView { +@interface EAGLView : UIView { @private // The pixel dimensions of the CAEAGLLayer. GLint framebufferWidth; @@ -56,5 +57,4 @@ -(void) keyboardDoneClicked; -(void) keyboardNextClicked; -(void) addPeerComponent:(UIView*) view; --(void) removePeerComponent:(UIView*) view; @end diff --git a/Ports/iOSPort/nativeSources/ExecutableOp.h b/Ports/iOSPort/nativeSources/ExecutableOp.h index 722083b8cd..78d0fa3ed0 100644 --- a/Ports/iOSPort/nativeSources/ExecutableOp.h +++ b/Ports/iOSPort/nativeSources/ExecutableOp.h @@ -21,6 +21,11 @@ * need additional information or have any questions. */ #import +// CN1ES2compat.h gates CN1_USE_METAL (the iOS builder uncomments +// //#define CN1_USE_METAL at the top). The ExecutableOp.target ivar +// and accessors below are conditional on CN1_USE_METAL, so .h and .m +// must both see the same definition. Importing here ensures that. +#import "CN1ES2compat.h" extern void logGlErrorAt(const char *f, int l); extern int nextPowerOf2(int val); @@ -48,8 +53,16 @@ green:((float)((rgbValue >> 8) & 0xff))/255.0 blue:((float)(rgbValue & 0xff))/25 #endif -@interface ExecutableOp : NSObject { +@class GLUIImage; +@interface ExecutableOp : NSObject { +#ifdef CN1_USE_METAL + // Phase 3: render target for this op. nil = screen drawable (default, + // existing GL/Metal screen pipeline). non-nil = a mutable image whose + // backing MTLTexture should receive this op. drawFrame walks the queue + // and switches encoders when target changes between ops. + __unsafe_unretained GLUIImage *target; +#endif } +(natural_t) get_free_memory; @@ -58,4 +71,8 @@ green:((float)((rgbValue >> 8) & 0xff))/255.0 blue:((float)(rgbValue & 0xff))/25 -(void)execute; -(void)executeWithLog; -(NSString*)getName; +#ifdef CN1_USE_METAL +-(GLUIImage*)target; +-(void)setTarget:(GLUIImage*)t; +#endif @end diff --git a/Ports/iOSPort/nativeSources/ExecutableOp.m b/Ports/iOSPort/nativeSources/ExecutableOp.m index 64eebf3157..c20f1df167 100644 --- a/Ports/iOSPort/nativeSources/ExecutableOp.m +++ b/Ports/iOSPort/nativeSources/ExecutableOp.m @@ -120,4 +120,13 @@ -(NSString*)getName { return nil; } +#ifdef CN1_USE_METAL +-(GLUIImage*)target { + return target; +} +-(void)setTarget:(GLUIImage*)t { + target = t; +} +#endif + @end diff --git a/Ports/iOSPort/nativeSources/FillPolygon.m b/Ports/iOSPort/nativeSources/FillPolygon.m index 6b1aa43502..3ca61bccbe 100644 --- a/Ports/iOSPort/nativeSources/FillPolygon.m +++ b/Ports/iOSPort/nativeSources/FillPolygon.m @@ -33,6 +33,9 @@ #import #import "CN1ES2compat.h" #import "xmlvm.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 @@ -122,8 +125,11 @@ -(id)initWithArgs:(JAVA_FLOAT*)xCoords y:(JAVA_FLOAT*)yCoords num:(int)num color } -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalFillPolygon(x, y, numPoints, color, alpha); +#else glUseProgram(getOGLProgram()); - + float alph = ((float)alpha)/255.0; GLKVector4 colorV = GLKVector4Make(((float)((color >> 16) & 0xff))/255.0 * alph, @@ -186,11 +192,11 @@ -(void)execute //glUseProgram(CN1activeProgram); //GLErrorLog; - - + + // ---------- end - +#endif // CN1_USE_METAL } -(void)dealloc diff --git a/Ports/iOSPort/nativeSources/FillRect.m b/Ports/iOSPort/nativeSources/FillRect.m index a6e18120a5..f8236ed616 100644 --- a/Ports/iOSPort/nativeSources/FillRect.m +++ b/Ports/iOSPort/nativeSources/FillRect.m @@ -24,6 +24,9 @@ #import "CodenameOne_GLViewController.h" #include "xmlvm.h" #include "TargetConditionals.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 extern GLKMatrix4 CN1modelViewMatrix; @@ -107,6 +110,9 @@ -(id)initWithArgs:(int)c a:(int)a xpos:(int)xpos ypos:(int)ypos w:(int)w h:(int) } #ifdef USE_ES2 -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalFillRect(color, alpha, x, y, width, height); +#else //[UIColorFromRGB(color, alpha) set]; //CGContextFillRect(context, CGRectMake(x, y, width, height)); //GlColorFromRGB(color, alpha); @@ -168,9 +174,10 @@ -(void)execute { glDisableVertexAttribArray(vertexCoordAtt); GLErrorLog; - + //glUseProgram(CN1activeProgram); //GLErrorLog; +#endif } #else -(void)execute { diff --git a/Ports/iOSPort/nativeSources/GLUIImage.h b/Ports/iOSPort/nativeSources/GLUIImage.h index 81869c2f30..a156265ea2 100644 --- a/Ports/iOSPort/nativeSources/GLUIImage.h +++ b/Ports/iOSPort/nativeSources/GLUIImage.h @@ -28,6 +28,10 @@ #import #import #import +#import "CN1ES2compat.h" +#ifdef CN1_USE_METAL +@import Metal; +#endif @interface GLUIImage : NSObject { UIImage* img; @@ -35,6 +39,33 @@ NSString* name; int textureWidth; int textureHeight; +#ifdef CN1_USE_METAL + id mtlTexture; + // Phase 3 v2: mutable-image render target. Allocated lazily by + // CN1MetalEnsureMutableTexture sized to the mutable image's logical + // dimensions. drawFrame opens an MTLRenderCommandEncoder against this + // texture for any queued op whose target == this GLUIImage. Pixels + // accumulate across frames via MTLLoadActionLoad. The screen-side + // drawImage pipeline samples this texture (getMTLTexture returns it + // when present, else falls back to building from the UIImage). + id mtlMutableTexture; + int mtlMutableWidth; + int mtlMutableHeight; + // The most-recently-committed command buffer that wrote to + // mtlMutableTexture. Readback paths (Image.getRGB, PNG/JPEG encode, + // toImage, cross-image consumption) call waitUntilCompleted on this + // before sampling the texture. nil = no pending GPU work, safe to read. + id mtlMutableCommandBuffer; + // Initial fill colour passed to createNativeMutableImage(w, h, argb) + // -- 0xAARRGGBB. CN1MetalEnsureMutableTexture clears the freshly- + // allocated mtlMutableTexture to this colour so mutable images behave + // like the CG path's UIRectFill(argb). Sentinel 0 means "honor + // CN1's createImage(w,h) default of 0xffffffff opaque white" -- + // a literal argb of 0 (fully transparent black) is also the Metal + // texture's natural cleared state, so the sentinel collision is a + // no-op in practice. + int mtlMutableInitialARGB; +#endif } -(id)initWithImage:(UIImage*)i; -(UIImage*)getImage; @@ -43,4 +74,25 @@ -(void)setImage:(UIImage*)i; -(int)getTextureWidth; -(int)getTextureHeight; +#ifdef CN1_USE_METAL +// Lazily build (and cache on the GLUIImage instance) an MTLTexture for +// this image. Invalidated automatically by setImage:. nil if the device +// is unavailable or the image is empty. If a mutable texture exists +// (Phase 3 mutable-image render target), that is returned instead -- it +// is the freshest pixel source. +-(id)getMTLTexture; + +// Phase 3 v2 mutable-image accessors. CN1Metalcompat owns the lifecycle; +// these are the storage hooks. Accessors only -- consumers outside this +// file route through the CN1Metal*MutableImage API rather than poking +// these directly. +-(id)mtlMutableTexture; +-(void)setMtlMutableTexture:(id)t width:(int)w height:(int)h; +-(int)mtlMutableWidth; +-(int)mtlMutableHeight; +-(id)mtlMutableCommandBuffer; +-(void)setMtlMutableCommandBuffer:(id)cb; +-(int)mtlMutableInitialARGB; +-(void)setMtlMutableInitialARGB:(int)argb; +#endif @end diff --git a/Ports/iOSPort/nativeSources/GLUIImage.m b/Ports/iOSPort/nativeSources/GLUIImage.m index cfce49800d..273e43dce1 100644 --- a/Ports/iOSPort/nativeSources/GLUIImage.m +++ b/Ports/iOSPort/nativeSources/GLUIImage.m @@ -24,6 +24,9 @@ #import "CodenameOne_GLViewController.h" #import #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif extern int nextPowerOf2(int val); @@ -131,6 +134,14 @@ -(void)setImage:(UIImage*)i { img = i; #ifndef CN1_USE_ARC [img retain]; +#endif +#ifdef CN1_USE_METAL + // Invalidate the cached MTLTexture — it was built from the previous + // UIImage's pixels. The CN1MetalTextureFromUIImage assignment + // transferred a +1 retain; release it explicitly so swapping the + // backing UIImage doesn't leak the old GPU texture. + [mtlTexture release]; + mtlTexture = nil; #endif if(textureName != 0) { int tname = textureName; @@ -156,9 +167,57 @@ -(void)setName:(NSString*)s { #endif } +#ifdef CN1_USE_METAL +-(id)getMTLTexture { + // Phase 3 v2: a mutable-image render target, if present, is the freshest + // pixel source. Screen-side DrawImage samples this; the cached UIImage- + // derived mtlTexture is only relevant for never-drawn-into images. + if (mtlMutableTexture != nil) return mtlMutableTexture; + if (mtlTexture != nil) return mtlTexture; + if (img == nil) return nil; + mtlTexture = CN1MetalTextureFromUIImage(img); + return mtlTexture; +} + +-(id)mtlMutableTexture { return mtlMutableTexture; } +-(void)setMtlMutableTexture:(id)t width:(int)w height:(int)h { + // Retain new, release old. Under MRR direct ivar assignment doesn't + // auto-retain; without this the new texture would be autoreleased + // out from under us when the next pool drains. Same fix pattern as + // CN1MetalGlyphAtlas (commit b9c5add52). Setting the same texture + // again is rare in practice but the retain-then-release order is + // safe regardless. + [t retain]; + [mtlMutableTexture release]; + mtlMutableTexture = t; + mtlMutableWidth = w; + mtlMutableHeight = h; + // Stale cached read-only texture: future getMTLTexture should sample + // mtlMutableTexture instead of the UIImage-derived one. Release the + // +1 retain transferred in by getMTLTexture's CN1MetalTextureFromUIImage + // assignment; without this the read-only texture leaks. + [mtlTexture release]; + mtlTexture = nil; +} +-(int)mtlMutableWidth { return mtlMutableWidth; } +-(int)mtlMutableHeight { return mtlMutableHeight; } +-(id)mtlMutableCommandBuffer { return mtlMutableCommandBuffer; } +-(void)setMtlMutableCommandBuffer:(id)cb { + // [queue commandBuffer] returns an autoreleased object; without + // retaining it here, the cb dangles after the next pool drain and + // [cb commit] / [cb waitUntilCompleted] crash later. Same MRR + // discipline as setMtlMutableTexture above. + [cb retain]; + [mtlMutableCommandBuffer release]; + mtlMutableCommandBuffer = cb; +} +-(int)mtlMutableInitialARGB { return mtlMutableInitialARGB; } +-(void)setMtlMutableInitialARGB:(int)argb { mtlMutableInitialARGB = argb; } +#endif + -(void)dealloc { if(name != nil) { - //CN1Log(@"Deleting image name %@", name); + //CN1Log(@"Deleting image name %@", name); #ifndef CN1_USE_ARC [name release]; #endif @@ -174,10 +233,23 @@ -(void)dealloc { //int fm = [ExecutableOp get_free_memory]; glDeleteTextures(1, &tname); GLErrorLog; - //CN1Log(@"Texture deletion freed up: %i", [ExecutableOp get_free_memory] - fm); + //CN1Log(@"Texture deletion freed up: %i", [ExecutableOp get_free_memory] - fm); }); } } +#ifdef CN1_USE_METAL + // Both ivars hold a +1 MTLTexture retain (newTextureWithDescriptor / + // CN1MetalTextureFromUIImage both return owned references). Without + // these explicit releases under MRR every transient Metal-backed image + // leaks a GPU texture: the animation/transition test suite creates + // 7 mutable images per test × ~17 tests, and the simulator runs out + // of Metal device memory mid-suite, hanging the next test. + [mtlTexture release]; mtlTexture = nil; + [mtlMutableTexture release]; mtlMutableTexture = nil; + // Same +1 retain ownership rule for the cached command buffer (the + // setter retains; dealloc must release). + [mtlMutableCommandBuffer release]; mtlMutableCommandBuffer = nil; +#endif #ifndef CN1_USE_ARC [img release]; [super dealloc]; diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 9336dd1c1a..44ce0c89ba 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -28,6 +28,9 @@ #include "xmlvm.h" #include "java_lang_String.h" #import "CN1ES2compat.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #import #ifndef NEW_CODENAME_ONE_VM @@ -437,6 +440,15 @@ void com_codename1_impl_ios_IOSNative_initVM__(CN1_THREAD_STATE_MULTI_ARG JAVA_O POOL_END(); } +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isMetalRendering__(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) +{ +#ifdef CN1_USE_METAL + return JAVA_TRUE; +#else + return JAVA_FALSE; +#endif +} + void xmlvm_init_native_com_codename1_impl_ios_IOSNative() { } @@ -780,8 +792,34 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_gausianBlurImage___long_float(CN1_THR Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl(); } - UIImage* original = [glu getImage]; - + UIImage* original = nil; +#ifdef CN1_USE_METAL + // On Metal the mutable's pixels live in mtlMutableTexture, not in + // [glu getImage]; the latter returns the original (likely empty) + // UIImage that was used to construct the GLUIImage. Read the GPU + // texture back to a UIImage so CIGaussianBlur sees actual pixels. + // Switch's createRoundThumbImage depends on this -- without it the + // blur runs on transparent input, returns empty, and the pre-blur + // shadow rings end up showing through as visible artefacts on the + // final thumb composite. + if ([glu mtlMutableTexture] != nil) { + // Force drawFrame to drain any pending ExecutableOps for this image + // before sampling. Without the flush the GPU never executes the + // shadow-ring fillArc calls; CN1MetalReadMutableImageAsUIImage + // would then sample the cleared (zero-alpha) texture and the blur + // input is empty. Mirrors imageRgbToIntArrayImpl's drain dance. + extern int displayWidth; + extern int displayHeight; + [[CodenameOne_GLViewController instance] flushBuffer:nil x:0 y:0 width:displayWidth height:displayHeight]; + original = CN1MetalReadMutableImageAsUIImage(glu); + } + if (original == nil) { + original = [glu getImage]; + } +#else + original = [glu getImage]; +#endif + // taken from: http://stackoverflow.com/a/19433086/756809 CIFilter *gaussianBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; [gaussianBlurFilter setDefaults]; @@ -789,14 +827,14 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_gausianBlurImage___long_float(CN1_THR [gaussianBlurFilter setValue:inputImage forKey:kCIInputImageKey]; NSNumber *radiusNumber = [NSNumber numberWithFloat:radius]; [gaussianBlurFilter setValue:radiusNumber forKey:kCIInputRadiusKey]; - + CIImage *outputImage = [gaussianBlurFilter outputImage]; CIContext *context = [CIContext contextWithOptions:nil]; CGImageRef cgimg = [context createCGImage:outputImage fromRect:[inputImage extent]]; UIImage *image = [UIImage imageWithCGImage:cgimg]; CGImageRelease(cgimg); GLUIImage* gl = [[GLUIImage alloc] initWithImage:image]; - + POOL_END(); return (BRIDGE_CAST void*)gl; } @@ -1023,23 +1061,35 @@ void com_codename1_impl_ios_IOSNative_nativeDrawShadowMutable___long_int_int_int static CGContextRef drawPath(CN1_THREAD_STATE_MULTI_ARG JAVA_INT commandsLen, JAVA_OBJECT commandsArr, JAVA_INT pointsLen, JAVA_OBJECT pointsArr) { return Java_com_codename1_impl_ios_IOSImplementation_drawPath(CN1_THREAD_STATE_PASS_ARG commandsLen, commandsArr, pointsLen, pointsArr); - - + + } //native void nativeFillShapeMutable(int color, int alpha, int commandsLen, byte[] commandsArr, int pointsLen, float[] pointsArr); void com_codename1_impl_ios_IOSNative_nativeFillShapeMutable___int_int_int_byte_1ARRAY_int_float_1ARRAY(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT color, JAVA_INT alpha, JAVA_INT commandsLen, JAVA_OBJECT commandsArr, JAVA_INT pointsLen, JAVA_OBJECT pointsArr) { +#ifdef CN1_USE_METAL + // Dead under Metal -- MutableGraphics.nativeFillShape now routes + // through createAlphaMask + drawTextureAlphaMask (alpha-mask Metal + // pipeline tagged with currentMutableImage). The Java side gates + // with `metalRendering` before calling this JNI. + (void)color; (void)alpha; (void)commandsLen; (void)commandsArr; (void)pointsLen; (void)pointsArr; +#else POOL_BEGIN(); [UIColorFromRGB(color, alpha) set]; CGContextRef context = drawPath(CN1_THREAD_STATE_PASS_ARG commandsLen, commandsArr, pointsLen, pointsArr); CGContextFillPath(context); POOL_END(); - +#endif } void com_codename1_impl_ios_IOSNative_nativeDrawShapeMutable___int_int_int_byte_1ARRAY_int_float_1ARRAY_float_int_int_float(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT color, JAVA_INT alpha, JAVA_INT commandsLen, JAVA_OBJECT commandsArr, JAVA_INT pointsLen, JAVA_OBJECT pointsArr, JAVA_FLOAT lineWidth, JAVA_INT capStyle, JAVA_INT joinStyle, JAVA_FLOAT mitreLimit) { +#ifdef CN1_USE_METAL + // Same rationale as nativeFillShapeMutable above. + (void)color; (void)alpha; (void)commandsLen; (void)commandsArr; (void)pointsLen; (void)pointsArr; + (void)lineWidth; (void)capStyle; (void)joinStyle; (void)mitreLimit; +#else POOL_BEGIN(); if ([CodenameOne_GLViewController isCurrentMutableTransformSet]) { CGContextSaveGState(UIGraphicsGetCurrentContext()); @@ -1094,6 +1144,7 @@ void com_codename1_impl_ios_IOSNative_nativeDrawShapeMutable___int_int_int_byte_ CGContextRestoreGState(context); } POOL_END(); +#endif } @@ -2429,6 +2480,31 @@ void com_codename1_impl_ios_IOSNative_fillLinearGradientGlobal___int_int_int_int } void com_codename1_impl_ios_IOSNative_fillRectRadialGradientMutable___int_int_int_int_int_int_float_float_float(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT n1, JAVA_INT n2, JAVA_INT n3, JAVA_INT n4, JAVA_INT width, JAVA_INT height, JAVA_FLOAT relativeX, JAVA_FLOAT relativeY, JAVA_FLOAT relativeSize) { +#ifdef CN1_USE_METAL + { + // Phase 3 v2: route through ExecutableOp queue. type=1 is + // GRAD_TYPE_RADIAL inside DrawGradient; CN1MetalDrawGradient + // handles the radial branch identically to the global path. + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + DrawGradient *d = [[DrawGradient alloc] initWithArgs:1 + startColorA:n1 + endColorA:n2 + xA:n3 + yA:n4 + widthA:width + heightA:height + relativeXA:relativeX + relativeYA:relativeY + relativeSizeA:relativeSize]; + [d setTarget:target]; + [CodenameOne_GLViewController upcoming:d]; +#ifndef CN1_USE_ARC + [d release]; +#endif + return; + } +#endif POOL_BEGIN(); float alpha1 = 1.0; if (((n1 >> 24) & 0xff) != 0) { @@ -2467,6 +2543,32 @@ void com_codename1_impl_ios_IOSNative_fillRectRadialGradientMutable___int_int_in } void com_codename1_impl_ios_IOSNative_fillLinearGradientMutable___int_int_int_int_int_int_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_INT n1, JAVA_INT n2, JAVA_INT n3, JAVA_INT n4, JAVA_INT width, JAVA_INT height, JAVA_BOOLEAN n7) { +#ifdef CN1_USE_METAL + { + // Phase 3 v2: route the mutable linear-gradient through the + // ExecutableOp queue so it lands in the mutable's MTLTexture, + // not the now-nil UIGraphicsGetCurrentContext(). 2 = horizontal, + // 3 = vertical -- matches DrawGradient's enum. + GLUIImage *target = [CodenameOne_GLViewController instance].currentMutableImage; + if (target == nil) return; + DrawGradient *d = [[DrawGradient alloc] initWithArgs:(n7 ? 2 : 3) + startColorA:n1 + endColorA:n2 + xA:n3 + yA:n4 + widthA:width + heightA:height + relativeXA:0 + relativeYA:0 + relativeSizeA:0]; + [d setTarget:target]; + [CodenameOne_GLViewController upcoming:d]; +#ifndef CN1_USE_ARC + [d release]; +#endif + return; + } +#endif POOL_BEGIN(); float alpha1 = 1.0; @@ -6064,11 +6166,75 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_createImageFile___long_boolean_int_in __block NSData* data = nil; #ifndef CN1_USE_ARC [(BRIDGE_CAST GLUIImage*)((void *)imagePeer) retain]; +#endif +#ifdef CN1_USE_METAL + // Phase 3 v2: PNG/JPEG encoding sources from [GLUIImage getImage] which + // is the original UIImage backing — initial-fill colour for any mutable + // that's been drawn into via Metal. Drain the op queue first so the + // mutable's MTLTexture has the latest pixels, then read those pixels + // into a fresh UIImage and encode that. flushBuffer already dispatches + // sync to the main thread and runs drawFrame; doing it OUTSIDE the + // dispatch_sync block below avoids the nested-dispatch_sync deadlock + // that would otherwise occur (we'd be waiting on main to run drawFrame + // while main is waiting on us to free the dispatch_sync slot). + { + GLUIImage *glllOuter = (BRIDGE_CAST GLUIImage*)((void *)imagePeer); + if ([glllOuter mtlMutableTexture] != nil) { + BOOL stillDrawing = (((BRIDGE_CAST void*)[CodenameOne_GLViewController instance].currentMutableImage) == ((void *)imagePeer)); + if (stillDrawing) { + Java_com_codename1_impl_ios_IOSImplementation_finishDrawingOnImageImpl(); + } + int dw = Java_com_codename1_impl_ios_IOSImplementation_getDisplayWidthImpl(); + int dh = Java_com_codename1_impl_ios_IOSImplementation_getDisplayHeightImpl(); + [[CodenameOne_GLViewController instance] flushBuffer:nil x:0 y:0 width:dw height:dh]; + if (stillDrawing) { + int restoreW = [glllOuter mtlMutableWidth]; + int restoreH = [glllOuter mtlMutableHeight]; + Java_com_codename1_impl_ios_IOSImplementation_startDrawingOnImageImpl(restoreW, restoreH, (void *)imagePeer); + } + } + } #endif dispatch_sync(dispatch_get_main_queue(), ^{ POOL_BEGIN(); GLUIImage* glll = (BRIDGE_CAST GLUIImage*)((void *)imagePeer); - UIImage* i = [glll getImage]; + UIImage* i = nil; +#ifdef CN1_USE_METAL + // If the image has live Metal pixels, blit-and-read into a fresh + // UIImage so PNG/JPEG encoding sees post-draw content rather than + // the stale UIImage initial-fill backing. + if ([glll mtlMutableTexture] != nil) { + int srcW = [glll mtlMutableWidth]; + int srcH = [glll mtlMutableHeight]; + if (srcW > 0 && srcH > 0) { + int *pixels = (int *)malloc((size_t)srcW * (size_t)srcH * sizeof(int)); + if (pixels != NULL) { + if (CN1MetalReadMutableImagePixels(glll, pixels, 0, 0, srcW, srcH, srcW, srcH)) { + // CN1MetalReadMutableImagePixels writes ARGB ints; wrap as a + // CGImage with RGBA byte order (the alpha-first/last and + // R/B swap matches what UIKit expects for iOS little-endian). + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctx = CGBitmapContextCreate(pixels, (size_t)srcW, (size_t)srcH, 8, + (size_t)srcW * 4, cs, + kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); + CGColorSpaceRelease(cs); + if (ctx != NULL) { + CGImageRef cgImg = CGBitmapContextCreateImage(ctx); + CGContextRelease(ctx); + if (cgImg != NULL) { + i = [UIImage imageWithCGImage:cgImg scale:1.0 orientation:UIImageOrientationUp]; + CGImageRelease(cgImg); + } + } + } + free(pixels); + } + } + } +#endif + if (i == nil) { + i = [glll getImage]; + } if(width == -1) { float aspect = height / i.size.height; blockWidth = (int)(i.size.width * aspect); @@ -6085,7 +6251,7 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_createImageFile___long_boolean_int_in } else { data = UIImagePNGRepresentation(i); } - + #ifndef CN1_USE_ARC [data retain]; #endif @@ -8382,22 +8548,29 @@ void com_codename1_impl_ios_IOSNative_nativeDrawPath___int_int_long(CN1_THREAD_S } -extern void Java_com_codename1_impl_ios_IOSImplementation_drawTextureAlphaMaskImpl(GLuint textureName, int color, int alpha, int x, int y, int w, int h); +extern void Java_com_codename1_impl_ios_IOSImplementation_drawTextureAlphaMaskImpl(JAVA_LONG textureName, int color, int alpha, int x, int y, int w, int h); void com_codename1_impl_ios_IOSNative_drawTextureAlphaMask___long_int_int_int_int_int_int(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG textureName, JAVA_INT color, JAVA_INT alpha, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h) { - Java_com_codename1_impl_ios_IOSImplementation_drawTextureAlphaMaskImpl((GLuint)textureName, color, alpha, x, y, w, h); - - + Java_com_codename1_impl_ios_IOSImplementation_drawTextureAlphaMaskImpl(textureName, color, alpha, x, y, w, h); + + } void com_codename1_impl_ios_IOSNative_nativeDeleteTexture___long(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_LONG textureName) { + if (textureName == 0) return; +#ifdef CN1_USE_METAL + // Texture handle is a CFBridgingRetain'd id; release it to + // drop the retain that nativePathRendererCreateTexture took. + CFBridgingRelease((CFTypeRef)(void *)(uintptr_t)textureName); +#else dispatch_async(dispatch_get_main_queue(), ^{ GLuint tex = (GLuint)textureName; //POOL_BEGIN(); glDeleteTextures(1, &tex); //POOL_END(); }); +#endif } @@ -8467,8 +8640,46 @@ JAVA_OBJECT com_codename1_impl_ios_IOSNative_nativePathRendererToARGB___long_int JAVA_LONG com_codename1_impl_ios_IOSNative_nativePathRendererCreateTexture___long(JAVA_OBJECT instanceObject, JAVA_LONG renderer) { +#ifdef CN1_USE_METAL + { + Renderer *r = (Renderer*)renderer; + JAVA_INT outputBounds[4]; + Renderer_getOutputBounds(renderer, (JAVA_INT*)&outputBounds); + if (outputBounds[2] < 0 || outputBounds[3] < 0) return 0; + JAVA_INT x = min(outputBounds[0], outputBounds[2]); + JAVA_INT y = min(outputBounds[1], outputBounds[3]); + JAVA_INT width = outputBounds[2] - outputBounds[0]; + JAVA_INT height = outputBounds[3] - outputBounds[1]; + if (width < 0) width = -width; + if (height < 0) height = -height; + if (width == 0 || height == 0) return 0; + AlphaConsumer ac; + ac.originX = x; ac.originY = y; ac.width = width; ac.height = height; + jbyte *maskArray = malloc(sizeof(jbyte) * ac.width * ac.height); + ac.alphas = maskArray; + Renderer_produceAlphas(renderer, &ac); + // Build R8 MTLTexture from the alpha bytes; CFBridgingRetain so the + // Java-side handle (returned as JAVA_LONG) keeps the texture alive + // until nativeDeleteTexture releases it. + id tex = CN1MetalCreateAlphaMaskTexture((const uint8_t *)maskArray, width, height); + free(maskArray); + if (tex == nil) return 0; + // Under MRR, CFBridgingRetain calls CFRetain (no ownership transfer + // like ARC's __bridge_retained). CN1MetalCreateAlphaMaskTexture + // returns a +1 (newTextureWithDescriptor), and CFBridgingRetain adds + // a second +1, for a net +2. Java's nativeDeleteTexture only + // CFBridgingReleases once on dispose, so we'd leak one full alpha- + // mask MTLTexture per drawShape call. Release the local tex once + // CF holds its retain via CFBridgingRetain. + JAVA_LONG handle = (JAVA_LONG)(uintptr_t)CFBridgingRetain(tex); +#ifndef CN1_USE_ARC + [tex release]; +#endif + return handle; + } +#endif #ifdef USE_ES2 - + __block JAVA_LONG outTexture = NULL; dispatch_sync(dispatch_get_main_queue(), ^{ @@ -8914,6 +9125,10 @@ JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isPainted___R_boolean(CN1_THREAD_S return com_codename1_impl_ios_IOSNative_isPainted__(CN1_THREAD_STATE_PASS_ARG instanceObject); } +JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isMetalRendering___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { + return com_codename1_impl_ios_IOSNative_isMetalRendering__(CN1_THREAD_STATE_PASS_ARG instanceObject); +} + JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_nativeIsAlphaMaskSupportedGlobal___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { return com_codename1_impl_ios_IOSNative_nativeIsAlphaMaskSupportedGlobal__(instanceObject); diff --git a/Ports/iOSPort/nativeSources/METALView.h b/Ports/iOSPort/nativeSources/METALView.h index 47e667b460..0583764408 100644 --- a/Ports/iOSPort/nativeSources/METALView.h +++ b/Ports/iOSPort/nativeSources/METALView.h @@ -17,35 +17,47 @@ * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * - * Please contact Codename One through http://www.codenameone.com/ if you + * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ +#import "CN1ES2compat.h" #ifdef CN1_USE_METAL #import +#import @import Metal; +@import simd; #import "GLUIImage.h" +#import "CN1RenderingView.h" -// This class wraps the CAEAGLLayer from CoreAnimation into a convenient UIView subclass. -// The view content is basically an EAGL surface you render your OpenGL scene into. -// Note that setting the view non-opaque will only work if the EAGL surface has an alpha channel. -@interface METALView : UIView { +// Metal-backed rendering view. Wraps a CAMetalLayer into a UIView subclass. +// Gated by CN1_USE_METAL; the OpenGL ES 2 backend (EAGLView) is the default. +@interface METALView : UIView { @private - // The pixel dimensions of the CAEAGLLayer. + // The pixel dimensions of the CAMetalLayer's drawable. int framebufferWidth; int framebufferHeight; - - // The OpenGL ES names for the framebuffer and renderbuffer used to render to this view. - GLuint defaultFramebuffer, colorRenderbuffer; - + + // Orthographic projection matrix sized to (framebufferWidth, framebufferHeight). + // Rebuilt by updateFrameBufferSize:h: and uploaded to shaders as a uniform. + simd_float4x4 projectionMatrix; } -@property (nonatomic, retain) MTLCommandQueue* commandQueue; -@property (nonatomic, retain) MTLCommandBuffer* commandBuffer; +@property (nonatomic, retain) id commandQueue; +@property (nonatomic, retain) id commandBuffer; @property (nonatomic, retain) MTLRenderPassDescriptor* renderPassDescriptor; -@property (nonatomic, retain) MTLRenderCommandEncoder* renderCommandEncoder; -@property (nonatomic, retain) MTLDrawable* drawable; +@property (nonatomic, retain) id renderCommandEncoder; +@property (nonatomic, retain) id drawable; +// Persistent offscreen render target that accumulates ops across frames. +// CN1's drawFrame only queues the ops that have changed since the previous +// frame; on OpenGL the renderbuffer persists, so that works. Metal drawables +// are ephemeral (each is cleared on acquire), so we render into this +// reusable texture and blit it to the drawable at present time. +@property (nonatomic, retain) id screenTexture; @property (nonatomic, retain) UIView* peerComponentsLayer; +@property (nonatomic, readonly) int framebufferWidth; +@property (nonatomic, readonly) int framebufferHeight; +@property (nonatomic, readonly) simd_float4x4 projectionMatrix; -(void)textViewDidChange:(UITextView *)textView; -(void)deleteFramebuffer; @@ -56,6 +68,5 @@ -(void) keyboardDoneClicked; -(void) keyboardNextClicked; -(void) addPeerComponent:(UIView*) view; --(void) removePeerComponent:(UIView*) view; @end #endif \ No newline at end of file diff --git a/Ports/iOSPort/nativeSources/METALView.m b/Ports/iOSPort/nativeSources/METALView.m index ea036c3eda..21b85ed535 100644 --- a/Ports/iOSPort/nativeSources/METALView.m +++ b/Ports/iOSPort/nativeSources/METALView.m @@ -20,10 +20,14 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ +#import "CN1ES2compat.h" #ifdef CN1_USE_METAL #import +@import Metal; +@import simd; #import "METALView.h" +#import "CN1Metalcompat.h" #import "ExecutableOp.h" #import "CodenameOne_GLViewController.h" #include "com_codename1_impl_ios_IOSImplementation.h" @@ -37,18 +41,32 @@ extern BOOL isVKBAlwaysOpen(); extern void repaintUI(); -@interface METALView (PrivateMethods) -- (void)createFramebuffer; -- (void)deleteFramebuffer; -@end - @implementation METALView @synthesize commandQueue; @synthesize commandBuffer; @synthesize renderPassDescriptor; @synthesize renderCommandEncoder; +@synthesize drawable; +@synthesize screenTexture; @synthesize peerComponentsLayer; +@synthesize framebufferWidth; +@synthesize framebufferHeight; +@synthesize projectionMatrix; + +static simd_float4x4 CN1MetalOrtho(float left, float right, float bottom, float top, float near, float far) { + // Metal NDC: x,y in [-1,1], z in [0,1]. Column-major construction matching Apple's conventions. + float rl = 1.0f / (right - left); + float tb = 1.0f / (top - bottom); + float fn = 1.0f / (far - near); + simd_float4x4 m = (simd_float4x4){{ + { 2.0f * rl, 0.0f, 0.0f, 0.0f }, + { 0.0f, 2.0f * tb, 0.0f, 0.0f }, + { 0.0f, 0.0f, -fn, 0.0f }, + { -(right + left) * rl, -(top + bottom) * tb, -near * fn, 1.0f } + }}; + return m; +} // You must implement this method + (Class)layerClass @@ -120,15 +138,58 @@ - (id)initWithCoder:(NSCoder*)coder metalLayer.opaque = TRUE; metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; metalLayer.framebufferOnly = YES; - self.commandQueue = [metalLayer.device makeCommandQueue]; - + // 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. + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + if (cs != NULL) { + metalLayer.colorspace = cs; + CGColorSpaceRelease(cs); + } + // 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 + // most CAMetalLayer use cases. Combined with our nextDrawable + // skip-frame fallback in presentFramebuffer this keeps the + // pipeline non-blocking under pressure. + metalLayer.maximumDrawableCount = 3; + // `makeCommandQueue` is the Swift name; Objective-C uses `newCommandQueue`. + // newCommandQueue returns +1 (NARC family); release the local after + // the synthesized retain setter takes its own retain so we end up at + // +1 owned by the property, not +2. + id newQueue = [metalLayer.device newCommandQueue]; + self.commandQueue = newQueue; +#ifndef CN1_USE_ARC + [newQueue release]; +#endif + CGSize sz = self.bounds.size; + CGFloat s = self.contentScaleFactor; + [self updateFrameBufferSize:(int)(sz.width * s) h:(int)(sz.height * s)]; + + // Drop the glyph atlas + text cache + gradient cache on memory + // pressure. Pipeline state cache stays — those are precious to + // rebuild and small. The screen texture also stays; updateFrame- + // BufferSize: handles its replacement on resize. + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(memoryWarning) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; } - + return self; } +- (void)memoryWarning { + extern void CN1MetalReleaseCaches(void); + CN1MetalReleaseCaches(); +} + - (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; #ifndef CN1_USE_ARC [super dealloc]; #endif @@ -145,53 +206,165 @@ - (void)deleteFramebuffer -(void)updateFrameBufferSize:(int)w h:(int)h { - + // Ignore the passed w/h -- CodenameOne_GLViewController.m calls this with + // logical points (self.view.bounds.size), but the Metal drawable and + // projection must be in physical pixels. The GL path tolerates the + // logical-point argument because EAGLView.updateFrameBufferSize: is a + // no-op (dimensions get read back from the renderbuffer after the layer + // is bound). For Metal we always compute from our own layer bounds. + CGSize sz = self.bounds.size; + CGFloat s = self.contentScaleFactor; + int pw = (int)(sz.width * s); + int ph = (int)(sz.height * s); + if (pw <= 0 || ph <= 0) return; + if (pw == framebufferWidth && ph == framebufferHeight) { + return; + } + // An encoder may be mid-frame (awakeFromNib fires setFramebuffer before + // layoutSubviews, so the first encoder references the xib's placeholder + // bounds). Tear it down cleanly so the next setFramebuffer creates a + // fresh encoder against the new screenTexture. Otherwise draws land on + // a texture we're about to replace, and the stale dimensions get cached + // inside CN1Metalcompat (breaking scissor clamping etc.). + if (self.renderCommandEncoder != nil) { + CN1MetalEndFrame(); + [self.renderCommandEncoder endEncoding]; + self.renderCommandEncoder = nil; + } + if (self.commandBuffer != nil) { + [self.commandBuffer commit]; + self.commandBuffer = nil; + self.renderPassDescriptor = nil; + self.drawable = nil; + } + framebufferWidth = pw; + framebufferHeight = ph; + // Match iOS UIKit's Y-down convention: origin at top-left. + // Passing bottom=h, top=0 makes y_ndc = 1 - 2*y_input/h, so y_input=0 + // maps to NDC y=+1 (top of the drawable) and y_input=h maps to NDC y=-1 + // (bottom). That avoids the _glScalef(1,-1,1) + _glTranslatef(0,-h,0) + // workaround the GL path does in CodenameOne_GLViewController.drawFrame. + projectionMatrix = CN1MetalOrtho(0.0f, (float)pw, (float)ph, 0.0f, -1.0f, 1.0f); + CAMetalLayer *layer = (CAMetalLayer*)self.layer; + layer.drawableSize = CGSizeMake(pw, ph); + + // Rebuild the persistent screen render target at the new size. Anything + // previously rendered into the old texture is lost; the next frame will + // re-clear from black as CN1 repaints -- Form.paint() always issues a + // full-screen background fill on layout changes so this is safe. + MTLTextureDescriptor *desc = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:pw height:ph mipmapped:NO]; + desc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + desc.storageMode = MTLStorageModePrivate; + // newTextureWithDescriptor returns +1 (NARC family); the synthesized + // retain setter adds another +1 for a net +2 under MRR. Release the + // local once the property holds its own retain so we don't leak the + // previous screenTexture every time the framebuffer is resized + // (rotation, window resize, etc.). + id newScreen = [layer.device newTextureWithDescriptor:desc]; + self.screenTexture = newScreen; +#ifndef CN1_USE_ARC + [newScreen release]; +#endif + + // Prime the texture to opaque black: private-storage textures come back + // uninitialised, so the first frame (which uses MTLLoadActionLoad) would + // sample garbage for any pixel CN1 hasn't drawn yet. + id clearCb = [self.commandQueue commandBuffer]; + MTLRenderPassDescriptor *clearPass = [MTLRenderPassDescriptor renderPassDescriptor]; + clearPass.colorAttachments[0].texture = self.screenTexture; + clearPass.colorAttachments[0].loadAction = MTLLoadActionClear; + clearPass.colorAttachments[0].storeAction = MTLStoreActionStore; + clearPass.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0); + [[clearCb renderCommandEncoderWithDescriptor:clearPass] endEncoding]; + [clearCb commit]; } -(void)createRenderPassDescriptor { - if (self.renderPassDescriptor != nil) { + if (self.screenTexture == nil) { + self.renderPassDescriptor = nil; return; } - CAMetalLayer *layer = (CAMetalLayer*)self.layer; self.renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - self.drawable = [layer nextDrawable]; - MTLRenderPipelineColorAttachmentDescriptor* colorAttachment = self.renderPassDescriptor.colorAttachments[0]; - colorAttachment.texture = self.drawable.texture; - colorAttachment.loadAction = MTLLoadActionClear; - colorAttachment.isBlendingEnabled = YES; - colorAttachment.sourceRGBBlendFactor = MTLBlendFactorOne; - colorAttachment.destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; - colorAttachment.sourceAlphaBlendFactor = MTLBlendFactorOne; - colorAttachment.destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + MTLRenderPassColorAttachmentDescriptor* colorAttachment = self.renderPassDescriptor.colorAttachments[0]; + // Render into the persistent screen texture so incremental draws from + // subsequent drawFrame calls accumulate on top of whatever was there + // before. MTLLoadActionLoad preserves previous pixels (vs MTLLoadActionClear + // which would wipe everything each frame) — CN1 only queues diff ops + // per frame; the OpenGL path relies on its renderbuffer persisting. + colorAttachment.texture = self.screenTexture; + colorAttachment.loadAction = MTLLoadActionLoad; + colorAttachment.storeAction = MTLStoreActionStore; } - (void)setFramebuffer { - + // setFramebuffer may be called multiple times per frame (awakeFromNib + // issues one unpaired call during init; drawFrame can be invoked + // out-of-band alongside the CADisplayLink path). The GL backend tolerates + // this because binding the same framebuffer twice is a no-op. For Metal + // we keep the same encoder alive across those extra calls -- creating a + // fresh encoder each time would throw away any ops queued between setup + // and presentFramebuffer. Only presentFramebuffer ends+commits+presents. + if (self.renderCommandEncoder != nil) { + return; + } CAMetalLayer *layer = (CAMetalLayer*)self.layer; - self.commandBuffer = [self.commandQueue makeCommandBuffer]; + self.commandBuffer = [self.commandQueue commandBuffer]; [self createRenderPassDescriptor]; - self.renderCommandEncoder = [self.commandBuffer makeRenderCommandEncoderWithDescriptor:self.renderPassDescriptor]; - [self.renderCommandEncoder setViewport: (MTLViewport){ 0.0, 0.0, layer.drawableSize.width, layer.drawableSize.height, 0.0, 1.0 }]; - - _glMatrixMode(GL_PROJECTION); - _glLoadIdentity(); - _glOrthof(0, framebufferWidth, 0, framebufferHeight, -1, 1); - _glMatrixMode(GL_MODELVIEW); - _glLoadIdentity(); + if (self.renderPassDescriptor == nil) { + // nextDrawable returned nil; skip this frame. + self.renderCommandEncoder = nil; + return; + } + self.renderCommandEncoder = [self.commandBuffer renderCommandEncoderWithDescriptor:self.renderPassDescriptor]; + [self.renderCommandEncoder setViewport: (MTLViewport){ 0.0, 0.0, (double)framebufferWidth, (double)framebufferHeight, 0.0, 1.0 }]; + // Publish the encoder + projection to the CN1Metalcompat layer; each + // ExecutableOp's Metal branch pulls the encoder from there. + CN1MetalBeginFrame(self.renderCommandEncoder, projectionMatrix, framebufferWidth, framebufferHeight); } - (BOOL)presentFramebuffer { - BOOL success = FALSE; - - if (self.renderCommandEncoder) { - [self.renderCommandEncoder ] - [self.commandBuffer present:self.drawable]; + if (self.renderCommandEncoder == nil) { + // Nothing was encoded (setFramebuffer was not called after the + // previous present). Nothing to do. + self.commandBuffer = nil; + return NO; + } + CN1MetalEndFrame(); + [self.renderCommandEncoder endEncoding]; + self.renderCommandEncoder = nil; + self.renderPassDescriptor = nil; + + // Acquire the drawable here (not in setFramebuffer) to minimise its + // dwell time -- holding a drawable across the whole op-encoding phase + // stalls nextDrawable for subsequent frames. + CAMetalLayer *layer = (CAMetalLayer*)self.layer; + id dr = [layer nextDrawable]; + if (dr == nil) { + // Memory pressure dropped the drawable. Commit render work so + // screenTexture still updates; skip this frame's present. [self.commandBuffer commit]; + self.commandBuffer = nil; + return NO; } - - return success; + self.drawable = dr; + id blit = [self.commandBuffer blitCommandEncoder]; + [blit copyFromTexture:self.screenTexture + sourceSlice:0 sourceLevel:0 + sourceOrigin:MTLOriginMake(0, 0, 0) + sourceSize:MTLSizeMake(framebufferWidth, framebufferHeight, 1) + toTexture:dr.texture + destinationSlice:0 destinationLevel:0 + destinationOrigin:MTLOriginMake(0, 0, 0)]; + [blit endEncoding]; + [self.commandBuffer presentDrawable:dr]; + [self.commandBuffer commit]; + self.drawable = nil; + self.commandBuffer = nil; + return YES; } /** @@ -315,6 +488,19 @@ -(void)layoutSubviews [self deleteFramebuffer]; firstTime=NO; } + // Keep the Metal drawable + projection in sync with the actual runtime + // view size. initWithCoder runs with the xib's default size (often the + // legacy 320x480 placeholder), so without this the projection stays + // scaled to that default and anything drawn outside those bounds gets + // clipped at NDC edges -- the Form ends up only covering a portion of + // the screen. + CGSize sz = self.bounds.size; + CGFloat s = self.contentScaleFactor; + int w = (int)(sz.width * s); + int h = (int)(sz.height * s); + if (w > 0 && h > 0) { + [self updateFrameBufferSize:w h:h]; + } [super layoutSubviews]; } diff --git a/Ports/iOSPort/nativeSources/MainWindowMETAL.xib b/Ports/iOSPort/nativeSources/MainWindowMETAL.xib new file mode 100644 index 0000000000..972a0c4d32 --- /dev/null +++ b/Ports/iOSPort/nativeSources/MainWindowMETAL.xib @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ports/iOSPort/nativeSources/Scale.m b/Ports/iOSPort/nativeSources/Scale.m index 5c1b851ce1..bb383df7cd 100644 --- a/Ports/iOSPort/nativeSources/Scale.m +++ b/Ports/iOSPort/nativeSources/Scale.m @@ -24,6 +24,9 @@ #import "ClipRect.h" #import "CodenameOne_GLViewController.h" #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif float currentScaleX = 1; float currentScaleY = 1; @@ -37,6 +40,15 @@ -(id)initWithArgs:(float)xx yy:(float)yy { } -(void)execute { +#ifdef CN1_USE_METAL + { + GLKMatrix4 scaleM = GLKMatrix4MakeScale(x, y, 0); + CN1MetalSetTransform(GLKMatrix4Multiply(CN1MetalGetTransform(), scaleM)); + currentScaleX = x; + currentScaleY = y; + return; + } +#endif #ifdef USE_ES2 GLKMatrix4 scale = GLKMatrix4MakeScale(x, y, 0); glSetTransformES2(GLKMatrix4Multiply(glGetTransformES2(), scale)); diff --git a/Ports/iOSPort/nativeSources/SetTransform.m b/Ports/iOSPort/nativeSources/SetTransform.m index 8455fae9f8..3738df3c71 100644 --- a/Ports/iOSPort/nativeSources/SetTransform.m +++ b/Ports/iOSPort/nativeSources/SetTransform.m @@ -24,6 +24,9 @@ #ifdef USE_ES2 #import #import "CodenameOne_GLViewController.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif static GLKMatrix4 currentTransform; static BOOL currentTransformInitialized = NO; @@ -49,8 +52,11 @@ -(id)initWithArgs:(GLKMatrix4)matrix originX:(int)xx originY:(int)yy -(void)execute { +#ifdef CN1_USE_METAL + CN1MetalSetTransform(m); +#else glSetTransformES2(m); - +#endif } +(GLKMatrix4)currentTransform diff --git a/Ports/iOSPort/nativeSources/TileImage.m b/Ports/iOSPort/nativeSources/TileImage.m index d34a64a099..f71ef66f57 100644 --- a/Ports/iOSPort/nativeSources/TileImage.m +++ b/Ports/iOSPort/nativeSources/TileImage.m @@ -23,6 +23,9 @@ #import "TileImage.h" #import "CodenameOne_GLViewController.h" #include "xmlvm.h" +#ifdef CN1_USE_METAL +#import "CN1Metalcompat.h" +#endif #ifdef USE_ES2 @@ -145,6 +148,17 @@ -(void)execute { if (width <= 0 || height <= 0) { return; } +#ifdef CN1_USE_METAL + { + UIImage *src = [img getImage]; + int imageWidth = (int)src.size.width; + int imageHeight = (int)src.size.height; + if (imageWidth > 0 && imageHeight > 0) { + CN1MetalTileImage([img getMTLTexture], alpha, x, y, width, height, imageWidth, imageHeight); + } + return; + } +#endif glUseProgram(getOGLProgram()); GLKVector4 color = GLKVector4Make(((float)alpha) / 255.0f, ((float)alpha) / 255.0f, ((float)alpha) / 255.0f, ((float)alpha) / 255.0f); diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index 7a930b52e7..f043ce2b7a 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -137,6 +137,21 @@ public class IOSImplementation extends CodenameOneImplementation { private NativeGraphics currentlyDrawingOn; //private NativeImage backBuffer; private NativeGraphics globalGraphics; + /// True when iOS was built with -Dios.metal=true. Phase 3 v2 mutable- + /// image rendering paths (tileImage etc.) need to know this to choose + /// between queueing an op and falling through to the CG-loop super + /// implementation. + /// + /// Static rather than instance: the gate is read from inner classes + /// (NativeGraphics / GlobalGraphics / NativeImage), and the synthetic + /// outer-instance field accessor that javac generates for non-static + /// inner classes was returning false on the iOS Metal port even though + /// `nativeInstance.isMetalRendering()` returned true at postInit. CI bisect + /// (run 25259320137) traced 'mutable shape ops render via the CG fallback + /// instead of the alpha-mask Metal pipeline' to that gate not firing. + /// Making it static + populating eagerly in init() side-steps the + /// inner-class accessor entirely. + static boolean metalRendering; static IOSImplementation instance; private TextArea currentEditing; private static boolean initialized; @@ -213,6 +228,12 @@ protected void initDefaultUserAgent() { public void init(Object m) { instance = this; + // Set the metalRendering static gate as early as possible -- before any + // NativeImage / NativeGraphics is constructed, so mutable-image + // rendering routes through the alpha-mask Metal pipeline from the + // very first paint instead of falling through to the CG-rasterise + // helpers in IOSNative.m. + metalRendering = nativeInstance.isMetalRendering(); setUseNativeCookieStore(false); Display.getInstance().setTransitionYield(10); Display.getInstance().setDefaultVirtualKeyboard(new IOSVirtualKeyboard(this)); @@ -1981,6 +2002,25 @@ private static void nativeDrawStringGlobal(int color, int alpha, long fontPeer, nativeInstance.nativeDrawStringGlobal(color, alpha, fontPeer, str, x, y); } + @Override + public void drawString(Object graphics, Object nativeFont, String str, int x, int y, int textDecoration) { + // Re-sync ng.font with the Java-side current font before drawing. + // Display.impl.drawLabelComponent calls setNativeFont(ng, labelStyleFont) + // directly to push the label's style font into NativeGraphics for fast + // native rendering, but does NOT update Graphics.current. After the + // title bar (or any Label) renders, ng.font holds the label's font + // while Graphics.current holds the Java-side font from before the + // label's draw. The user's next g.drawString() on the same Graphics + // expects to use Graphics.current; the iOS 4-arg drawString below + // reads ng.font instead, so they diverge. Graphics.drawString already + // passes Graphics.current as the nativeFont parameter -- pin ng.font + // to it here so the 4-arg drawString picks up the correct font. + if (nativeFont != null && graphics instanceof NativeGraphics) { + ((NativeGraphics) graphics).font = (NativeFont) nativeFont; + } + super.drawString(graphics, nativeFont, str, x, y, textDecoration); + } + public void drawString(Object graphics, String str, int x, int y) { NativeGraphics ng = (NativeGraphics)graphics; ng.checkControl(); @@ -2024,6 +2064,21 @@ public void tileImage(Object graphics, Object img, int x, int y, int w, int h) { ng.applyClip(); NativeImage nm = (NativeImage)img; nativeInstance.nativeTileImageGlobal(nm.peer, ng.alpha, x, y, w, h); + } else if (metalRendering) { + // Phase 3 v2: queue a single TileImage op tagged with the + // current mutable image as target. nativeTileImageGlobal's + // C side picks up [GLViewController.instance.currentMutableImage] + // and tags accordingly. Mirrors the GlobalGraphics branch + // above, except that ng.checkControl already ran on the + // mutable so currentMutableImage is set. Avoids + // super.tileImage's 1500-iter drawImage loop which queues + // ~1500 ops per panel and stalls the EDT past the test + // timeout on slow CI runners. + ng.checkControl(); + ng.applyTransform(); + ng.applyClip(); + NativeImage nm = (NativeImage)img; + nativeInstance.nativeTileImageGlobal(nm.peer, ng.alpha, x, y, w, h); } else { super.tileImage(graphics, img, x, y, w, h); } @@ -4855,21 +4910,114 @@ void nativeDrawRect(int color, int alpha, int x, int y, int width, int height) { } void nativeDrawRoundRect(int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) { + if (metalRendering) { + // Build a round-rect GeneralPath and route through nativeDrawShape + // so the alpha-mask Metal pipeline renders it. No CG bitmap. + GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight); + if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f); + nativeDrawShape(p, tmpStroke1px); + return; + } nativeDrawRoundRectMutable(color, alpha, x, y, width, height, arcWidth, arcHeight); } void nativeFillRoundRect(int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) { + if (metalRendering) { + GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight); + nativeFillShape(p); + return; + } nativeFillRoundRectMutable(color, alpha, x, y, width, height, arcWidth, arcHeight); } void nativeDrawArc(int color, int alpha, int x, int y, int width, int height, int startAngle, int arcAngle) { + if (metalRendering) { + // Same approach GlobalGraphics uses (drawingArcPath + nativeDrawShape). + if (drawingArcPath == null) drawingArcPath = new GeneralPath(); + if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f); + drawingArcPath.reset(); + drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, false); + nativeDrawShape(drawingArcPath, tmpStroke1px); + return; + } nativeDrawArcMutable(color, alpha, x, y, width, height, startAngle, arcAngle); } void nativeFillArc(int color, int alpha, int x, int y, int width, int height, int startAngle, int arcAngle) { + if (metalRendering) { + if (drawingArcPath == null) drawingArcPath = new GeneralPath(); + drawingArcPath.reset(); + if (arcAngle >= 360 || arcAngle <= -360) { + // Full circle/ellipse: omit the moveTo(center). With it + // the path is center -> arc start -> 360 around -> close + // back to center, which Renderer.c rasterises as a + // pacman with a visible slice line from center to the + // start point. Switch's thumb fillArc(0, 360) was + // rendering that slice as a dark line through the + // white thumb. + drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, false); + } else { + // Partial arc renders as a pie slice: center -> arc -> back to center. + drawingArcPath.moveTo(x + width / 2, y + height / 2); + drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, true); + } + drawingArcPath.closePath(); + nativeFillShape(drawingArcPath); + return; + } nativeFillArcMutable(color, alpha, x, y, width, height, startAngle, arcAngle); } + private Stroke tmpStroke1px; + private GeneralPath drawingArcPath; + + // Build a round-rect path from the parametric (x,y,w,h,arcW,arcH) form + // so the alpha-mask pipeline can rasterise it. arcW/arcH are full + // ellipse-axis lengths (matching the Java2D / cn1 roundRect contract); + // half each gives the corner radii. + private GeneralPath roundRectPath(int x, int y, int width, int height, int arcWidth, int arcHeight) { + GeneralPath p = new GeneralPath(); + float rx = Math.min(arcWidth / 2f, width / 2f); + float ry = Math.min(arcHeight / 2f, height / 2f); + if (rx <= 0 || ry <= 0) { + // Degenerate: just emit a rectangle outline. + p.moveTo(x, y); + p.lineTo(x + width, y); + p.lineTo(x + width, y + height); + p.lineTo(x, y + height); + p.closePath(); + return p; + } + // joinPath=true on each arc so the corners connect to the + // adjacent line segments via lineTo instead of starting a new + // sub-path. With joinPath=false (the prior code) each arc was + // an independent moveTo'd sub-path, and Renderer.c rendered the + // whole thing as 4 disconnected pacman pieces. + // + // Skip the lineTos when their endpoints would coincide with the + // arc's join target (pill case: rx == width/2 collapses the top + // and bottom edges; ry == height/2 collapses the left and right + // edges). Emitting a zero-length lineTo into the path leaves a + // phantom edge that Renderer.c's winding pass interprets as a + // tear, which is what made the Switch pill render with a + // triangular wedge cut into it. + boolean hasTopBottomEdges = rx < width / 2f; + boolean hasLeftRightEdges = ry < height / 2f; + float twoRx = 2f * rx; + float twoRy = 2f * ry; + p.moveTo(x + rx, y); + if (hasTopBottomEdges) p.lineTo(x + width - rx, y); + p.arc(x + width - twoRx, y, twoRx, twoRy, -Math.PI / 2, Math.PI / 2, true); + if (hasLeftRightEdges) p.lineTo(x + width, y + height - ry); + p.arc(x + width - twoRx, y + height - twoRy, twoRx, twoRy, 0, Math.PI / 2, true); + if (hasTopBottomEdges) p.lineTo(x + rx, y + height); + p.arc(x, y + height - twoRy, twoRx, twoRy, Math.PI / 2, Math.PI / 2, true); + if (hasLeftRightEdges) p.lineTo(x, y + ry); + p.arc(x, y, twoRx, twoRy, Math.PI, Math.PI / 2, true); + p.closePath(); + return p; + } + void nativeDrawString(int color, int alpha, long fontPeer, String str, int x, int y) { boolean antialiasTextChanged = false; if (isAntiAliased() != isAntiAliasedText()) { @@ -4894,7 +5042,14 @@ void nativeDrawImage(long peer, int alpha, int x, int y, int width, int height) // BEGIN DRAW SHAPE METHODS void nativeDrawAlphaMask(TextureAlphaMask mask){ - + // Mirror GlobalGraphics: hand the alpha-mask MTLTexture handle + // off to drawTextureAlphaMask. The JNI side picks up + // currentMutableImage and tags the queued op so drawFrame's + // drain (Phase 3 v2) routes it to the mutable's encoder. + if (mask != null && mask.getTextureName() != 0) { + Rectangle r = mask.getBounds(); + nativeInstance.drawTextureAlphaMask(mask.getTextureName(), this.color, this.alpha, r.getX(), r.getY(), r.getWidth(), r.getHeight()); + } } @@ -4940,44 +5095,116 @@ private byte[] getTmpNativeDrawShape_commands(int size) { * @param shape * @param stroke */ - void nativeDrawShape(Shape shape, Stroke stroke){//float lineWidth, int capStyle, int miterStyle, float miterLimit){ - if (shape.getClass() == GeneralPath.class) { - // GeneralPath gives us some easy access to the points - GeneralPath p = (GeneralPath)shape; - int commandsLen = p.getTypesSize(); - int pointsLen = p.getPointsSize(); - byte[] commandsArr = getTmpNativeDrawShape_commands(commandsLen); - float[] pointsArr = getTmpNativeDrawShape_coords(pointsLen); - p.getTypes(commandsArr); - p.getPoints(pointsArr); - - nativeInstance.nativeDrawShapeMutable(color, alpha, commandsLen, commandsArr, pointsLen, pointsArr, stroke.getLineWidth(), stroke.getCapStyle(), stroke.getJoinStyle(), stroke.getMiterLimit()); + void nativeDrawShape(Shape shape, Stroke stroke){ + if (!metalRendering) { + // GL keeps the original CG-backed path (nativeDrawShapeMutable + // JNI) so its goldens stay valid. + if (shape.getClass() == GeneralPath.class) { + GeneralPath p = (GeneralPath)shape; + int commandsLen = p.getTypesSize(); + int pointsLen = p.getPointsSize(); + byte[] commandsArr = getTmpNativeDrawShape_commands(commandsLen); + float[] pointsArr = getTmpNativeDrawShape_coords(pointsLen); + p.getTypes(commandsArr); + p.getPoints(pointsArr); + nativeInstance.nativeDrawShapeMutable(color, alpha, commandsLen, commandsArr, pointsLen, pointsArr, stroke.getLineWidth(), stroke.getCapStyle(), stroke.getJoinStyle(), stroke.getMiterLimit()); + } else { + Log.p("Drawing shapes that are not GeneralPath objects is not yet supported on mutable images."); + } + return; + } + // Metal: route through the Renderer.c-driven alpha-mask path. On + // the iOS Metal port that path emits an R8 MTLTexture and a + // DrawTextureAlphaMask op; the JNI side (drawTextureAlphaMaskImpl) + // tags the op with currentMutableImage so the drain loop binds + // the mutable's offscreen MTLTexture as the render target -- same + // Metal alpha-mask shader, just a different attachment. No more + // CGContextStrokePath, no more separate nativeDrawShapeMutable JNI. + if (shape instanceof GeneralPath) { + if (transform == null || transform.isIdentity()) { + TextureAlphaMask mask = textureCache.get(shape, stroke); + if (mask == null) { + mask = createAlphaMask(shape, stroke); + textureCache.add(shape, stroke, mask); + } + if (mask == null) return; + nativeDrawAlphaMask(mask); + } else { + // Non-identity transform path: bake the transform into + // a copy of the shape, then alpha-mask + draw. Mirrors + // GlobalGraphics.nativeDrawShape exactly. + if (tmpDrawShape == null) tmpDrawShape = new GeneralPath(); + if (tmpTransform == null) tmpTransform = Transform.makeIdentity(); + if (tmpDrawStroke == null) tmpDrawStroke = new Stroke(); + GeneralPath p = (GeneralPath) shape; + if (tmpRect2 == null) tmpRect2 = new Rectangle(); + Rectangle origBounds = reusableRect; + Rectangle transformedBounds = tmpRect2; + p.getBounds(origBounds); + tmpDrawShape.setShape(shape, transform); + tmpDrawShape.getBounds(transformedBounds); + double h1 = Math.sqrt(origBounds.getWidth()*origBounds.getWidth() + origBounds.getHeight()*origBounds.getHeight()); + double h2 = Math.sqrt(transformedBounds.getWidth()*transformedBounds.getWidth() + transformedBounds.getHeight()*transformedBounds.getHeight()); + if (h2 < 1) h2 = 1; + if (h1 < 1) h1 = 1; + float scale = (float)(h2 / h1); + tmpTransform.setScale(scale, scale); + tmpDrawShape.setShape(shape, tmpTransform); + if (stroke != null) { + tmpDrawStroke.setStroke(stroke); + tmpDrawStroke.setLineWidth(tmpDrawStroke.getLineWidth() * scale); + } + TextureAlphaMask mask = textureCache.get(tmpDrawShape, stroke == null ? null : tmpDrawStroke); + if (mask == null) { + mask = createAlphaMask(tmpDrawShape, stroke == null ? null : tmpDrawStroke); + textureCache.add(tmpDrawShape, stroke == null ? null : tmpDrawStroke, mask); + } + if (mask == null) return; + Transform saved = transform; + Transform inv = Transform.makeIdentity(); + inv.setTransform(transform); + inv.scale(1f / scale, 1f / scale); + setTransform(inv); + try { + nativeDrawAlphaMask(mask); + } finally { + setTransform(saved); + } + } } else { Log.p("Drawing shapes that are not GeneralPath objects is not yet supported on mutable images."); } - - } - + + private GeneralPath tmpDrawShape; + private Transform tmpTransform; + private Stroke tmpDrawStroke; + private Rectangle tmpRect2; + /** * Fills a shape in the graphics context. * @param shape */ void nativeFillShape(Shape shape) { - if (shape.getClass() == GeneralPath.class) { - // GeneralPath gives us some easy access to the points - GeneralPath p = (GeneralPath)shape; - int commandsLen = p.getTypesSize(); - int pointsLen = p.getPointsSize(); - byte[] commandsArr = getTmpNativeDrawShape_commands(commandsLen); - float[] pointsArr = getTmpNativeDrawShape_coords(pointsLen); - p.getTypes(commandsArr); - p.getPoints(pointsArr); - - nativeInstance.nativeFillShapeMutable(color, alpha, commandsLen, commandsArr, pointsLen, pointsArr); - } else { - Log.p("Drawing shapes that are not GeneralPath objects is not yet supported on mutable images."); + if (!metalRendering) { + if (shape.getClass() == GeneralPath.class) { + GeneralPath p = (GeneralPath)shape; + int commandsLen = p.getTypesSize(); + int pointsLen = p.getPointsSize(); + byte[] commandsArr = getTmpNativeDrawShape_commands(commandsLen); + float[] pointsArr = getTmpNativeDrawShape_coords(pointsLen); + p.getTypes(commandsArr); + p.getPoints(pointsArr); + nativeInstance.nativeFillShapeMutable(color, alpha, commandsLen, commandsArr, pointsLen, pointsArr); + } else { + Log.p("Drawing shapes that are not GeneralPath objects is not yet supported on mutable images."); + } + return; } + // Same architectural shift as nativeDrawShape -- alpha-mask path + // on Metal, no CGContextFillPath. fillShape passes a null stroke + // to flag fill-rather-than-stroke through the Renderer.c side. + nativeDrawShape(shape, null); } boolean isDrawShadowSupported() { @@ -5001,7 +5228,12 @@ boolean isShapeSupported(){ } boolean isAlphaMaskSupported(){ - return false; + // On Metal, nativeDrawShape/nativeFillShape route through the + // Renderer.c-driven alpha-mask pipeline (same as GlobalGraphics). + // Letting the framework know unblocks code paths that prefer + // alpha-mask rendering for mutable targets. On GL we keep the + // legacy CG path -- same as before. + return metalRendering; } // END DRAW SHAPE METHODS @@ -5311,13 +5543,65 @@ void nativeDrawRect(int color, int alpha, int x, int y, int width, int height) { } void nativeDrawRoundRect(int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) { + if (metalRendering) { + // Route through the alpha-mask Metal pipeline (build a path, + // then nativeDrawShape uses Renderer.c -> R8 MTLTexture -> + // DrawTextureAlphaMask op). No more CGContextStrokePath. + GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight); + if (tmpStroke1px == null) tmpStroke1px = new Stroke(1, Stroke.CAP_BUTT, Stroke.JOIN_ROUND, 1f); + nativeDrawShape(p, tmpStroke1px); + return; + } nativeDrawRoundRectGlobal(color, alpha, x, y, width, height, arcWidth, arcHeight); } void nativeFillRoundRect(int color, int alpha, int x, int y, int width, int height, int arcWidth, int arcHeight) { + if (metalRendering) { + GeneralPath p = roundRectPath(x, y, width, height, arcWidth, arcHeight); + nativeFillShape(p); + return; + } nativeFillRoundRectGlobal(color, alpha, x, y, width, height, arcWidth, arcHeight); } + // Build a round-rect path from the parametric (x,y,w,h,arcW,arcH) form + // so the alpha-mask Metal pipeline can rasterise it. arcW/arcH are + // full ellipse-axis lengths (cn1 / Java2D contract); half each gives + // the corner radii. Mirrors MutableGraphics.roundRectPath. + private GeneralPath roundRectPath(int x, int y, int width, int height, int arcWidth, int arcHeight) { + GeneralPath p = new GeneralPath(); + float rx = Math.min(arcWidth / 2f, width / 2f); + float ry = Math.min(arcHeight / 2f, height / 2f); + if (rx <= 0 || ry <= 0) { + p.moveTo(x, y); + p.lineTo(x + width, y); + p.lineTo(x + width, y + height); + p.lineTo(x, y + height); + p.closePath(); + return p; + } + // joinPath=true on each arc so corners connect to adjacent line + // segments via lineTo (single sub-path); skip lineTos that + // would have zero length (pill / circle cases). See + // MutableGraphics.roundRectPath for the rendering bug this + // avoids. + boolean hasTopBottomEdges = rx < width / 2f; + boolean hasLeftRightEdges = ry < height / 2f; + float twoRx = 2f * rx; + float twoRy = 2f * ry; + p.moveTo(x + rx, y); + if (hasTopBottomEdges) p.lineTo(x + width - rx, y); + p.arc(x + width - twoRx, y, twoRx, twoRy, -Math.PI / 2, Math.PI / 2, true); + if (hasLeftRightEdges) p.lineTo(x + width, y + height - ry); + p.arc(x + width - twoRx, y + height - twoRy, twoRx, twoRy, 0, Math.PI / 2, true); + if (hasTopBottomEdges) p.lineTo(x + rx, y + height); + p.arc(x, y + height - twoRy, twoRx, twoRy, Math.PI / 2, Math.PI / 2, true); + if (hasLeftRightEdges) p.lineTo(x, y + ry); + p.arc(x, y, twoRx, twoRy, Math.PI, Math.PI / 2, true); + p.closePath(); + return p; + } + private Stroke tmpStroke1px; void nativeDrawArc(int color, int alpha, int x, int y, int width, int height, int startAngle, int arcAngle) { // Turns out that using a Shape instead of using a Shader is much faster so we just pipe this @@ -5343,14 +5627,22 @@ void nativeDrawArc(int color, int alpha, int x, int y, int width, int height, in void nativeFillArc(int color, int alpha, int x, int y, int width, int height, int startAngle, int arcAngle) { // Turns out that using a Shape instead of using a Shader is much faster so we just pipe this // through to DrawShape. - // See https://gist.github.com/shannah/85d93674d709c7733e98 for Shader implementation that we decided + // See https://gist.github.com/shannah/85d93674d709c7733e98 for Shader implementation that we decided // not to use. if (drawingArcPath == null) { drawingArcPath = new GeneralPath(); } drawingArcPath.reset(); - drawingArcPath.moveTo(x + width / 2, y + height / 2); - drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, true); + if (arcAngle >= 360 || arcAngle <= -360) { + // Full circle/ellipse: skip moveTo(center). Without this the + // path is center -> arc start -> 360 -> close back to + // center, which rasterises as a pacman with a visible + // slice line through the fill (broken thumb on Switch). + drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, false); + } else { + drawingArcPath.moveTo(x + width / 2, y + height / 2); + drawingArcPath.arc(x, y, width, height, startAngle * Math.PI / 180, arcAngle * Math.PI / 180, true); + } drawingArcPath.closePath(); nativeFillShape(drawingArcPath); } diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index 056a5a2ed5..4e1a2da799 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -46,6 +46,14 @@ public final class IOSNative { //native void startMainThread(Runnable r); native void initVM(); + + /// Returns true on iOS builds compiled with -Dios.metal=true (i.e. + /// CN1_USE_METAL is defined in CN1ES2compat.h). Java-side code that + /// needs to branch between the GL and Metal mutable-image rendering + /// paths queries this once at init -- there is no other reliable + /// source of truth on the Java side since the build flag only + /// affects native compilation. + native boolean isMetalRendering(); static native void deinitializeVM(); native boolean isPainted(); native int getDisplayWidth(); diff --git a/scripts/ci/metal-screenshot-summary.py b/scripts/ci/metal-screenshot-summary.py new file mode 100644 index 0000000000..eeb8fa74aa --- /dev/null +++ b/scripts/ci/metal-screenshot-summary.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Render the Metal screenshot-comparison summary for a GitHub Actions job. + +Called from .github/workflows/scripts-ios.yml's build-ios-metal job. +Reads the screenshot-compare.json produced by scripts/run-ios-ui-tests.sh +and emits either: + + --markdown : a markdown section (headline + per-test table) suitable + for appending to $GITHUB_STEP_SUMMARY. + --headline : a one-liner of the form "N/T matched against golden + images" suitable for a ::notice title. + +Kept as a separate file because embedding Python heredocs inside a +YAML "run: |" block is fragile -- any non-indented line breaks the +block scalar. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +def load_results(path: Path) -> list[dict]: + with path.open() as f: + data = json.load(f) + return data.get("results", []) + + +def counts(results: list[dict]) -> tuple[int, int, int, int]: + total = len(results) + matched = sum(1 for r in results if r.get("status") == "equal") + different = sum(1 for r in results if r.get("status") == "different") + other = total - matched - different + return total, matched, different, other + + +def emit_markdown(results: list[dict]) -> None: + total, matched, different, other = counts(results) + print(f"**Headline:** {matched}/{total} matched, {different} differ, {other} other.") + print() + print("| Test | Status | Mismatch % |") + print("| --- | --- | --- |") + for r in results: + pct = "" + if r.get("status") == "different": + mm = r.get("details", {}).get("mismatch_percent", 0.0) + pct = f"{mm:.1f}%" + print(f"| {r.get('test', '?')} | {r.get('status', '?')} | {pct} |") + + +def emit_headline(results: list[dict]) -> None: + total, matched, _different, _other = counts(results) + print(f"{matched}/{total} matched against golden images") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("compare_json", help="Path to screenshot-compare.json") + mode = ap.add_mutually_exclusive_group(required=True) + mode.add_argument("--markdown", action="store_const", dest="mode", const="markdown") + mode.add_argument("--headline", action="store_const", dest="mode", const="headline") + args = ap.parse_args() + + path = Path(args.compare_json) + if not path.is_file() or path.stat().st_size == 0: + print(f"[metal-screenshot-summary] Comparison JSON missing or empty: {path}", file=sys.stderr) + return 2 + + results = load_results(path) + if args.mode == "markdown": + emit_markdown(results) + else: + emit_headline(results) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/common/java/ProcessScreenshots.java b/scripts/common/java/ProcessScreenshots.java index f184e948ae..a8da1a7fd9 100644 --- a/scripts/common/java/ProcessScreenshots.java +++ b/scripts/common/java/ProcessScreenshots.java @@ -446,12 +446,20 @@ private static PNGImage loadPng(Path path) throws IOException { int colorType = 0; int interlace = 0; List idatChunks = new ArrayList<>(); + boolean sawIend = false; while (offset + 8 <= data.length) { int length = readInt(data, offset); byte[] type = java.util.Arrays.copyOfRange(data, offset + 4, offset + 8); offset += 8; - if (offset + length + 4 > data.length) { - throw new IOException("PNG chunk truncated before CRC while processing: " + path); + // PNG chunk length is a 31-bit unsigned int; readInt returns it as + // signed, so a negative value here is by definition out-of-range. + // This typically means we've walked off the end of the valid chunks + // into trailing garbage (e.g., a truncated capture missing IEND); + // surface a clear message instead of letting Arrays.copyOfRange + // throw the cryptic " > " IllegalArgumentException. + if (length < 0 || offset + length + 4 > data.length) { + throw new IOException("PNG chunk truncated or out-of-range length while processing: " + path + + " (chunk length=" + length + ", offset=" + offset + ", file size=" + data.length + ")"); } byte[] chunkData = java.util.Arrays.copyOfRange(data, offset, offset + length); offset += length + 4; // skip data + CRC @@ -470,9 +478,13 @@ private static PNGImage loadPng(Path path) throws IOException { } else if ("IDAT".equals(chunkType)) { idatChunks.add(chunkData); } else if ("IEND".equals(chunkType)) { + sawIend = true; break; } } + if (!sawIend) { + throw new IOException("PNG missing IEND chunk (truncated capture?) while processing: " + path); + } if (width <= 0 || height <= 0) { throw new IOException("Missing IHDR chunk on " + path); } diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index b92a2a943c..3d9b298306 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -46,7 +46,12 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { // throttles 500-byte chunks at 30ms each to keep logcat from dropping // lines, so a 60KB PNG plus its preview takes ~6s per appearance, and // a dual-appearance test like SpanLabelTheme legitimately needs ~12s - // even on a healthy device). + // even on a healthy device). The 30s figure also covers the iOS Metal + // port's mutable rendering, which allocates a temp UIImage + MTLTexture + // per round-rect / arc / gradient call (the CG-rasterise-then-DrawImage + // bridge), so FillRoundRect / DrawRoundRect and DrawImage on slow CI + // simulators legitimately blow past the 10s budget while still making + // forward progress. private static final int TEST_TIMEOUT_MS_HTML5 = 10000; private static final int TEST_TIMEOUT_MS_NATIVE = 30000; private static final int TEST_POLL_INTERVAL_MS = 50; @@ -152,7 +157,8 @@ private static int testTimeoutMs() { new CallDetectionAPITest(), new LocalNotificationOverrideTest(), new Base64NativePerformanceTest(), - new AccessibilityTest() + new AccessibilityTest(), + new MutableImageReadbackTest() }; private static BaseTest prependedTest; diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/MutableImageReadbackTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/MutableImageReadbackTest.java new file mode 100644 index 0000000000..58e2f1a284 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/MutableImageReadbackTest.java @@ -0,0 +1,145 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.ui.Graphics; +import com.codename1.ui.Image; +import com.codename1.ui.geom.GeneralPath; + +/** + * Phase 3 milestone test: round-trip through Image.getGraphics().drawXxx(...) + * → image.getRGB() and verify the pixels read back match what was drawn. + * + * The Metal port's mutable-image rendering is asynchronous -- draw calls + * append ops to a per-image command queue and the GPU only sees them when + * drawFrame's drain runs. Pixel-reading paths (getRGB, encode-as-PNG/JPEG, + * toImage) must commit and wait on the queue before reading, otherwise + * callers see stale or zeroed bytes. This test exercises that contract. + */ +public class MutableImageReadbackTest extends BaseTest { + + @Override + public boolean shouldTakeScreenshot() { + return false; + } + + @Override + public boolean runTest() { + try { + // Step 1: a fillRect-only mutable. The simplest case -- + // verifies that pixel readback sees the most recent solid + // fill. + if (!testFillRectReadback()) { + return false; + } + + // Step 2: a fillRect followed by a smaller fillRect inside it. + // Verifies that ops applied later override pixels of earlier + // ops -- catches a 'commit only the first op' bug or any + // out-of-order drain. + if (!testStackedFillsReadback()) { + return false; + } + + // Step 3: a fillShape (alpha-mask Metal pipeline) into the + // mutable. Verifies the alpha-mask path also flushes through + // to readback. This is the path that was silently dead before + // commit 1e2f6a2bd. + if (!testFillShapeReadback()) { + return false; + } + + done(); + return true; + } catch (Throwable t) { + fail("Unexpected exception: " + t.getMessage()); + return false; + } + } + + private boolean testFillRectReadback() { + int w = 8, h = 8; + Image img = Image.createImage(w, h); + Graphics g = img.getGraphics(); + g.setColor(0xff0000); // red + g.fillRect(0, 0, w, h); + + int[] pixels = img.getRGB(); + if (pixels == null || pixels.length != w * h) { + fail("FillRect readback: getRGB returned " + (pixels == null ? "null" : "length=" + pixels.length) + + ", expected " + (w * h)); + return false; + } + for (int i = 0; i < pixels.length; i++) { + if ((pixels[i] & 0xffffff) != 0xff0000) { + fail("FillRect readback: pixel " + i + " expected red 0xff0000 in low 24 bits, got 0x" + + Integer.toHexString(pixels[i])); + return false; + } + } + return true; + } + + private boolean testStackedFillsReadback() { + int w = 16, h = 16; + Image img = Image.createImage(w, h); + Graphics g = img.getGraphics(); + g.setColor(0xff0000); // red base + g.fillRect(0, 0, w, h); + g.setColor(0x00ff00); // green inset + g.fillRect(4, 4, 8, 8); + + int[] pixels = img.getRGB(); + // Corner pixel (0, 0) should still be red. + if ((pixels[0] & 0xffffff) != 0xff0000) { + fail("Stacked fills readback: corner pixel expected red, got 0x" + + Integer.toHexString(pixels[0])); + return false; + } + // Center pixel (8, 8) should be green from the inset. + int center = pixels[8 * w + 8]; + if ((center & 0xffffff) != 0x00ff00) { + fail("Stacked fills readback: center pixel expected green 0x00ff00, got 0x" + + Integer.toHexString(center)); + return false; + } + return true; + } + + private boolean testFillShapeReadback() { + int w = 16, h = 16; + Image img = Image.createImage(w, h); + Graphics g = img.getGraphics(); + g.setColor(0xffffff); // white base + g.fillRect(0, 0, w, h); + + // Triangle covering the centre. + GeneralPath p = new GeneralPath(); + p.moveTo(0, 0); + p.lineTo(w, 0); + p.lineTo(w / 2, h); + p.closePath(); + g.setColor(0x0000ff); // blue + g.fillShape(p); + + int[] pixels = img.getRGB(); + // (8, 4) should be inside the triangle -- blue (or close to it + // after alpha-mask anti-aliasing). Accept any pixel where the + // blue channel dominates. + int sample = pixels[4 * w + 8]; + int r = (sample >> 16) & 0xff; + int gc = (sample >> 8) & 0xff; + int b = sample & 0xff; + if (b <= r || b <= gc) { + fail("FillShape readback: center pixel inside triangle expected blue-dominant, got rgb=(" + + r + "," + gc + "," + b + ")"); + return false; + } + // (0, h-1) is outside the triangle -- white from the base fill. + int outside = pixels[(h - 1) * w + 0]; + if ((outside & 0xffffff) != 0xffffff) { + fail("FillShape readback: pixel outside triangle expected white, got 0x" + + Integer.toHexString(outside)); + return false; + } + return true; + } +} diff --git a/scripts/ios/screenshots-metal/BrowserComponent.png b/scripts/ios/screenshots-metal/BrowserComponent.png new file mode 100644 index 0000000000..ebd13fb07c Binary files /dev/null and b/scripts/ios/screenshots-metal/BrowserComponent.png differ diff --git a/scripts/ios/screenshots-metal/ButtonTheme_dark.png b/scripts/ios/screenshots-metal/ButtonTheme_dark.png new file mode 100644 index 0000000000..8b306cb023 Binary files /dev/null and b/scripts/ios/screenshots-metal/ButtonTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/ButtonTheme_light.png b/scripts/ios/screenshots-metal/ButtonTheme_light.png new file mode 100644 index 0000000000..8c171b128f Binary files /dev/null and b/scripts/ios/screenshots-metal/ButtonTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/CheckBoxRadioTheme_dark.png b/scripts/ios/screenshots-metal/CheckBoxRadioTheme_dark.png new file mode 100644 index 0000000000..1ed8a79ba8 Binary files /dev/null and b/scripts/ios/screenshots-metal/CheckBoxRadioTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/CheckBoxRadioTheme_light.png b/scripts/ios/screenshots-metal/CheckBoxRadioTheme_light.png new file mode 100644 index 0000000000..3cb5aa1cb2 Binary files /dev/null and b/scripts/ios/screenshots-metal/CheckBoxRadioTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/DialogTheme_dark.png b/scripts/ios/screenshots-metal/DialogTheme_dark.png new file mode 100644 index 0000000000..835c0f2402 Binary files /dev/null and b/scripts/ios/screenshots-metal/DialogTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/DialogTheme_light.png b/scripts/ios/screenshots-metal/DialogTheme_light.png new file mode 100644 index 0000000000..280ab7bf59 Binary files /dev/null and b/scripts/ios/screenshots-metal/DialogTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/FloatingActionButtonTheme_dark.png b/scripts/ios/screenshots-metal/FloatingActionButtonTheme_dark.png new file mode 100644 index 0000000000..76cee3b8d2 Binary files /dev/null and b/scripts/ios/screenshots-metal/FloatingActionButtonTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/FloatingActionButtonTheme_light.png b/scripts/ios/screenshots-metal/FloatingActionButtonTheme_light.png new file mode 100644 index 0000000000..97226354cf Binary files /dev/null and b/scripts/ios/screenshots-metal/FloatingActionButtonTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/ImageViewerNavigationModes.png b/scripts/ios/screenshots-metal/ImageViewerNavigationModes.png new file mode 100644 index 0000000000..93775be535 Binary files /dev/null and b/scripts/ios/screenshots-metal/ImageViewerNavigationModes.png differ diff --git a/scripts/ios/screenshots-metal/LightweightPickerButtons.png b/scripts/ios/screenshots-metal/LightweightPickerButtons.png new file mode 100644 index 0000000000..b832921691 Binary files /dev/null and b/scripts/ios/screenshots-metal/LightweightPickerButtons.png differ diff --git a/scripts/ios/screenshots-metal/ListTheme_dark.png b/scripts/ios/screenshots-metal/ListTheme_dark.png new file mode 100644 index 0000000000..ad96d80d09 Binary files /dev/null and b/scripts/ios/screenshots-metal/ListTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/ListTheme_light.png b/scripts/ios/screenshots-metal/ListTheme_light.png new file mode 100644 index 0000000000..c5d8b303ba Binary files /dev/null and b/scripts/ios/screenshots-metal/ListTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/MainActivity.png b/scripts/ios/screenshots-metal/MainActivity.png new file mode 100644 index 0000000000..dbae84bf0f Binary files /dev/null and b/scripts/ios/screenshots-metal/MainActivity.png differ diff --git a/scripts/ios/screenshots-metal/MediaPlayback.png b/scripts/ios/screenshots-metal/MediaPlayback.png new file mode 100644 index 0000000000..4f3bac6951 Binary files /dev/null and b/scripts/ios/screenshots-metal/MediaPlayback.png differ diff --git a/scripts/ios/screenshots-metal/MultiButtonTheme_dark.png b/scripts/ios/screenshots-metal/MultiButtonTheme_dark.png new file mode 100644 index 0000000000..d2ac8291ae Binary files /dev/null and b/scripts/ios/screenshots-metal/MultiButtonTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/MultiButtonTheme_light.png b/scripts/ios/screenshots-metal/MultiButtonTheme_light.png new file mode 100644 index 0000000000..86e9f604dd Binary files /dev/null and b/scripts/ios/screenshots-metal/MultiButtonTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/PaletteOverrideTheme_dark.png b/scripts/ios/screenshots-metal/PaletteOverrideTheme_dark.png new file mode 100644 index 0000000000..da9a335b1b Binary files /dev/null and b/scripts/ios/screenshots-metal/PaletteOverrideTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/PaletteOverrideTheme_light.png b/scripts/ios/screenshots-metal/PaletteOverrideTheme_light.png new file mode 100644 index 0000000000..f4d8d85df8 Binary files /dev/null and b/scripts/ios/screenshots-metal/PaletteOverrideTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/PickerTheme_dark.png b/scripts/ios/screenshots-metal/PickerTheme_dark.png new file mode 100644 index 0000000000..afcd2db19e Binary files /dev/null and b/scripts/ios/screenshots-metal/PickerTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/PickerTheme_light.png b/scripts/ios/screenshots-metal/PickerTheme_light.png new file mode 100644 index 0000000000..a8440f46d2 Binary files /dev/null and b/scripts/ios/screenshots-metal/PickerTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/README.md b/scripts/ios/screenshots-metal/README.md new file mode 100644 index 0000000000..79e08fed39 --- /dev/null +++ b/scripts/ios/screenshots-metal/README.md @@ -0,0 +1,22 @@ +# iOS Metal screenshot baselines + +Reference images for the Metal rendering backend (`codename1.arg.ios.metal=true`). The `build-ios-metal` job in `.github/workflows/scripts-ios.yml` compares `scripts/hellocodenameone` simulator output against these PNGs via `run-ios-ui-tests.sh`'s `SCREENSHOT_REF_DIR` override. + +## Scope + +The Metal backend is a work-in-progress port — see [`Ports/iOSPort/METAL_PORT_STATUS.md`](../../../Ports/iOSPort/METAL_PORT_STATUS.md). The golden images here started as copies of the OpenGL baselines in [`../screenshots/`](../screenshots/) and are expected to drift once: + +- `DrawString` lands (Phase 4's CoreText glyph atlas will sub-pixel-position differently from the current whole-string rasterisation; Phase 2 parity-level text will be closer but still not bit-identical), +- `ClipRect` scissor is re-enabled at the correct coord-space, +- Gradient, path, and remaining ops are ported. + +The expectation is **not** pixel parity with the GL baselines. These images exist so we can track Metal's own drift over time and accept intentional improvements. + +## Updating + +When a Metal-side change is expected to modify a screenshot: + +1. Run the CI `build-ios-metal` job (or `scripts/run-ios-ui-tests.sh` locally with `SCREENSHOT_REF_DIR=$(pwd)/scripts/ios/screenshots-metal`). +2. Download the `ios-ui-tests-metal` artifact and pull the `*.png` files for the tests that are now "different". +3. Inspect them side-by-side with the previous baseline. Accept only what's intentional. +4. Copy the accepted PNGs into this directory and commit them, naming them after the test IDs (same names as in `../screenshots/`). diff --git a/scripts/ios/screenshots-metal/Sheet.png b/scripts/ios/screenshots-metal/Sheet.png new file mode 100644 index 0000000000..cbe15ef902 Binary files /dev/null and b/scripts/ios/screenshots-metal/Sheet.png differ diff --git a/scripts/ios/screenshots-metal/ShowcaseTheme_dark.png b/scripts/ios/screenshots-metal/ShowcaseTheme_dark.png new file mode 100644 index 0000000000..e805dfe44f Binary files /dev/null and b/scripts/ios/screenshots-metal/ShowcaseTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/ShowcaseTheme_light.png b/scripts/ios/screenshots-metal/ShowcaseTheme_light.png new file mode 100644 index 0000000000..c5430c8636 Binary files /dev/null and b/scripts/ios/screenshots-metal/ShowcaseTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/SpanLabelTheme_dark.png b/scripts/ios/screenshots-metal/SpanLabelTheme_dark.png new file mode 100644 index 0000000000..99985d536d Binary files /dev/null and b/scripts/ios/screenshots-metal/SpanLabelTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/SpanLabelTheme_light.png b/scripts/ios/screenshots-metal/SpanLabelTheme_light.png new file mode 100644 index 0000000000..091a5367a1 Binary files /dev/null and b/scripts/ios/screenshots-metal/SpanLabelTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/SwitchTheme_dark.png b/scripts/ios/screenshots-metal/SwitchTheme_dark.png new file mode 100644 index 0000000000..695e515368 Binary files /dev/null and b/scripts/ios/screenshots-metal/SwitchTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/SwitchTheme_light.png b/scripts/ios/screenshots-metal/SwitchTheme_light.png new file mode 100644 index 0000000000..eeaa98f24a Binary files /dev/null and b/scripts/ios/screenshots-metal/SwitchTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/TabsBehavior.png b/scripts/ios/screenshots-metal/TabsBehavior.png new file mode 100644 index 0000000000..ce2048cc42 Binary files /dev/null and b/scripts/ios/screenshots-metal/TabsBehavior.png differ diff --git a/scripts/ios/screenshots-metal/TabsTheme_dark.png b/scripts/ios/screenshots-metal/TabsTheme_dark.png new file mode 100644 index 0000000000..b3021c309d Binary files /dev/null and b/scripts/ios/screenshots-metal/TabsTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/TabsTheme_light.png b/scripts/ios/screenshots-metal/TabsTheme_light.png new file mode 100644 index 0000000000..f1009fc502 Binary files /dev/null and b/scripts/ios/screenshots-metal/TabsTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/TextAreaAlignmentStates.png b/scripts/ios/screenshots-metal/TextAreaAlignmentStates.png new file mode 100644 index 0000000000..a101e328f9 Binary files /dev/null and b/scripts/ios/screenshots-metal/TextAreaAlignmentStates.png differ diff --git a/scripts/ios/screenshots-metal/TextFieldTheme_dark.png b/scripts/ios/screenshots-metal/TextFieldTheme_dark.png new file mode 100644 index 0000000000..0c14e60e78 Binary files /dev/null and b/scripts/ios/screenshots-metal/TextFieldTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/TextFieldTheme_light.png b/scripts/ios/screenshots-metal/TextFieldTheme_light.png new file mode 100644 index 0000000000..f44d1ef922 Binary files /dev/null and b/scripts/ios/screenshots-metal/TextFieldTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/ToastBarTopPosition.png b/scripts/ios/screenshots-metal/ToastBarTopPosition.png new file mode 100644 index 0000000000..561954a99a Binary files /dev/null and b/scripts/ios/screenshots-metal/ToastBarTopPosition.png differ diff --git a/scripts/ios/screenshots-metal/ToolbarTheme_dark.png b/scripts/ios/screenshots-metal/ToolbarTheme_dark.png new file mode 100644 index 0000000000..faa780e3e3 Binary files /dev/null and b/scripts/ios/screenshots-metal/ToolbarTheme_dark.png differ diff --git a/scripts/ios/screenshots-metal/ToolbarTheme_light.png b/scripts/ios/screenshots-metal/ToolbarTheme_light.png new file mode 100644 index 0000000000..00b6a7fda1 Binary files /dev/null and b/scripts/ios/screenshots-metal/ToolbarTheme_light.png differ diff --git a/scripts/ios/screenshots-metal/ValidatorLightweightPicker.png b/scripts/ios/screenshots-metal/ValidatorLightweightPicker.png new file mode 100644 index 0000000000..cdf7498014 Binary files /dev/null and b/scripts/ios/screenshots-metal/ValidatorLightweightPicker.png differ diff --git a/scripts/ios/screenshots-metal/graphics-affine-scale.png b/scripts/ios/screenshots-metal/graphics-affine-scale.png new file mode 100644 index 0000000000..049689f9e3 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-affine-scale.png differ diff --git a/scripts/ios/screenshots-metal/graphics-clip.png b/scripts/ios/screenshots-metal/graphics-clip.png new file mode 100644 index 0000000000..e3abda2695 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-clip.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-arc.png b/scripts/ios/screenshots-metal/graphics-draw-arc.png new file mode 100644 index 0000000000..97bc5f1923 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-arc.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-gradient.png b/scripts/ios/screenshots-metal/graphics-draw-gradient.png new file mode 100644 index 0000000000..eae01e5465 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-gradient.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-image-rect.png b/scripts/ios/screenshots-metal/graphics-draw-image-rect.png new file mode 100644 index 0000000000..8ba6b8f0a9 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-image-rect.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-line.png b/scripts/ios/screenshots-metal/graphics-draw-line.png new file mode 100644 index 0000000000..099a1596b1 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-line.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-rect.png b/scripts/ios/screenshots-metal/graphics-draw-rect.png new file mode 100644 index 0000000000..b0ada1d6f6 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-rect.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-round-rect.png b/scripts/ios/screenshots-metal/graphics-draw-round-rect.png new file mode 100644 index 0000000000..5cae7491bc Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-round-rect.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-shape.png b/scripts/ios/screenshots-metal/graphics-draw-shape.png new file mode 100644 index 0000000000..bae0a508e1 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-shape.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-string-decorated.png b/scripts/ios/screenshots-metal/graphics-draw-string-decorated.png new file mode 100644 index 0000000000..665d4284bc Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-string-decorated.png differ diff --git a/scripts/ios/screenshots-metal/graphics-draw-string.png b/scripts/ios/screenshots-metal/graphics-draw-string.png new file mode 100644 index 0000000000..c4d996162f Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-draw-string.png differ diff --git a/scripts/ios/screenshots-metal/graphics-fill-arc.png b/scripts/ios/screenshots-metal/graphics-fill-arc.png new file mode 100644 index 0000000000..fbd87e2971 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-fill-arc.png differ diff --git a/scripts/ios/screenshots-metal/graphics-fill-polygon.png b/scripts/ios/screenshots-metal/graphics-fill-polygon.png new file mode 100644 index 0000000000..2d44cc58a7 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-fill-polygon.png differ diff --git a/scripts/ios/screenshots-metal/graphics-fill-rect.png b/scripts/ios/screenshots-metal/graphics-fill-rect.png new file mode 100644 index 0000000000..bd05fe33ae Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-fill-rect.png differ diff --git a/scripts/ios/screenshots-metal/graphics-fill-round-rect.png b/scripts/ios/screenshots-metal/graphics-fill-round-rect.png new file mode 100644 index 0000000000..7e26f7e6ec Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-fill-round-rect.png differ diff --git a/scripts/ios/screenshots-metal/graphics-fill-shape.png b/scripts/ios/screenshots-metal/graphics-fill-shape.png new file mode 100644 index 0000000000..b35f0dc655 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-fill-shape.png differ diff --git a/scripts/ios/screenshots-metal/graphics-fill-triangle.png b/scripts/ios/screenshots-metal/graphics-fill-triangle.png new file mode 100644 index 0000000000..2390360cae Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-fill-triangle.png differ diff --git a/scripts/ios/screenshots-metal/graphics-rotate.png b/scripts/ios/screenshots-metal/graphics-rotate.png new file mode 100644 index 0000000000..db64cfe2af Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-rotate.png differ diff --git a/scripts/ios/screenshots-metal/graphics-scale.png b/scripts/ios/screenshots-metal/graphics-scale.png new file mode 100644 index 0000000000..d3031cd421 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-scale.png differ diff --git a/scripts/ios/screenshots-metal/graphics-stroke-test.png b/scripts/ios/screenshots-metal/graphics-stroke-test.png new file mode 100644 index 0000000000..0d6823bca6 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-stroke-test.png differ diff --git a/scripts/ios/screenshots-metal/graphics-tile-image.png b/scripts/ios/screenshots-metal/graphics-tile-image.png new file mode 100644 index 0000000000..72b5a51bf5 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-tile-image.png differ diff --git a/scripts/ios/screenshots-metal/graphics-transform-camera.png b/scripts/ios/screenshots-metal/graphics-transform-camera.png new file mode 100644 index 0000000000..0a1a02f462 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-transform-camera.png differ diff --git a/scripts/ios/screenshots-metal/graphics-transform-perspective.png b/scripts/ios/screenshots-metal/graphics-transform-perspective.png new file mode 100644 index 0000000000..f83f02308b Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-transform-perspective.png differ diff --git a/scripts/ios/screenshots-metal/graphics-transform-rotation.png b/scripts/ios/screenshots-metal/graphics-transform-rotation.png new file mode 100644 index 0000000000..b82202ce0f Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-transform-rotation.png differ diff --git a/scripts/ios/screenshots-metal/graphics-transform-translation.png b/scripts/ios/screenshots-metal/graphics-transform-translation.png new file mode 100644 index 0000000000..c123ef9089 Binary files /dev/null and b/scripts/ios/screenshots-metal/graphics-transform-translation.png differ diff --git a/scripts/ios/screenshots-metal/kotlin.png b/scripts/ios/screenshots-metal/kotlin.png new file mode 100644 index 0000000000..bd7d3f2bb9 Binary files /dev/null and b/scripts/ios/screenshots-metal/kotlin.png differ diff --git a/scripts/ios/screenshots-metal/landscape.png b/scripts/ios/screenshots-metal/landscape.png new file mode 100644 index 0000000000..7b83eca267 Binary files /dev/null and b/scripts/ios/screenshots-metal/landscape.png differ diff --git a/scripts/run-ios-ui-tests.sh b/scripts/run-ios-ui-tests.sh index 935d690d34..b494cf7952 100755 --- a/scripts/run-ios-ui-tests.sh +++ b/scripts/run-ios-ui-tests.sh @@ -163,7 +163,22 @@ fi SCHEME="$REQUESTED_SCHEME" ri_log "Using scheme $SCHEME" -SCREENSHOT_REF_DIR="$SCRIPT_DIR/ios/screenshots" +# The golden-image directory defaults to scripts/ios/screenshots for the +# OpenGL backend. Callers can override via SCREENSHOT_REF_DIR (absolute or +# relative to the repo root) so parallel backends -- like the Metal port -- +# can ship their own golden set. See Ports/iOSPort/METAL_PORT_STATUS.md. +if [ -n "${SCREENSHOT_REF_DIR:-}" ]; then + if [ ! -d "$SCREENSHOT_REF_DIR" ]; then + ri_log "SCREENSHOT_REF_DIR override '$SCREENSHOT_REF_DIR' is not a directory" >&2 + exit 3 + fi + # Convert to absolute so downstream tools (cn1ss-helpers, etc.) don't + # trip over cwd changes. + SCREENSHOT_REF_DIR="$(cd "$SCREENSHOT_REF_DIR" && pwd)" + ri_log "Using screenshot reference dir from SCREENSHOT_REF_DIR: $SCREENSHOT_REF_DIR" +else + SCREENSHOT_REF_DIR="$SCRIPT_DIR/ios/screenshots" +fi SCREENSHOT_TMP_DIR="$(mktemp -d "${TMPDIR}/cn1-ios-tests-XXXXXX" 2>/dev/null || echo "${TMPDIR}/cn1-ios-tests")" SCREENSHOT_RAW_DIR="$SCREENSHOT_TMP_DIR/raw" SCREENSHOT_PREVIEW_DIR="$SCREENSHOT_TMP_DIR/previews" @@ -663,7 +678,13 @@ APP_PROCESS_NAME="${WRAPPER_NAME%.app}" echo "App Launch : $(( (LAUNCH_END - LAUNCH_START) * 1000 )) ms" >> "$ARTIFACTS_DIR/ios-test-stats.txt" END_MARKER="CN1SS:SUITE:FINISHED" -TIMEOUT_SECONDS=300 +# 600s budget for the entire suite. Was 300s, but iOS simulator perf +# varies between CI runners and the 37-test suite plus a couple of +# 10-second test timeouts (FillRoundRect / DrawRoundRect on the Metal +# build) was hitting the cap intermittently -- producing 10 screenshots +# instead of 37 when the simulator was slow that day. 600s is still well +# under any reasonable CI slot length and gives headroom for outliers. +TIMEOUT_SECONDS=600 START_TIME="$(date +%s)" ri_log "Waiting for DeviceRunner completion marker ($END_MARKER)" while true; do @@ -841,4 +862,29 @@ if [ -n "$BASE64_BENCHMARK_FAILURE_LINE" ]; then exit 16 fi +# Guard: the suite must produce at least this many screenshots. A bug in +# the rendering pipeline (e.g. a hang during one test) used to surface as +# "Compared 1 screenshot" -- the suite would silently exit early after +# SIGTERM and we'd accept the run as green. The threshold is intentionally +# below the current suite size (~37 graphics tests) to allow legitimate +# additions/removals; raise it deliberately when adding tests. +MIN_SCREENSHOTS="${CN1SS_MIN_SCREENSHOTS:-30}" +if [ -s "$COMPARE_JSON" ]; then + ACTUAL_COUNT="$(python3 -c "import json,sys +try: + with open(sys.argv[1]) as f: + d = json.load(f) + print(len(d.get('results', []))) +except Exception as e: + print(0)" "$COMPARE_JSON" 2>/dev/null || echo 0)" +else + ACTUAL_COUNT=0 +fi +if [ "$ACTUAL_COUNT" -lt "$MIN_SCREENSHOTS" ]; then + ri_log "STAGE:SCREENSHOT_COUNT_REGRESSION -> got $ACTUAL_COUNT, expected >= $MIN_SCREENSHOTS" + ri_log "Suite likely hung or crashed early; check device-runner.log for SIGTERM and the last CN1SS:METAL_DIAG / CN1SS:INFO:suite entries." + exit 17 +fi +ri_log "Screenshot count check passed: $ACTUAL_COUNT >= $MIN_SCREENSHOTS" + exit $comment_rc diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 52055f96ea..48d98d5729 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -448,8 +448,9 @@ private static void handleIosOutput(ByteCodeTranslator b, File[] sources, File d } } else { fileListEntry.append("; path = \""); - if(file.endsWith(".m") || file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".mm") || file.endsWith(".h") || - file.endsWith(".swift") || file.endsWith(".bundle") || file.endsWith(".xcdatamodeld") || file.endsWith(".hh") || file.endsWith(".hpp") || file.endsWith(".xib")) { + if(file.endsWith(".m") || file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".mm") || file.endsWith(".h") || + file.endsWith(".swift") || file.endsWith(".bundle") || file.endsWith(".xcdatamodeld") || file.endsWith(".hh") || file.endsWith(".hpp") || file.endsWith(".xib") || + file.endsWith(".metal")) { fileListEntry.append(file); } else { fileListEntry.append(appName); @@ -483,8 +484,9 @@ private static void handleIosOutput(ByteCodeTranslator b, File[] sources, File d .append(" };\n"); } - if(file.endsWith(".m") || file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".hh") || file.endsWith(".hpp") || - file.endsWith(".swift") || file.endsWith(".mm") || file.endsWith(".h") || file.endsWith(".bundle") || file.endsWith(".xcdatamodeld") || file.endsWith(".xib")) { + if(file.endsWith(".m") || file.endsWith(".c") || file.endsWith(".cpp") || file.endsWith(".hh") || file.endsWith(".hpp") || + file.endsWith(".swift") || file.endsWith(".mm") || file.endsWith(".h") || file.endsWith(".bundle") || file.endsWith(".xcdatamodeld") || file.endsWith(".xib") || + file.endsWith(".metal")) { // bundle also needs to be a runtime resource if(file.endsWith(".bundle") || file.endsWith(".xcdatamodeld")) { @@ -612,6 +614,9 @@ private static String getFileType(String s) { if(s.endsWith(".swift")) { return "sourcecode.swift"; } + if(s.endsWith(".metal")) { + return "sourcecode.metal"; + } if(s.endsWith(".xib")) { return "file.xib"; }