Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
156270a
feat!: Scratch for Java 7.0 on LWJGL
mikebarkmin Oct 22, 2026
2fa9401
build: scripts and CI for the module layout
mikebarkmin Oct 22, 2026
5f81e9e
docs: documentation for 7.0
mikebarkmin Oct 22, 2026
4995080
chore: LWJGL spike
mikebarkmin Oct 22, 2026
21223a6
docs: AGENTS.md describes the repository
mikebarkmin Oct 22, 2026
adafc4c
fix(migrate-7): leave tools' folders alone; row-form hint only for 6.…
mikebarkmin Oct 22, 2026
9748db4
perf: faster collision and colour checks
mikebarkmin Oct 22, 2026
987d5e4
feat(scripts): benchmark.sh, with a history of results per commit
mikebarkmin Oct 22, 2026
8ff0e5d
chore(benchmark): first run on a GPU (Radeon 780M, Wayland), 6.x and 7.0
mikebarkmin Oct 22, 2026
aedc9d6
perf: touching checks look up the asking sprite's hitbox once
mikebarkmin Oct 22, 2026
2ab344f
perf: draw sprites in batches, small costumes from shared textures
mikebarkmin Oct 22, 2026
12bd337
fix: own frame pacing where vsync does not wait, judged per second of…
mikebarkmin Oct 22, 2026
9341d4c
chore(benchmark): runs after batching, the costume atlas and the fram…
mikebarkmin Oct 22, 2026
b378b11
feat: switch the debug overlay per option in an in-scene panel
mikebarkmin Oct 23, 2026
4bfae1d
fix(demos): donutIO scenery must not touch; drop leftover demo code
mikebarkmin Oct 23, 2026
4dfcd64
feat: the scratch-gamepad extension; smooth pixel art letterboxes to fit
mikebarkmin Oct 25, 2026
2828271
chore(scripts): run the lifecycle parity probe on the 7.0 window in t…
mikebarkmin Oct 26, 2026
035f8cb
docs(shader): drop the leftover Processing vertex shader from the dem…
mikebarkmin Oct 26, 2026
83ee1e7
chore(compatibility): the browser reads pixels; extension plumbing is…
mikebarkmin Oct 26, 2026
6f7cad1
Merge remote-tracking branch 'origin/main' into v7
mikebarkmin Oct 26, 2026
d5b8168
test(parity): record the debug frame with the sprite's name in the po…
mikebarkmin Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions .changeset/gamepad-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
type: minor
---

Add the `scratch-gamepad` extension: controller support for up to four gamepads. `Gamepad` asks
anywhere for buttons, sticks (y up, as on the stage, with a dead zone), triggers and connections;
stages and sprites implementing `GamepadListener` are told about button presses, releases and
controllers connecting, at the same point of the tick as key events. Buttons are named in Xbox
positions (A, B, X, Y); controllers without a known layout are skipped and there is no rumble.
11 changes: 11 additions & 0 deletions .changeset/lwjgl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
type: major
---

Scratch for Java no longer runs on Processing. It draws with its own renderer on LWJGL (OpenGL 3.3, 4.1 on macOS), plays sound through OpenAL and needs no `java.desktop`. `run()` is called exactly 60 times per second of game time on every computer, and what a program sees (collisions, colour sensing, text layout) no longer depends on the graphics card or the screen.

The extensions are jars of their own: `scratch-tiled`, `scratch-save`, `scratch-recorder` and `scratch-drawing`, with short packages (`org.openpatch.scratch.tiled`, …). Camera, shaders, sound, pixels and sorting are part of `org.openpatch.scratch`. The `-all` jar still contains everything, and a public extension API lets others write extensions.

The API was cleaned up for learners, and programs written for 6.x need changes: time is in seconds everywhere (`say("Hi", 2)`, `waitSeconds(0.5)`, `timer.every(0.5)`), animations and nine-slicing are on every sprite (`AnimatedSprite` and `UISprite` are gone), `createClone()` replaces `clone()`, layers replace the UI flags, `Color` cannot be changed, the colour effect and the tint are separate, and shaders have a new, shorter format. The page "Moving from 6.x to 7.0" lists every change, and `scripts/migrate-7.py` makes the mechanical ones.

