Fix Settings/desktop window left black after a canvas surface swap (#5443)#5464
Conversation
Investigating issue #5443: the black Control Center window survived the 7.0.261 fix. The existing Windows CI gate accepts the exact screenshot from the report (63 distinct colours, 89% dominant), so it could never have caught it. - ScreenshotSanity gains a coverage check: the share of grid cells with more than one colour. Healthy Settings windows cover 63-72% and the simulator 89-95%, while the reported black window covers 16-18%. - The Settings stub can dump render state (settings.diagnostics) and grab the real on-screen pixels next to the offscreen paint, so a window that paints correctly into a BufferedImage while showing nothing is visible. - The Windows harness now runs a render matrix over HiDPI scale, dark mode and locale instead of only the runner's 100%/light/en-US desktop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…5443) The JavaSE canvas paints into a TYPE_INT_RGB buffer that is discarded and reallocated whenever the surface geometry changes. The replacement starts out blank - black - but nothing marked the form dirty, so the next paintDirty() refilled only whatever was in the dirty list. A window that was up and correct became a black rectangle with a stray component still painted in it, and stayed that way: the only thing that healed it was an AWT-initiated paint, which a settled desktop window with no native peers may never get. That is the shape of the Control Center screenshots in issue #5443 - a pure #000000 surface, matching neither the light (#F3F4F7) nor the dark (#071B4D) theme background, with the project icon preview the one thing drawn. Replacing the buffer now invalidates the whole form. The repaint is queued rather than run inline because the swap happens both while the EDT is acquiring paint graphics and from the AWT thread inside blit(). CanvasSurfaceSwapRepaintTest drives a real port: it shows a form, forces a surface swap with no size-change event behind it, marks a single child dirty and asserts the form repaints in full. It fails without the fix. Also adds a settings.diagnostics dump (environment, HiDPI scale, window and canvas geometry, form size, resolved theme colours and font metrics, plus an EDT stack when the EDT is wedged) and documents it, so the next report of this failure class arrives actionable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Robot grab of a window that is not on screen captures whatever is behind it, which reads as an empty window rather than as a missing capture - a misleading artifact is worse than no artifact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes the Windows “Settings/Control Center window stays black after a canvas surface swap” regression by ensuring that when the JavaSE port reallocates its backing buffer, the current form is invalidated so the next paint repopulates the entire surface. The PR also strengthens Windows CI detection (so this failure mode is caught reliably) and adds a diagnostics dump to make future reports actionable.
Changes:
- JavaSEPort: when the paint buffer is replaced, queue a full-form repaint to avoid leaving the new buffer as an unpainted black surface.
- Windows tooling CI: extend screenshot sanity checks with a “content coverage” metric and run a Settings render matrix (HiDPI/dark mode/locale) using both offscreen and on-screen captures.
- Settings tool: add render-state diagnostics output and corresponding unit tests; document how to collect diagnostics.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/windows/ScreenshotSanity.java | Adds a “content coverage” heuristic to reject screenshots that aren’t globally painted (catches the #5443 black-surface case). |
| scripts/settings/javase/src/test/java/com/codename1/settings/SettingsDiagnosticsTest.java | Adds focused tests for diagnostics output and screenshot on-screen filename derivation. |
| scripts/settings/javase/src/desktop/java/com/codename1/settings/SettingsDiagnostics.java | Implements a best-effort diagnostics report covering environment, scale, window geometry, theme sampling, and EDT stack. |
| scripts/settings/javase/src/desktop/java/com/codename1/settings/CodenameOneSettingsStub.java | Captures on-screen pixels alongside offscreen paint; writes diagnostics without risking AWT↔EDT deadlocks; coordinates exit behavior. |
| scripts/run-windows-tooling-tests.sh | Adds a Settings render scenario matrix (HiDPI/dark/locale) and gates primarily on on-screen captures when available. |
| Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java | Invalidates the current form after buffer swaps to prevent a blank/black window after surface reallocation. |
| maven/javase/src/test/java/com/codename1/impl/javase/CanvasSurfaceSwapRepaintTest.java | Adds a regression test ensuring a surface swap triggers a whole-form repaint (prevents the black-buffer scenario). |
| docs/developer-guide/Maven-Getting-Started.adoc | Documents how to collect Settings diagnostics (and optional screenshots) for blank-window reports. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Compared 12 screenshots: 12 matched. |
The developer guide requires source blocks to be include-backed; validate-guide-snippets.py rejected the inline block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cloudflare Preview
|
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
|
Compared 148 screenshots: 148 matched. Benchmark Results
Detailed Performance Metrics
|
|
Compared 146 screenshots: 146 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 217 screenshots: 217 matched. |
|
Compared 144 screenshots: 144 matched. |
|
Compared 149 screenshots: 149 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
|
Compared 143 screenshots: 143 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
A merely slow EDT could run its probe after the timeout had already written the unresponsive-EDT fallback, overwriting it and discarding the stack dump naming whatever the EDT was stuck on - the reason the fallback exists. Both writers now claim the report first, and the timeout path waits for an in-flight EDT write rather than racing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The count is meant to distinguish a collapsed layout from a healthy one, which only works if hidden and zero-sized components stay out of it. A hidden or collapsed container takes its whole subtree with it - its children cannot paint through it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 181 screenshots: 181 matched. |
Follow-up to #5445, which did not fix issue #5443 — the reporter still sees the black Control Center on 7.0.261.
What the screenshots actually say
The reported window is pure
#000000. That matches neither the light (#F3F4F7) nor the dark (#071B4D)SettingsFormbackground, so it is not a theme that failed to load — it is the rawTYPE_INT_RGBcanvas buffer with nothing painted into it. The one thing drawn is the project's icon preview, at its correct pixel size, and it survives the reporter resizing the window between their two screenshots.Root cause
JavaSEPort.C.updateBufferSize()discards and reallocates the paint buffer whenever the surface geometry changes. The replacement is blank, but nothing marked the form dirty, so the nextpaintDirty()refilled only whatever happened to be in the dirty list. Everything else stayed at the buffer's initial black. The only thing that healed it was an AWT-initiated paint (paintComponentqueues a fullf.repaint()), which a settled desktop window with no native peers may never receive.Replacing the buffer now invalidates the whole form. The repaint is queued rather than run inline: the swap happens both while the EDT is acquiring paint graphics (it may be iterating the dirty list) and from the AWT thread inside
blit().CanvasSurfaceSwapRepaintTestdrives a real port — shows a form, forces a surface swap with no size-change event behind it, marks a single child dirty, and asserts the form repaints in full. It fails without the fix.Why CI did not catch this
The
windows-toolinggate added in #5445 accepts the exact screenshot from the issue: 63 distinct colours and 89% single-colour dominance both pass.ScreenshotSanitygains a coverage check — the share of grid cells holding more than one colour. Healthy Settings windows measure 63–72% and the simulator 89–95%; the reported black window measures 16–18%. The gate now rejects both screenshots from the issue and passes every healthy capture with margin.The job also only ever ran the runner's own desktop: 100% scale, light mode, en-US. It now runs a render matrix over HiDPI scale (125/150/175%), dark mode and locale, and captures the real on-screen pixels next to the offscreen paint, since
contentPane.paint()can look healthy while the window shows something else. All seven scenarios pass on Windows — which is what ruled those variables out and pointed at the surface swap.Diagnostics
mvn cn1:settings -Dsettings.diagnostics=diag.txtdumps the environment, the HiDPI scale the JVM reports, window/canvas geometry, the size Codename One laid the form out at, and the resolved theme colours and font metrics — plus an EDT stack trace if the EDT is wedged. Documented in the Maven getting-started guide so the next report of this failure class arrives actionable in one round trip.Honest scope
The mechanism is confirmed by a test that fails without the fix, and it is the only mechanism consistent with the reported pixels. What triggers the surface swap on the reporter's specific machine is not reproduced here — CI is green across the whole matrix — so this fixes the failure rather than a demonstrated trigger. If a blank window survives this, the diagnostics dump will say why.
Verification
CanvasSurfaceSwapRepaintTestfails before the change, passes aftercodenameone-javasesuite: 175 tests, 0 failurescn1-settingssuite: 34 tests, 0 failures🤖 Generated with Claude Code