Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bb0fcef
Graphics.useMatrixTranslation: route g.translate through impl matrix
shai-almog May 16, 2026
78ab733
Graphics matrix mode: drop shadow accumulator, fix bypass-Graphics ca…
shai-almog May 16, 2026
6731497
Graphics matrix mode: restore shadow + preserve framework translate a…
shai-almog May 16, 2026
2777670
Graphics matrix mode: matrix is the single source of truth, callsites…
shai-almog May 16, 2026
dedb269
Use short names: import Transform, drop com.codename1.ui.Graphics. FQN
shai-almog May 16, 2026
07dbb5f
Graphics matrix mode: route setTransform(null) through impl.resetAffine
shai-almog May 16, 2026
fec80d2
Graphics matrix mode: keep userTransform null to prevent stale-state …
shai-almog May 16, 2026
9e87346
Android: AsyncGraphics.translateMatrix must queue an op like scale/ro…
shai-almog May 17, 2026
7aba905
Graphics matrix mode: conjugate user setTransform around framework an…
shai-almog May 17, 2026
1d050c3
Graphics matrix mode: subtract framework anchor from peer clearRect c…
shai-almog May 17, 2026
1a19634
Graphics matrix mode: framework anchor accessor + revert snapshot-res…
shai-almog May 17, 2026
6622726
Revert "Graphics matrix mode: framework anchor accessor + revert snap…
shai-almog May 17, 2026
59d872a
Revert Graphics matrix mode to known-good state (commit 7af87077f)
shai-almog May 17, 2026
3323cdf
Graphics matrix mode: framework-anchor accessor + setTransform conjug…
shai-almog May 17, 2026
9a4277c
Android matrix mode: peer screen position + 2 goldens
shai-almog May 17, 2026
bdf9ee7
JS port: reset canvas transform at drain start (matrix-mode title fix)
shai-almog May 17, 2026
3858a63
JS port: BufferedGraphics override for translateMatrix
shai-almog May 17, 2026
4eb1ef3
JS port goldens: promote matrix-mode renderings (17 + 1 new)
shai-almog May 17, 2026
fab590c
iOS Metal: save/restore currentTransform on mutable side-trip + 5 gol…
shai-almog May 18, 2026
ec99622
iOS Metal: targeted mutable-image drain in gausianBlurImage
shai-almog May 18, 2026
ac4de5f
Re-trigger CI to repopulate iOS port cache
shai-almog May 18, 2026
7984086
iOS Metal goldens: use CI-runner actuals (1179x2556)
shai-almog May 18, 2026
c5f6d2b
iOS GL goldens: matrix-mode renderings + 1 new
shai-almog May 18, 2026
1caa528
Fix matrixFrameworkTranslateX legacy-mode regression + revert iOS GL …
shai-almog May 19, 2026
679f583
JS golden: graphics-inscribed-triangle-grid CI actual on rebased branch
shai-almog May 19, 2026
c653299
Probe: log polygon clip + next 6 ops on iOS GL stencil path
shai-almog May 19, 2026
b1e6a72
Probe: rename CN1DBG -> CN1SS:DBG so run-ios-ui-tests.sh log filter f…
shai-almog May 19, 2026
b9fd77f
Probe: dump full polygon vertices + GL state at FillRect.exec
shai-almog May 19, 2026
75b8bbc
Probe: read actual stencil byte at panel 1 vs panel 2 polygon center
shai-almog May 19, 2026
0c162b9
Probe: replace GL_STENCIL_INDEX read (not in ES2) with framebuffer state
shai-almog May 19, 2026
eec5e5d
Probe: framebuffer pixel readback around clip-under-rotation FillRect…
shai-almog May 19, 2026
b0e6ec1
Probe: stencil marker at panel 1/2 polygon centers after polygon clip
shai-almog May 19, 2026
5cc13ad
Revert callsite churn: keep master snapshot-reset pattern, make getTr…
shai-almog May 19, 2026
8ea0c5f
Run Metal tests twice: matrix-on (with overlay) + matrix-off (legacy)
shai-almog May 19, 2026
2d775ff
Restore matrixFrameworkTranslateX for snapshot-reset; getTranslateX k…
shai-almog May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions .github/workflows/scripts-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,15 @@ jobs:
# 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
# The matrix-translation flag is on by default in this job
# (HelloCodenameOne.kt reads codename1.arg.matrixTranslation,
# build-ios-app.sh passes the MATRIX_TRANSLATION env through).
# Overlay the matrix-mode-specific goldens on top of the base set
# so the few tests that diverge between modes (inscribed-triangle,
# translate-then-scale, etc.) check against their matrix-mode
# reference while every other test still compares against the
# single shared base golden.
SCREENSHOT_REF_OVERLAY_DIR: ${{ github.workspace }}/scripts/ios/screenshots-metal-matrix
# Override the PR comment marker / preview path / title so this
# job posts its own comment instead of overwriting the GL job's
# comment (both jobs ran with the same default marker before, so
Expand Down Expand Up @@ -453,3 +462,218 @@ jobs:
path: artifacts
if-no-files-found: warn
retention-days: 14