BlueJ is no longer supported; Scratch for Java Studio is the IDE.
7 changes: 7 additions & 0 deletions .changeset/smooth-pixel-art-letterbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: patch
---

Smooth pixel art scales the window's letterbox to fit, like its camera zoom, instead of whole
numbers: with `Window.useSmoothPixelArt()` the stage fills the window at any size, while plain
`usePixelArt()` keeps stepping whole pixels.
319 changes: 139 additions & 180 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions .github/workflows/macos-spike.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: macOS spike (gate G1)

# The LWJGL spike on macOS: do the two macOS launch routes open a window, keep main
# free and run the game loop? Started by hand; its captures and logs are the
# evidence. A runner without OpenGL makes the run inconclusive, not a failure
# of the routes: then a real Mac decides.
on:
workflow_dispatch:

jobs:
spike:
strategy:
fail-fast: false
matrix:
os: [macos-latest, macos-15-intel]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
defaults:
run:
working-directory: spikes/lwjgl

steps:
- uses: actions/checkout@v7

- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 25
cache: maven

- name: Build the spike
run: mvn -q compile dependency:build-classpath -Dmdep.outputFile=target/cp.txt

- name: System
run: |
sw_vers
uname -m
system_profiler SPDisplaysDataType || true

# macOS has no timeout(1); perl's alarm ends a run that hangs
- name: Route 1 - plain java, glfw_async, no -XstartOnFirstThread
run: |
perl -e 'alarm 60; exec @ARGV' java --enable-native-access=ALL-UNNAMED -cp "target/classes:$(cat target/cp.txt)" \
-Dspike.exitAfter=5 -Dspike.capture=target/route1.png spike.GameLoopCheck 2>&1 | tee target/route1.log
grep -q "main: returns" target/route1.log
grep -q "window: closed" target/route1.log

- name: Route 2 - Launcher with -XstartOnFirstThread (gate G1)
if: always()
run: |
perl -e 'alarm 60; exec @ARGV' java -XstartOnFirstThread --enable-native-access=ALL-UNNAMED -cp "target/classes:$(cat target/cp.txt)" \
-Dspike.exitAfter=5 -Dspike.capture=target/route2.png spike.Launcher spike.GameLoopCheck 2>&1 | tee target/route2.log
grep -q "window: runs on thread 'main'" target/route2.log
grep -q "window: closed" target/route2.log

- name: Route 2 with learner code that never returns (watchdog, close)
if: always()
run: |
perl -e 'alarm 60; exec @ARGV' java -XstartOnFirstThread --enable-native-access=ALL-UNNAMED -cp "target/classes:$(cat target/cp.txt)" \
-Dspike.exitAfter=5 -Dspike.hangAt=60 spike.Launcher spike.GameLoopCheck 2>&1 | tee target/route2-hang.log
grep -q "has not returned for 2 seconds" target/route2-hang.log
grep -q "did not answer, skipped" target/route2-hang.log

- name: Original spike window through glfw_async
if: always()
run: |
perl -e 'alarm 60; exec @ARGV' java --enable-native-access=ALL-UNNAMED -cp "target/classes:$(cat target/cp.txt)" \
-Dspike.exitAfter=4 -Dspike.capture=target/spike.png spike.Main 2>&1 | tee target/spike.log

- name: Audio (a runner usually has no output device; then exactly one warning)
if: always()
run: |
perl -e 'alarm 60; exec @ARGV' java --enable-native-access=ALL-UNNAMED -cp "target/classes:$(cat target/cp.txt)" \
spike.AudioCheck 2>&1 | tee target/audio.log

