Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,15 @@ private int getClientY(MouseEvent evt) {
}

private boolean hitTest(int x, int y) {
// With no native peer behind the canvas there is nothing a pointer could
// fall through to, so the canvas owns every point. Probing anyway cost a
// blocking round trip and a pixel read-back of the display canvas on every
// touchstart and wheel event, and Chrome answers repeated read-backs by
// taking the canvas off the GPU for good: on a low-end Android phone every
// later frame was then rasterized in software, ~80ms each (#6123).
if (!(Accessor.getActivePeerCount() > 0 && paintNativePeersBehind())) {
return true;
}
if (outputCanvas != null) {
// Single-pixel alpha probe of the display surface. A fully
// transparent pixel means the pointer falls through (no component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
package com.codenameone.examples.bastion.net;

import com.codenameone.examples.bastion.AppConfig;
import com.codename1.impl.javase.JavaSEPort;
import com.codename1.io.ConnectionRequest;
import com.codename1.io.Util;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.io.IOException;
Expand All @@ -32,6 +35,14 @@

/// What the network guard lets through.
class GuardTest {
/// `new ConnectionRequest()` asks the platform whether it supports access
/// points. Without this the test passed only when an earlier test class in
/// the same JVM had installed a port, and failed whenever it ran first.
@BeforeAll
static void port() {
Util.setImplementation(new JavaSEPort());
}

@Test
void cleartextAndUnlistedHostsAreRefused() {
withDevelopment(() -> {
Expand Down
62 changes: 62 additions & 0 deletions scripts/test-javascript-composited-rendering.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
* cancelled those keys on the accessibility tree's listbox element);
* phone (Chromium with touch emulation):
* - a swipe scrolls the content (#5912: it did nothing at all);
* - a swipe, a wheel and a mouse drag read no pixels back from the display: this app
* has no native peers, and a read-back takes Chrome's canvas off the GPU (#6123);
* - a tap on the hamburger opens the side menu, and a tap on a button fires it (#5912);
* - with the side menu open, none of the form's DOM text floats over the menu's shade.
* iOS user agent (Chromium presenting itself as an iPhone):
Expand Down Expand Up @@ -139,6 +141,39 @@ function canvasPixels(page, points) {
}, points);
}

// Pixel read-backs of the display while a pointer is used. An application without native
// peers has nothing a pointer could fall through to, so it must read nothing back: each
// touchstart and wheel event used to cost a worker round trip and a getImageData on the
// display canvas, and Chrome answers repeated read-backs by taking that canvas off the GPU
// for good -- a nine-row form then scrolled at ~12fps on a budget Android phone (#6123).
// Both are counted: the worker's __cn1_surface_read__ requests catch the port asking, and a
// direct getImageData on the display canvas catches any bridge path that reads it in place.
// Installed after boot and read before the test itself samples pixels (canvasPixels).
function watchReadbacks(page) {
return page.evaluate(() => {
const state = { surfaceReads: 0, displayReads: 0 };
const worker = window.__parparWorker;
if (!worker) throw new Error('no __parparWorker to observe');
worker.addEventListener('message', (e) => {
const d = e.data || {};
if (d.symbol === '__cn1_surface_read__') state.surfaceReads++;
});
const original = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = function(...args) {
if (this.canvas && this.canvas.id === 'codenameone-canvas') state.displayReads++;
return original.apply(this, args);
};
window.__cn1ReadbackWatch = state;
});
}

async function checkNoReadbacks(page, label) {
const r = await page.evaluate(() => window.__cn1ReadbackWatch);
check(r && r.surfaceReads === 0 && r.displayReads === 0,
`${label} reads no pixels back from the display (no native peers)`,
r ? `${r.surfaceReads} surface read request(s), ${r.displayReads} display getImageData call(s)` : 'watch missing');
}

function luminance(px) {
return 0.2126 * px[0] + 0.7152 * px[1] + 0.0722 * px[2];
}
Expand Down Expand Up @@ -340,9 +375,11 @@ async function desktop(name, browserType) {
const title = await span(page, 'Hi World');
const toolbarBottom = Math.max(20, Math.floor(title.bottom));
const a = await page.screenshot({ path: path.join(outDir, `${name}-scroll-0.png`) });
await watchReadbacks(page);
await page.mouse.move(500, 300);
for (let i = 0; i < 8; i++) { await page.mouse.wheel(0, 120); await page.waitForTimeout(120); }
await page.waitForTimeout(1500);
await checkNoReadbacks(page, `${name}: the wheel`);
const b = await page.screenshot({ path: path.join(outDir, `${name}-scroll-1.png`) });
const toolbarDiff = await diffFraction(page, a, b, [0, toolbarBottom]);
const contentDiff = await diffFraction(page, a, b, [toolbarBottom + 20, 440]);
Expand All @@ -367,6 +404,7 @@ async function desktop(name, browserType) {
await boot(page, logs);
const title = await span(page, 'Hi World');
const a = await page.screenshot();
await watchReadbacks(page);
await page.mouse.move(600, 400);
await page.mouse.down();
for (let i = 1; i <= 15; i++) { await page.mouse.move(600, 400 - i * 15); await page.waitForTimeout(20); }
Expand All @@ -375,6 +413,16 @@ async function desktop(name, browserType) {
const b = await page.screenshot({ path: path.join(outDir, `${name}-drag.png`) });
const moved = await diffFraction(page, a, b, [Math.floor(title.bottom) + 20, 440]);
check(moved > 0.05, `${name}: a mouse drag scrolls the content`, `${(moved * 100).toFixed(2)}% changed`);
// More presses from the same point: the first one of a page never probed (see the
// phone swipe).
for (let k = 0; k < 2; k++) {
await page.mouse.move(600, 400);
await page.mouse.down();
for (let i = 1; i <= 15; i++) { await page.mouse.move(600, 400 - i * 15); await page.waitForTimeout(20); }
await page.mouse.up();
await page.waitForTimeout(1000);
}
await checkNoReadbacks(page, `${name}: three mouse drags`);
fs.writeFileSync(path.join(outDir, `${name}-drag-console.txt`), logs.join('\n'));
await page.close();
}
Expand Down Expand Up @@ -445,6 +493,7 @@ async function phone() {
const band = [Math.floor(title.bottom) + 30, vp.height - 10];
const a = await page.screenshot({ path: path.join(outDir, 'phone-swipe-0.png') });
const cdp = await context.newCDPSession(page);
await watchReadbacks(page);
const x = Math.round(vp.width / 2);
const y0 = Math.round(vp.height * 0.8);
const y1 = Math.round(vp.height * 0.3);
Expand All @@ -458,6 +507,19 @@ async function phone() {
const b = await page.screenshot({ path: path.join(outDir, 'phone-swipe-1.png') });
const moved = await diffFraction(page, a, b, band);
check(moved > 0.05, 'phone: a swipe scrolls the content', `${(moved * 100).toFixed(2)}% changed`);
// More swipes from the same point: the probe never ran on the first press of a page,
// and a single gesture passed with the read-back still in place (measured on #6123's
// build: one read per press from the second on).
for (let k = 0; k < 2; k++) {
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y: y0 }] });
for (let i = 1; i <= 15; i++) {
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y: Math.round(y0 + (y1 - y0) * i / 15) }] });
await page.waitForTimeout(16);
}
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await page.waitForTimeout(1000);
}
await checkNoReadbacks(page, 'phone: three swipes');
fs.writeFileSync(path.join(outDir, 'phone-swipe-console.txt'), logs.join('\n'));
await page.close();
}
Expand Down
93 changes: 77 additions & 16 deletions vm/ByteCodeTranslator/src/javascript/browser_bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -2835,6 +2835,65 @@
// with JavaSEPort.applyLensBuffer(): the Simulator is the browser renderer's
// pixel reference for the glass-tab animation.
var LENS_MAG_FLAT = 0.75;

// Reads RGBA bytes of a canvas region WITHOUT calling getImageData on that
// canvas. Chrome counts read-backs per context and, after a couple, moves the
// canvas off the GPU for the rest of the page's life ("Multiple readback
// operations using getImageData..."). For the full-screen display canvas that
// meant every later frame was rasterized in software -- ~80ms a frame on a
// low-end Android phone (#6123). Copying the region into a scratch canvas made
// for reading keeps the read-back off the source's count. Out-of-bounds pixels
// read as transparent, as getImageData's do.
var READBACK_SCRATCH_KEEP_PIXELS = 1 << 20;
var readbackScratch = null;
var readbackScratchCtx = null;
function readCanvasPixels(canvas, ctx, x, y, w, h) {
if (w <= 0 || h <= 0) {
return null;
}
var doc = global.document || (global.window && global.window.document);
if (!canvas || !doc || !doc.createElement) {
try {
return ctx.getImageData(x, y, w, h).data;
} catch (_ed) {
return null;
}
}
try {
if (!readbackScratch) {
readbackScratch = doc.createElement('canvas');
}
if (readbackScratch.width < w || readbackScratch.height < h) {
readbackScratch.width = Math.max(readbackScratch.width, w);
readbackScratch.height = Math.max(readbackScratch.height, h);
readbackScratchCtx = null;
}
if (!readbackScratchCtx) {
readbackScratchCtx = readbackScratch.getContext('2d', { willReadFrequently: true });
}
var sc = readbackScratchCtx;
sc.setTransform(1, 0, 0, 1, 0, 0);
sc.globalAlpha = 1;
sc.globalCompositeOperation = 'source-over';
sc.clearRect(0, 0, w, h);
// The source rect is clipped to the canvas; draw only the part that is
// inside it, at its own offset, so the rest stays transparent.
var sx = Math.max(0, x), sy = Math.max(0, y);
var ex = Math.min(canvas.width, x + w), ey = Math.min(canvas.height, y + h);
if (ex > sx && ey > sy) {
sc.drawImage(canvas, sx, sy, ex - sx, ey - sy, sx - x, sy - y, ex - sx, ey - sy);
}
var data = sc.getImageData(0, 0, w, h).data;
if (w * h > READBACK_SCRATCH_KEEP_PIXELS) {
// One large getRGB must not pin a big CPU-backed canvas for the page's life.
readbackScratch = null;
readbackScratchCtx = null;
}
return data;
} catch (_er) {
return null;
}
}
var LENS_TINT_HI = 150;
var LENS_TINT_LO = 55;
var LENS_LIFT_COEF = 0.40;
Expand Down Expand Up @@ -2917,8 +2976,11 @@
return;
}

