Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,22 @@ public boolean isTranslationSupported() {
return false;
}

/// When `#isTranslationSupported()` returns false, Graphics.java accumulates
/// xTranslate/yTranslate locally and bakes them into the vertex coordinates
/// passed to the impl's fill primitives. If the impl's render path then
/// applies the user's setTransform matrix on top of those already-translated
/// vertices (e.g. iOS Metal's GPU vertex shader does
/// `projection * modelView * userTransform * pos`), the translation is
/// double-counted for any non-translation matrix and the output ends up
/// off-screen. Override this and return true so Graphics.setTransform
/// conjugates the user's matrix with T(xTranslate, yTranslate) before
/// passing it to the impl, restoring translate-independent semantics.
/// Default: false (Android's setTransform path produces visible output
/// without the conjugation, so opting in there would change pixels).
public boolean isSetTransformTranslationConjugationRequired() {
return false;
}

/// Translates the X/Y location for drawing on the underlying surface. Translation
/// is incremental so the new value will be added to the current translation and
/// in order to reset translation we have to invoke
Expand Down
57 changes: 56 additions & 1 deletion CodenameOne/src/com/codename1/ui/Graphics.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public final class Graphics {
private int xTranslate;
private int yTranslate;
private Transform translation;
/// User's last setTransform() argument (or null if identity). On platforms
/// where impl.isTranslationSupported()=false the impl actually stores a
/// translation-conjugated version of this matrix so the user-visible
/// transform is independent of g.translate(); getTransform() must therefore
/// return the original.
private Transform userTransform;
private GeneralPath tmpClipShape;
/// A buffer shape to use when we need to transform a shape
private int color;
Expand Down Expand Up @@ -137,6 +143,17 @@ public void translate(int x, int y) {
} else {
xTranslate += x;
yTranslate += y;
// The conjugation in setTransform() depends on the current
// xTranslate/yTranslate. If the user accumulated more translation
// after a non-identity setTransform on a platform that requires
// conjugation (iOS), re-conjugate so the impl's baked matrix
// stays consistent with the new translation.
if (userTransform != null && impl.isSetTransformTranslationConjugationRequired()) {
Transform composed = Transform.makeTranslation(xTranslate, yTranslate);
composed.concatenate(userTransform);
composed.translate(-xTranslate, -yTranslate);
impl.setTransform(nativeGraphics, composed);
}
}
}