- uses: actions/upload-artifact@v4
if: always()
with:
name: macos-spike-${{ matrix.os }}
path: |
spikes/lwjgl/target/*.png
spikes/lwjgl/target/*.log
17 changes: 9 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,16 @@ jobs:
- name: Build NRW jar
# The NRW build returns the List of the NRW Zentralabitur from find,
# getAll and getTouchingSprites. It is only offered as a download, and
# the next step cleans target/, so the jars are put aside first.
# the next step cleans the build, so the jar is put aside first.
run: |
mvn -B clean package -Pall,nrw
mvn -B clean package -Pnrw -DskipTests
mkdir -p dist
cp target/*-nrw-all.jar target/*-nrw-sources.jar dist/
cp all/target/*-nrw-all.jar dist/

- name: Build fat jar
# Tests already ran as part of the deploy step above; skip them here
# to avoid paying for the full suite twice in the same job.
run: mvn -B clean package -Pall -DskipTests
- name: Build fat jar, sources and Javadoc
# Tests already ran as part of the deploy step above. The central
# profile adds the sources and Javadoc jars; nothing is signed here.
run: mvn -B clean package -Pcentral -Dgpg.skip=true -DskipTests

- name: Require current catalogs and assemble offline examples
run: |
Expand Down Expand Up @@ -134,7 +134,8 @@ jobs:
name: v${{ env.VERSION }}
body_path: ${{ runner.temp }}/release-notes.md
files: |
target/*.jar
all/target/scratch-*-all.jar
core/target/scratch-*.jar
dist/*.jar
dist/*.zip

Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
push:
branches:
- main
- v7

jobs:
test:
Expand All @@ -29,7 +30,10 @@ jobs:
cache: maven

- name: Run tests
run: mvn -B test
run: mvn -B verify

# The online IDE gets its 7.0 branch in Phase 8; until then the
# 7.0 library cannot be checked against it.
- name: Check browser compatibility and generated references
if: github.ref_name != 'v7' && github.head_ref != 'v7'
uses: ./.github/actions/check-browser
4 changes: 2 additions & 2 deletions .github/workflows/visual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ name: Visual
# longer, and can fail for a reason the tests never can: a different machine
# drawing the same thing slightly differently.
#
# The recorded frames in src/test/resources/parity/frames are pixel-for-pixel.
# The recorded frames in examples/src/test/resources/parity/frames are pixel-for-pixel.
# Two runs on one machine are bit-identical, but two different machines are only
# identical if they rasterise the same way, and the runner is not the machine the
# frames were recorded on. If this job fails on something nobody changed, take
# the drawn-frames artifact below, put it in src/test/resources/parity/frames,
# the drawn-frames artifact below, put it in examples/src/test/resources/parity/frames,
# and commit that: the runner then becomes what the frames are recorded against.

on:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ natives

# Ignore Gradle build output directory
build
# but not the modules' build scripts (BuildChecks, NrwVariant)
!**/src/build/

### Maven ###
target/
Expand All @@ -33,3 +35,6 @@ dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar

# Python bytecode
__pycache__/
158 changes: 158 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# AGENTS.md

Guidance for coding agents (and people) working in this repository.

## 7.0 rewrite (branch `v7`) — read this first

7.0 replaces Processing with LWJGL and moves the extensions into jars of their own. Where they
exist, `plan.md` (specification and decision log) and `v7-execution.md` (requirements, evidence,
log) are local working notes and are not committed. 7.0 breaks 6.x on purpose wherever the design needs it
(`docs/book/migrate-to-7.md` lists every change). The 6.x sources are on `main` and in the
history; everything in them has been ported.

## What this is

Scratch for Java (`org.openpatch.scratch`) is a Java library that ports the Scratch (scratch.mit.edu)
block-based programming model to Java, so learners can move from blocks to text-based code while
keeping the same mental model (Window / Stage / Sprite, costumes, backdrops, broadcasts, etc.).

## Build & commands