build-ios-metal-legacy:
# Companion job to build-ios-metal: runs the same iOS Metal screenshot
# suite with Graphics.useMatrixTranslation off (codename1.arg.matrixTranslation=false)
# so any regression that legacy callers would see on Metal gets caught
# before the opt-in flag is enabled in real apps. The base golden set
# in scripts/ios/screenshots-metal/ tracks legacy-mode renders; the
# matrix-mode job overlays the per-test differences from
# scripts/ios/screenshots-metal-matrix/ on top.
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-legacy-${{ github.ref_name }}
cancel-in-progress: true

env:
GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
MATRIX_TRANSLATION: 'false'

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 native-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
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
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
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
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, legacy mode)
id: build-ios-app
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30

- name: Run iOS UI screenshot tests (Metal, legacy mode)
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/ios-ui-tests-metal-legacy
# Legacy mode reads against the base Metal goldens with no
# matrix-mode overlay -- the goal here is to catch regressions
# in the legacy code path that the opt-in flag should leave
# untouched.
SCREENSHOT_REF_DIR: ${{ github.workspace }}/scripts/ios/screenshots-metal
CN1SS_COMMENT_MARKER: '<!-- CN1SS_IOS_METAL_LEGACY_COMMENT -->'
CN1SS_PREVIEW_SUBDIR: ios-metal-legacy
CN1SS_REPORT_TITLE: 'iOS Metal (legacy mode) screenshot updates'
CN1SS_SUCCESS_MESSAGE: '✅ Native iOS Metal screenshot tests passed in legacy mode.'
CN1SS_COMMENT_LOG_PREFIX: '[run-ios-device-tests-metal-legacy]'
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-legacy screenshot summary
if: always()
env:
COMPARE_JSON: ${{ github.workspace }}/artifacts/ios-ui-tests-metal-legacy/screenshot-compare.json
COMMENT_MD: ${{ github.workspace }}/artifacts/ios-ui-tests-metal-legacy/screenshot-comment.md
run: |
set -eu
{
echo "## iOS Metal screenshot comparison (legacy mode)"
echo
echo "Ran with \`codename1.arg.matrixTranslation=false\` so the legacy code path is exercised under iOS Metal."
echo "Golden images: \`scripts/ios/screenshots-metal/\` (no matrix overlay)."
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 legacy-mode screenshot comparison::${NOTICE}"
fi
fi

- name: Upload iOS Metal legacy artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: ios-ui-tests-metal-legacy
path: artifacts
if-no-files-found: warn
retention-days: 14
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,12 @@ public void paintDirty() {
continue;
}
paintQueueTemp[iter] = null;
wrapper.translate(-wrapper.getTranslateX(), -wrapper.getTranslateY());
// Snapshot-reset uses matrixFrameworkTranslateX (returns
// the framework anchor in both modes) so the wrapper
// graphics starts each queued paint at identity in matrix
// mode too; bare getTranslateX returns zero there and the
// translate would no-op.
wrapper.translate(-wrapper.matrixFrameworkTranslateX(), -wrapper.matrixFrameworkTranslateY());
wrapper.resetAffine();
wrapper.setClip(0, 0, dwidth, dheight);
if (ani instanceof Component) {
Expand Down
23 changes: 18 additions & 5 deletions CodenameOne/src/com/codename1/maps/MapComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.codename1.ui.Graphics;
import com.codename1.ui.Image;
import com.codename1.ui.ImageFactory;
import com.codename1.ui.Transform;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.geom.Dimension;
Expand Down Expand Up @@ -269,11 +270,23 @@ public void paintBackground(Graphics g) {
tx = -tx * (float) (scaleX - getWidth()) / sx;
float ty = (float) zoomCenterY / (float) getHeight();
ty = -ty * (float) (scaleY - getHeight()) / sy;
g.translate((int) tx, (int) ty);
g.scale(sx, sy);
paintmap(g);
g.resetAffine();
g.translate(-(int) tx, -(int) ty);
if (Graphics.useMatrixTranslation) {
// Matrix mode: resetAffine wipes the impl matrix to
// identity, which destroys the framework painting-chain
// translates the matrix carries. Use save/restore the
// impl matrix around the scale + user-translate instead.
Transform savedMatrix = g.getTransform();
g.translate((int) tx, (int) ty);
g.scale(sx, sy);
paintmap(g);
g.setTransform(savedMatrix);
} else {
g.translate((int) tx, (int) ty);
g.scale(sx, sy);
paintmap(g);
g.resetAffine();
g.translate(-(int) tx, -(int) ty);
}
} else {
g.translate(-translateX, -translateY);
paintmap(g);
Expand Down
8 changes: 4 additions & 4 deletions CodenameOne/src/com/codename1/ui/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -3031,8 +3031,8 @@ void internalPaintImpl(Graphics g, boolean paintIntersects) {
public void paintIntersectingComponentsAbove(Graphics g) {
Container parent = getParent();
Component component = this;
int tx = g.getTranslateX();
int ty = g.getTranslateY();
int tx = g.matrixFrameworkTranslateX();
int ty = g.matrixFrameworkTranslateY();

g.translate(-tx, -ty);
int x1 = getAbsoluteX() + getScrollX();
Expand Down Expand Up @@ -3344,8 +3344,8 @@ private void drawPainters(Graphics g, Component par, Component c,
paintBackgroundImpl(tg);
putClientProperty("$FLAT", i);
}
int tx = g.getTranslateX();
int ty = g.getTranslateY();
int tx = g.matrixFrameworkTranslateX();
int ty = g.matrixFrameworkTranslateY();
g.translate(-tx + absX, -ty + absY);
g.drawImage(i, 0, 0);
g.translate(tx - absX, ty - absY);
Expand Down
4 changes: 2 additions & 2 deletions CodenameOne/src/com/codename1/ui/Container.java
Original file line number Diff line number Diff line change
Expand Up @@ -2186,8 +2186,8 @@ public void paint(Graphics g) {
paintElevatedPane(g);
}

int tx = g.getTranslateX();
int ty = g.getTranslateY();
int tx = g.matrixFrameworkTranslateX();
int ty = g.matrixFrameworkTranslateY();
g.translate(-tx, -ty);
if (sidemenuBarTranslation > 0) {
g.translate(sidemenuBarTranslation, 0);
Expand Down
8 changes: 4 additions & 4 deletions CodenameOne/src/com/codename1/ui/FontImage.java
Original file line number Diff line number Diff line change
Expand Up @@ -7796,8 +7796,8 @@ protected void drawImage(Graphics g, Object nativeGraphics, int x, int y) {
g.concatenateAlpha(fgAlpha);
}
if (rotated != 0) {
int tX = g.getTranslateX();
int tY = g.getTranslateY();
int tX = g.matrixFrameworkTranslateX();
int tY = g.matrixFrameworkTranslateY();
g.translate(-tX, -tY);
g.rotate((float) Math.toRadians(rotated % 360), tX + x + width / 2, tY + y + height / 2);
g.drawString(text, tX + x + width / 2 - w / 2, tY + y + height / 2 - h / 2);
Expand Down Expand Up @@ -7837,8 +7837,8 @@ protected void drawImage(Graphics g, Object nativeGraphics, int x, int y, int w,
}
//int paddingPixels = Display.getInstance().convertToPixels(padding, true);
if (rotated != 0) {
int tX = g.getTranslateX();
int tY = g.getTranslateY();
int tX = g.matrixFrameworkTranslateX();
int tY = g.matrixFrameworkTranslateY();
g.translate(-tX, -tY);
g.rotate((float) Math.toRadians(rotated % 360), tX + x + w / 2, tY + y + h / 2);
g.drawString(text, tX + x + w / 2 - ww / 2, tY + y);
Expand Down
4 changes: 2 additions & 2 deletions CodenameOne/src/com/codename1/ui/Form.java
Original file line number Diff line number Diff line change
Expand Up @@ -1117,8 +1117,8 @@ void paintGlassImpl(Graphics g) {
return;
}
if (glassPane != null) {
int tx = g.getTranslateX();
int ty = g.getTranslateY();
int tx = g.matrixFrameworkTranslateX();
int ty = g.matrixFrameworkTranslateY();
g.translate(-tx, -ty);
glassPane.paint(g, getBounds());
g.translate(tx, ty);
Expand Down
Loading
Loading