Expand Down Expand Up @@ -1129,6 +1146,9 @@ public void transform(Transform transform) {
///
/// - #setTransform
public Transform getTransform() {
if (userTransform != null) {
return userTransform.copy();
}
return impl.getTransform(nativeGraphics);

}
Expand Down Expand Up @@ -1160,7 +1180,37 @@ public Transform getTransform() {
///
/// - #setTransform(com.codename1.ui.geom.Matrix, int, int)
public void setTransform(Transform transform) {
impl.setTransform(nativeGraphics, transform);
// Some platforms accumulate xTranslate/yTranslate in Graphics.java
// (because impl.isTranslationSupported()=false) AND apply the user's
// setTransform matrix on top of the xTranslate-shifted vertex
// coordinates in the GPU pipeline -- this double-counts the
// translation for any non-translation matrix (rotate, scale, shear)
// and throws output off-screen. iOS Metal in particular surfaces the
// bug starkly: graphics-affine-scale's screen-mode top cells render
// blank because translate(18,18)*scale(2.65,5.36) * (1134,279) lands
// at (3023,1513), outside the 1170×2532 framebuffer. To make the
// user-visible setTransform consistent with platforms that fold
// translate into the canvas matrix (Android), conjugate the user's
// matrix with T(xTranslate, yTranslate) so its effect is independent
// of any prior g.translate() calls. Other platforms whose impl
// already gives setTransform that semantics (Android Skia, where the
// canvas matrix concat happens at draw time and accumulates with the
// user's matrix in a way that produced "shift but not vanish" before
// this fix) opt out by leaving
// isSetTransformTranslationConjugationRequired() false so the path
// is purely additive for the platforms that need it.
if (transform != null && !transform.isIdentity()
&& (xTranslate != 0 || yTranslate != 0)
&& impl.isSetTransformTranslationConjugationRequired()) {
userTransform = transform.copy();
Transform composed = Transform.makeTranslation(xTranslate, yTranslate);
composed.concatenate(transform);
composed.translate(-xTranslate, -yTranslate);
impl.setTransform(nativeGraphics, composed);
} else {
userTransform = null;
impl.setTransform(nativeGraphics, transform);
}
}

/// Loads the provided transform with the current transform applied to this graphics context.
Expand All @@ -1169,6 +1219,10 @@ public void setTransform(Transform transform) {
///
/// - `t`: An "out" parameter to be filled with the current transform.
public void getTransform(Transform t) {
if (userTransform != null) {
t.setTransform(userTransform);
return;
}
impl.getTransform(nativeGraphics, t);
}

Expand Down Expand Up @@ -1576,6 +1630,7 @@ public void resetAffine() {
impl.resetAffine(nativeGraphics);
scaleX = 1;
scaleY = 1;
userTransform = null;
}

/// Scales the coordinate system using the affine transform
Expand Down
10 changes: 10 additions & 0 deletions CodenameOne/src/com/codename1/ui/Transform.java
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,16 @@ public void setTransform(Transform t) {
initNativeTransform();
t.initNativeTransform();
impl.copyTransform(t.nativeTransform, nativeTransform);
// Mark the cached native matrix as dirty so subsequent
// getNativeTransform() calls re-run initNativeTransform.
// For TYPE_UNKNOWN this is a no-op for the matrix data
// itself, but it triggers any platform-side code that
// listens on initNativeTransform to refresh its cache --
// the iOS Metal port has shown that without this flag
// setTransform(composed) silently fails to apply on the
// form-Graphics screen encoder while the equivalent
// g.rotate / g.scale / g.translate path renders correctly.
dirty = true;
break;
}

Expand Down
29 changes: 23 additions & 6 deletions Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
Original file line number Diff line number Diff line change
Expand Up @@ -2402,9 +2402,15 @@ public void setTransform(Object graphics, Transform transform) {
ng.transform = transform == null ? null : transform.copy();
}
ng.transformApplied = false;
// Match the rotate/scale/translate/resetAffine paths: the cached
// clip / inverseClip / inverseTransform are derived from the previous
// transform, so replacing the transform must invalidate them or
// subsequent loadClipBounds / inverseClip calls return stale values.
ng.clipDirty = true;
ng.inverseClipDirty = true;
ng.inverseTransformDirty = true;
ng.checkControl();
ng.applyTransform();

}

public void setNativeTransformGlobal(Transform transform){
Expand Down Expand Up @@ -4213,15 +4219,26 @@ public void rotate(Object nativeGraphics, float angle, int x, int y) {
@Override
public boolean isTranslationSupported() {
//return true;
// We'll leave this as false until the next iteration...
// ES2 should allow us to do all of this using transforms but
// We'll leave this as false until the next iteration...
// ES2 should allow us to do all of this using transforms but
// let's take small steps first
return false;
}




@Override
public boolean isSetTransformTranslationConjugationRequired() {
// The iOS render path bakes xTranslate/yTranslate into vertex coords
// (since isTranslationSupported() is false) and the GPU then applies
// the user's setTransform matrix on top, double-counting the
// translation for any non-translation matrix and pushing output
// off-screen. Conjugating in Graphics.setTransform restores
// translate-independent semantics on this port.
return true;
}




public void shear(Object nativeGraphics, float x, float y) {
((NativeGraphics)nativeGraphics).shear(x, y);
}
Expand Down
Binary file modified scripts/android/screenshots/graphics-affine-scale.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/android/screenshots/graphics-scale.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/android/screenshots/graphics-transform-camera.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/android/screenshots/graphics-transform-perspective.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 74 additions & 3 deletions scripts/common/java/Cn1ssChunkTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -169,19 +169,87 @@ private static void runExtract(String[] args) throws IOException {
for (Chunk chunk : chunks) {
payload.append(chunk.payload);
}
byte[] data = null;
if (decode) {
byte[] data;
try {
data = Base64.getDecoder().decode(payload.toString());
} catch (IllegalArgumentException ex) {
data = new byte[0];
}
}
// Verify the reassembled binary matches the advertised FNV-1a 64
// hash from the emitter (only on the default PNG channel; the
// PREVIEW channel has its own JPEG bytes that don't match this
// hash). Hash mismatch means the chunk stream got corrupted in a
// way the gap detection above didn't catch -- e.g. a chunk's
// payload was rewritten in transit. Refuse to emit a stream that
// disagrees with its own integrity marker.
if (decode && (channel == null || channel.isEmpty())) {
String advertisedHash = readAdvertisedHash(path, targetTest);
if (advertisedHash != null) {
String actual = fnv1a64Hex(data);
if (!advertisedHash.equalsIgnoreCase(actual)) {
System.err.println("ERROR: reassembled bytes for test '" + targetTest
+ "' in " + path + " hash mismatch:");
System.err.println(" - advertised png_fnv1a64=" + advertisedHash);
System.err.println(" - reassembled png_fnv1a64=" + actual);
System.err.println(" - reassembled length=" + data.length);
System.err.println(" Refusing to emit a corrupted stream.");
System.exit(1);
}
}
}
if (decode) {
System.out.write(data);
} else {
System.out.print(payload.toString());
}
}

/// Returns the advertised FNV-1a 64-bit hash for the given test's PNG
/// payload, or null if no INFO line includes one. The emitter logs
/// `CN1SS:INFO:test=<name> png_bytes=<n> png_fnv1a64=<hex>` once the
/// image bytes are encoded; matching against the assembled stream's
/// hash gives an integrity check against silent chunk corruption.
///
/// The negative lookahead `(?![A-Za-z0-9_.\-])` after the test name is
/// load-bearing -- a plain `\b` word boundary lets the regex match
/// `graphics-draw-string-decorated` when the caller asked for
/// `graphics-draw-string`, because `\b` is satisfied by the boundary
/// between `g` (word char) and `-` (non-word char). The lookahead
/// rejects the suffix continuation by checking the next char is not in
/// the test-name character class used by CHUNK_PATTERN.
private static String readAdvertisedHash(Path path, String testName) throws IOException {
String text = Files.readString(path, StandardCharsets.UTF_8);
Pattern info = Pattern.compile(
"CN1SS:INFO:test=" + Pattern.quote(testName)
+ "(?![A-Za-z0-9_.\\-])[^\\n]*?\\bpng_fnv1a64=([0-9a-fA-F]{16})");
Matcher m = info.matcher(text);
String latest = null;
while (m.find()) {
latest = m.group(1);
}
return latest;
}

/// Mirror of Cn1ssDeviceRunnerHelper.fnv1a64Hex on the consumer side --
/// keep the algorithm identical (FNV-1a 64-bit, lowercase hex, leading
/// zeros) so the integrity check holds.
private static String fnv1a64Hex(byte[] bytes) {
long h = 0xcbf29ce484222325L;
long prime = 0x100000001b3L;
for (int i = 0; i < bytes.length; i++) {
h ^= bytes[i] & 0xff;
h *= prime;
}
StringBuilder sb = new StringBuilder(16);
for (int i = 60; i >= 0; i -= 4) {
int nib = (int) ((h >>> i) & 0xf);
sb.append((char) (nib < 10 ? '0' + nib : 'a' + (nib - 10)));
}
return sb.toString();
}

/**
* Returns the total base64 length advertised by the emitter for the given
* test/channel, or -1 if no matching INFO line was found. The emitter logs
Expand All @@ -192,11 +260,14 @@ private static void runExtract(String[] args) throws IOException {
private static long readTotalBase64Length(Path path, String testName, String channel) throws IOException {
// The INFO line is always emitted on the default channel regardless of
// whether the chunks themselves go to a side channel like PREVIEW, so
// we only filter by test name here.
// we only filter by test name here. See readAdvertisedHash for why
// the lookahead is required instead of `\b` -- prefixes like
// `graphics-draw-string` would otherwise match `graphics-draw-
// string-decorated`.
String text = Files.readString(path, StandardCharsets.UTF_8);
Pattern info = Pattern.compile(
"CN1SS:INFO:test=" + Pattern.quote(testName)
+ "\\b[^\\n]*?\\btotal_b64_len=(\\d+)");
+ "(?![A-Za-z0-9_.\\-])[^\\n]*?\\btotal_b64_len=(\\d+)");
Matcher m = info.matcher(text);
long latest = -1;
// The same test may emit multiple channels (PNG + PREVIEW). Without a
Expand Down
42 changes: 39 additions & 3 deletions scripts/common/java/PostPrComment.java
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,45 @@ private static Map<String, String> publishPreviewsToBranch(Path previewDir, Stri
ProcessResult status = runGit(worktree, env, true, "status", "--porcelain");
if (!status.stdout.trim().isEmpty()) {
runGit(worktree, env, "commit", "-m", "Add previews for PR #" + prNumber);
ProcessResult push = runGit(worktree, env, false, "push", "origin", "HEAD:cn1ss-previews");
if (push.exitCode != 0) {
throw new IOException(push.stderr.isEmpty() ? push.stdout : push.stderr);
// Concurrent jobs (build-ios + build-ios-metal) can both try to
// push to cn1ss-previews; the loser gets "rejected (fetch first)"
// which previously aborted the comment-post step and left the PR
// showing stale screenshots. Retry with a fetch + rebase so each
// CI job's preview commit is appended onto the latest tip.
int maxAttempts = 5;
ProcessResult push = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
push = runGit(worktree, env, false, "push", "origin", "HEAD:cn1ss-previews");
if (push.exitCode == 0) {
break;
}
if (attempt == maxAttempts) {
throw new IOException(push.stderr.isEmpty() ? push.stdout : push.stderr);
}
log("Preview push attempt " + attempt + " rejected; fetching + rebasing and retrying");
runGit(worktree, env, false, "fetch", "origin", "cn1ss-previews");
ProcessResult rebase = runGit(worktree, env, false, "rebase", "FETCH_HEAD");
if (rebase.exitCode != 0) {
runGit(worktree, env, false, "rebase", "--abort");
// The same prNumber/subdir directory was overwritten by
// the other job. Reset our index to FETCH_HEAD's tree and
// re-apply our preview files on top so we get a clean
// single commit.
runGit(worktree, env, false, "reset", "--hard", "FETCH_HEAD");
Files.createDirectories(dest);
for (Path source : imageFiles) {
Files.copy(source, dest.resolve(source.getFileName()),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
runGit(worktree, env, "add", "-A", ".");
ProcessResult status2 = runGit(worktree, env, true, "status", "--porcelain");
if (status2.stdout.trim().isEmpty()) {
log("Preview branch already up-to-date after rebase for PR #" + prNumber);
push = new ProcessResult(0, "", "");
break;
}
runGit(worktree, env, "commit", "-m", "Add previews for PR #" + prNumber);
}
}
log("Published " + imageFiles.size() + " preview(s) to cn1ss-previews/pr-" + prNumber);
} else {
Expand Down
Loading
Loading