- **Never `mvn install` on `v7`.** The version is still the released one (6.5.0, managed by the
release workflow), so an install overwrites the real `org.openpatch:scratch:6.5.0` in `~/.m2`
with 7.0 code, and every other project on the machine (Studio) silently builds against it.
Build inside the reactor instead (`-pl <module> -am`).
- `mvn test` — all modules, no window needed. If a test fails with "Unresolved compilation
problems", an IDE (Eclipse compiler) wrote into `target/`: run `mvn clean test`.
- `mvn verify` — additionally builds the fat jar `all/target/scratch-<version>-all.jar` and runs
`all/src/build/BuildChecks.java` (no `java.desktop` in shipped code, extensions use no
`internal`, no NRW-sensitive calls or GLFW callbacks in extensions, fat-jar
manifest/natives/notices).
- `mvn package -pl all,examples -am -DskipTests` — the fat jar and the examples, quickly.
- `mvn package -Pnrw` — the NRW fat jar (`scratch-<version>-nrw-all.jar`): sources rewritten by
`core/src/build/NrwVariant.java` from the `// nrw:` markers in Stage and Sprite, so `find`,
`getAll` and `getTouchingSprites` return the `List` of the NRW Zentralabitur.
- `mvn deploy -Pcentral` — release to Maven Central; done by the release workflow, not by hand.
- `./scripts/run.sh` — pick a demo or reference example and run it (`--each` runs every demo,
`-- <jvm args>` passes JVM arguments); builds in the reactor first.
- `./scripts/benchmark.sh [--quick] [--xvfb] [--with-6x]` — the stress test, collision and
colour checks; appends the results with the commit to `scripts/benchmark/history.csv`
(commit that with the change it measures). `--with-6x` builds `main` in a worktree for comparison.
- `python3 scripts/migrate-7.py --out <dir> <6.x folder>` migrates 6.x code, shaders and the
```java blocks of Markdown pages (run once per project; it marks what needs a person with
`// TODO 7.0:`).
- `./build.sh` — full doc site build (reference pages, demo pages, asset pages, version
substitution, then `npx hyperbook build` in `docs/`); `cd docs && npx hyperbook@latest dev` runs
the site locally (always the newest Hyperbook, as in CI).

Java 17 builds the library (`maven.compiler.release`), but the tests want Java 25: the
documentation's interactive examples are compact source files (`void main()` beside the classes),
and the tests that compile them skip on anything older. CI runs 25.

## Seeing and testing without a window