var source = ctx.getImageData(rx, ry, rw, rh);
var src = source.data;
// Read through a scratch canvas: see readCanvasPixels (#6123).
var src = readCanvasPixels(ctx.canvas, ctx, rx, ry, rw, rh);
if (!src) {
return;
}
var result = ctx.createImageData(rw, rh);
var out = result.data;
var hw = rw / 2.0, hh = rh / 2.0;
Expand Down Expand Up @@ -3250,7 +3312,10 @@
if (availableWidth <= 0 || availableHeight <= 0) {
return;
}
var available = ctx.getImageData(ax0, ay0, availableWidth, availableHeight).data;
var available = readCanvasPixels(ctx.canvas, ctx, ax0, ay0, availableWidth, availableHeight);
if (!available) {
return;
}
var padded = new Uint8ClampedArray(bufferWidth * bufferHeight * 4);
for (var by = 0; by < bufferHeight; by++) {
var sourceY = ry - pad + by - ay0;
Expand Down Expand Up @@ -3440,7 +3505,10 @@
if (rw <= 0 || rh <= 0) {
return;
}
var part = ctx.getImageData(rx, ry, rw, rh).data;
var part = readCanvasPixels(ctx.canvas, ctx, rx, ry, rw, rh);
if (!part) {
return;
}
var full = new Uint8ClampedArray(fullW * fullH * 4);
var row;
for (row = 0; row < rh; row++) {
Expand Down Expand Up @@ -3867,13 +3935,10 @@
return null;
}
var x = r.x | 0, y = r.y | 0, w = r.w | 0, h = r.h | 0;
var img;
try {
img = s.ctx.getImageData(x, y, w, h);
} catch (_er) {
var data = readCanvasPixels(s.canvas, s.ctx, x, y, w, h);
if (!data) {
return null;
}
var data = img.data;
var n = w * h;
var out = new Array(n);
for (var i = 0; i < n; i++) {
Expand Down Expand Up @@ -7617,7 +7682,6 @@
// TRANSPARENT (an actual punched hole), so we additionally require the canvas
// pixel under the cursor to be (near-)transparent before yielding to the peer.
function installPeerPointerToggle() {
var alphaCtx = null;
function canvasAlphaAt(canvas, x, y) {
// Returns the canvas' painted alpha (0-255) at client point (x,y), or -1 if
// it cannot be read. Maps CSS coords -> backing-store pixels (devicePixelRatio).
Expand All @@ -7627,12 +7691,9 @@
var px = Math.round((x - rect.left) * (canvas.width / rect.width));
var py = Math.round((y - rect.top) * (canvas.height / rect.height));
if (px < 0 || py < 0 || px >= canvas.width || py >= canvas.height) { return -1; }
if (!alphaCtx) {
alphaCtx = canvas.getContext('2d', { willReadFrequently: true })
|| canvas.getContext('2d');
}
if (!alphaCtx || typeof alphaCtx.getImageData !== 'function') { return -1; }
return alphaCtx.getImageData(px, py, 1, 1).data[3];
// Never getImageData on the display canvas itself: see readCanvasPixels.
var px1 = readCanvasPixels(canvas, null, px, py, 1, 1);
return px1 ? px1[3] : -1;
} catch (e) { return -1; }
}
function pointerOverPeer(x, y) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ void generatesBrowserBundleForJavascriptTarget(CompilerHelper.CompilerConfig con
assertTrue(browserBridge.contains("cn1HostBridge") && browserBridge.contains("host-callback") && browserBridge.contains("host-call"),
"Browser bridge should expose host-call plumbing for the JavaScript port shell");
assertTrue(browserBridge.contains("function applyLensSelfRegion")
&& browserBridge.contains("ctx.getImageData(rx, ry, rw, rh)")
&& browserBridge.contains("readCanvasPixels(ctx.canvas, ctx, rx, ry, rw, rh)")
&& browserBridge.contains("LENS_GLASS_TINT_STR")
&& browserBridge.contains("_lab = nums[ni++]")
&& browserBridge.contains("_ltintStrength = nums[ni++]"),
Expand Down
Loading