- Scene logic is tested headless: `Game.headless(480, 360)`, `game.setStage(stage)`,
`game.runTicks(n)`, `game.drawList()` (see `core/src/test/.../SceneTest.java`). Sprites,
costumes, hitboxes, clones, pens, colour sensing (`Pixels.of(stage)`), text layout, timers and
transitions all work there.
- To see a frame: run a program with `-Dscratch4j.preview=<dir>` (writes `frame.png` and the
sprites in `stage.json`, then ends). `-Dscratch4j.capture=<dir>` does the same per stage while the
program keeps running (Studio uses it for its stage designer).
- Under Xvfb in a Wayland session: `XDG_SESSION_TYPE=x11 xvfb-run -a java -Dscratch4j.platform=x11 …`
(GLFW picks Wayland otherwise). Under Xvfb the window is exactly the render size, so letterbox
and HiDPI bugs do not reproduce there. `-Dscratch4j.debugGl` prints window, framebuffer, monitor
and letterbox geometry and vsync switches.
- Reference examples load their assets relative to `examples/src/main/java/reference`: run them
from there (`run.sh` does).
- Parity: `ParityTest` and `LifecycleParityTest` (core, headless; `./scripts/parity.sh --record`),
`./scripts/visual-parity.sh [--record]` (23 frames under Xvfb, compared exactly).
- Documentation guards in the examples module: `DocumentationSnippetsTest` compiles every
```java block on the hand-written pages under `docs/book` — **if you change a public API, this
tells you which tutorial you just broke**; `DemoPagesTest` checks that `docs/book/examples/*.md`
match the demos they are generated from (a page opts in with `<!-- demo: <folder> -->` above its
`:::onlineide` block). **After changing a demo, run `./scripts/update-demo-pages.sh`** and commit
the pages it touches (it also copies the demos' assets to `docs/public/examples`, ignored).
`OnlineExampleTest` compiles the interactive examples.

## Source layout

```
pom.xml parent (scratch-parent); modules in lockstep, one version
core/ artifact `scratch`
src/main/java/org/openpatch/scratch/
Window, Stage, Sprite, Text, Pen, Camera, Layer, Shader(s), Transition, Pixels, Timer,
Color, Vector2, shapes, Operators, KeyCode, ... the learner API, one flat package
Launcher macOS: window on the first thread (-XstartOnFirstThread)
extension/ public extension API (Extension, ExtensionContext, Canvas)
internal/ not exported: Game, loop/ (game thread, tick clock), draw/ (draw list),
render/ (OpenGL), window/ (GLFW), canvas/ + compose/ (CPU pen canvases,
compositor for colour sensing), text/ (stb_truetype engine), audio/
(OpenAL), image/ (stb_image), geom/, input/, Assets, Preview, Release
src/build/NrwVariant.java
extensions/{tiled,save,recorder,drawing}/ `scratch-<name>`, package org.openpatch.scratch.<name>,
public API only, registered through ServiceLoader
all/ fat jar + src/build/BuildChecks.java
examples/ demos/ (runnable projects), reference/ (one example per documented
method), parity/; src/tools/java (doclet, docs.GenerateDemoPages,
docs.GenerateAssetPages); built, never published
templates/extension-starter/ standalone Maven project for third-party extensions
docs/ Hyperbook site (English)
```

## Architecture

Three concepts mirror Scratch directly and almost every feature hangs off one of them:

- **Window** — the singleton window (`new Window(480, 360)`, then `setStage(...)`). Settings
that must be known before it opens are static and called first (`usePixelArt()`,
`useFullScreen()`, ...).
- **Stage** — one scene: sprites, texts, pens, backdrops, the `when*` event handlers.
- **Sprite** — costumes, animations, movement, collision/sensing, sound, and a `run()` method
called every tick (a Scratch script attached to a sprite).

Threads: a **window thread** owns GLFW and OpenGL, a **game thread** runs
every tick and all learner callbacks, the learner's `main` runs on its own thread. They work in
lockstep: a tick is exactly 1/60 s of game time; at the end of a batch of ticks the game thread
builds a **draw list**, and that is all the renderer sees — the scene never draws itself.
Everything learners can observe (collisions, colour sensing through the CPU compositor, text
layout) is computed on the CPU and is the same on every GPU and in headless tests.

Extensions build on the public extension API only (`ExtensionContext` hooks: `beforeTick`,
layer and overlay drawers on a `Canvas`, `afterFrame`, `dispatch` to listener interfaces,
attachments via `stage.extension(...)`), never on `internal`, and register themselves through
`ServiceLoader`. New capabilities go into an extension if they can be built that way and pull in
a dependency (Jackson); things that need the renderer or the game loop stay in the core.

No `java.awt`/`java.desktop` in shipped code (BuildChecks enforces it); tests and build tools may
use it. Jackson (`tools.jackson.*`) is used by tiled and save.

## Javadoc → documentation pipeline

The public API's Javadoc is the source of truth for the reference documentation on
https://scratch4j.openpatch.org. `./scripts/reference-docs.sh` runs the doclet
(`examples/src/tools/java/doclets`) over core and the extensions and writes
`docs/book/reference`:

- `@example.files a.java;b.java` — files from `examples/src/main/java/reference` shown as source
(`@example.folder <Name>` for a folder of them).
- `@scratchblock`, `@index-in-docs`, `@name-in-docs`, `@ignore-in-docs` — the Scratch block shown,
ordering, display name, exclusion.

When adding or changing a public method that should be documented, add/update its reference
example and the tags on the Javadoc comment. Other generators: `docs.GenerateDemoPages --write`
(example pages from the demos) and `docs.GenerateAssetPages` (built-in sprites and sounds).

## Versioning & release flow

Releases are changeset-driven (see `.changeset/README.md`):

1. Add a Markdown file to `.changeset/` with frontmatter `type: patch|minor|major` and a description
of the change, as part of the PR. **Only when the change reaches the user**: the public API moved,
or a bug is gone. Documentation, tests, examples, tooling and CI changes get no changeset — they
would put a release note in front of readers about something they cannot see from the library.
2. Merging to `main` triggers `.github/workflows/version.yml`, which bumps the version of every module
(`mvn versions:set`, all poms in lockstep) and opens a version-bump PR (updates `CHANGELOG.md` too).
3. Merging that PR triggers `.github/workflows/release.yml`, which publishes to Maven Central and
creates a GitHub release with the fat jars (`-all`, `-nrw-all`) and Javadocs.

Don't hand-edit the version in the poms — it's managed by that workflow.
Loading
Loading