From 76c96664dd8e39204dc5edf2b0abe7fbf485354c Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:53:24 +0200 Subject: [PATCH 01/20] Embedded Linux port, hardware API and a garbage-free backend fast path The embedded variant of the native Linux port (DRM/KMS, fbdev or memory display, evdev input, a software rasterizer, no GTK), a core hardware API (GPIO, I2C, SPI, serial, PWM, CAN, watchdog, backlight) for the Linux port, the backend and the simulator, board profiles and a deploy-to-device goal for Maven and Gradle, and Yocto, Buildroot and Raspberry Pi OS recipes. The backend answers a JSON request without allocating anything beyond the handler's own result: the Host header is reused per connection and each generated route encodes through one static Json.Encoder. CI: embedded-linux.yml builds and runs the demo on an arm64 runner and checks its frame, speed and memory; backend-performance.yml runs the headless comparison against Rust and Go, counts per-request allocation exactly and fails when the developer guide's numbers slip. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/backend-performance.yml | 118 ++ .github/workflows/embedded-linux.yml | 100 + .github/workflows/parparvm-tests.yml | 2 + .../src/com/codename1/hardware/CanBus.java | 87 + .../src/com/codename1/hardware/CanFrame.java | 77 + .../com/codename1/hardware/GpioListener.java | 44 + .../src/com/codename1/hardware/GpioPin.java | 197 ++ .../src/com/codename1/hardware/Hardware.java | 430 +++++ .../codename1/hardware/HardwareException.java | 56 + .../src/com/codename1/hardware/I2cDevice.java | 135 ++ .../com/codename1/hardware/PwmChannel.java | 122 ++ .../com/codename1/hardware/SerialPort.java | 144 ++ .../src/com/codename1/hardware/SpiDevice.java | 87 + .../src/com/codename1/hardware/Watchdog.java | 77 + .../com/codename1/hardware/package-info.java | 31 + .../impl/CodenameOneImplementation.java | 6 + .../com/codename1/impl/KernelHardware.java | 283 +++ CodenameOne/src/com/codename1/ui/Display.java | 8 + .../codename1/impl/javase/JavaSEHardware.java | 399 ++++ .../com/codename1/impl/javase/JavaSEPort.java | 16 + Ports/LinuxPort/nativeSources/cn1_linux.h | 5 + .../LinuxPort/nativeSources/cn1_linux_codec.c | 159 ++ .../nativeSources/cn1_linux_crypto.c | 208 ++- .../nativeSources/cn1_linux_embedded.c | 1406 +++++++++++++++ .../nativeSources/cn1_linux_embedded_stubs.c | 679 +++++++ .../nativeSources/cn1_linux_events.c | 272 +++ .../nativeSources/cn1_linux_faults.c | 249 +++ Ports/LinuxPort/nativeSources/cn1_linux_gfx.h | 27 +- Ports/LinuxPort/nativeSources/cn1_linux_gl.c | 4 + .../nativeSources/cn1_linux_hardware.c | 117 ++ .../LinuxPort/nativeSources/cn1_linux_image.c | 60 + .../nativeSources/cn1_linux_raster.c | 1602 +++++++++++++++++ .../nativeSources/cn1_linux_raster.h | 197 ++ .../nativeSources/cn1_linux_surface.c | 93 + .../nativeSources/cn1_linux_text_ft.c | 900 +++++++++ .../nativeSources/cn1_linux_window.c | 504 +----- .../impl/linux/LinuxEmbeddedEditor.java | 210 +++ .../codename1/impl/linux/LinuxHardware.java | 158 ++ .../impl/linux/LinuxImplementation.java | 30 +- .../com/codename1/impl/linux/LinuxNative.java | 29 + docs/developer-guide/Embedded-Linux.asciidoc | 178 ++ docs/developer-guide/developer-guide.asciidoc | 2 + docs/developer-guide/embedded-benchmarks.adoc | 130 ++ .../img/embedded-gui-first-frame.svg | 33 + docs/developer-guide/img/embedded-gui-fps.svg | 33 + .../img/embedded-gui-memory.svg | 33 + .../img/embedded-server-memory.svg | 66 + .../img/embedded-server-start.svg | 36 + .../img/embedded-server-throughput.svg | 36 + .../builders/EmbeddedBoardProfile.java | 105 ++ .../builders/LinuxNativeBuilder.java | 103 +- .../com/codename1/maven/DeviceDeployer.java | 252 +++ .../RestControllerAnnotationProcessor.java | 36 +- .../codename1/maven/DeviceDeployerTest.java | 148 ++ .../build/shared/BuildHintsDesktop.java | 50 + .../com/codename1/gradle/ToolSupport.java | 4 + .../gradle/tasks/DeployDeviceTask.java | 66 + .../com/codename1/maven/DeployDeviceMojo.java | 93 + .../impl/javase/JavaSEHardwareTest.java | 117 ++ scripts/embedded/Containerfile.dev | 12 + scripts/embedded/README.md | 65 + scripts/embedded/bench/Containerfile | 31 + scripts/embedded/bench/charts.py | 243 +++ scripts/embedded/bench/chromium/gauge.html | 31 + scripts/embedded/bench/common.h | 82 + scripts/embedded/bench/headless/Containerfile | 26 + scripts/embedded/bench/headless/census.sh | 46 + scripts/embedded/bench/headless/gate.py | 116 ++ scripts/embedded/bench/headless/go/go.mod | 3 + scripts/embedded/bench/headless/go/main.go | 57 + .../embedded/bench/headless/node/server.js | 39 + scripts/embedded/bench/headless/python/app.py | 16 + scripts/embedded/bench/headless/results.txt | 48 + scripts/embedded/bench/headless/run.sh | 118 ++ .../embedded/bench/headless/rust/Cargo.toml | 15 + .../embedded/bench/headless/rust/src/main.rs | 19 + .../embedded/bench/headless/spring/pom.xml | 31 + .../spring/src/main/java/bench/Bench.java | 51 + scripts/embedded/bench/javafx/Gauge.java | 125 ++ scripts/embedded/bench/qt/CMakeLists.txt | 6 + scripts/embedded/bench/qt/gauge.cpp | 88 + scripts/embedded/bench/results.txt | 32 + scripts/embedded/bench/run.sh | 112 ++ scripts/embedded/bench/slint/Cargo.toml | 14 + scripts/embedded/bench/slint/src/main.rs | 103 ++ scripts/embedded/build-dist.sh | 43 + .../package/codenameone-app/Config.in | 29 + .../codenameone-app/codenameone-app.mk | 40 + scripts/embedded/check-demo-run.py | 112 ++ scripts/embedded/demo/src/EmbeddedDemo.java | 226 +++ scripts/embedded/gen-embedded-stubs.py | 148 ++ scripts/embedded/pios/setup-device.sh | 43 + scripts/embedded/translate.sh | 56 + .../embedded/yocto/meta-codenameone/README.md | 21 + .../classes/codenameone-app.bbclass | 61 + .../yocto/meta-codenameone/conf/layer.conf | 8 + .../codenameone-app/codenameone-app_1.0.bb | 11 + scripts/test_linux_event_pump.py | 5 +- scripts/test_native_hover_queue.py | 2 +- vm/BIBOP-INVARIANTS.md | 12 + .../src/cn1_db_sqlite_impl.h | 4 + vm/ByteCodeTranslator/src/cn1_globals.h | 36 +- vm/ByteCodeTranslator/src/cn1_globals.m | 77 +- vm/ByteCodeTranslator/src/cn1_hardware_impl.h | 1056 +++++++++++ .../tools/translator/ByteCodeTranslator.java | 68 +- vm/ByteCodeTranslator/src/nativeMethods.m | 24 + vm/backend/build.sh | 2 +- vm/backend/demo/bench/com/demo/Bench.java | 182 +- vm/backend/demo/hwcheck/com/demo/HwCheck.java | 61 + vm/backend/docker/Containerfile.musl | 4 +- .../com/codename1/backend/BoardHardware.java | 38 + .../com/codename1/backend/ServerSocket.java | 5 + .../com/codename1/backend/BoardHardware.java | 210 +++ .../com/codename1/backend/ServerSocket.java | 12 + vm/backend/native/cn1_backend_hardware.c | 117 ++ vm/backend/native/cn1_backend_server.c | 105 +- .../src/com/codename1/backend/Backend.java | 2 + .../src/com/codename1/backend/HttpServer.java | 193 +- .../src/com/codename1/backend/Json.java | 13 + 119 files changed, 14607 insertions(+), 797 deletions(-) create mode 100644 .github/workflows/backend-performance.yml create mode 100644 .github/workflows/embedded-linux.yml create mode 100644 CodenameOne/src/com/codename1/hardware/CanBus.java create mode 100644 CodenameOne/src/com/codename1/hardware/CanFrame.java create mode 100644 CodenameOne/src/com/codename1/hardware/GpioListener.java create mode 100644 CodenameOne/src/com/codename1/hardware/GpioPin.java create mode 100644 CodenameOne/src/com/codename1/hardware/Hardware.java create mode 100644 CodenameOne/src/com/codename1/hardware/HardwareException.java create mode 100644 CodenameOne/src/com/codename1/hardware/I2cDevice.java create mode 100644 CodenameOne/src/com/codename1/hardware/PwmChannel.java create mode 100644 CodenameOne/src/com/codename1/hardware/SerialPort.java create mode 100644 CodenameOne/src/com/codename1/hardware/SpiDevice.java create mode 100644 CodenameOne/src/com/codename1/hardware/Watchdog.java create mode 100644 CodenameOne/src/com/codename1/hardware/package-info.java create mode 100644 CodenameOne/src/com/codename1/impl/KernelHardware.java create mode 100644 Ports/JavaSE/src/com/codename1/impl/javase/JavaSEHardware.java create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_codec.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_embedded.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_embedded_stubs.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_events.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_faults.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_hardware.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_raster.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_raster.h create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_surface.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_text_ft.c create mode 100644 Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java create mode 100644 Ports/LinuxPort/src/com/codename1/impl/linux/LinuxHardware.java create mode 100644 docs/developer-guide/Embedded-Linux.asciidoc create mode 100644 docs/developer-guide/embedded-benchmarks.adoc create mode 100644 docs/developer-guide/img/embedded-gui-first-frame.svg create mode 100644 docs/developer-guide/img/embedded-gui-fps.svg create mode 100644 docs/developer-guide/img/embedded-gui-memory.svg create mode 100644 docs/developer-guide/img/embedded-server-memory.svg create mode 100644 docs/developer-guide/img/embedded-server-start.svg create mode 100644 docs/developer-guide/img/embedded-server-throughput.svg create mode 100644 maven/build-engine/src/main/java/com/codename1/builders/EmbeddedBoardProfile.java create mode 100644 maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java create mode 100644 maven/build-engine/src/test/java/com/codename1/maven/DeviceDeployerTest.java create mode 100644 maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java create mode 100644 maven/javase/src/test/java/com/codename1/impl/javase/JavaSEHardwareTest.java create mode 100644 scripts/embedded/Containerfile.dev create mode 100644 scripts/embedded/README.md create mode 100644 scripts/embedded/bench/Containerfile create mode 100644 scripts/embedded/bench/charts.py create mode 100644 scripts/embedded/bench/chromium/gauge.html create mode 100644 scripts/embedded/bench/common.h create mode 100644 scripts/embedded/bench/headless/Containerfile create mode 100755 scripts/embedded/bench/headless/census.sh create mode 100644 scripts/embedded/bench/headless/gate.py create mode 100644 scripts/embedded/bench/headless/go/go.mod create mode 100644 scripts/embedded/bench/headless/go/main.go create mode 100644 scripts/embedded/bench/headless/node/server.js create mode 100644 scripts/embedded/bench/headless/python/app.py create mode 100644 scripts/embedded/bench/headless/results.txt create mode 100755 scripts/embedded/bench/headless/run.sh create mode 100644 scripts/embedded/bench/headless/rust/Cargo.toml create mode 100644 scripts/embedded/bench/headless/rust/src/main.rs create mode 100644 scripts/embedded/bench/headless/spring/pom.xml create mode 100644 scripts/embedded/bench/headless/spring/src/main/java/bench/Bench.java create mode 100644 scripts/embedded/bench/javafx/Gauge.java create mode 100644 scripts/embedded/bench/qt/CMakeLists.txt create mode 100644 scripts/embedded/bench/qt/gauge.cpp create mode 100644 scripts/embedded/bench/results.txt create mode 100755 scripts/embedded/bench/run.sh create mode 100644 scripts/embedded/bench/slint/Cargo.toml create mode 100644 scripts/embedded/bench/slint/src/main.rs create mode 100755 scripts/embedded/build-dist.sh create mode 100644 scripts/embedded/buildroot/package/codenameone-app/Config.in create mode 100644 scripts/embedded/buildroot/package/codenameone-app/codenameone-app.mk create mode 100755 scripts/embedded/check-demo-run.py create mode 100644 scripts/embedded/demo/src/EmbeddedDemo.java create mode 100755 scripts/embedded/gen-embedded-stubs.py create mode 100755 scripts/embedded/pios/setup-device.sh create mode 100755 scripts/embedded/translate.sh create mode 100644 scripts/embedded/yocto/meta-codenameone/README.md create mode 100644 scripts/embedded/yocto/meta-codenameone/classes/codenameone-app.bbclass create mode 100644 scripts/embedded/yocto/meta-codenameone/conf/layer.conf create mode 100644 scripts/embedded/yocto/meta-codenameone/recipes-codenameone/codenameone-app/codenameone-app_1.0.bb create mode 100644 vm/ByteCodeTranslator/src/cn1_hardware_impl.h create mode 100644 vm/backend/demo/hwcheck/com/demo/HwCheck.java create mode 100644 vm/backend/impl/javase/com/codename1/backend/BoardHardware.java create mode 100644 vm/backend/impl/parparvm/com/codename1/backend/BoardHardware.java create mode 100644 vm/backend/native/cn1_backend_hardware.c diff --git a/.github/workflows/backend-performance.yml b/.github/workflows/backend-performance.yml new file mode 100644 index 00000000000..2e13b93d970 --- /dev/null +++ b/.github/workflows/backend-performance.yml @@ -0,0 +1,118 @@ +# Holds the Codename One backend to its headless benchmark results. +# +# Builds the bench demo (vm/backend/demo/bench) twice: as shipped, and with the +# allocation census compiled in. The census run counts what each request +# allocates, exactly. The shipped binary then takes turns with Rust's axum and +# Go's net/http on the same two cores of an arm64 runner, through +# scripts/embedded/bench/headless/run.sh, the harness behind the developer +# guide's tables. gate.py judges speed against Rust from the same job and memory +# and allocation absolutely; its docstring says why, and where the limits live. +# +# The job uploads the raw results, the census and charts drawn from this run, so +# the guide's numbers can be refreshed from a CI machine rather than a laptop. +name: Backend performance gate + +on: + workflow_dispatch: + push: + branches: [master] + paths: + - 'vm/backend/**' + - 'vm/ByteCodeTranslator/src/**' + - 'vm/JavaAPI/src/**' + - 'scripts/embedded/bench/charts.py' + - 'scripts/embedded/bench/headless/**' + - '.github/workflows/backend-performance.yml' + pull_request: + paths: + - 'vm/backend/**' + - 'vm/ByteCodeTranslator/src/**' + - 'vm/JavaAPI/src/**' + - 'scripts/embedded/bench/charts.py' + - 'scripts/embedded/bench/headless/**' + - '.github/workflows/backend-performance.yml' + +concurrency: + group: backend-performance-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + gate: + # Four cores: the servers get 0-1 and the load generator 2-3. + runs-on: ubuntu-24.04-arm + timeout-minutes: 90 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '8' + cache: 'maven' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Go, Rust, oha and the comparison servers; cached, since none of it + # depends on this repository's code. + - name: Build the benchmark image + uses: docker/build-push-action@v6 + with: + context: scripts/embedded/bench/headless + file: scripts/embedded/bench/headless/Containerfile + tags: cn1-headless:ci + load: true + cache-from: type=gha,scope=backend-performance + cache-to: type=gha,mode=max,scope=backend-performance + + - name: Build the bench demo, shipped and with the census + working-directory: vm/backend + env: + JDK_8_HOME: ${{ env.JAVA_HOME }} + CN1_BACKEND_DEMO: demo/bench + CN1_BACKEND_ENGINE: docker + run: | + mkdir -p "$RUNNER_TEMP/dist" + ./package.sh Bench com.demo glibc-arm64 + cp target/dist/bench-linux-glibc-arm64 "$RUNNER_TEMP/dist/bench" + CN1_BACKEND_CFLAGS="-DCN1_ALLOC_CENSUS -DCN1_HEAP_HISTOGRAM" \ + ./package.sh Bench com.demo glibc-arm64 + cp target/dist/bench-linux-glibc-arm64 "$RUNNER_TEMP/dist/bench-census" + + - name: Count what a request allocates + run: | + mkdir -p "$RUNNER_TEMP/out" + docker run --rm -v "$RUNNER_TEMP/dist:/dist:ro" \ + -v "$GITHUB_WORKSPACE/scripts/embedded/bench/headless/census.sh:/census.sh:ro" \ + cn1-headless:ci /census.sh /dist/bench-census 200000 | tee "$RUNNER_TEMP/out/census.txt" + + # Privileged so run.sh can drop the page cache before each cold start. + - name: Run the comparison + run: | + docker run --rm --privileged -e RUNTIMES="codenameone rust go" \ + -v "$RUNNER_TEMP/dist:/dist:ro" \ + -v "$GITHUB_WORKSPACE/scripts/embedded/bench/headless/run.sh:/bench/run.sh:ro" \ + cn1-headless:ci /bench/run.sh /dist/bench 3 | tee "$RUNNER_TEMP/out/results.txt" + + - name: Draw the charts + run: | + mkdir -p "$RUNNER_TEMP/out/img" + python3 scripts/embedded/bench/charts.py "$RUNNER_TEMP/out/img" "$RUNNER_TEMP/out/results.txt" + + - name: Judge the results + run: | + python3 scripts/embedded/bench/headless/gate.py "$RUNNER_TEMP/out/results.txt" \ + "$(cat "$RUNNER_TEMP/out/census.txt")" "$GITHUB_STEP_SUMMARY" + + - name: Upload the results + if: always() + uses: actions/upload-artifact@v4 + with: + name: backend-performance + path: ${{ runner.temp }}/out + if-no-files-found: ignore diff --git a/.github/workflows/embedded-linux.yml b/.github/workflows/embedded-linux.yml new file mode 100644 index 00000000000..8da6e2b4606 --- /dev/null +++ b/.github/workflows/embedded-linux.yml @@ -0,0 +1,100 @@ +# Builds and runs the embedded Linux port (CN1_LINUX_EMBEDDED: DRM/KMS, fbdev or +# memory display, evdev input, no GTK) the way scripts/embedded/README.md tells a +# user to: translate scripts/embedded/demo with translate.sh, build it for arm64 +# with build-dist.sh in Containerfile.dev, and run it on the memory display. +# +# check-demo-run.py then fails the job when frame 60 is missing, wrong or blank, +# or when the speed and memory the developer guide reports have slipped. The +# generated stub file is checked against the natives too, since a native added to +# LinuxNative without a stub breaks only the embedded link. +name: Embedded Linux + +on: + workflow_dispatch: + push: + branches: [master] + paths: + - 'Ports/LinuxPort/**' + - 'CodenameOne/src/**' + - 'vm/ByteCodeTranslator/src/**' + - 'vm/JavaAPI/src/**' + - 'maven/linux/**' + - 'scripts/embedded/**' + - '.github/workflows/embedded-linux.yml' + pull_request: + paths: + - 'Ports/LinuxPort/**' + - 'CodenameOne/src/**' + - 'vm/ByteCodeTranslator/src/**' + - 'vm/JavaAPI/src/**' + - 'maven/linux/**' + - 'scripts/embedded/**' + - '.github/workflows/embedded-linux.yml' + +concurrency: + group: embedded-linux-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + demo: + runs-on: ubuntu-24.04-arm + timeout-minutes: 75 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: The embedded stubs match the natives + run: python3 scripts/embedded/gen-embedded-stubs.py --check + + - name: Set up JDK 8 + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '8' + cache: 'maven' + + # Retried: Maven Central answers 429 from the runner CDN edge (see + # linux-build-run.yml). This step only builds. + - name: Build core, the Linux port, JavaAPI and the translator + env: + RETRY_ATTEMPTS: '5' + RETRY_DELAY_SECONDS: '60' + run: | + cd maven + bash ../scripts/ci/retry.sh mvn -B -pl core,linux -am install \ + -DskipTests -Dmaven.javadoc.skip=true -Plocal-dev-javase \ + -Dspotbugs.skip=true -Dpmd.skip=true -Dcheckstyle.skip=true \ + -Dmaven.repo.local="$GITHUB_WORKSPACE/.m2-repo" + cd .. + bash scripts/ci/retry.sh mvn -B -f vm/pom.xml -pl JavaAPI,ByteCodeTranslator -am package \ + -DskipTests -Dmaven.repo.local="$GITHUB_WORKSPACE/.m2-repo" + + - name: Translate and build the demo for arm64 + run: | + B="$RUNNER_TEMP/demo" + mkdir -p "$B/classes" + javac -source 1.8 -target 1.8 -cp maven/core/target/classes:maven/linux/target/classes \ + -d "$B/classes" scripts/embedded/demo/src/EmbeddedDemo.java + scripts/embedded/translate.sh "$B/classes" EmbeddedDemo "$B/out" + scripts/embedded/build-dist.sh "$B/out" arm64 + + - name: Run the demo on the memory display + run: | + B="$RUNNER_TEMP/demo" + mkdir -p "$B/result" + docker run --rm -v "$B/out/build-arm64:/cn1:ro" -v "$B/result:/result" cn1-embedded-dev:arm64 \ + bash -c 'cd /tmp && CN1_DISPLAY=memory CN1_SIZE=800x480 CN1_BENCH_SECONDS=10 \ + CN1_SNAPSHOT=/result/frame60.png timeout 120 /cn1/EmbeddedDemo > /result/demo.log 2>&1' + python3 scripts/embedded/check-demo-run.py "$B/result/demo.log" "$B/result/frame60.png" \ + "$GITHUB_STEP_SUMMARY" + + - name: Upload the run + if: always() + uses: actions/upload-artifact@v4 + with: + name: embedded-demo + path: ${{ runner.temp }}/demo/result + if-no-files-found: ignore diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index a62373cb724..63981a5d24c 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -43,6 +43,7 @@ on: # marker; a class marked outside these directories needs its path here. - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' + - 'CodenameOne/src/com/codename1/hardware/**' - 'CodenameOne/src/com/codename1/orm/session/**' - 'CodenameOne/src/com/codename1/orm/repository/**' - 'CodenameOne/src/com/codename1/annotations/db/**' @@ -107,6 +108,7 @@ on: # marker; a class marked outside these directories needs its path here. - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' + - 'CodenameOne/src/com/codename1/hardware/**' - 'CodenameOne/src/com/codename1/orm/session/**' - 'CodenameOne/src/com/codename1/orm/repository/**' - 'CodenameOne/src/com/codename1/annotations/db/**' diff --git a/CodenameOne/src/com/codename1/hardware/CanBus.java b/CodenameOne/src/com/codename1/hardware/CanBus.java new file mode 100644 index 00000000000..9c3c62c3016 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/CanBus.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +/// A CAN network interface, opened through [Hardware#openCan(String)]. +@com.codename1.impl.SharedWithBackend +public final class CanBus { + + private final Hardware hw; + private final int handle; + private final String iface; + private boolean closed; + + CanBus(Hardware hw, int handle, String iface) { + this.hw = hw; + this.handle = handle; + this.iface = iface; + } + + /// The interface name. + public String getInterface() { + return iface; + } + + /// Sends one frame. + /// + /// #### Throws + /// + /// - `HardwareException`: when the send fails or the bus is closed + public void send(CanFrame frame) throws HardwareException { + checkOpen(); + hw.canSend(handle, frame.getId(), frame.isExtended(), frame.getData(), frame.getLength()); + } + + /// Waits up to `timeoutMs` for a frame (negative waits indefinitely) and + /// returns it, or `null` on timeout. Call it off the event dispatch thread. + /// + /// #### Throws + /// + /// - `HardwareException`: when the receive fails or the bus is closed + public CanFrame receive(int timeoutMs) throws HardwareException { + checkOpen(); + int[] id = new int[2]; + byte[] data = new byte[8]; + int len = hw.canReceive(handle, id, data, timeoutMs); + if (len < 0) { + return null; + } + byte[] payload = new byte[len]; + System.arraycopy(data, 0, payload, 0, len); + return new CanFrame(id[0], id[1] != 0, payload); + } + + /// Closes the interface. Closing twice is harmless. + public void close() { + if (!closed) { + closed = true; + hw.close(handle, false); + } + } + + private void checkOpen() throws HardwareException { + if (closed) { + throw new HardwareException("CAN interface " + iface + " is closed"); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/CanFrame.java b/CodenameOne/src/com/codename1/hardware/CanFrame.java new file mode 100644 index 00000000000..aa0cace5ffe --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/CanFrame.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +/// One classic CAN frame: an identifier and up to eight data bytes. +@com.codename1.impl.SharedWithBackend +public final class CanFrame { + + private final int id; + private final boolean extended; + private final byte[] data; + + /// Creates a frame. + /// + /// #### Parameters + /// + /// - `id`: an 11-bit identifier, or 29-bit when `extended` + /// + /// - `extended`: `true` for a 29-bit identifier + /// + /// - `data`: zero to eight payload bytes; the array is copied + public CanFrame(int id, boolean extended, byte[] data) { + int max = extended ? 0x1fffffff : 0x7ff; + if (id < 0 || id > max) { + throw new IllegalArgumentException("CAN identifier out of range: " + id); + } + if (data == null || data.length > 8) { + throw new IllegalArgumentException("CAN payload must be 0..8 bytes"); + } + this.id = id; + this.extended = extended; + this.data = new byte[data.length]; + System.arraycopy(data, 0, this.data, 0, data.length); + } + + /// The identifier. + public int getId() { + return id; + } + + /// `true` for a 29-bit identifier. + public boolean isExtended() { + return extended; + } + + /// A copy of the payload. + public byte[] getData() { + byte[] out = new byte[data.length]; + System.arraycopy(data, 0, out, 0, data.length); + return out; + } + + /// The payload length. + public int getLength() { + return data.length; + } +} diff --git a/CodenameOne/src/com/codename1/hardware/GpioListener.java b/CodenameOne/src/com/codename1/hardware/GpioListener.java new file mode 100644 index 00000000000..c6ecb423a42 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/GpioListener.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +/// Receives edge events from a [GpioPin] that was opened as an input with +/// edge detection. +/// +/// Events are delivered through [Hardware#deliver(Runnable)] (the event dispatch +/// thread on a client port), in the order the kernel reported them. +@com.codename1.impl.SharedWithBackend +public interface GpioListener { + + /// Called once per detected edge. + /// + /// #### Parameters + /// + /// - `pin`: the pin the edge occurred on + /// + /// - `rising`: `true` for a low-to-high transition, `false` for high-to-low + /// + /// - `timestampNanos`: the kernel's monotonic timestamp of the edge, in + /// nanoseconds; `0` when the port has no timestamp + void edgeDetected(GpioPin pin, boolean rising, long timestampNanos); +} diff --git a/CodenameOne/src/com/codename1/hardware/GpioPin.java b/CodenameOne/src/com/codename1/hardware/GpioPin.java new file mode 100644 index 00000000000..e5e187bba01 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/GpioPin.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +import java.util.concurrent.atomic.AtomicBoolean; + +/// A claimed GPIO line, opened through [Hardware#openGpioOutput(String, int, boolean)] +/// or [Hardware#openGpioInput(String, int, int, int)]. +/// +/// The line stays claimed until [#close()]; another process cannot drive it in +/// the meantime. +@com.codename1.impl.SharedWithBackend +public final class GpioPin { + + /// Leave the line's bias as the board configured it. + public static final int BIAS_DEFAULT = 0; + /// Disable internal pull resistors. + public static final int BIAS_DISABLED = 1; + /// Enable the internal pull-up resistor. + public static final int BIAS_PULL_UP = 2; + /// Enable the internal pull-down resistor. + public static final int BIAS_PULL_DOWN = 3; + + /// Report no edges. + public static final int EDGE_NONE = 0; + /// Report low-to-high transitions. + public static final int EDGE_RISING = 1; + /// Report high-to-low transitions. + public static final int EDGE_FALLING = 2; + /// Report both transitions. + public static final int EDGE_BOTH = 3; + + private static final int WAIT_SLICE_MS = 250; + + private final Hardware hw; + private final int handle; + private final String chip; + private final int line; + private final boolean output; + private boolean high; + private boolean closed; + private GpioListener listener; + // Read by the edge thread, written on the caller's thread. + private AtomicBoolean edgeThreadRunning; + + GpioPin(Hardware hw, int handle, String chip, int line, boolean output, boolean initialHigh) { + this.hw = hw; + this.handle = handle; + this.chip = chip; + this.line = line; + this.output = output; + this.high = initialHigh; + } + + /// The controller this line belongs to. + public String getChip() { + return chip; + } + + /// The line offset within its controller. + public int getLine() { + return line; + } + + /// `true` when the line was opened as an output. + public boolean isOutput() { + return output; + } + + /// Reads the line level. For an output this is the level it is driven to. + /// + /// #### Throws + /// + /// - `HardwareException`: when the read fails or the pin is closed + public boolean isHigh() throws HardwareException { + checkOpen(); + if (output) { + return high; + } + return hw.gpioRead(handle); + } + + /// Drives an output line. + /// + /// #### Throws + /// + /// - `HardwareException`: when the line is an input, closed, or the write fails + public void setHigh(boolean value) throws HardwareException { + checkOpen(); + if (!output) { + throw new HardwareException("GPIO line " + line + " is an input"); + } + hw.gpioWrite(handle, value); + high = value; + } + + /// Delivers the edges this input was opened for to `l` on the event + /// dispatch thread, or stops delivery when `l` is `null`. Edges are + /// waited for on a background thread owned by this pin. + /// + /// #### Throws + /// + /// - `HardwareException`: when the line is an output or closed + public void setListener(GpioListener l) throws HardwareException { + checkOpen(); + if (output) { + throw new HardwareException("GPIO line " + line + " is an output"); + } + stopEdgeThread(); + listener = l; + if (l != null) { + final AtomicBoolean running = new AtomicBoolean(true); + final GpioListener target = l; + edgeThreadRunning = running; + Thread t = new Thread(new Runnable() { + @Override + public void run() { + edgeLoop(running, target); + } + }, "GPIO-" + chip + "-" + line); + t.start(); + } + } + + /// The listener set with [#setListener(GpioListener)], or `null`. + public GpioListener getListener() { + return listener; + } + + private void edgeLoop(AtomicBoolean running, final GpioListener target) { + long[] ts = new long[1]; + while (running.get()) { + int r; + try { + r = hw.gpioWaitEdge(handle, WAIT_SLICE_MS, ts); + } catch (HardwareException e) { + return; + } + if (r == Hardware.EDGE_TIMEOUT || !running.get()) { + continue; + } + final boolean rising = r == Hardware.EDGE_RESULT_RISING; + final long when = ts[0]; + hw.deliver(new Runnable() { + @Override + public void run() { + target.edgeDetected(GpioPin.this, rising, when); + } + }); + } + } + + private void stopEdgeThread() { + if (edgeThreadRunning != null) { + edgeThreadRunning.set(false); + edgeThreadRunning = null; + } + } + + /// Releases the line. Further calls on this pin throw; closing twice is + /// harmless. + public void close() { + if (closed) { + return; + } + closed = true; + stopEdgeThread(); + listener = null; + hw.close(handle, false); + } + + private void checkOpen() throws HardwareException { + if (closed) { + throw new HardwareException("GPIO line " + line + " is closed"); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/Hardware.java b/CodenameOne/src/com/codename1/hardware/Hardware.java new file mode 100644 index 00000000000..2d6947324a4 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/Hardware.java @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + + +/// Entry point to the buses and pins of the device the application runs on: +/// GPIO lines, I2C, SPI, serial ports, PWM, CAN, the hardware watchdog and the +/// display backlight. +/// +/// This is the API for boards such as the Raspberry Pi, i.MX, Rockchip and +/// other embedded Linux devices. Every method is available on every platform; +/// a port without the hardware reports it through [#isSupported(int)] and its +/// `open` methods throw [HardwareException], so the same application runs on a +/// phone, a desktop or the simulator. +/// +/// ```java +/// Hardware hw = Hardware.getInstance(); +/// if (hw.isSupported(Hardware.FEATURE_GPIO)) { +/// final GpioPin led = hw.openGpioOutput("gpiochip0", 17, false); +/// led.setHigh(true); +/// +/// GpioPin button = hw.openGpioInput("gpiochip0", 27, GpioPin.BIAS_PULL_UP, GpioPin.EDGE_FALLING); +/// button.setListener(new GpioListener() { +/// public void edgeDetected(GpioPin pin, boolean rising, long timestampNanos) { +/// led.setHigh(!led.isHigh()); +/// } +/// }); +/// } +/// ``` +/// +/// #### Threading +/// +/// The transfer methods block for the duration of the bus transaction, which +/// is microseconds for GPIO, I2C and SPI. Serial and CAN reads can wait for +/// data; call them off the event dispatch thread, or use their timeout +/// arguments. Listener callbacks always arrive on the event dispatch thread. +/// +/// #### For port implementors +/// +/// A port subclasses this class and overrides the protected primitives, which +/// take and return integer handles. The device classes ([GpioPin], +/// [I2cDevice], ...) are written once over those primitives, so a port never +/// subclasses them. Every primitive's default throws [HardwareException]. +@com.codename1.impl.SharedWithBackend +public class Hardware { + + /// GPIO lines through the GPIO character device. + public static final int FEATURE_GPIO = 1; + /// I2C buses. + public static final int FEATURE_I2C = 2; + /// SPI buses. + public static final int FEATURE_SPI = 4; + /// Serial ports (UART, USB serial adapters). + public static final int FEATURE_SERIAL = 8; + /// Hardware PWM channels. + public static final int FEATURE_PWM = 16; + /// CAN buses. + public static final int FEATURE_CAN = 32; + /// The hardware watchdog. + public static final int FEATURE_WATCHDOG = 64; + /// Display backlight brightness control. + public static final int FEATURE_BACKLIGHT = 128; + + /// Edge-wait result: the wait timed out. + protected static final int EDGE_TIMEOUT = -1; + /// Edge-wait result: a falling edge. + protected static final int EDGE_RESULT_FALLING = 0; + /// Edge-wait result: a rising edge. + protected static final int EDGE_RESULT_RISING = 1; + + private static final Hardware DEFAULT = new Hardware(); + private static Hardware installed; + + /// Ports construct subclasses; applications call [#getInstance()]. + protected Hardware() { + } + + /// Returns the hardware entry point of the current port. On a port without + /// hardware access this is a base instance that supports nothing, so + /// callers never need a `null` check. + public static Hardware getInstance() { + Hardware h = installed; + return h != null ? h : DEFAULT; + } + + /// Makes `hardware` the instance [#getInstance()] returns; `null` restores + /// the unsupported default. `Display` installs the port's instance when it + /// starts, and the backend runtime installs its own, which is why this + /// class never names `Display` itself. + public static void install(Hardware hardware) { + installed = hardware; + } + + /// Hands a listener callback to the thread listeners run on. The client + /// ports post it to the event dispatch thread; the default, used by the + /// headless backend, runs it on the calling thread. + protected void deliver(Runnable callback) { + callback.run(); + } + + /// `true` when the port supports at least one hardware feature. + public boolean isSupported() { + return getSupportedFeatures() != 0; + } + + /// `true` when the port supports the given feature, one of the `FEATURE_` + /// constants. Support means the port can drive the bus; whether the board + /// exposes one is answered by the matching `list` method. + public boolean isSupported(int feature) { + return (getSupportedFeatures() & feature) == feature; + } + + /// Bit mask of the `FEATURE_` constants this port implements. Ports + /// override this; the default supports nothing. + protected int getSupportedFeatures() { + return 0; + } + + // ------------------------------------------------------------ discovery + + /// Names of the GPIO controllers on this device, for example `gpiochip0`. + /// Empty when there are none or GPIO is unsupported. + public String[] listGpioChips() { + return new String[0]; + } + + /// Numbers of the I2C buses on this device (`/dev/i2c-N` on Linux). + public int[] listI2cBuses() { + return new int[0]; + } + + /// SPI devices as `bus.chipSelect` strings, for example `0.1`. + public String[] listSpiDevices() { + return new String[0]; + } + + /// Device paths of the serial ports on this device, for example + /// `/dev/ttyAMA0` or `/dev/ttyUSB0`. + public String[] listSerialPorts() { + return new String[0]; + } + + /// Names of the CAN network interfaces, for example `can0`. + public String[] listCanInterfaces() { + return new String[0]; + } + + // ------------------------------------------------------------ factories + + /// Claims a GPIO line as an output. + /// + /// #### Parameters + /// + /// - `chip`: the controller name from [#listGpioChips()] + /// + /// - `line`: the line offset within the controller (the BCM number on a + /// Raspberry Pi's `gpiochip0`) + /// + /// - `initialHigh`: the level the line is driven to when claimed + /// + /// #### Throws + /// + /// - `HardwareException`: when the line does not exist, is claimed by + /// another consumer, or GPIO is unsupported + public GpioPin openGpioOutput(String chip, int line, boolean initialHigh) throws HardwareException { + int h = gpioOpen(chip, line, true, initialHigh, GpioPin.BIAS_DEFAULT, GpioPin.EDGE_NONE); + return new GpioPin(this, h, chip, line, true, initialHigh); + } + + /// Claims a GPIO line as an input. + /// + /// #### Parameters + /// + /// - `chip`: the controller name from [#listGpioChips()] + /// + /// - `line`: the line offset within the controller + /// + /// - `bias`: one of the `GpioPin.BIAS_` constants + /// + /// - `edges`: which edges [GpioPin#setListener(GpioListener)] reports, one + /// of the `GpioPin.EDGE_` constants + /// + /// #### Throws + /// + /// - `HardwareException`: when the line cannot be claimed + public GpioPin openGpioInput(String chip, int line, int bias, int edges) throws HardwareException { + int h = gpioOpen(chip, line, false, false, bias, edges); + return new GpioPin(this, h, chip, line, false, false); + } + + /// Opens the device at a 7-bit address on an I2C bus. + /// + /// #### Throws + /// + /// - `HardwareException`: when the bus does not exist or I2C is unsupported + public I2cDevice openI2c(int bus, int address) throws HardwareException { + if (address < 0 || address > 0x7f) { + throw new HardwareException("I2C address out of range: " + address); + } + return new I2cDevice(this, i2cOpen(bus, address), bus, address); + } + + /// Opens an SPI device. + /// + /// #### Parameters + /// + /// - `bus`: the SPI controller number + /// + /// - `chipSelect`: the chip-select line + /// + /// - `mode`: SPI mode 0 to 3 (clock polarity and phase) + /// + /// - `speedHz`: the clock rate + /// + /// - `bitsPerWord`: the word size, usually 8 + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not exist or rejects the + /// configuration + public SpiDevice openSpi(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) throws HardwareException { + if (mode < 0 || mode > 3) { + throw new HardwareException("SPI mode must be 0..3: " + mode); + } + return new SpiDevice(this, spiOpen(bus, chipSelect, mode, speedHz, bitsPerWord), bus, chipSelect); + } + + /// Opens a serial port in raw mode. + /// + /// #### Parameters + /// + /// - `device`: the port, from [#listSerialPorts()] + /// + /// - `baud`: the baud rate, for example 9600 or 115200 + /// + /// - `dataBits`: 5 to 8 + /// + /// - `parity`: one of the `SerialPort.PARITY_` constants + /// + /// - `stopBits`: 1 or 2 + /// + /// #### Throws + /// + /// - `HardwareException`: when the port cannot be opened or the rate is + /// not supported + public SerialPort openSerial(String device, int baud, int dataBits, int parity, int stopBits) throws HardwareException { + return new SerialPort(this, serialOpen(device, baud, dataBits, parity, stopBits), device); + } + + /// Opens a hardware PWM channel. The channel starts disabled. + /// + /// #### Throws + /// + /// - `HardwareException`: when the channel does not exist + public PwmChannel openPwm(int chip, int channel) throws HardwareException { + return new PwmChannel(this, pwmOpen(chip, channel), chip, channel); + } + + /// Opens a CAN network interface. The interface must already be up and + /// configured with its bit rate (`ip link set can0 up type can bitrate + /// 500000`), which needs privileges an application normally lacks. + /// + /// #### Throws + /// + /// - `HardwareException`: when the interface does not exist + public CanBus openCan(String iface) throws HardwareException { + return new CanBus(this, canOpen(iface), iface); + } + + /// Opens and starts the hardware watchdog. From this call on the device + /// resets unless [Watchdog#kick()] is called within the timeout. + /// + /// #### Throws + /// + /// - `HardwareException`: when there is no watchdog or it is in use + public Watchdog openWatchdog(int timeoutSeconds) throws HardwareException { + return new Watchdog(this, watchdogOpen(timeoutSeconds)); + } + + /// The current backlight brightness, from `0` to + /// [#getMaxBacklightBrightness()], or `-1` when not supported. + public int getBacklightBrightness() { + return -1; + } + + /// The highest backlight brightness value, or `-1` when not supported. + public int getMaxBacklightBrightness() { + return -1; + } + + /// Sets the backlight brightness, clamped to the valid range. + /// + /// #### Throws + /// + /// - `HardwareException`: when not supported or not permitted + public void setBacklightBrightness(int value) throws HardwareException { + throw unsupported("backlight"); + } + + // ----------------------------------------------------------- primitives + + /// Creates the exception the default primitives throw. + protected static HardwareException unsupported(String what) { + return new HardwareException(what + " is not supported on this platform"); + } + + /// Claims a GPIO line and returns its handle. + protected int gpioOpen(String chip, int line, boolean output, boolean initialHigh, int bias, int edges) throws HardwareException { + throw unsupported("GPIO"); + } + + /// Reads a GPIO line: `true` for high. + protected boolean gpioRead(int handle) throws HardwareException { + throw unsupported("GPIO"); + } + + /// Drives an output GPIO line. + protected void gpioWrite(int handle, boolean high) throws HardwareException { + throw unsupported("GPIO"); + } + + /// Waits up to `timeoutMs` for an edge on an input line. Returns one of + /// [#EDGE_TIMEOUT], [#EDGE_RESULT_FALLING] or [#EDGE_RESULT_RISING] and + /// stores the edge's timestamp in nanoseconds in `timestampOut[0]`. + /// Called from a background thread, never the event dispatch thread. + protected int gpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) throws HardwareException { + throw unsupported("GPIO"); + } + + /// Opens an I2C device and returns its handle. + protected int i2cOpen(int bus, int address) throws HardwareException { + throw unsupported("I2C"); + } + + /// Performs one combined I2C transaction: writes `writeLen` bytes from + /// `write`, then, with a repeated start and no stop between, reads + /// `readLen` bytes into `read`. Either length may be zero. + protected void i2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) throws HardwareException { + throw unsupported("I2C"); + } + + /// Opens and configures an SPI device and returns its handle. + protected int spiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) throws HardwareException { + throw unsupported("SPI"); + } + + /// Performs one full-duplex SPI transfer of `len` bytes. `rx` may be the + /// same array as `tx`. + protected void spiTransfer(int handle, byte[] tx, byte[] rx, int len) throws HardwareException { + throw unsupported("SPI"); + } + + /// Opens and configures a serial port and returns its handle. + protected int serialOpen(String device, int baud, int dataBits, int parity, int stopBits) throws HardwareException { + throw unsupported("Serial"); + } + + /// Reads up to `len` bytes, waiting at most `timeoutMs` (negative waits + /// forever) for the first one. Returns the count read, `0` on timeout. + protected int serialRead(int handle, byte[] buf, int off, int len, int timeoutMs) throws HardwareException { + throw unsupported("Serial"); + } + + /// Writes all `len` bytes. + protected void serialWrite(int handle, byte[] buf, int off, int len) throws HardwareException { + throw unsupported("Serial"); + } + + /// Opens a PWM channel and returns its handle. + protected int pwmOpen(int chip, int channel) throws HardwareException { + throw unsupported("PWM"); + } + + /// Configures a PWM channel. Duty may not exceed the period. + protected void pwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) throws HardwareException { + throw unsupported("PWM"); + } + + /// Opens a CAN interface and returns its handle. + protected int canOpen(String iface) throws HardwareException { + throw unsupported("CAN"); + } + + /// Sends one CAN frame. + protected void canSend(int handle, int id, boolean extended, byte[] data, int len) throws HardwareException { + throw unsupported("CAN"); + } + + /// Waits up to `timeoutMs` for a CAN frame. Returns the payload length, + /// or `-1` on timeout; the identifier goes into `idOut[0]` and `idOut[1]` + /// is `1` for an extended identifier. + protected int canReceive(int handle, int[] idOut, byte[] data, int timeoutMs) throws HardwareException { + throw unsupported("CAN"); + } + + /// Opens the watchdog with a timeout and returns its handle. + protected int watchdogOpen(int timeoutSeconds) throws HardwareException { + throw unsupported("Watchdog"); + } + + /// Resets the watchdog timer. + protected void watchdogKick(int handle) throws HardwareException { + throw unsupported("Watchdog"); + } + + /// Closes any handle. `disarm` applies to a watchdog only: `true` stops it + /// where the driver allows, `false` leaves it running so the device resets. + protected void close(int handle, boolean disarm) { + } +} diff --git a/CodenameOne/src/com/codename1/hardware/HardwareException.java b/CodenameOne/src/com/codename1/hardware/HardwareException.java new file mode 100644 index 00000000000..6c1e053da4c --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/HardwareException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +import java.io.IOException; + +/// Thrown when a hardware bus or pin cannot be opened or an I/O operation on +/// it fails: the device node is absent, the process lacks permission for it, +/// the line is already claimed, or the peripheral did not acknowledge. +/// +/// [#getErrno()] carries the operating system error number when the port has +/// one (Linux `errno`), and `0` otherwise, so code can tell "no such device" +/// from "permission denied" without parsing the message. +@com.codename1.impl.SharedWithBackend +public class HardwareException extends IOException { + + private final int errno; + + /// Creates an exception with a message and no operating system error. + public HardwareException(String message) { + this(message, 0); + } + + /// Creates an exception with a message and the operating system error + /// number that caused it. + public HardwareException(String message, int errno) { + super(message); + this.errno = errno; + } + + /// The operating system error number behind this failure, or `0` when the + /// port did not report one. + public int getErrno() { + return errno; + } +} diff --git a/CodenameOne/src/com/codename1/hardware/I2cDevice.java b/CodenameOne/src/com/codename1/hardware/I2cDevice.java new file mode 100644 index 00000000000..53372c6ecc3 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/I2cDevice.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +/// A device on an I2C bus, opened through [Hardware#openI2c(int, int)]. +/// +/// The register helpers cover the common sensor convention: write the register +/// address, then read with a repeated start. +/// +/// ```java +/// I2cDevice bme280 = Hardware.getInstance().openI2c(1, 0x76); +/// int chipId = bme280.readRegister(0xd0); // 0x60 on a BME280 +/// ``` +@com.codename1.impl.SharedWithBackend +public final class I2cDevice { + + private final Hardware hw; + private final int handle; + private final int bus; + private final int address; + private boolean closed; + + I2cDevice(Hardware hw, int handle, int bus, int address) { + this.hw = hw; + this.handle = handle; + this.bus = bus; + this.address = address; + } + + /// The bus number. + public int getBus() { + return bus; + } + + /// The 7-bit device address. + public int getAddress() { + return address; + } + + /// Writes `data` in one transaction. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public void write(byte[] data) throws HardwareException { + checkOpen(); + hw.i2cTransfer(handle, data, data.length, null, 0); + } + + /// Reads `len` bytes in one transaction. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public byte[] read(int len) throws HardwareException { + checkOpen(); + byte[] out = new byte[len]; + hw.i2cTransfer(handle, null, 0, out, len); + return out; + } + + /// Writes `write`, then reads `readLen` bytes after a repeated start, as a + /// single transaction with no stop condition between them. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public byte[] writeRead(byte[] write, int readLen) throws HardwareException { + checkOpen(); + byte[] out = new byte[readLen]; + hw.i2cTransfer(handle, write, write.length, out, readLen); + return out; + } + + /// Reads one 8-bit register. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public int readRegister(int register) throws HardwareException { + return writeRead(new byte[] {(byte) register}, 1)[0] & 0xff; + } + + /// Reads `len` consecutive registers starting at `register`. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public byte[] readRegisters(int register, int len) throws HardwareException { + return writeRead(new byte[] {(byte) register}, len); + } + + /// Writes one 8-bit register. + /// + /// #### Throws + /// + /// - `HardwareException`: when the device does not acknowledge + public void writeRegister(int register, int value) throws HardwareException { + write(new byte[] {(byte) register, (byte) value}); + } + + /// Releases the device. Closing twice is harmless. + public void close() { + if (!closed) { + closed = true; + hw.close(handle, false); + } + } + + private void checkOpen() throws HardwareException { + if (closed) { + throw new HardwareException("I2C device 0x" + Integer.toHexString(address) + " is closed"); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/PwmChannel.java b/CodenameOne/src/com/codename1/hardware/PwmChannel.java new file mode 100644 index 00000000000..5d441540f0a --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/PwmChannel.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +/// A hardware PWM channel, opened through [Hardware#openPwm(int, int)]. +/// +/// ```java +/// PwmChannel servo = Hardware.getInstance().openPwm(0, 0); +/// servo.configure(20000000L, 1500000L, true); // 50 Hz, 1.5 ms pulse: centred +/// ``` +@com.codename1.impl.SharedWithBackend +public final class PwmChannel { + + private final Hardware hw; + private final int handle; + private final int chip; + private final int channel; + private long periodNanos; + private long dutyNanos; + private boolean enabled; + private boolean closed; + + PwmChannel(Hardware hw, int handle, int chip, int channel) { + this.hw = hw; + this.handle = handle; + this.chip = chip; + this.channel = channel; + } + + /// The PWM controller number. + public int getChip() { + return chip; + } + + /// The channel within the controller. + public int getChannel() { + return channel; + } + + /// Sets the period, the high time and whether the output runs. + /// + /// #### Throws + /// + /// - `HardwareException`: when the duty exceeds the period, the + /// controller rejects the values, or the channel is closed + public void configure(long periodNanos, long dutyNanos, boolean enabled) throws HardwareException { + if (closed) { + throw new HardwareException("PWM channel " + chip + "/" + channel + " is closed"); + } + if (periodNanos <= 0 || dutyNanos < 0 || dutyNanos > periodNanos) { + throw new HardwareException("PWM duty must be within 0..period"); + } + hw.pwmConfigure(handle, periodNanos, dutyNanos, enabled); + this.periodNanos = periodNanos; + this.dutyNanos = dutyNanos; + this.enabled = enabled; + } + + /// Sets the duty cycle as a fraction of the current period, keeping the + /// period and enabled state. + /// + /// #### Throws + /// + /// - `HardwareException`: when no period was configured yet + public void setDutyCycle(double fraction) throws HardwareException { + if (periodNanos <= 0) { + throw new HardwareException("Configure a PWM period first"); + } + double f = fraction < 0 ? 0 : (fraction > 1 ? 1 : fraction); + configure(periodNanos, (long) (periodNanos * f), enabled); + } + + /// The configured period in nanoseconds, `0` before [#configure(long, long, boolean)]. + public long getPeriodNanos() { + return periodNanos; + } + + /// The configured high time in nanoseconds. + public long getDutyNanos() { + return dutyNanos; + } + + /// `true` while the output runs. + public boolean isEnabled() { + return enabled; + } + + /// Disables the output and releases the channel. Closing twice is harmless. + public void close() { + if (!closed) { + if (enabled) { + try { + hw.pwmConfigure(handle, periodNanos, dutyNanos, false); + } catch (HardwareException ignored) { + // Releasing is what matters; the controller may already be gone. + } + } + closed = true; + hw.close(handle, false); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/SerialPort.java b/CodenameOne/src/com/codename1/hardware/SerialPort.java new file mode 100644 index 00000000000..7cecb5e6fe1 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/SerialPort.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/// A serial port in raw mode, opened through +/// [Hardware#openSerial(String, int, int, int, int)]. +/// +/// Reads block; call them off the event dispatch thread, or use +/// [#read(byte[], int, int, int)] with a timeout. +@com.codename1.impl.SharedWithBackend +public final class SerialPort { + + /// No parity bit. + public static final int PARITY_NONE = 0; + /// Odd parity. + public static final int PARITY_ODD = 1; + /// Even parity. + public static final int PARITY_EVEN = 2; + + private final Hardware hw; + private final int handle; + private final String device; + private boolean closed; + private InputStream in; + private OutputStream out; + + SerialPort(Hardware hw, int handle, String device) { + this.hw = hw; + this.handle = handle; + this.device = device; + } + + /// The device path this port was opened on. + public String getDevice() { + return device; + } + + /// Reads up to `len` bytes, waiting at most `timeoutMs` for the first + /// (negative waits indefinitely). Returns the number read, `0` on timeout. + /// + /// #### Throws + /// + /// - `HardwareException`: when the read fails or the port is closed + public int read(byte[] buf, int off, int len, int timeoutMs) throws HardwareException { + checkOpen(); + return hw.serialRead(handle, buf, off, len, timeoutMs); + } + + /// Writes all of `buf[off..off+len)`. + /// + /// #### Throws + /// + /// - `HardwareException`: when the write fails or the port is closed + public void write(byte[] buf, int off, int len) throws HardwareException { + checkOpen(); + hw.serialWrite(handle, buf, off, len); + } + + /// A blocking stream over this port. Its `read` waits until data arrives + /// and returns `-1` only after [#close()]. + public InputStream getInputStream() { + if (in == null) { + in = new InputStream() { + @Override + public int read() throws IOException { + byte[] one = new byte[1]; + int n = read(one, 0, 1); + return n <= 0 ? -1 : one[0] & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + while (!closed) { + int n = hw.serialRead(handle, b, off, len, 250); + if (n > 0) { + return n; + } + } + return -1; + } + }; + } + return in; + } + + /// A stream that writes to this port. + public OutputStream getOutputStream() { + if (out == null) { + out = new OutputStream() { + @Override + public void write(int b) throws IOException { + write(new byte[] {(byte) b}, 0, 1); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + SerialPort.this.write(b, off, len); + } + }; + } + return out; + } + + /// Closes the port. Closing twice is harmless. + public void close() { + if (!closed) { + closed = true; + hw.close(handle, false); + } + } + + private void checkOpen() throws HardwareException { + if (closed) { + throw new HardwareException("Serial port " + device + " is closed"); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/SpiDevice.java b/CodenameOne/src/com/codename1/hardware/SpiDevice.java new file mode 100644 index 00000000000..cc5654f472f --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/SpiDevice.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +/// A device on an SPI bus, opened through +/// [Hardware#openSpi(int, int, int, int, int)]. +@com.codename1.impl.SharedWithBackend +public final class SpiDevice { + + private final Hardware hw; + private final int handle; + private final int bus; + private final int chipSelect; + private boolean closed; + + SpiDevice(Hardware hw, int handle, int bus, int chipSelect) { + this.hw = hw; + this.handle = handle; + this.bus = bus; + this.chipSelect = chipSelect; + } + + /// The SPI controller number. + public int getBus() { + return bus; + } + + /// The chip-select line. + public int getChipSelect() { + return chipSelect; + } + + /// Clocks `tx` out and returns the bytes clocked in at the same time. + /// + /// #### Throws + /// + /// - `HardwareException`: when the transfer fails or the device is closed + public byte[] transfer(byte[] tx) throws HardwareException { + byte[] rx = new byte[tx.length]; + transfer(tx, rx); + return rx; + } + + /// Clocks `tx` out while filling `rx`; both must be the same length, and + /// may be the same array. + /// + /// #### Throws + /// + /// - `HardwareException`: when the transfer fails or the device is closed + public void transfer(byte[] tx, byte[] rx) throws HardwareException { + if (closed) { + throw new HardwareException("SPI device " + bus + "." + chipSelect + " is closed"); + } + if (rx.length != tx.length) { + throw new HardwareException("SPI tx and rx lengths differ"); + } + hw.spiTransfer(handle, tx, rx, tx.length); + } + + /// Releases the device. Closing twice is harmless. + public void close() { + if (!closed) { + closed = true; + hw.close(handle, false); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/Watchdog.java b/CodenameOne/src/com/codename1/hardware/Watchdog.java new file mode 100644 index 00000000000..39596babb68 --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/Watchdog.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.hardware; + +/// The hardware watchdog, opened through [Hardware#openWatchdog(int)]. +/// +/// Once open, the device resets unless [#kick()] is called within the +/// timeout. Kick it from a timer that only runs while the application is +/// healthy, so a hung event dispatch thread causes a reset: +/// +/// ```java +/// final Watchdog wd = Hardware.getInstance().openWatchdog(15); +/// UITimer.timer(5000, true, form, () -> wd.kickQuietly()); +/// ``` +@com.codename1.impl.SharedWithBackend +public final class Watchdog { + + private final Hardware hw; + private final int handle; + private boolean closed; + + Watchdog(Hardware hw, int handle) { + this.hw = hw; + this.handle = handle; + } + + /// Restarts the countdown. + /// + /// #### Throws + /// + /// - `HardwareException`: when the watchdog is closed or the kick fails + public void kick() throws HardwareException { + if (closed) { + throw new HardwareException("Watchdog is closed"); + } + hw.watchdogKick(handle); + } + + /// [#kick()], ignoring failures, for use from a timer. + public void kickQuietly() { + try { + kick(); + } catch (HardwareException ignored) { + // A failed kick is what the watchdog exists to catch. + } + } + + /// Closes the watchdog. With `disarm` the timer stops where the driver + /// allows it; without, the device resets when the timeout runs out, which + /// is the right choice on the way down after an unrecoverable error. + public void close(boolean disarm) { + if (!closed) { + closed = true; + hw.close(handle, disarm); + } + } +} diff --git a/CodenameOne/src/com/codename1/hardware/package-info.java b/CodenameOne/src/com/codename1/hardware/package-info.java new file mode 100644 index 00000000000..0cdd9e62e7d --- /dev/null +++ b/CodenameOne/src/com/codename1/hardware/package-info.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +/// Buses and pins of embedded devices: GPIO lines (`GpioPin`, +/// `GpioListener`), I2C (`I2cDevice`), SPI (`SpiDevice`), serial ports +/// (`SerialPort`), PWM (`PwmChannel`), CAN (`CanBus`, `CanFrame`), the +/// hardware watchdog (`Watchdog`) and backlight brightness. +/// +/// `Hardware.getInstance()` is the entry point. Ports without hardware access +/// report every feature as unsupported, so an application written against this +/// package also runs on phones, desktops and the simulator. +package com.codename1.hardware; diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index d546b46a245..047fcbcefad 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -8618,6 +8618,12 @@ public SecureStorage getSecureStorage() { public com.codename1.nfc.Nfc getNfc() { return null; } + /// Returns the port-specific hardware entry point. Default implementation + /// returns {@code null}; ports with bus and pin access override this to + /// return a cached {@link com.codename1.hardware.Hardware} subclass. + public com.codename1.hardware.Hardware getHardware() { + return null; + } /// Returns the port-specific local calendar source, or null when the port /// has no device calendar integration. diff --git a/CodenameOne/src/com/codename1/impl/KernelHardware.java b/CodenameOne/src/com/codename1/impl/KernelHardware.java new file mode 100644 index 00000000000..2a7e28154cb --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/KernelHardware.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +import com.codename1.hardware.Hardware; +import com.codename1.hardware.HardwareException; +import java.util.ArrayList; + +/// [Hardware] over the Linux kernel's own interfaces: the GPIO character +/// device, i2c-dev, spidev, termios, sysfs PWM and backlight, SocketCAN and +/// `/dev/watchdog`. The Linux port and the headless backend both extend it and +/// supply only the `raw` primitives, which they implement over one shared C +/// body (`cn1_hardware_impl.h`), so the two cannot drift apart. +@SharedWithBackend +public abstract class KernelHardware extends Hardware { + + /// Linux `ETIMEDOUT`. + private static final int ETIMEDOUT = 110; + + @Override + protected int getSupportedFeatures() { + return FEATURE_GPIO | FEATURE_I2C | FEATURE_SPI | FEATURE_SERIAL | FEATURE_PWM + | FEATURE_CAN | FEATURE_WATCHDOG | FEATURE_BACKLIGHT; + } + + private static int check(int r, String what) throws HardwareException { + if (r < 0) { + throw new HardwareException(what + " failed (errno " + (-r) + ")", -r); + } + return r; + } + + private static String[] split(String s) { + ArrayList out = new ArrayList(); + if (s != null) { + int start = 0; + int len = s.length(); + for (int i = 0; i <= len; i++) { + if (i == len || s.charAt(i) == '\n') { + if (i > start) { + out.add(s.substring(start, i)); + } + start = i + 1; + } + } + } + return out.toArray(new String[out.size()]); + } + + @Override + public String[] listGpioChips() { + return split(rawList(0)); + } + + @Override + public int[] listI2cBuses() { + String[] names = split(rawList(1)); + int[] out = new int[names.length]; + int count = 0; + for (String name : names) { + try { + out[count] = Integer.parseInt(name.substring(4)); + count++; + } catch (NumberFormatException ignored) { + // A non-numeric i2c- entry is not a bus node. + } + } + int[] trimmed = new int[count]; + System.arraycopy(out, 0, trimmed, 0, count); + return trimmed; + } + + @Override + public String[] listSpiDevices() { + String[] names = split(rawList(2)); + for (int i = 0; i < names.length; i++) { + names[i] = names[i].substring(6); + } + return names; + } + + @Override + public String[] listSerialPorts() { + String[] names = split(rawList(3)); + for (int i = 0; i < names.length; i++) { + names[i] = "/dev/" + names[i]; + } + return names; + } + + @Override + public String[] listCanInterfaces() { + return split(rawList(4)); + } + + @Override + public int getBacklightBrightness() { + int r = rawBacklight(0, 0); + return r < 0 ? -1 : r; + } + + @Override + public int getMaxBacklightBrightness() { + int r = rawBacklight(1, 0); + return r < 0 ? -1 : r; + } + + @Override + public void setBacklightBrightness(int value) throws HardwareException { + check(rawBacklight(2, value), "Backlight"); + } + + @Override + protected int gpioOpen(String chip, int line, boolean output, boolean initialHigh, int bias, int edges) throws HardwareException { + return check(rawGpioOpen(chip, line, output, initialHigh, bias, edges), "GPIO " + chip + "/" + line); + } + + @Override + protected boolean gpioRead(int handle) throws HardwareException { + return check(rawGpioRead(handle), "GPIO read") != 0; + } + + @Override + protected void gpioWrite(int handle, boolean high) throws HardwareException { + check(rawGpioWrite(handle, high), "GPIO write"); + } + + @Override + protected int gpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) throws HardwareException { + int r = rawGpioWaitEdge(handle, timeoutMs, timestampOut); + if (r == -ETIMEDOUT) { + return EDGE_TIMEOUT; + } + return check(r, "GPIO edge wait") == 1 ? EDGE_RESULT_RISING : EDGE_RESULT_FALLING; + } + + @Override + protected int i2cOpen(int bus, int address) throws HardwareException { + return check(rawI2cOpen(bus, address), "I2C bus " + bus); + } + + @Override + protected void i2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) throws HardwareException { + check(rawI2cTransfer(handle, write, writeLen, read, readLen), "I2C transfer"); + } + + @Override + protected int spiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) throws HardwareException { + return check(rawSpiOpen(bus, chipSelect, mode, speedHz, bitsPerWord), "SPI " + bus + "." + chipSelect); + } + + @Override + protected void spiTransfer(int handle, byte[] tx, byte[] rx, int len) throws HardwareException { + check(rawSpiTransfer(handle, tx, rx, len), "SPI transfer"); + } + + @Override + protected int serialOpen(String device, int baud, int dataBits, int parity, int stopBits) throws HardwareException { + return check(rawSerialOpen(device, baud, dataBits, parity, stopBits), "Serial " + device); + } + + @Override + protected int serialRead(int handle, byte[] buf, int off, int len, int timeoutMs) throws HardwareException { + return check(rawSerialRead(handle, buf, off, len, timeoutMs), "Serial read"); + } + + @Override + protected void serialWrite(int handle, byte[] buf, int off, int len) throws HardwareException { + check(rawSerialWrite(handle, buf, off, len), "Serial write"); + } + + @Override + protected int pwmOpen(int chip, int channel) throws HardwareException { + return check(rawPwmOpen(chip, channel), "PWM " + chip + "/" + channel); + } + + @Override + protected void pwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) throws HardwareException { + check(rawPwmConfigure(handle, periodNanos, dutyNanos, enabled), "PWM configure"); + } + + @Override + protected int canOpen(String iface) throws HardwareException { + return check(rawCanOpen(iface), "CAN " + iface); + } + + @Override + protected void canSend(int handle, int id, boolean extended, byte[] data, int len) throws HardwareException { + check(rawCanSend(handle, id, extended, data, len), "CAN send"); + } + + @Override + protected int canReceive(int handle, int[] idOut, byte[] data, int timeoutMs) throws HardwareException { + int r = rawCanReceive(handle, idOut, data, timeoutMs); + if (r == -ETIMEDOUT) { + return -1; + } + return check(r, "CAN receive"); + } + + @Override + protected int watchdogOpen(int timeoutSeconds) throws HardwareException { + return check(rawWatchdogOpen(timeoutSeconds), "Watchdog"); + } + + @Override + protected void watchdogKick(int handle) throws HardwareException { + check(rawWatchdogKick(handle), "Watchdog kick"); + } + + @Override + protected void close(int handle, boolean disarm) { + rawClose(handle, disarm); + } + + // The kernel primitives. Each answers a handle or a result >= 0, or -errno; + // the runtime implements them over the shared C in cn1_hardware_impl.h. + + /// `kind`: 0 GPIO chips, 1 I2C buses, 2 SPI devices, 3 serial ports, + /// 4 CAN interfaces. Names separated by newlines. + protected abstract String rawList(int kind); + + /// `op`: 0 read, 1 maximum, 2 write `value`. + protected abstract int rawBacklight(int op, int value); + + protected abstract int rawGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges); + + protected abstract int rawGpioRead(int handle); + + protected abstract int rawGpioWrite(int handle, boolean high); + + protected abstract int rawGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut); + + protected abstract int rawI2cOpen(int bus, int address); + + protected abstract int rawI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen); + + protected abstract int rawSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord); + + protected abstract int rawSpiTransfer(int handle, byte[] tx, byte[] rx, int len); + + protected abstract int rawSerialOpen(String device, int baud, int dataBits, int parity, int stopBits); + + protected abstract int rawSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs); + + protected abstract int rawSerialWrite(int handle, byte[] buf, int off, int len); + + protected abstract int rawPwmOpen(int chip, int channel); + + protected abstract int rawPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled); + + protected abstract int rawCanOpen(String iface); + + protected abstract int rawCanSend(int handle, int id, boolean extended, byte[] data, int len); + + protected abstract int rawCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs); + + protected abstract int rawWatchdogOpen(int timeoutSeconds); + + protected abstract int rawWatchdogKick(int handle); + + protected abstract void rawClose(int handle, boolean disarm); +} diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index 144056b7572..d95ee8635f7 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -665,6 +665,7 @@ public static void init(Object m) { newEdt.start(); } impl.postInit(); + com.codename1.hardware.Hardware.install(impl.getHardware()); INSTANCE.setCommandBehavior(commandBehaviour); } else { impl.confirmControlView(); @@ -6805,6 +6806,13 @@ public com.codename1.security.vault.spi.DeviceProtection getDeviceProtection() { public com.codename1.nfc.Nfc getNfc() { return impl.getNfc(); } + /// Returns the platform hardware entry point (GPIO, I2C, SPI, serial, PWM, + /// CAN, watchdog, backlight). Prefer + /// {@link com.codename1.hardware.Hardware#getInstance()} in application code, + /// which substitutes an unsupported stub when the port returns `null`. + public com.codename1.hardware.Hardware getHardware() { + return impl.getHardware(); + } /// Returns the active port's local device-calendar source. Applications /// should normally use diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEHardware.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEHardware.java new file mode 100644 index 00000000000..733d760a4e1 --- /dev/null +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEHardware.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.javase; + +import com.codename1.hardware.Hardware; +import com.codename1.hardware.HardwareException; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.Map; + +/// A virtual board for the simulator, so an application that drives hardware +/// can be developed and tested with none attached. It answers every feature: +/// +/// - GPIO: `gpiochip0` with 32 lines. An output reads back what was written; +/// an input reads what [#setInput(String, int, boolean)] last set, which also +/// delivers an edge to a waiting listener. +/// - I2C bus 1: a TMP102-style temperature sensor at `0x48` (register 0 holds +/// the temperature, [#setTemperature(double)]) and a 256-byte EEPROM at `0x50` +/// (write the address byte, then data; or write the address and read). +/// - SPI `0.0`: a loopback, MISO wired to MOSI. +/// - Serial `/dev/ttySIM0`: a loopback, everything written is read back. +/// - PWM chip 0, channels 0 and 1, and CAN `vcan0`, which loops frames back. +/// - A watchdog that logs a simulated reset when it is not kicked in time. +/// - A backlight from 0 to 255. +/// +/// Tests reach the same state through the static methods. +public class JavaSEHardware extends Hardware { + + /// Listeners run on the event dispatch thread. + @Override + protected void deliver(Runnable callback) { + com.codename1.ui.Display.getInstance().callSerially(callback); + } + + private static final int LINES = 32; + private static final Object LOCK = new Object(); + private static final boolean[] PIN_LEVEL = new boolean[LINES]; + private static final ArrayDeque EDGES = new ArrayDeque(); + private static double temperature = 22.5; + private static int backlight = 255; + + private final Map handles = new HashMap(); + private final byte[] eeprom = new byte[256]; + private int eepromPointer; + private final ArrayDeque serialLoop = new ArrayDeque(); + private final ArrayDeque canLoop = new ArrayDeque(); + private int nextHandle = 1; + + /// Drives a simulated input line, delivering an edge to any listener. + public static void setInput(String chip, int line, boolean high) { + if (line < 0 || line >= LINES) { + return; + } + synchronized (LOCK) { + if (PIN_LEVEL[line] != high) { + PIN_LEVEL[line] = high; + EDGES.add(new long[]{line, high ? 1 : 0, System.nanoTime()}); + LOCK.notifyAll(); + } + } + } + + /// The level a simulated line is at, input or output. + public static boolean getLevel(int line) { + synchronized (LOCK) { + return line >= 0 && line < LINES && PIN_LEVEL[line]; + } + } + + /// Sets what the simulated temperature sensor at I2C `0x48` reports. + public static void setTemperature(double celsius) { + synchronized (LOCK) { + temperature = celsius; + } + } + + @Override + protected int getSupportedFeatures() { + return FEATURE_GPIO | FEATURE_I2C | FEATURE_SPI | FEATURE_SERIAL | FEATURE_PWM | FEATURE_CAN + | FEATURE_WATCHDOG | FEATURE_BACKLIGHT; + } + + @Override + public String[] listGpioChips() { + return new String[]{"gpiochip0"}; + } + + @Override + public int[] listI2cBuses() { + return new int[]{1}; + } + + @Override + public String[] listSpiDevices() { + return new String[]{"0.0"}; + } + + @Override + public String[] listSerialPorts() { + return new String[]{"/dev/ttySIM0"}; + } + + @Override + public String[] listCanInterfaces() { + return new String[]{"vcan0"}; + } + + @Override + public int getBacklightBrightness() { + synchronized (LOCK) { + return backlight; + } + } + + @Override + public int getMaxBacklightBrightness() { + return 255; + } + + @Override + public void setBacklightBrightness(int value) { + synchronized (LOCK) { + backlight = Math.max(0, Math.min(255, value)); + } + } + + private int register(String kind, Object state) { + synchronized (LOCK) { + int h = nextHandle++; + handles.put(h, new Object[]{kind, state}); + return h; + } + } + + private Object state(int handle, String kind) throws HardwareException { + synchronized (LOCK) { + Object[] h = handles.get(handle); + if (h == null || !kind.equals(h[0])) { + throw new HardwareException("Closed or invalid " + kind + " handle"); + } + return h[1]; + } + } + + @Override + protected int gpioOpen(String chip, int line, boolean output, boolean initialHigh, int bias, int edges) + throws HardwareException { + if (!"gpiochip0".equals(chip) || line < 0 || line >= LINES) { + throw new HardwareException("No such GPIO line " + chip + ":" + line); + } + if (output) { + synchronized (LOCK) { + PIN_LEVEL[line] = initialHigh; + } + } + return register("gpio", Integer.valueOf(line)); + } + + @Override + protected boolean gpioRead(int handle) throws HardwareException { + return getLevel(((Integer) state(handle, "gpio")).intValue()); + } + + @Override + protected void gpioWrite(int handle, boolean high) throws HardwareException { + int line = ((Integer) state(handle, "gpio")).intValue(); + synchronized (LOCK) { + PIN_LEVEL[line] = high; + } + } + + @Override + protected int gpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) throws HardwareException { + int line = ((Integer) state(handle, "gpio")).intValue(); + long deadline = timeoutMs < 0 ? Long.MAX_VALUE : System.currentTimeMillis() + timeoutMs; + synchronized (LOCK) { + while (true) { + for (java.util.Iterator it = EDGES.iterator(); it.hasNext();) { + long[] e = it.next(); + if (e[0] == line) { + it.remove(); + if (timestampOut != null && timestampOut.length > 0) { + timestampOut[0] = e[2]; + } + return e[1] == 1 ? EDGE_RESULT_RISING : EDGE_RESULT_FALLING; + } + } + long left = deadline - System.currentTimeMillis(); + if (left <= 0) { + return EDGE_TIMEOUT; + } + try { + LOCK.wait(left); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return EDGE_TIMEOUT; + } + } + } + } + + @Override + protected int i2cOpen(int bus, int address) throws HardwareException { + if (bus != 1 || (address != 0x48 && address != 0x50)) { + throw new HardwareException("No device at I2C " + bus + ":0x" + Integer.toHexString(address)); + } + return register("i2c", Integer.valueOf(address)); + } + + @Override + protected void i2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) + throws HardwareException { + int address = ((Integer) state(handle, "i2c")).intValue(); + synchronized (LOCK) { + if (address == 0x48) { + // TMP102: 12-bit two's complement, 0.0625 C per bit, left aligned. + int raw = (int) Math.round(temperature / 0.0625) << 4; + for (int i = 0; i < readLen; i++) { + read[i] = (byte) (i == 0 ? raw >> 8 : i == 1 ? raw : 0); + } + return; + } + if (writeLen > 0) { + eepromPointer = write[0] & 0xff; + for (int i = 1; i < writeLen; i++) { + eeprom[eepromPointer] = write[i]; + eepromPointer = (eepromPointer + 1) & 0xff; + } + } + for (int i = 0; i < readLen; i++) { + read[i] = eeprom[eepromPointer]; + eepromPointer = (eepromPointer + 1) & 0xff; + } + } + } + + @Override + protected int spiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) throws HardwareException { + if (bus != 0 || chipSelect != 0) { + throw new HardwareException("No SPI device " + bus + "." + chipSelect); + } + return register("spi", null); + } + + @Override + protected void spiTransfer(int handle, byte[] tx, byte[] rx, int len) throws HardwareException { + state(handle, "spi"); + if (rx != null && tx != null) { + System.arraycopy(tx, 0, rx, 0, len); + } + } + + @Override + protected int serialOpen(String device, int baud, int dataBits, int parity, int stopBits) throws HardwareException { + if (!"/dev/ttySIM0".equals(device)) { + throw new HardwareException("No serial port " + device); + } + return register("serial", null); + } + + @Override + protected int serialRead(int handle, byte[] buf, int off, int len, int timeoutMs) throws HardwareException { + state(handle, "serial"); + long deadline = timeoutMs < 0 ? Long.MAX_VALUE : System.currentTimeMillis() + timeoutMs; + synchronized (LOCK) { + while (serialLoop.isEmpty()) { + long left = deadline - System.currentTimeMillis(); + if (left <= 0) { + return 0; + } + try { + LOCK.wait(left); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return 0; + } + } + int n = 0; + while (n < len && !serialLoop.isEmpty()) { + buf[off + n++] = serialLoop.poll().byteValue(); + } + return n; + } + } + + @Override + protected void serialWrite(int handle, byte[] buf, int off, int len) throws HardwareException { + state(handle, "serial"); + synchronized (LOCK) { + for (int i = 0; i < len; i++) { + serialLoop.add(Byte.valueOf(buf[off + i])); + } + LOCK.notifyAll(); + } + } + + @Override + protected int pwmOpen(int chip, int channel) throws HardwareException { + if (chip != 0 || channel < 0 || channel > 1) { + throw new HardwareException("No PWM channel " + chip + ":" + channel); + } + return register("pwm", null); + } + + @Override + protected void pwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) throws HardwareException { + state(handle, "pwm"); + } + + @Override + protected int canOpen(String iface) throws HardwareException { + if (!"vcan0".equals(iface)) { + throw new HardwareException("No CAN interface " + iface); + } + return register("can", null); + } + + @Override + protected void canSend(int handle, int id, boolean extended, byte[] data, int len) throws HardwareException { + state(handle, "can"); + byte[] copy = new byte[len]; + System.arraycopy(data, 0, copy, 0, len); + synchronized (LOCK) { + canLoop.add(new Object[]{Integer.valueOf(id), Boolean.valueOf(extended), copy}); + LOCK.notifyAll(); + } + } + + @Override + protected int canReceive(int handle, int[] idOut, byte[] data, int timeoutMs) throws HardwareException { + state(handle, "can"); + long deadline = timeoutMs < 0 ? Long.MAX_VALUE : System.currentTimeMillis() + timeoutMs; + synchronized (LOCK) { + while (canLoop.isEmpty()) { + long left = deadline - System.currentTimeMillis(); + if (left <= 0) { + return -1; + } + try { + LOCK.wait(left); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return -1; + } + } + Object[] f = canLoop.poll(); + byte[] payload = (byte[]) f[2]; + idOut[0] = ((Integer) f[0]).intValue(); + if (idOut.length > 1) { + idOut[1] = ((Boolean) f[1]).booleanValue() ? 1 : 0; + } + System.arraycopy(payload, 0, data, 0, Math.min(payload.length, data.length)); + return payload.length; + } + } + + @Override + protected int watchdogOpen(int timeoutSeconds) throws HardwareException { + return register("watchdog", new long[]{System.currentTimeMillis(), Math.max(1, timeoutSeconds) * 1000L}); + } + + @Override + protected void watchdogKick(int handle) throws HardwareException { + long[] w = (long[]) state(handle, "watchdog"); + long now = System.currentTimeMillis(); + if (now - w[0] > w[1]) { + System.out.println("[Hardware simulation] watchdog expired " + (now - w[0] - w[1]) + + "ms before this kick: a real device would have reset"); + } + w[0] = now; + } + + @Override + protected void close(int handle, boolean disarm) { + synchronized (LOCK) { + handles.remove(handle); + } + } +} diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index daab9161991..f2d8dc565f7 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -18623,6 +18623,22 @@ public com.codename1.nfc.Nfc getNfc() { return nfc; } + private JavaSEHardware hardware; + + /// The simulator's virtual board (see [JavaSEHardware]), so hardware code + /// runs with nothing attached. A desktop application built from this port + /// has no such board and reports no hardware. + @Override + public com.codename1.hardware.Hardware getHardware() { + if (!isSimulator()) { + return null; + } + if (hardware == null) { + hardware = new JavaSEHardware(); + } + return hardware; + } + @Override public com.codename1.calendar.LocalCalendarSource getLocalCalendarSource() { if (!isSimulator()) { diff --git a/Ports/LinuxPort/nativeSources/cn1_linux.h b/Ports/LinuxPort/nativeSources/cn1_linux.h index b52049ecdec..46e0c956ba9 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux.h +++ b/Ports/LinuxPort/nativeSources/cn1_linux.h @@ -182,6 +182,11 @@ const unsigned char* cn1LinuxFindResource(const char* name, int* lenOut); */ void cn1LinuxStubOnce(const char* tag); +/* Converts SIGSEGV/SIGBUS in translated code into a Java exception and dumps a + * native backtrace on abort (cn1_linux_faults.c). Idempotent; honours + * CN1_LINUX_NO_FAULT_HANDLER. Called first thing by each display's initDisplay. */ +void cn1LinuxInstallFaultHandlers(void); + /* Writes one line to stderr (and the journal) -- the nativeLog backing. */ void cn1LinuxLog(const char* message); diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_codec.c b/Ports/LinuxPort/nativeSources/cn1_linux_codec.c new file mode 100644 index 00000000000..2d0be5e5b2d --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_codec.c @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * Image decoding for the embedded target: PNG through libpng, JPEG through + * libjpeg, straight into the premultiplied ARGB32 surfaces the renderer draws. + * The desktop target decodes through GdkPixbuf instead (cn1_linux_image.c), + * which also reads GIF and BMP; here those two answer NULL, which the port + * reports as an image that could not be created. + */ +#ifdef CN1_LINUX_EMBEDDED + +#include "cn1_linux_gfx.h" +#include +#include +#include +#include +#include +#include + +static cairo_surface_t* cn1DecodePng(const unsigned char* bytes, size_t length) { + png_image image; + cairo_surface_t* s; + uint32_t* px; + size_t i, count; + memset(&image, 0, sizeof(image)); + image.version = PNG_IMAGE_VERSION; + if (!png_image_begin_read_from_memory(&image, bytes, length)) { + return 0; + } + /* BGRA in memory is a native-endian 0xAARRGGBB word on a little-endian CPU; + * ARGB in memory is the same word on a big-endian one. */ +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + image.format = PNG_FORMAT_BGRA; +#else + image.format = PNG_FORMAT_ARGB; +#endif + s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, (int) image.width, (int) image.height); + if (s == 0) { + png_image_free(&image); + return 0; + } + px = (uint32_t*) cairo_image_surface_get_data(s); + if (!png_image_finish_read(&image, 0, px, cairo_image_surface_get_stride(s), 0)) { + png_image_free(&image); + cairo_surface_destroy(s); + return 0; + } + count = (size_t) image.width * image.height; + for (i = 0; i < count; i++) { + uint32_t p = px[i]; + uint32_t a = p >> 24; + if (a != 255) { + uint32_t r = ((p >> 16) & 0xff) * a / 255; + uint32_t g = ((p >> 8) & 0xff) * a / 255; + uint32_t b = (p & 0xff) * a / 255; + px[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + } + return s; +} + +typedef struct { + struct jpeg_error_mgr base; + jmp_buf escape; +} CN1JpegError; + +/* libjpeg's default handler calls exit(); a damaged picture must not end the + * application. */ +static void cn1JpegFail(j_common_ptr info) { + longjmp(((CN1JpegError*) info->err)->escape, 1); +} + +static void cn1JpegQuiet(j_common_ptr info, int level) { + (void) info; + (void) level; +} + +static cairo_surface_t* cn1DecodeJpeg(const unsigned char* bytes, size_t length) { + struct jpeg_decompress_struct info; + CN1JpegError err; + cairo_surface_t* volatile s = 0; + unsigned char* volatile row = 0; + info.err = jpeg_std_error(&err.base); + err.base.error_exit = cn1JpegFail; + err.base.emit_message = cn1JpegQuiet; + if (setjmp(err.escape)) { + jpeg_destroy_decompress(&info); + free(row); + if (s != 0) { + cairo_surface_destroy(s); + } + return 0; + } + jpeg_create_decompress(&info); + jpeg_mem_src(&info, (unsigned char*) bytes, (unsigned long) length); + jpeg_read_header(&info, TRUE); + info.out_color_space = JCS_RGB; + jpeg_start_decompress(&info); + s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, (int) info.output_width, (int) info.output_height); + row = (unsigned char*) malloc((size_t) info.output_width * 3); + if (s == 0 || row == 0) { + longjmp(err.escape, 1); + } + { + unsigned char* base = cairo_image_surface_get_data(s); + int stride = cairo_image_surface_get_stride(s); + while (info.output_scanline < info.output_height) { + JSAMPROW rows[1]; + uint32_t* out = (uint32_t*) (base + (size_t) info.output_scanline * (size_t) stride); + unsigned x; + rows[0] = row; + jpeg_read_scanlines(&info, rows, 1); + for (x = 0; x < info.output_width; x++) { + out[x] = 0xff000000u | ((uint32_t) row[x * 3] << 16) | ((uint32_t) row[x * 3 + 1] << 8) | row[x * 3 + 2]; + } + } + } + jpeg_finish_decompress(&info); + jpeg_destroy_decompress(&info); + free(row); + return s; +} + +cairo_surface_t* cn1LinuxDecodeImage(const unsigned char* bytes, size_t length) { + static const unsigned char pngMagic[8] = { 0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a }; + if (bytes == 0 || length < 8) { + return 0; + } + if (memcmp(bytes, pngMagic, 8) == 0) { + return cn1DecodePng(bytes, length); + } + if (bytes[0] == 0xff && bytes[1] == 0xd8) { + return cn1DecodeJpeg(bytes, length); + } + return 0; +} + +#endif /* CN1_LINUX_EMBEDDED */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_crypto.c b/Ports/LinuxPort/nativeSources/cn1_linux_crypto.c index e4a8275c653..29c74b98b21 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_crypto.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_crypto.c @@ -51,6 +51,176 @@ #include #include +#include +#include + +/* + * libcrypto is loaded on the first crypto call, not linked: it is 4 MB that an + * application which never encrypts would otherwise map, and read from storage, + * on every start (most visibly on the embedded target, whose whole stack is + * smaller than that). The functions are reached through pointers named after + * them, as cn1_linux_net.c reaches libcurl. Resolving by name against the + * OpenSSL 3 ABI also means the binary does not carry the build host's symbol + * versions. + */ +#if !defined(OPENSSL_VERSION_MAJOR) || OPENSSL_VERSION_MAJOR < 3 +#error "The Linux port's crypto bridge needs the OpenSSL 3 headers" +#endif + +/* The names dlsym finds: where OpenSSL 3 keeps an old name as a macro (OPENSSL_free, + * EVP_PKEY_id), the function it expands to is listed, and the old name reaches + * the pointer through that expansion. */ +#define CN1_CRYPTO_FUNCTIONS(X) \ + X(ERR_clear_error) \ + X(ERR_error_string_n) \ + X(ERR_get_error) \ + X(EVP_CIPHER_CTX_ctrl) \ + X(EVP_CIPHER_CTX_free) \ + X(EVP_CIPHER_CTX_new) \ + X(EVP_CIPHER_CTX_set_padding) \ + X(EVP_CipherFinal_ex) \ + X(EVP_CipherInit_ex) \ + X(EVP_CipherUpdate) \ + X(EVP_DigestSign) \ + X(EVP_DigestSignInit) \ + X(EVP_DigestVerify) \ + X(EVP_DigestVerifyInit) \ + X(EVP_MD_CTX_free) \ + X(EVP_MD_CTX_new) \ + X(EVP_PKCS82PKEY) \ + X(EVP_PKEY2PKCS8) \ + X(EVP_PKEY_CTX_free) \ + X(EVP_PKEY_CTX_new) \ + X(EVP_PKEY_CTX_new_id) \ + X(EVP_PKEY_CTX_set_ec_paramgen_curve_nid) \ + X(EVP_PKEY_CTX_set_rsa_keygen_bits) \ + X(EVP_PKEY_CTX_set_rsa_mgf1_md) \ + X(EVP_PKEY_CTX_set_rsa_oaep_md) \ + X(EVP_PKEY_CTX_set_rsa_padding) \ + X(EVP_PKEY_get_base_id) \ + X(EVP_PKEY_decrypt) \ + X(EVP_PKEY_decrypt_init) \ + X(EVP_PKEY_derive) \ + X(EVP_PKEY_derive_init) \ + X(EVP_PKEY_derive_set_peer) \ + X(EVP_PKEY_encrypt) \ + X(EVP_PKEY_encrypt_init) \ + X(EVP_PKEY_free) \ + X(EVP_PKEY_get_id) \ + X(EVP_PKEY_keygen) \ + X(EVP_PKEY_keygen_init) \ + X(EVP_aes_128_cbc) \ + X(EVP_aes_128_ecb) \ + X(EVP_aes_128_gcm) \ + X(EVP_aes_192_cbc) \ + X(EVP_aes_192_ecb) \ + X(EVP_aes_192_gcm) \ + X(EVP_aes_256_cbc) \ + X(EVP_aes_256_ecb) \ + X(EVP_aes_256_gcm) \ + X(EVP_sha256) \ + X(EVP_sha384) \ + X(EVP_sha512) \ + X(OPENSSL_cleanse) \ + X(CRYPTO_free) \ + X(PKCS8_PRIV_KEY_INFO_free) \ + X(RAND_bytes) \ + X(d2i_AutoPrivateKey) \ + X(d2i_PKCS8_PRIV_KEY_INFO) \ + X(d2i_PUBKEY) \ + X(i2d_PKCS8_PRIV_KEY_INFO) \ + X(i2d_PUBKEY) + +#define CN1_CRYPTO_POINTER(name) static __typeof__(name)* p_##name; +CN1_CRYPTO_FUNCTIONS(CN1_CRYPTO_POINTER) +#undef CN1_CRYPTO_POINTER + +static pthread_once_t cn1CryptoOnce = PTHREAD_ONCE_INIT; +static int cn1CryptoAvailable; + +static void cn1LoadCrypto(void) { + void* library = dlopen("libcrypto.so.3", RTLD_NOW | RTLD_LOCAL); + if (library == NULL) { + fprintf(stderr, "Codename One: unable to load libcrypto.so.3: %s\n", dlerror()); + return; + } +#define CN1_CRYPTO_RESOLVE(name) \ + if ((*(void**) (&p_##name) = dlsym(library, #name)) == NULL) { \ + fprintf(stderr, "Codename One: libcrypto.so.3 is missing %s\n", #name); \ + dlclose(library); \ + return; \ + } + CN1_CRYPTO_FUNCTIONS(CN1_CRYPTO_RESOLVE) +#undef CN1_CRYPTO_RESOLVE + cn1CryptoAvailable = 1; +} + +static int cn1CryptoLoaded(void) { + pthread_once(&cn1CryptoOnce, cn1LoadCrypto); + return cn1CryptoAvailable; +} + +#define ERR_clear_error p_ERR_clear_error +#define ERR_error_string_n p_ERR_error_string_n +#define ERR_get_error p_ERR_get_error +#define EVP_CIPHER_CTX_ctrl p_EVP_CIPHER_CTX_ctrl +#define EVP_CIPHER_CTX_free p_EVP_CIPHER_CTX_free +#define EVP_CIPHER_CTX_new p_EVP_CIPHER_CTX_new +#define EVP_CIPHER_CTX_set_padding p_EVP_CIPHER_CTX_set_padding +#define EVP_CipherFinal_ex p_EVP_CipherFinal_ex +#define EVP_CipherInit_ex p_EVP_CipherInit_ex +#define EVP_CipherUpdate p_EVP_CipherUpdate +#define EVP_DigestSign p_EVP_DigestSign +#define EVP_DigestSignInit p_EVP_DigestSignInit +#define EVP_DigestVerify p_EVP_DigestVerify +#define EVP_DigestVerifyInit p_EVP_DigestVerifyInit +#define EVP_MD_CTX_free p_EVP_MD_CTX_free +#define EVP_MD_CTX_new p_EVP_MD_CTX_new +#define EVP_PKCS82PKEY p_EVP_PKCS82PKEY +#define EVP_PKEY2PKCS8 p_EVP_PKEY2PKCS8 +#define EVP_PKEY_CTX_free p_EVP_PKEY_CTX_free +#define EVP_PKEY_CTX_new p_EVP_PKEY_CTX_new +#define EVP_PKEY_CTX_new_id p_EVP_PKEY_CTX_new_id +#define EVP_PKEY_CTX_set_ec_paramgen_curve_nid p_EVP_PKEY_CTX_set_ec_paramgen_curve_nid +#define EVP_PKEY_CTX_set_rsa_keygen_bits p_EVP_PKEY_CTX_set_rsa_keygen_bits +#define EVP_PKEY_CTX_set_rsa_mgf1_md p_EVP_PKEY_CTX_set_rsa_mgf1_md +#define EVP_PKEY_CTX_set_rsa_oaep_md p_EVP_PKEY_CTX_set_rsa_oaep_md +#define EVP_PKEY_CTX_set_rsa_padding p_EVP_PKEY_CTX_set_rsa_padding +#define EVP_PKEY_get_base_id p_EVP_PKEY_get_base_id +#define EVP_PKEY_decrypt p_EVP_PKEY_decrypt +#define EVP_PKEY_decrypt_init p_EVP_PKEY_decrypt_init +#define EVP_PKEY_derive p_EVP_PKEY_derive +#define EVP_PKEY_derive_init p_EVP_PKEY_derive_init +#define EVP_PKEY_derive_set_peer p_EVP_PKEY_derive_set_peer +#define EVP_PKEY_encrypt p_EVP_PKEY_encrypt +#define EVP_PKEY_encrypt_init p_EVP_PKEY_encrypt_init +#define EVP_PKEY_free p_EVP_PKEY_free +#define EVP_PKEY_get_id p_EVP_PKEY_get_id +#define EVP_PKEY_keygen p_EVP_PKEY_keygen +#define EVP_PKEY_keygen_init p_EVP_PKEY_keygen_init +#define EVP_aes_128_cbc p_EVP_aes_128_cbc +#define EVP_aes_128_ecb p_EVP_aes_128_ecb +#define EVP_aes_128_gcm p_EVP_aes_128_gcm +#define EVP_aes_192_cbc p_EVP_aes_192_cbc +#define EVP_aes_192_ecb p_EVP_aes_192_ecb +#define EVP_aes_192_gcm p_EVP_aes_192_gcm +#define EVP_aes_256_cbc p_EVP_aes_256_cbc +#define EVP_aes_256_ecb p_EVP_aes_256_ecb +#define EVP_aes_256_gcm p_EVP_aes_256_gcm +#define EVP_sha256 p_EVP_sha256 +#define EVP_sha384 p_EVP_sha384 +#define EVP_sha512 p_EVP_sha512 +#define OPENSSL_cleanse p_OPENSSL_cleanse +#define CRYPTO_free p_CRYPTO_free +#define PKCS8_PRIV_KEY_INFO_free p_PKCS8_PRIV_KEY_INFO_free +#define RAND_bytes p_RAND_bytes +#define d2i_AutoPrivateKey p_d2i_AutoPrivateKey +#define d2i_PKCS8_PRIV_KEY_INFO p_d2i_PKCS8_PRIV_KEY_INFO +#define d2i_PUBKEY p_d2i_PUBKEY +#define i2d_PKCS8_PRIV_KEY_INFO p_i2d_PKCS8_PRIV_KEY_INFO +#define i2d_PUBKEY p_i2d_PUBKEY + + #define CN1_GCM_TAG_BYTES 16 /* Per-thread: crypto failures on different threads would otherwise overwrite @@ -59,7 +229,7 @@ static __thread char cn1CryptoError[512]; static void cn1CryptoFail(const char* what) { - unsigned long code = ERR_get_error(); + unsigned long code = cn1CryptoAvailable ? ERR_get_error() : 0; char detail[256]; detail[0] = 0; if (code != 0) { @@ -67,7 +237,9 @@ static void cn1CryptoFail(const char* what) { } snprintf(cn1CryptoError, sizeof(cn1CryptoError), "%s%s%s", what, detail[0] ? ": " : "", detail); - ERR_clear_error(); + if (cn1CryptoAvailable) { + ERR_clear_error(); + } } /* Clears the per-thread message so a caller can tell whether the operation it @@ -97,6 +269,10 @@ static const unsigned char* cn1Bytes(JAVA_OBJECT array, int* length) { /* ------------------------------------------------------------ random */ JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_secureRandomBytes___byte_1ARRAY_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + if (!cn1CryptoLoaded()) { + cn1CryptoFail("libcrypto.so.3 could not be loaded"); + return JAVA_FALSE; + } int length = 0; unsigned char* data = (unsigned char*) cn1Bytes(out, &length); if (data == 0 || length <= 0) { @@ -170,6 +346,10 @@ static const EVP_CIPHER* cn1AesCipher(const char* transformation, int keyLength) JAVA_OBJECT com_codename1_impl_linux_LinuxNative_aesCrypt___java_lang_String_boolean_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY( CODENAME_ONE_THREAD_STATE, JAVA_OBJECT transformation, JAVA_BOOLEAN encrypt, JAVA_OBJECT keyArray, JAVA_OBJECT ivArray, JAVA_OBJECT aadArray, JAVA_OBJECT dataArray) { + if (!cn1CryptoLoaded()) { + cn1CryptoFail("libcrypto.so.3 could not be loaded"); + return JAVA_NULL; + } const char* mode = transformation == JAVA_NULL ? "" : stringToUTF8(threadStateData, transformation); int keyLength = 0, ivLength = 0, aadLength = 0, dataLength = 0; const unsigned char* key = cn1Bytes(keyArray, &keyLength); @@ -364,6 +544,10 @@ static int cn1ApplyRsaPadding(EVP_PKEY_CTX* ctx, const char* transformation) { JAVA_OBJECT com_codename1_impl_linux_LinuxNative_rsaCrypt___java_lang_String_boolean_byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY( CODENAME_ONE_THREAD_STATE, JAVA_OBJECT transformation, JAVA_BOOLEAN encrypt, JAVA_OBJECT keyArray, JAVA_OBJECT dataArray) { + if (!cn1CryptoLoaded()) { + cn1CryptoFail("libcrypto.so.3 could not be loaded"); + return JAVA_NULL; + } const char* mode = transformation == JAVA_NULL ? "" : stringToUTF8(threadStateData, transformation); int keyLength = 0, dataLength = 0; const unsigned char* keyDer = cn1Bytes(keyArray, &keyLength); @@ -439,6 +623,10 @@ static const EVP_MD* cn1SignatureDigest(const char* algorithm) { JAVA_OBJECT com_codename1_impl_linux_LinuxNative_signData___java_lang_String_byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY( CODENAME_ONE_THREAD_STATE, JAVA_OBJECT algorithm, JAVA_OBJECT keyArray, JAVA_OBJECT dataArray) { + if (!cn1CryptoLoaded()) { + cn1CryptoFail("libcrypto.so.3 could not be loaded"); + return JAVA_NULL; + } const char* name = algorithm == JAVA_NULL ? "" : stringToUTF8(threadStateData, algorithm); int keyLength = 0, dataLength = 0; const unsigned char* keyDer = cn1Bytes(keyArray, &keyLength); @@ -497,6 +685,10 @@ JAVA_OBJECT com_codename1_impl_linux_LinuxNative_signData___java_lang_String_byt JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_verifyData___java_lang_String_byte_1ARRAY_byte_1ARRAY_byte_1ARRAY_R_boolean( CODENAME_ONE_THREAD_STATE, JAVA_OBJECT algorithm, JAVA_OBJECT keyArray, JAVA_OBJECT dataArray, JAVA_OBJECT signatureArray) { + if (!cn1CryptoLoaded()) { + cn1CryptoFail("libcrypto.so.3 could not be loaded"); + return JAVA_FALSE; + } const char* name = algorithm == JAVA_NULL ? "" : stringToUTF8(threadStateData, algorithm); int keyLength = 0, dataLength = 0, signatureLength = 0; const unsigned char* keyDer = cn1Bytes(keyArray, &keyLength); @@ -555,6 +747,10 @@ JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_verifyData___java_lang_String_ /* Same layout as generateRsaKeyPair below. Keygen straight from an EC context * with the curve set works on OpenSSL 1.1 and 3 alike. */ JAVA_OBJECT com_codename1_impl_linux_LinuxNative_generateEcKeyPair___R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE) { + if (!cn1CryptoLoaded()) { + cn1CryptoFail("libcrypto.so.3 could not be loaded"); + return JAVA_NULL; + } EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, 0); EVP_PKEY* key = 0; PKCS8_PRIV_KEY_INFO* info = 0; @@ -612,6 +808,10 @@ JAVA_OBJECT com_codename1_impl_linux_LinuxNative_generateEcKeyPair___R_byte_1ARR JAVA_OBJECT com_codename1_impl_linux_LinuxNative_ecdh___byte_1ARRAY_byte_1ARRAY_R_byte_1ARRAY( CODENAME_ONE_THREAD_STATE, JAVA_OBJECT privateArray, JAVA_OBJECT publicArray) { + if (!cn1CryptoLoaded()) { + cn1CryptoFail("libcrypto.so.3 could not be loaded"); + return JAVA_NULL; + } int privateLength = 0, publicLength = 0; const unsigned char* privateDer = cn1Bytes(privateArray, &privateLength); const unsigned char* publicDer = cn1Bytes(publicArray, &publicLength); @@ -655,6 +855,10 @@ JAVA_OBJECT com_codename1_impl_linux_LinuxNative_ecdh___byte_1ARRAY_byte_1ARRAY_ * the X.509 public key, then the PKCS#8 private key. A pair has to come from * a single call -- two calls would produce two unrelated keys. */ JAVA_OBJECT com_codename1_impl_linux_LinuxNative_generateRsaKeyPair___int_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_INT bits) { + if (!cn1CryptoLoaded()) { + cn1CryptoFail("libcrypto.so.3 could not be loaded"); + return JAVA_NULL; + } EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, 0); EVP_PKEY* key = 0; PKCS8_PRIV_KEY_INFO* info = 0; diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c new file mode 100644 index 00000000000..028ef7d43b0 --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_embedded.c @@ -0,0 +1,1406 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The embedded Linux display: the application owns the screen with no window + * system, no GTK and no X11 or Wayland. Compiled only when the generated + * project is configured with CN1_LINUX_EMBEDDED (LinuxNativeBuilder sets it + * for linux.embedded=true); the desktop target compiles this file empty and + * uses cn1_linux_window.c instead. The two define the same bridge natives. + * + * Rendering is the same Cairo/Pango code the desktop target uses -- every + * primitive in cn1_linux_graphics.c / cn1_linux_text.c / cn1_linux_image.c + * draws into the back buffer here -- so an application looks identical on + * both. Presenting a frame copies the damaged region of that buffer to the + * screen through one of these backends, picked at start-up (CN1_DISPLAY, or + * the first that works in this order): + * + * kms DRM/KMS dumb buffers, double buffered with a page flip on vblank. + * Works on every KMS driver (vc4/v3d on a Raspberry Pi, i.MX + * DCSS/LCDIF, Rockchip VOP, simpledrm, virtio-gpu) with no GPU + * driver at all, so it is the default. Needs DRM master, which a + * process started outside a desktop session has. + * fbdev /dev/fb0, for kernels and boards with no KMS driver. 32 and 16 bpp. + * memory an in-memory scanout of the same layout as kms. The frame is + * copied exactly as it would be to a screen and then discarded; + * for CI and benchmarks, where the copy must still be paid. + * headless no output; captureWindowToPngBytes and screenshots still work. + * + * Input is read straight from evdev (/dev/input/event*) on one thread, with + * hot-plug through inotify: multitouch (protocol B) and single-touch screens, + * mice (with a software cursor), keyboards (US layout) and rotary encoders, + * which arrive as up/down keys so focus-based HMI navigation works with no + * touch screen at all. Devices are grabbed (EVIOCGRAB) so keystrokes do not + * also reach the text console underneath. + * + * Environment (the builder bakes the board profile's defaults for the first + * three in as CN1_DEFAULT_DISPLAY_NAME, CN1_DEFAULT_ROTATION and CN1_DEFAULT_DPI): + * + * CN1_DISPLAY kms | fbdev | memory | headless (default: auto) + * CN1_DRM_DEVICE /dev/dri/cardN (default: scan) + * CN1_FB_DEVICE /dev/fbN (default: /dev/fb0) + * CN1_ROTATION 0 | 90 | 180 | 270, clockwise + * CN1_DPI override the density the panel reports + * CN1_SIZE WxH for memory / headless (default: 800x480) + * CN1_CURSOR 1 | 0 (default: with a mouse) + * CN1_TOUCH_SWAP_XY, CN1_TOUCH_INVERT_X, CN1_TOUCH_INVERT_Y touch calibration + * CN1_INPUT_NO_GRAB leave input devices shared with the console + * CN1_SNAPSHOT write one frame (CN1_SNAPSHOT_FRAME, default 60) as a PNG + */ + +#ifdef CN1_LINUX_EMBEDDED + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include "cn1_linux_gfx.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef CN1_EMBEDDED_HAS_DRM +#include +#include +#endif + +/* Board-profile defaults (LinuxNativeBuilder, EmbeddedBoardProfile). The display + * arrives as a bare token, CN1_DEFAULT_DISPLAY_NAME=kms, because a quoted string + * does not survive the trip through a CMake list. */ +#define CN1_EMB_STR2(x) #x +#define CN1_EMB_STR(x) CN1_EMB_STR2(x) +#ifdef CN1_DEFAULT_DISPLAY_NAME +#define CN1_DEFAULT_DISPLAY CN1_EMB_STR(CN1_DEFAULT_DISPLAY_NAME) +#else +#define CN1_DEFAULT_DISPLAY "auto" +#endif +#ifndef CN1_DEFAULT_ROTATION +#define CN1_DEFAULT_ROTATION 0 +#endif +#ifndef CN1_DEFAULT_DPI +#define CN1_DEFAULT_DPI 0 +#endif + +enum { OUT_HEADLESS = 0, OUT_MEMORY, OUT_FBDEV, OUT_KMS }; + +typedef struct { int x, y, w, h; } CN1EmbRect; + +/* ---------------------------------------------------------------- state */ + +static CN1Graphics cn1WindowG; /* the logical back buffer every paint goes to */ +static int cn1Output = OUT_HEADLESS; +static int cn1Rotation = 0; /* 0, 90, 180, 270 clockwise */ +static int cn1LogicalW = 800, cn1LogicalH = 480; /* what the application sees */ +static int cn1PhysW = 800, cn1PhysH = 480; /* the scanout */ +static int cn1DisplayUp = 0; /* initDisplay has sized the buffers */ +static int cn1Dpi = 0; +static int cn1Initialized = 0; +static int cn1HeadlessForced = 0; /* enableHeadlessScreenshot was called */ +static volatile int cn1Running = 1; +static volatile int cn1CurrentModifiers = 0; +static int cn1HasTouch = 0, cn1HasPointer = 0; + +/* The scanout: one or two buffers of cn1PhysW x cn1PhysH. Each remembers the + * region it is stale in, because with page flipping the buffer being drawn + * was last updated two frames ago. */ +typedef struct { + unsigned char* map; + size_t size; + int pitch; + int bpp; /* 32 or 16 */ + unsigned fbId; + unsigned handle; + CN1EmbRect stale; /* region still to be copied into this buffer */ + CN1EmbRect cursor; /* where the cursor was drawn into it, w == 0 if not */ +} CN1Scanout; + +static CN1Scanout cn1Buf[2]; +static int cn1BufCount = 0; +static int cn1Front = 0; +static pthread_mutex_t cn1PresentLock = PTHREAD_MUTEX_INITIALIZER; +static int cn1FbRedOffset = 16, cn1FbBlueOffset = 0; + +#ifdef CN1_EMBEDDED_HAS_DRM +static int cn1DrmFd = -1; +static unsigned cn1DrmCrtc, cn1DrmConnector; +static drmModeModeInfo cn1DrmMode; +static drmModeCrtcPtr cn1DrmSavedCrtc; +static int cn1FlipPending = 0; +#endif +static int cn1FbFd = -1; +static int cn1TtyFd = -1; +static int cn1TtySavedKbMode = -1; + +/* Software cursor, in logical coordinates. */ +static int cn1CursorEnabled = 0; +static volatile int cn1CursorX = 0, cn1CursorY = 0; +#define CN1_CURSOR_W 12 +#define CN1_CURSOR_H 19 + +/* Wakes pumpMessages when the input thread queued something. */ +static int cn1WakePipe[2] = { -1, -1 }; + +/* ---------------------------------------------------------- utilities */ + +static const char* cn1Env(const char* name, const char* def) { + const char* v = getenv(name); + return v != 0 && *v != 0 ? v : def; +} + +static int cn1EnvInt(const char* name, int def) { + const char* v = getenv(name); + return v != 0 && *v != 0 ? atoi(v) : def; +} + +static CN1EmbRect cn1RectUnion(CN1EmbRect a, CN1EmbRect b) { + CN1EmbRect r; + int x2, y2; + if (a.w <= 0 || a.h <= 0) return b; + if (b.w <= 0 || b.h <= 0) return a; + r.x = a.x < b.x ? a.x : b.x; + r.y = a.y < b.y ? a.y : b.y; + x2 = a.x + a.w > b.x + b.w ? a.x + a.w : b.x + b.w; + y2 = a.y + a.h > b.y + b.h ? a.y + a.h : b.y + b.h; + r.w = x2 - r.x; + r.h = y2 - r.y; + return r; +} + +static CN1EmbRect cn1RectClip(CN1EmbRect r, int w, int h) { + if (r.x < 0) { r.w += r.x; r.x = 0; } + if (r.y < 0) { r.h += r.y; r.y = 0; } + if (r.x + r.w > w) r.w = w - r.x; + if (r.y + r.h > h) r.h = h - r.y; + if (r.w < 0) r.w = 0; + if (r.h < 0) r.h = 0; + return r; +} + +static void cn1Wake(void) { + if (cn1WakePipe[1] >= 0) { + char c = 1; + ssize_t n = write(cn1WakePipe[1], &c, 1); + (void) n; + } +} + +/* Logical (application) -> physical (scanout) point for the current rotation. */ +static void cn1ToPhysical(int lx, int ly, int* px, int* py) { + switch (cn1Rotation) { + case 90: *px = cn1PhysW - 1 - ly; *py = lx; break; + case 180: *px = cn1PhysW - 1 - lx; *py = cn1PhysH - 1 - ly; break; + case 270: *px = ly; *py = cn1PhysH - 1 - lx; break; + default: *px = lx; *py = ly; break; + } +} + +/* Physical -> logical, the inverse, for input. */ +static void cn1ToLogical(int px, int py, int* lx, int* ly) { + switch (cn1Rotation) { + case 90: *lx = py; *ly = cn1PhysW - 1 - px; break; + case 180: *lx = cn1PhysW - 1 - px; *ly = cn1PhysH - 1 - py; break; + case 270: *lx = cn1PhysH - 1 - py; *ly = px; break; + default: *lx = px; *ly = py; break; + } +} + +/* ----------------------------------------------------------- back buffer */ + +static void cn1ResizeBackBuffer(int w, int h) { + if (cn1WindowG.cr) { + cairo_destroy(cn1WindowG.cr); + } + if (cn1WindowG.surface) { + cairo_surface_destroy(cn1WindowG.surface); + } + cn1LinuxFreeClipShape(&cn1WindowG); + memset(&cn1WindowG, 0, sizeof(cn1WindowG)); + cn1WindowG.surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + cn1WindowG.cr = cairo_create(cn1WindowG.surface); + cn1WindowG.width = w; + cn1WindowG.height = h; + cn1WindowG.alpha = 255; + cn1WindowG.clipIsRect = 1; + cn1WindowG.clipW = w; + cn1WindowG.clipH = h; + cn1WindowG.isWindowTarget = 1; + cairo_matrix_init_identity(&cn1WindowG.transform); + cairo_set_source_rgb(cn1WindowG.cr, 0, 0, 0); + cairo_paint(cn1WindowG.cr); +} + +CN1Graphics* cn1LinuxWindowGraphics(void) { + return &cn1WindowG; +} + +/* No main loop to marshal onto: every caller already runs where it may. */ +void cn1LinuxRunOnMainAndWait(void (*fn)(void*), void* arg) { + fn(arg); +} + +/* ------------------------------------------------------- console / VT */ + +/* Stops the text console from drawing over the application or receiving its + * keystrokes. Best effort: without a VT (a container, a serial console) there + * is nothing to hide. Restored at exit. */ +static void cn1ConsoleGraphics(void) { + int mode; + cn1TtyFd = open("/dev/tty0", O_RDWR | O_CLOEXEC); + if (cn1TtyFd < 0) { + cn1TtyFd = open("/dev/tty", O_RDWR | O_CLOEXEC); + } + if (cn1TtyFd < 0) { + return; + } + if (ioctl(cn1TtyFd, KDSETMODE, KD_GRAPHICS) < 0) { + close(cn1TtyFd); + cn1TtyFd = -1; + return; + } + if (ioctl(cn1TtyFd, KDGKBMODE, &mode) == 0) { + cn1TtySavedKbMode = mode; + ioctl(cn1TtyFd, KDSKBMODE, K_OFF); + } +} + +static void cn1ConsoleRestore(void) { + if (cn1TtyFd >= 0) { + if (cn1TtySavedKbMode >= 0) { + ioctl(cn1TtyFd, KDSKBMODE, cn1TtySavedKbMode); + } + ioctl(cn1TtyFd, KDSETMODE, KD_TEXT); + close(cn1TtyFd); + cn1TtyFd = -1; + } +} + +/* ------------------------------------------------------------- outputs */ + +#ifdef CN1_EMBEDDED_HAS_DRM +static int cn1DrmCreateBuffer(CN1Scanout* b, int w, int h) { + struct drm_mode_create_dumb create; + struct drm_mode_map_dumb map; + memset(&create, 0, sizeof(create)); + create.width = (unsigned) w; + create.height = (unsigned) h; + create.bpp = 32; + if (drmIoctl(cn1DrmFd, DRM_IOCTL_MODE_CREATE_DUMB, &create) < 0) { + return -1; + } + b->handle = create.handle; + b->pitch = (int) create.pitch; + b->size = (size_t) create.size; + b->bpp = 32; + b->fbId = 0; + /* The blit writes pitch * h bytes from the map; a driver answering with a + * smaller buffer is refused rather than written past. */ + if (create.pitch < (unsigned) w * 4u || b->size < (size_t) create.pitch * (size_t) h + || drmModeAddFB(cn1DrmFd, (unsigned) w, (unsigned) h, 24, 32, create.pitch, create.handle, &b->fbId) != 0) { + b->fbId = 0; + goto fail; + } + memset(&map, 0, sizeof(map)); + map.handle = create.handle; + if (drmIoctl(cn1DrmFd, DRM_IOCTL_MODE_MAP_DUMB, &map) < 0) { + goto fail; + } + b->map = (unsigned char*) mmap(0, b->size, PROT_READ | PROT_WRITE, MAP_SHARED, cn1DrmFd, (off_t) map.offset); + if (b->map == MAP_FAILED) { + b->map = 0; + goto fail; + } + memset(b->map, 0, b->size); + return 0; +fail: + /* Handed back, so a fallback or a retry does not pile up buffers. */ + if (b->fbId != 0) { + drmModeRmFB(cn1DrmFd, b->fbId); + b->fbId = 0; + } + { + struct drm_mode_destroy_dumb destroy; + memset(&destroy, 0, sizeof(destroy)); + destroy.handle = create.handle; + drmIoctl(cn1DrmFd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy); + } + return -1; +} + +/* Opens one card and picks its first connected connector, that connector's + * preferred mode and a CRTC that can drive it. */ +static int cn1DrmTryCard(const char* path) { + drmModeResPtr res; + drmModeConnectorPtr conn = 0; + drmModeEncoderPtr enc; + int i, j, fd; + unsigned crtc = 0; + fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + return -1; + } + res = drmModeGetResources(fd); + if (res == 0) { + close(fd); + return -1; + } + for (i = 0; i < res->count_connectors && conn == 0; i++) { + drmModeConnectorPtr c = drmModeGetConnector(fd, res->connectors[i]); + if (c != 0 && c->connection == DRM_MODE_CONNECTED && c->count_modes > 0) { + conn = c; + } else if (c != 0) { + drmModeFreeConnector(c); + } + } + if (conn == 0) { + drmModeFreeResources(res); + close(fd); + return -1; + } + cn1DrmMode = conn->modes[0]; + for (i = 0; i < conn->count_modes; i++) { + if (conn->modes[i].type & DRM_MODE_TYPE_PREFERRED) { + cn1DrmMode = conn->modes[i]; + break; + } + } + if (conn->encoder_id != 0) { + enc = drmModeGetEncoder(fd, conn->encoder_id); + if (enc != 0) { + crtc = enc->crtc_id; + drmModeFreeEncoder(enc); + } + } + for (i = 0; crtc == 0 && i < conn->count_encoders; i++) { + enc = drmModeGetEncoder(fd, conn->encoders[i]); + if (enc == 0) continue; + for (j = 0; j < res->count_crtcs; j++) { + if (enc->possible_crtcs & (1u << j)) { + crtc = res->crtcs[j]; + break; + } + } + drmModeFreeEncoder(enc); + } + if (crtc == 0) { + drmModeFreeConnector(conn); + drmModeFreeResources(res); + close(fd); + return -1; + } + cn1DrmFd = fd; + cn1DrmCrtc = crtc; + cn1DrmConnector = conn->connector_id; + cn1PhysW = cn1DrmMode.hdisplay; + cn1PhysH = cn1DrmMode.vdisplay; + if (cn1Dpi == 0 && conn->mmWidth > 0) { + cn1Dpi = (int) (cn1PhysW * 25.4 / conn->mmWidth + 0.5); + } + drmModeFreeConnector(conn); + drmModeFreeResources(res); + return 0; +} + +static int cn1InitKms(void) { + char path[64]; + const char* dev = getenv("CN1_DRM_DEVICE"); + int i; + if (dev != 0 && *dev) { + if (cn1DrmTryCard(dev) != 0) return -1; + } else { + for (i = 0; i < 8; i++) { + snprintf(path, sizeof(path), "/dev/dri/card%d", i); + if (cn1DrmTryCard(path) == 0) break; + } + if (cn1DrmFd < 0) return -1; + } + for (i = 0; i < 2; i++) { + if (cn1DrmCreateBuffer(&cn1Buf[i], cn1PhysW, cn1PhysH) != 0) { + close(cn1DrmFd); + cn1DrmFd = -1; + return -1; + } + } + cn1BufCount = 2; + cn1DrmSavedCrtc = drmModeGetCrtc(cn1DrmFd, cn1DrmCrtc); + if (drmModeSetCrtc(cn1DrmFd, cn1DrmCrtc, cn1Buf[0].fbId, 0, 0, &cn1DrmConnector, 1, &cn1DrmMode) != 0) { + /* Not DRM master: a compositor owns the screen. */ + close(cn1DrmFd); + cn1DrmFd = -1; + cn1BufCount = 0; + return -1; + } + cn1Front = 0; + return 0; +} + +static void cn1PageFlipDone(int fd, unsigned frame, unsigned sec, unsigned usec, void* data) { + (void) fd; (void) frame; (void) sec; (void) usec; (void) data; + cn1FlipPending = 0; +} + +static void cn1KmsWaitFlip(void) { + drmEventContext ev; + struct pollfd p; + memset(&ev, 0, sizeof(ev)); + ev.version = 2; + ev.page_flip_handler = cn1PageFlipDone; + while (cn1FlipPending) { + p.fd = cn1DrmFd; + p.events = POLLIN; + p.revents = 0; + if (poll(&p, 1, 100) <= 0) { + /* A flip that never completes (display unplugged) must not hang the EDT. */ + cn1FlipPending = 0; + break; + } + drmHandleEvent(cn1DrmFd, &ev); + } +} +#endif /* CN1_EMBEDDED_HAS_DRM */ + +static int cn1InitFbdev(void) { + struct fb_var_screeninfo var; + struct fb_fix_screeninfo fix; + const char* dev = cn1Env("CN1_FB_DEVICE", "/dev/fb0"); + cn1FbFd = open(dev, O_RDWR | O_CLOEXEC); + if (cn1FbFd < 0) { + return -1; + } + if (ioctl(cn1FbFd, FBIOGET_VSCREENINFO, &var) < 0 || ioctl(cn1FbFd, FBIOGET_FSCREENINFO, &fix) < 0 + || (var.bits_per_pixel != 32 && var.bits_per_pixel != 16) + /* The blit writes yres rows of xres pixels from the map's start; a + * driver whose memory or line length cannot hold that is refused. */ + || (size_t) var.xres * (var.bits_per_pixel / 8) > fix.line_length + || (size_t) var.yres * fix.line_length > fix.smem_len) { + close(cn1FbFd); + cn1FbFd = -1; + return -1; + } + cn1PhysW = (int) var.xres; + cn1PhysH = (int) var.yres; + cn1FbRedOffset = (int) var.red.offset; + cn1FbBlueOffset = (int) var.blue.offset; + cn1Buf[0].pitch = (int) fix.line_length; + cn1Buf[0].bpp = (int) var.bits_per_pixel; + cn1Buf[0].size = (size_t) fix.smem_len; + cn1Buf[0].map = (unsigned char*) mmap(0, cn1Buf[0].size, PROT_READ | PROT_WRITE, MAP_SHARED, cn1FbFd, 0); + if (cn1Buf[0].map == MAP_FAILED) { + cn1Buf[0].map = 0; + close(cn1FbFd); + cn1FbFd = -1; + return -1; + } + if (cn1Dpi == 0 && var.width > 0 && var.width < 10000) { + cn1Dpi = (int) (cn1PhysW * 25.4 / var.width + 0.5); + } + cn1BufCount = 1; + return 0; +} + +static int cn1InitMemory(void) { + cn1Buf[0].pitch = cn1PhysW * 4; + cn1Buf[0].bpp = 32; + cn1Buf[0].size = (size_t) cn1Buf[0].pitch * cn1PhysH; + cn1Buf[0].map = (unsigned char*) calloc(1, cn1Buf[0].size); + if (cn1Buf[0].map == 0) { + return -1; + } + cn1BufCount = 1; + return 0; +} + +/* --------------------------------------------------------------- blit */ + +static inline uint32_t cn1ToTarget32(uint32_t p) { + if (cn1FbRedOffset == 16) { + return p | 0xff000000u; + } + /* BGR panel: swap red and blue. */ + return 0xff000000u | ((p & 0xffu) << 16) | (p & 0xff00u) | ((p >> 16) & 0xffu); +} + +static inline uint16_t cn1ToRgb565(uint32_t p) { + return (uint16_t) (((p >> 8) & 0xf800u) | ((p >> 5) & 0x07e0u) | ((p >> 3) & 0x001fu)); +} + +/* Copies a logical rectangle of the back buffer into one scanout buffer, + * applying rotation and pixel format. */ +static void cn1Blit(CN1Scanout* b, CN1EmbRect r) { + const unsigned char* src; + int srcStride, x, y; + if (b->map == 0 || r.w <= 0 || r.h <= 0) { + return; + } + src = cairo_image_surface_get_data(cn1WindowG.surface); + srcStride = cairo_image_surface_get_stride(cn1WindowG.surface); + if (cn1Rotation == 0 && b->bpp == 32 && cn1FbRedOffset == 16) { + for (y = 0; y < r.h; y++) { + memcpy(b->map + (size_t) (r.y + y) * b->pitch + (size_t) r.x * 4, + src + (size_t) (r.y + y) * srcStride + (size_t) r.x * 4, (size_t) r.w * 4); + } + return; + } + for (y = r.y; y < r.y + r.h; y++) { + const uint32_t* row = (const uint32_t*) (src + (size_t) y * srcStride); + for (x = r.x; x < r.x + r.w; x++) { + int px, py; + cn1ToPhysical(x, y, &px, &py); + if (b->bpp == 32) { + ((uint32_t*) (b->map + (size_t) py * b->pitch))[px] = cn1ToTarget32(row[x]); + } else { + ((uint16_t*) (b->map + (size_t) py * b->pitch))[px] = cn1ToRgb565(row[x]); + } + } + } +} + +/* A 12x19 arrow: 1 = black outline, 2 = white fill. */ +static const char* const cn1CursorShape[CN1_CURSOR_H] = { + "1 ", "11 ", "121 ", "1221 ", "12221 ", + "122221 ", "1222221 ", "12222221 ", "122222221 ", "1222222221 ", + "12222222221 ", "122222111111", "1221221 ", "121 1221 ", "11 1221 ", + "1 1221 ", " 1221 ", " 1221 ", " 11 " }; + +static void cn1DrawCursor(CN1Scanout* b, int cx, int cy) { + int x, y; + for (y = 0; y < CN1_CURSOR_H; y++) { + for (x = 0; x < CN1_CURSOR_W; x++) { + char c = cn1CursorShape[y][x]; + int lx = cx + x, ly = cy + y, px, py; + uint32_t color; + if (c == ' ' || lx < 0 || ly < 0 || lx >= cn1LogicalW || ly >= cn1LogicalH) continue; + color = c == '1' ? 0xff000000u : 0xffffffffu; + cn1ToPhysical(lx, ly, &px, &py); + if (b->bpp == 32) { + ((uint32_t*) (b->map + (size_t) py * b->pitch))[px] = color; + } else { + ((uint16_t*) (b->map + (size_t) py * b->pitch))[px] = cn1ToRgb565(color); + } + } + } + b->cursor.x = cx; + b->cursor.y = cy; + b->cursor.w = CN1_CURSOR_W; + b->cursor.h = CN1_CURSOR_H; +} + +/* Brings the screen up to date with `damage` (logical coordinates). Called on + * the EDT after a frame, and from the input thread when only the cursor moved. */ +static void cn1Present(CN1EmbRect damage) { + CN1Scanout* b; + CN1EmbRect copy; + int i, target; + if (cn1BufCount == 0 || cn1WindowG.surface == 0) { + return; + } + damage = cn1RectClip(damage, cn1LogicalW, cn1LogicalH); + pthread_mutex_lock(&cn1PresentLock); + cairo_surface_flush(cn1WindowG.surface); + for (i = 0; i < cn1BufCount; i++) { + cn1Buf[i].stale = cn1RectUnion(cn1Buf[i].stale, damage); + } +#ifdef CN1_EMBEDDED_HAS_DRM + if (cn1Output == OUT_KMS) { + cn1KmsWaitFlip(); + } +#endif + target = cn1BufCount == 2 ? 1 - cn1Front : 0; + b = &cn1Buf[target]; + copy = b->stale; + if (b->cursor.w > 0) { + copy = cn1RectUnion(copy, b->cursor); + b->cursor.w = 0; + } + copy = cn1RectClip(copy, cn1LogicalW, cn1LogicalH); + cn1Blit(b, copy); + b->stale.w = 0; + b->stale.h = 0; + if (cn1CursorEnabled) { + cn1DrawCursor(b, cn1CursorX, cn1CursorY); + } +#ifdef CN1_EMBEDDED_HAS_DRM + if (cn1Output == OUT_KMS) { + if (drmModePageFlip(cn1DrmFd, cn1DrmCrtc, b->fbId, DRM_MODE_PAGE_FLIP_EVENT, 0) == 0) { + cn1FlipPending = 1; + cn1Front = target; + } else { + /* Flip refused (no vblank support): show it directly. */ + drmModeSetCrtc(cn1DrmFd, cn1DrmCrtc, b->fbId, 0, 0, &cn1DrmConnector, 1, &cn1DrmMode); + cn1Front = target; + } + } +#endif + pthread_mutex_unlock(&cn1PresentLock); +} + +static void cn1RestoreScreen(void) { +#ifdef CN1_EMBEDDED_HAS_DRM + if (cn1DrmFd >= 0) { + cn1KmsWaitFlip(); + if (cn1DrmSavedCrtc != 0) { + drmModeSetCrtc(cn1DrmFd, cn1DrmSavedCrtc->crtc_id, cn1DrmSavedCrtc->buffer_id, + cn1DrmSavedCrtc->x, cn1DrmSavedCrtc->y, &cn1DrmConnector, 1, &cn1DrmSavedCrtc->mode); + } + } +#endif + cn1ConsoleRestore(); +} + +/* ---------------------------------------------------------------- input */ + +#define CN1_MAX_INPUT 32 + +typedef struct { + int fd; + char name[64]; + int touch, pointer, keyboard, rotary; + int absMinX, absMaxX, absMinY, absMaxY; + int mt; /* multitouch protocol B */ + int slot; + int slotId[10]; + int slotX[10], slotY[10]; + int stX, stY, stDown; /* single-touch state */ + int down; /* primary contact currently pressed */ + int lastX, lastY; + double pinchDist; + int pinching; +} CN1InputDev; + +static CN1InputDev cn1Inputs[CN1_MAX_INPUT]; +static int cn1InputCount = 0; +static int cn1InotifyFd = -1; +static int cn1PointerButtons = 0; + +#define CN1_BIT(arr, b) (((arr)[(b) / (8 * sizeof(long))] >> ((b) % (8 * sizeof(long)))) & 1) +#define CN1_LONGS(n) (((n) + 8 * sizeof(long) - 1) / (8 * sizeof(long))) + +static void cn1OpenInput(const char* path) { + unsigned long evBits[CN1_LONGS(EV_MAX + 1)]; + unsigned long keyBits[CN1_LONGS(KEY_MAX + 1)]; + unsigned long absBits[CN1_LONGS(ABS_MAX + 1)]; + unsigned long relBits[CN1_LONGS(REL_MAX + 1)]; + CN1InputDev* d; + int fd, i; + struct input_absinfo ax, ay; + if (cn1InputCount >= CN1_MAX_INPUT) { + return; + } + for (i = 0; i < cn1InputCount; i++) { + /* Already open (inotify can report a node twice). */ + char link[64]; + snprintf(link, sizeof(link), "%s", path); + if (strcmp(cn1Inputs[i].name, link) == 0) return; + } + fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) { + return; + } + memset(evBits, 0, sizeof(evBits)); + memset(keyBits, 0, sizeof(keyBits)); + memset(absBits, 0, sizeof(absBits)); + memset(relBits, 0, sizeof(relBits)); + ioctl(fd, EVIOCGBIT(0, sizeof(evBits)), evBits); + ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBits)), keyBits); + ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBits)), absBits); + ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBits)), relBits); + d = &cn1Inputs[cn1InputCount]; + memset(d, 0, sizeof(*d)); + d->fd = fd; + snprintf(d->name, sizeof(d->name), "%s", path); + for (i = 0; i < 10; i++) d->slotId[i] = -1; + if (CN1_BIT(evBits, EV_ABS) && CN1_BIT(absBits, ABS_MT_POSITION_X)) { + d->touch = 1; + d->mt = 1; + ioctl(fd, EVIOCGABS(ABS_MT_POSITION_X), &ax); + ioctl(fd, EVIOCGABS(ABS_MT_POSITION_Y), &ay); + } else if (CN1_BIT(evBits, EV_ABS) && CN1_BIT(absBits, ABS_X) && CN1_BIT(keyBits, BTN_TOUCH)) { + d->touch = 1; + ioctl(fd, EVIOCGABS(ABS_X), &ax); + ioctl(fd, EVIOCGABS(ABS_Y), &ay); + } + if (d->touch) { + d->absMinX = ax.minimum; d->absMaxX = ax.maximum > ax.minimum ? ax.maximum : ax.minimum + 1; + d->absMinY = ay.minimum; d->absMaxY = ay.maximum > ay.minimum ? ay.maximum : ay.minimum + 1; + } + if (CN1_BIT(evBits, EV_REL) && CN1_BIT(relBits, REL_X) && CN1_BIT(keyBits, BTN_LEFT)) { + d->pointer = 1; + } else if (CN1_BIT(evBits, EV_REL) && (CN1_BIT(relBits, REL_DIAL) || CN1_BIT(relBits, REL_WHEEL) || CN1_BIT(relBits, REL_X))) { + d->rotary = 1; + } + if (CN1_BIT(evBits, EV_KEY) && (CN1_BIT(keyBits, KEY_A) || CN1_BIT(keyBits, KEY_ENTER) || CN1_BIT(keyBits, KEY_UP))) { + d->keyboard = 1; + } + if (!d->touch && !d->pointer && !d->keyboard && !d->rotary) { + close(fd); + return; + } + if (getenv("CN1_INPUT_NO_GRAB") == 0) { + ioctl(fd, EVIOCGRAB, 1); + } + cn1HasTouch |= d->touch; + cn1HasPointer |= d->pointer; + cn1InputCount++; +} + +static void cn1ScanInputs(void) { + DIR* dir = opendir("/dev/input"); + struct dirent* e; + char path[300]; + if (dir == 0) { + return; + } + while ((e = readdir(dir)) != 0) { + if (strncmp(e->d_name, "event", 5) == 0) { + snprintf(path, sizeof(path), "/dev/input/%s", e->d_name); + cn1OpenInput(path); + } + } + closedir(dir); +} + +static void cn1CloseInput(int i) { + close(cn1Inputs[i].fd); + cn1Inputs[i] = cn1Inputs[cn1InputCount - 1]; + cn1InputCount--; +} + +/* Touch panel coordinates -> logical, through calibration and rotation. */ +static void cn1TouchToLogical(CN1InputDev* d, int rx, int ry, int* lx, int* ly) { + long px, py, t; + if (getenv("CN1_TOUCH_SWAP_XY") != 0) { + t = rx; rx = ry; ry = (int) t; + } + px = (long) (rx - d->absMinX) * (cn1PhysW - 1) / (d->absMaxX - d->absMinX); + py = (long) (ry - d->absMinY) * (cn1PhysH - 1) / (d->absMaxY - d->absMinY); + if (getenv("CN1_TOUCH_INVERT_X") != 0) px = cn1PhysW - 1 - px; + if (getenv("CN1_TOUCH_INVERT_Y") != 0) py = cn1PhysH - 1 - py; + cn1ToLogical((int) px, (int) py, lx, ly); +} + +/* US layout. Printable keys push their character (shifted when Shift is held); + * the rest push the X keysym the desktop port uses, which LinuxImplementation + * already maps (arrows to game keys and so on). */ +static int cn1KeyCode(int code, int shift) { + static const char plain[] = "\0\0" "1234567890-=\0\0" "qwertyuiop[]\0\0" "asdfghjkl;'`\0\\" "zxcvbnm,./"; + static const char shifted[] = "\0\0" "!@#$%^&*()_+\0\0" "QWERTYUIOP{}\0\0" "ASDFGHJKL:\"~\0|" "ZXCVBNM<>?"; + switch (code) { + case KEY_BACKSPACE: return 8; + case KEY_TAB: return 9; + case KEY_ENTER: case KEY_KPENTER: case KEY_OK: case KEY_SELECT: return 13; + case KEY_ESC: case KEY_BACK: return 27; + case KEY_SPACE: return ' '; + case KEY_DELETE: return 127; + case KEY_HOME: return 0xff50; + case KEY_LEFT: return 0xff51; + case KEY_UP: return 0xff52; + case KEY_RIGHT: return 0xff53; + case KEY_DOWN: return 0xff54; + case KEY_PAGEUP: return 0xff55; + case KEY_PAGEDOWN: return 0xff56; + case KEY_END: return 0xff57; + case KEY_INSERT: return 0xff63; + case KEY_MENU: return 0xff67; + default: break; + } + if (code >= KEY_F1 && code <= KEY_F10) return 0xffbe + (code - KEY_F1); + if (code == KEY_F11) return 0xffc8; + if (code == KEY_F12) return 0xffc9; + if (code >= KEY_KP7 && code <= KEY_KPDOT) { + static const char kp[] = "789-456+1230."; + return kp[code - KEY_KP7]; + } + if (code > 0 && code < (int) sizeof(plain) - 1) { + char c = shift ? shifted[code] : plain[code]; + return c; + } + return 0; +} + +static int cn1Shift = 0, cn1Ctrl = 0, cn1Alt = 0; + +static void cn1HandleKey(int code, int value) { + int k; + switch (code) { + case KEY_LEFTSHIFT: case KEY_RIGHTSHIFT: cn1Shift = value != 0; break; + case KEY_LEFTCTRL: case KEY_RIGHTCTRL: cn1Ctrl = value != 0; break; + case KEY_LEFTALT: case KEY_RIGHTALT: cn1Alt = value != 0; break; + default: break; + } + cn1CurrentModifiers = (cn1Shift ? 1 : 0) | (cn1Ctrl ? 2 : 0) | (cn1Alt ? 4 : 0); + k = cn1KeyCode(code, cn1Shift); + if (k == 0) { + return; + } + /* value 2 is autorepeat: another press, as a desktop keyboard delivers it. */ + cn1LinuxPushEvent(value ? CN1_EVENT_KEY_PRESSED : CN1_EVENT_KEY_RELEASED, 0, 0, k); +} + +static void cn1MoveCursor(void) { + if (cn1CursorEnabled) { + CN1EmbRect none = { 0, 0, 0, 0 }; + cn1Present(none); + } +} + +static int cn1PointerMask(int code) { + switch (code) { + case BTN_LEFT: return CN1_PE_MASK_PRIMARY; + case BTN_RIGHT: return CN1_PE_MASK_SECONDARY; + case BTN_MIDDLE: return CN1_PE_MASK_MIDDLE; + case BTN_SIDE: return CN1_PE_MASK_BACK; + case BTN_EXTRA: return CN1_PE_MASK_FORWARD; + default: return 0; + } +} + +static void cn1TouchFrame(CN1InputDev* d) { + int active = 0, first = -1, second = -1, i, lx, ly; + const int flags = CN1_PE_MASK_PRIMARY | CN1_PE_TOUCH_FLAG; + if (d->mt) { + for (i = 0; i < 10; i++) { + if (d->slotId[i] >= 0) { + if (first < 0) first = i; else if (second < 0) second = i; + active++; + } + } + } else { + active = d->stDown ? 1 : 0; + } + if (active >= 2 && d->mt) { + int ax, ay, bx, by; + double dx, dy, dist; + cn1TouchToLogical(d, d->slotX[first], d->slotY[first], &ax, &ay); + cn1TouchToLogical(d, d->slotX[second], d->slotY[second], &bx, &by); + dx = bx - ax; + dy = by - ay; + dist = dx * dx + dy * dy; + if (!d->pinching) { + if (d->down) { + cn1LinuxPushEvent(CN1_EVENT_POINTER_RELEASED, d->lastX, d->lastY, flags); + d->down = 0; + } + d->pinching = 1; + d->pinchDist = dist; + cn1LinuxPushEvent(CN1_EVENT_PINCH_BEGIN, (ax + bx) / 2, (ay + by) / 2, 0); + } else if (d->pinchDist > 0 && dist > 0) { + /* Incremental scale, the convention CN1_EVENT_PINCH carries. */ + double ratio = 1.0; + double r2 = dist / d->pinchDist; + int it; + for (it = 0; it < 20; it++) ratio = 0.5 * (ratio + r2 / ratio); /* sqrt */ + cn1LinuxPushEvent(CN1_EVENT_PINCH, (ax + bx) / 2, (ay + by) / 2, (int) (ratio * CN1_GESTURE_FIXED)); + d->pinchDist = dist; + } + return; + } + if (d->pinching) { + d->pinching = 0; + cn1LinuxPushEvent(CN1_EVENT_PINCH_END, d->lastX, d->lastY, 0); + if (active == 0) { + return; + } + } + if (active == 0) { + if (d->down) { + cn1LinuxPushEvent(CN1_EVENT_POINTER_RELEASED, d->lastX, d->lastY, flags); + d->down = 0; + } + return; + } + if (d->mt) { + cn1TouchToLogical(d, d->slotX[first], d->slotY[first], &lx, &ly); + } else { + cn1TouchToLogical(d, d->stX, d->stY, &lx, &ly); + } + if (!d->down) { + d->down = 1; + cn1LinuxPushEvent(CN1_EVENT_POINTER_PRESSED, lx, ly, flags); + } else if (lx != d->lastX || ly != d->lastY) { + cn1LinuxPushEvent(CN1_EVENT_POINTER_DRAGGED, lx, ly, flags); + } + d->lastX = lx; + d->lastY = ly; +} + +static void cn1HandleInput(CN1InputDev* d, const struct input_event* ev, int* relX, int* relY) { + switch (ev->type) { + case EV_ABS: + if (d->mt) { + switch (ev->code) { + case ABS_MT_SLOT: d->slot = ev->value >= 0 && ev->value < 10 ? ev->value : 9; break; + case ABS_MT_TRACKING_ID: d->slotId[d->slot] = ev->value; break; + case ABS_MT_POSITION_X: d->slotX[d->slot] = ev->value; break; + case ABS_MT_POSITION_Y: d->slotY[d->slot] = ev->value; break; + default: break; + } + } else if (ev->code == ABS_X) { + d->stX = ev->value; + } else if (ev->code == ABS_Y) { + d->stY = ev->value; + } + break; + case EV_REL: + if (d->pointer && ev->code == REL_X) { + *relX += ev->value; + } else if (d->pointer && ev->code == REL_Y) { + *relY += ev->value; + } else if (d->pointer && ev->code == REL_WHEEL) { + cn1LinuxPushEvent(CN1_EVENT_MOUSE_WHEEL, cn1CursorX, cn1CursorY, ev->value * 120); + } else if (d->pointer && ev->code == REL_HWHEEL) { + cn1LinuxPushEvent(CN1_EVENT_MOUSE_HWHEEL, cn1CursorX, cn1CursorY, ev->value * 120); + } else if (d->rotary && ev->value != 0) { + /* A rotary encoder steps focus: clockwise is down. */ + int k = ev->value > 0 ? 0xff54 : 0xff52; + int n = ev->value > 0 ? ev->value : -ev->value; + while (n-- > 0) { + cn1LinuxPushEvent(CN1_EVENT_KEY_PRESSED, 0, 0, k); + cn1LinuxPushEvent(CN1_EVENT_KEY_RELEASED, 0, 0, k); + } + } + break; + case EV_KEY: + if (d->touch && ev->code == BTN_TOUCH) { + if (!d->mt) d->stDown = ev->value != 0; + } else if (d->pointer && cn1PointerMask(ev->code) != 0) { + int mask = cn1PointerMask(ev->code); + if (ev->value) { + cn1PointerButtons |= mask; + cn1LinuxPushEvent(CN1_EVENT_POINTER_PRESSED, cn1CursorX, cn1CursorY, mask); + } else { + cn1PointerButtons &= ~mask; + cn1LinuxPushEvent(CN1_EVENT_POINTER_RELEASED, cn1CursorX, cn1CursorY, mask); + } + } else if (ev->code < BTN_MISC || ev->code >= KEY_OK) { + cn1HandleKey(ev->code, ev->value); + } + break; + case EV_SYN: + if (ev->code == SYN_REPORT) { + if (d->touch) { + cn1TouchFrame(d); + } + if (d->pointer && (*relX != 0 || *relY != 0)) { + int dx = *relX, dy = *relY, x, y; + /* Relative motion is in the panel's orientation. */ + switch (cn1Rotation) { + case 90: x = dy; y = -dx; break; + case 180: x = -dx; y = -dy; break; + case 270: x = -dy; y = dx; break; + default: x = dx; y = dy; break; + } + x += cn1CursorX; + y += cn1CursorY; + cn1CursorX = x < 0 ? 0 : (x >= cn1LogicalW ? cn1LogicalW - 1 : x); + cn1CursorY = y < 0 ? 0 : (y >= cn1LogicalH ? cn1LogicalH - 1 : y); + cn1LinuxPushEvent(cn1PointerButtons ? CN1_EVENT_POINTER_DRAGGED : CN1_EVENT_POINTER_HOVER, + cn1CursorX, cn1CursorY, cn1PointerButtons); + *relX = 0; + *relY = 0; + cn1MoveCursor(); + } + } + break; + default: + break; + } +} + +static volatile sig_atomic_t cn1TermRequested = 0; + +static void cn1OnTerm(int sig) { + (void) sig; + if (cn1TermRequested) { + /* Second request: the application did not exit. */ + _exit(128 + SIGTERM); + } + cn1TermRequested = 1; +} + +static void* cn1InputThread(void* arg) { + struct pollfd fds[CN1_MAX_INPUT + 1]; + struct input_event evs[64]; + int relX[CN1_MAX_INPUT], relY[CN1_MAX_INPUT]; + (void) arg; + memset(relX, 0, sizeof(relX)); + memset(relY, 0, sizeof(relY)); + while (cn1Running) { + int n = 0, i, queued = 0; + for (i = 0; i < cn1InputCount; i++) { + fds[n].fd = cn1Inputs[i].fd; + fds[n].events = POLLIN; + fds[n].revents = 0; + n++; + } + fds[n].fd = cn1InotifyFd; + fds[n].events = POLLIN; + fds[n].revents = 0; + if (poll(fds, (nfds_t) (cn1InotifyFd >= 0 ? n + 1 : n), 200) < 0 && errno != EINTR) { + usleep(100000); + } + if (cn1TermRequested == 1) { + cn1TermRequested = 2; + cn1LinuxPushEvent(CN1_EVENT_CLOSE, 0, 0, 0); + queued = 1; + } + for (i = n - 1; i >= 0; i--) { + ssize_t r; + if (!(fds[i].revents & (POLLIN | POLLERR | POLLHUP))) { + continue; + } + r = read(cn1Inputs[i].fd, evs, sizeof(evs)); + if (r < 0 && (errno == EAGAIN || errno == EINTR)) { + continue; + } + if (r <= 0) { + /* Unplugged. */ + cn1CloseInput(i); + relX[i] = relX[cn1InputCount]; + relY[i] = relY[cn1InputCount]; + continue; + } + { + int k, count = (int) (r / (ssize_t) sizeof(struct input_event)); + for (k = 0; k < count; k++) { + cn1HandleInput(&cn1Inputs[i], &evs[k], &relX[i], &relY[i]); + } + } + queued = 1; + } + if (cn1InotifyFd >= 0 && (fds[n].revents & POLLIN)) { + char buf[4096]; + ssize_t r = read(cn1InotifyFd, buf, sizeof(buf)); + (void) r; + /* udev may not have applied permissions yet; give it a moment. */ + usleep(100000); + cn1ScanInputs(); + } + if (queued) { + cn1Wake(); + } + } + return 0; +} + +static void cn1StartInput(void) { + pthread_t t; + pthread_attr_t attr; + cn1ScanInputs(); + cn1InotifyFd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (cn1InotifyFd >= 0) { + inotify_add_watch(cn1InotifyFd, "/dev/input", IN_CREATE | IN_ATTRIB); + } + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_create(&t, &attr, cn1InputThread, 0); + pthread_attr_destroy(&attr); +} + +/* ------------------------------------------------------------- natives */ + +static void cn1AtExit(void) { + cn1Running = 0; + cn1RestoreScreen(); +} + +/* CN1_SNAPSHOT=file.png writes frame CN1_SNAPSHOT_FRAME (default 60) as a PNG, + * for CI and for checking a board's output remotely. Taken at a flush, so the + * frame is always complete -- the back buffer at an arbitrary moment can be + * half way through a paint. */ +static void cn1MaybeSnapshot(void) { + static int frame = 0; + const char* path = getenv("CN1_SNAPSHOT"); + if (path == 0 || *path == 0) { + return; + } + if (++frame == cn1EnvInt("CN1_SNAPSHOT_FRAME", 60)) { + cairo_surface_flush(cn1WindowG.surface); + cairo_surface_write_to_png(cn1WindowG.surface, path); + } +} + +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_isEmbeddedDisplay___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_TRUE; +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_initDisplay___java_lang_String_int_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT title, JAVA_INT width, JAVA_INT height) { + const char* want; + struct sigaction sa; + char sizeSpec[32]; + (void) title; + if (cn1Initialized) { + return; + } + cn1Initialized = 1; + cn1LinuxInstallFaultHandlers(); + if (pipe(cn1WakePipe) == 0) { + fcntl(cn1WakePipe[0], F_SETFL, O_NONBLOCK); + fcntl(cn1WakePipe[1], F_SETFL, O_NONBLOCK); + } + cn1Rotation = cn1EnvInt("CN1_ROTATION", CN1_DEFAULT_ROTATION); + if (cn1Rotation != 90 && cn1Rotation != 180 && cn1Rotation != 270) { + cn1Rotation = 0; + } + cn1Dpi = cn1EnvInt("CN1_DPI", CN1_DEFAULT_DPI); + snprintf(sizeSpec, sizeof(sizeSpec), "%s", cn1Env("CN1_SIZE", "")); + if (sizeSpec[0] && strchr(sizeSpec, 'x')) { + cn1PhysW = atoi(sizeSpec); + cn1PhysH = atoi(strchr(sizeSpec, 'x') + 1); + } else if (width > 0 && height > 0) { + cn1PhysW = width; + cn1PhysH = height; + } + cn1DisplayUp = 1; + want = cn1HeadlessForced ? "headless" : cn1Env("CN1_DISPLAY", CN1_DEFAULT_DISPLAY); + cn1Output = OUT_HEADLESS; +#ifdef CN1_EMBEDDED_HAS_DRM + if ((strcmp(want, "auto") == 0 || strcmp(want, "kms") == 0) && cn1InitKms() == 0) { + cn1Output = OUT_KMS; + } +#endif + if (cn1Output == OUT_HEADLESS && (strcmp(want, "auto") == 0 || strcmp(want, "fbdev") == 0) && cn1InitFbdev() == 0) { + cn1Output = OUT_FBDEV; + } + if (cn1Output == OUT_HEADLESS && strcmp(want, "memory") == 0 && cn1InitMemory() == 0) { + cn1Output = OUT_MEMORY; + } + if (cn1Output == OUT_HEADLESS && strcmp(want, "headless") != 0 && strcmp(want, "auto") != 0 + && strcmp(want, "memory") != 0) { + fprintf(stderr, "CN1: display '%s' unavailable, running headless\n", want); + } + if (cn1Rotation == 90 || cn1Rotation == 270) { + cn1LogicalW = cn1PhysH; + cn1LogicalH = cn1PhysW; + } else { + cn1LogicalW = cn1PhysW; + cn1LogicalH = cn1PhysH; + } + if (cn1Dpi <= 0) { + cn1Dpi = 96; + } + cn1ResizeBackBuffer(cn1LogicalW, cn1LogicalH); + atexit(cn1AtExit); + { + static const char* names[] = { "headless", "memory", "fbdev", "kms" }; + fprintf(stderr, "CN1: embedded display %s %dx%d rotation %d dpi %d\n", + names[cn1Output], cn1LogicalW, cn1LogicalH, cn1Rotation, cn1Dpi); + } + if (cn1Output == OUT_KMS || cn1Output == OUT_FBDEV) { + cn1ConsoleGraphics(); + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = cn1OnTerm; + sigemptyset(&sa.sa_mask); + sigaction(SIGTERM, &sa, 0); + sigaction(SIGINT, &sa, 0); + } + if (cn1Output != OUT_HEADLESS && cn1Output != OUT_MEMORY) { + cn1StartInput(); + cn1CursorX = cn1LogicalW / 2; + cn1CursorY = cn1LogicalH / 2; + cn1CursorEnabled = cn1EnvInt("CN1_CURSOR", cn1HasPointer && !cn1HasTouch ? 1 : 0); + } + { + CN1EmbRect all = { 0, 0, cn1LogicalW, cn1LogicalH }; + cn1Present(all); + } +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_mainWindowSetTitle___java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT title) { + (void) title; +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_currentModifiers___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1CurrentModifiers; +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_menuSetCommands___java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT spec) { + (void) spec; +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_getDisplayWidth___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1LogicalW; +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_getDisplayHeight___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1LogicalH; +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_screenDpi___R_int(CODENAME_ONE_THREAD_STATE) { + return cn1Dpi > 0 ? cn1Dpi : 96; +} + +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_isHighContrastEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_FALSE; +} + +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_isReduceMotionEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_FALSE; +} + +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_isScreenReaderEnabled___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_FALSE; +} + +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_isTouchDevice___R_boolean(CODENAME_ONE_THREAD_STATE) { + return cn1HasTouch ? JAVA_TRUE : JAVA_FALSE; +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_getWindowGraphics___R_long(CODENAME_ONE_THREAD_STATE) { + return (JAVA_LONG) (intptr_t) &cn1WindowG; +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_flushGraphics___long_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG graphics, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height) { + CN1EmbRect r; + (void) graphics; + r.x = x; + r.y = y; + r.w = width; + r.h = height; + /* Parked for the copy and the vblank wait so a collection does not stall on + * this thread; the back buffer is not touched by Java until we return. */ + CN1_YIELD_THREAD; + cn1Present(r); + cn1MaybeSnapshot(); + CN1_RESUME_THREAD; +} + +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_pollEvent___int_1ARRAY_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + int scratch[5]; + if (out == JAVA_NULL) { + return JAVA_FALSE; + } + if (cn1LinuxPopEvent(scratch)) { + JAVA_INT* arr = (JAVA_INT*) CN1_ARRAY_DATA(out); + int len = (int) (*(JAVA_ARRAY) out).length; + if (len >= 4) { + arr[0] = scratch[0]; + arr[1] = scratch[1]; + arr[2] = scratch[2]; + arr[3] = scratch[3]; + if (len >= 5) { + arr[4] = scratch[4]; + } + return JAVA_TRUE; + } + } + return JAVA_FALSE; +} + +/* No assistive technology runs on an appliance screen; the semantic tree the + * desktop mirrors into AT-SPI has nowhere to go. */ +JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityBegin__(CODENAME_ONE_THREAD_STATE) { +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityNode___long_long_java_lang_String_java_lang_String_java_lang_String_java_lang_String_int_int_int_int_int( + CODENAME_ONE_THREAD_STATE, JAVA_LONG id, JAVA_LONG parentId, JAVA_OBJECT role, JAVA_OBJECT label, + JAVA_OBJECT description, JAVA_OBJECT value, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height, JAVA_INT flags) { + (void) id; (void) parentId; (void) role; (void) label; (void) description; (void) value; + (void) x; (void) y; (void) width; (void) height; (void) flags; +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityAction___long_java_lang_String_int_java_lang_String( + CODENAME_ONE_THREAD_STATE, JAVA_LONG nodeId, JAVA_OBJECT actionId, JAVA_INT actionHash, JAVA_OBJECT label) { + (void) nodeId; (void) actionId; (void) actionHash; (void) label; +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_accessibilityEnd___int(CODENAME_ONE_THREAD_STATE, JAVA_INT changeType) { + (void) changeType; +} + +/* The main thread's loop body: wait (parked) until the input thread queued + * something or 100ms passed, then return so Java drains the queue. */ +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_pumpMessages___R_boolean(CODENAME_ONE_THREAD_STATE) { + struct pollfd p; + char buf[64]; + if (cn1WakePipe[0] < 0) { + CN1_YIELD_THREAD; + usleep(10000); + CN1_RESUME_THREAD; + return JAVA_TRUE; + } + p.fd = cn1WakePipe[0]; + p.events = POLLIN; + p.revents = 0; + CN1_YIELD_THREAD; + if (poll(&p, 1, 100) > 0) { + while (read(cn1WakePipe[0], buf, sizeof(buf)) > 0) { + } + } + CN1_RESUME_THREAD; + return JAVA_TRUE; +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_enableHeadlessScreenshot___java_lang_String_int_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT path, JAVA_INT width, JAVA_INT height) { + (void) path; + cn1HeadlessForced = 1; + /* Sizes only a display that is not up yet; the buffers are sized from them. */ + if (cn1DisplayUp) return; + if (width > 0) cn1PhysW = width; + if (height > 0) cn1PhysH = height; +} + +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_captureWindowToPngBytes___R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE) { + unsigned char* data = 0; + int len = 0; + JAVA_OBJECT arr; + if (!cn1WindowG.surface || !cn1LinuxSurfaceToPng(cn1WindowG.surface, &data, &len)) { + return JAVA_NULL; + } + arr = cn1LinuxNewByteArray(threadStateData, data, len); + free(data); + return arr; +} + +/* No window chrome on an appliance: no title bar to inset for, no drag regions. */ +JAVA_VOID com_codename1_impl_linux_LinuxNative_mainWindowTitleBarInsets___int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT result) { + if (result != JAVA_NULL) { + memset(CN1_ARRAY_DATA(result), 0, sizeof(JAVA_INT) * (size_t) (*(JAVA_ARRAY) result).length); + } +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_mainWindowTitleBarRegions___int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT regions) { + (void) regions; +} + +/* An appliance has no desktop colour scheme; the application's theme decides. */ +JAVA_INT com_codename1_impl_linux_LinuxNative_systemColorScheme___R_int(CODENAME_ONE_THREAD_STATE) { + return -1; +} + +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_nativeThemeSettings___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return newStringFromCString(threadStateData, ""); +} + +#endif /* CN1_LINUX_EMBEDDED */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_embedded_stubs.c b/Ports/LinuxPort/nativeSources/cn1_linux_embedded_stubs.c new file mode 100644 index 00000000000..89e360c4266 --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_embedded_stubs.c @@ -0,0 +1,679 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * GENERATED by scripts/embedded/gen-embedded-stubs.py -- do not edit. + * + * The embedded Linux target does not compile the port's GTK units (and the GL + * and GStreamer units unless the sysroot has those libraries). These stubs give + * every native those units define a symbol that answers "unsupported", so the + * same translated application links on both targets. Each logs once + * (cn1LinuxStubOnce). + */ +#ifdef CN1_LINUX_EMBEDDED +#include "cn1_linux_gfx.h" + +/* ---- cn1_linux_desktopwindow.c ---- */ +JAVA_INT com_codename1_impl_linux_LinuxNative_desktopWindowCreate___int_java_lang_String_int_int_int_int_boolean_boolean_int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT windowId, JAVA_OBJECT title, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height, JAVA_BOOLEAN decorated, JAVA_BOOLEAN resizable, JAVA_INT ownerSlot, JAVA_BOOLEAN positionSet) { + (void) windowId; (void) title; (void) x; (void) y; (void) width; (void) height; (void) decorated; (void) resizable; (void) ownerSlot; (void) positionSet; + cn1LinuxStubOnce("desktopWindowCreate"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowDestroy___int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + (void) slot; + cn1LinuxStubOnce("desktopWindowDestroy"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowShow___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_BOOLEAN visible) { + (void) slot; (void) visible; + cn1LinuxStubOnce("desktopWindowShow"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowSetTitle___int_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_OBJECT title) { + (void) slot; (void) title; + cn1LinuxStubOnce("desktopWindowSetTitle"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowSetBounds___int_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height) { + (void) slot; (void) x; (void) y; (void) width; (void) height; + cn1LinuxStubOnce("desktopWindowSetBounds"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowGetBounds___int_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_OBJECT out) { + (void) slot; (void) out; + cn1LinuxStubOnce("desktopWindowGetBounds"); +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_mainWindowGetBounds___int_1ARRAY_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + (void) out; + cn1LinuxStubOnce("mainWindowGetBounds"); + return JAVA_FALSE; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_mainWindowSetSize___int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT width, JAVA_INT height) { + (void) width; (void) height; + cn1LinuxStubOnce("mainWindowSetSize"); +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_mainWindowGetFrame___int_1ARRAY_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + (void) out; + cn1LinuxStubOnce("mainWindowGetFrame"); + return JAVA_FALSE; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_desktopWindowGetWidth___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + (void) slot; + cn1LinuxStubOnce("desktopWindowGetWidth"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_desktopWindowGetHeight___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + (void) slot; + cn1LinuxStubOnce("desktopWindowGetHeight"); + return 0; +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_desktopWindowGraphics___int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + (void) slot; + cn1LinuxStubOnce("desktopWindowGraphics"); + return 0; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_captureDesktopWindowToPngBytes___int_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + (void) slot; + cn1LinuxStubOnce("captureDesktopWindowToPngBytes"); + return JAVA_NULL; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowFlush___int_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height) { + (void) slot; (void) x; (void) y; (void) width; (void) height; + cn1LinuxStubOnce("desktopWindowFlush"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowSetFlag___int_int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_INT flag, JAVA_BOOLEAN value) { + (void) slot; (void) flag; (void) value; + cn1LinuxStubOnce("desktopWindowSetFlag"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowSetMinimumSize___int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_INT width, JAVA_INT height) { + (void) slot; (void) width; (void) height; + cn1LinuxStubOnce("desktopWindowSetMinimumSize"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_mainWindowSetSensitive___boolean(CODENAME_ONE_THREAD_STATE, JAVA_BOOLEAN sensitive) { + (void) sensitive; + cn1LinuxStubOnce("mainWindowSetSensitive"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_desktopWindowSetState___int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot, JAVA_INT state) { + (void) slot; (void) state; + cn1LinuxStubOnce("desktopWindowSetState"); +} +JAVA_INT com_codename1_impl_linux_LinuxNative_monitorCount___R_int(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("monitorCount"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_primaryMonitor___R_int(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("primaryMonitor"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_monitorBounds___int_boolean_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_INT monitor, JAVA_BOOLEAN workArea, JAVA_OBJECT out) { + (void) monitor; (void) workArea; (void) out; + cn1LinuxStubOnce("monitorBounds"); +} +JAVA_INT com_codename1_impl_linux_LinuxNative_monitorScale___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT monitor) { + (void) monitor; + cn1LinuxStubOnce("monitorScale"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_monitorDpi___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT monitor) { + (void) monitor; + cn1LinuxStubOnce("monitorDpi"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_monitorForWindow___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT slot) { + (void) slot; + cn1LinuxStubOnce("monitorForWindow"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_monitorForMainWindow___R_int(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("monitorForMainWindow"); + return 0; +} + +/* ---- cn1_linux_edit.c ---- */ +JAVA_LONG com_codename1_impl_linux_LinuxNative_editStringAt___int_int_int_int_java_lang_String_boolean_int_long_int_int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h, JAVA_OBJECT text, JAVA_BOOLEAN singleLine, JAVA_INT maxSize, JAVA_LONG fontPeer, JAVA_INT fgColor, JAVA_INT bgColor, JAVA_INT align, JAVA_INT slot) { + (void) x; (void) y; (void) w; (void) h; (void) text; (void) singleLine; (void) maxSize; (void) fontPeer; (void) fgColor; (void) bgColor; (void) align; (void) slot; + cn1LinuxStubOnce("editStringAt"); + return 0; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_editIsDone___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("editIsDone"); + return JAVA_FALSE; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_editTabbed___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("editTabbed"); + return 0; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_editGetText___long_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("editGetText"); + return JAVA_NULL; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_editClose___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("editClose"); +} + +/* ---- cn1_linux_browser.c ---- */ +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_browserSupported___R_boolean(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("browserSupported"); + return JAVA_FALSE; +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_browserCreate___int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT w, JAVA_INT h, JAVA_INT slot) { + (void) w; (void) h; (void) slot; + cn1LinuxStubOnce("browserCreate"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_browserSetHost___long_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT slot) { + (void) peer; (void) slot; + cn1LinuxStubOnce("browserSetHost"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_browserSetHtml___long_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT html) { + (void) peer; (void) html; + cn1LinuxStubOnce("browserSetHtml"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_browserSetUrl___long_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT url) { + (void) peer; (void) url; + cn1LinuxStubOnce("browserSetUrl"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_browserExecute___long_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT js) { + (void) peer; (void) js; + cn1LinuxStubOnce("browserExecute"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_browserSetBounds___long_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h) { + (void) peer; (void) x; (void) y; (void) w; (void) h; + cn1LinuxStubOnce("browserSetBounds"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_browserSetVisible___long_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_BOOLEAN visible) { + (void) peer; (void) visible; + cn1LinuxStubOnce("browserSetVisible"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_browserPollEvent___long_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + return JAVA_NULL; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_browserCapturePng___long_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("browserCapturePng"); + return JAVA_NULL; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_browserDestroy___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("browserDestroy"); +} + +/* ---- cn1_linux_peer.c ---- */ +JAVA_VOID com_codename1_impl_linux_LinuxNative_peerInitialized___long_int_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h, JAVA_INT slot) { + (void) peer; (void) x; (void) y; (void) w; (void) h; (void) slot; + cn1LinuxStubOnce("peerInitialized"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_peerSetBounds___long_int_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT x, JAVA_INT y, JAVA_INT w, JAVA_INT h, JAVA_INT slot) { + (void) peer; (void) x; (void) y; (void) w; (void) h; (void) slot; + cn1LinuxStubOnce("peerSetBounds"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_peerSetVisible___long_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_BOOLEAN visible) { + (void) peer; (void) visible; + cn1LinuxStubOnce("peerSetVisible"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_peerDeinitialized___long_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT slot) { + (void) peer; (void) slot; + cn1LinuxStubOnce("peerDeinitialized"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_peerCalcPreferredSize___long_int_int_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT dispW, JAVA_INT dispH, JAVA_OBJECT out) { + (void) peer; (void) dispW; (void) dispH; (void) out; + cn1LinuxStubOnce("peerCalcPreferredSize"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_peerCaptureArgb___long_int_1ARRAY_R_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT outDims) { + (void) peer; (void) outDims; + cn1LinuxStubOnce("peerCaptureArgb"); + return JAVA_NULL; +} + +/* ---- cn1_linux_print.c ---- */ +JAVA_INT com_codename1_impl_linux_LinuxNative_printDocument___java_lang_String_java_lang_String_java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT path, JAVA_OBJECT mimeType, JAVA_OBJECT jobName) { + (void) path; (void) mimeType; (void) jobName; + cn1LinuxStubOnce("printDocument"); + return 0; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_printLastError___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("printLastError"); + return JAVA_NULL; +} + +/* ---- cn1_linux_services.c ---- */ +JAVA_VOID com_codename1_impl_linux_LinuxNative_clipboardSetText___java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT text) { + (void) text; + cn1LinuxStubOnce("clipboardSetText"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_clipboardGetText___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("clipboardGetText"); + return JAVA_NULL; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_clipboardSetImage___byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT png) { + (void) png; + cn1LinuxStubOnce("clipboardSetImage"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_clipboardGetImage___R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("clipboardGetImage"); + return JAVA_NULL; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_clipboardSetFiles___java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT paths) { + (void) paths; + cn1LinuxStubOnce("clipboardSetFiles"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_clipboardGetFiles___R_java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("clipboardGetFiles"); + return JAVA_NULL; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_shellOpen___java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT target) { + (void) target; + cn1LinuxStubOnce("shellOpen"); + return JAVA_FALSE; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_showNotification___java_lang_String_java_lang_String_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT id, JAVA_OBJECT title, JAVA_OBJECT body) { + (void) id; (void) title; (void) body; + cn1LinuxStubOnce("showNotification"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_notificationPollClicked___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return JAVA_NULL; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_secretServiceAvailable___R_boolean(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("secretServiceAvailable"); + return JAVA_FALSE; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_dpapiProtect___byte_1ARRAY_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT data) { + (void) data; + cn1LinuxStubOnce("dpapiProtect"); + return JAVA_NULL; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_dpapiUnprotect___byte_1ARRAY_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT data) { + (void) data; + cn1LinuxStubOnce("dpapiUnprotect"); + return JAVA_NULL; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_dpapiForget___byte_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT data) { + (void) data; + cn1LinuxStubOnce("dpapiForget"); + return 0; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_fileDialog___boolean_int_java_lang_String_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_BOOLEAN save, JAVA_INT type, JAVA_OBJECT title) { + (void) save; (void) type; (void) title; + cn1LinuxStubOnce("fileDialog"); + return JAVA_NULL; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_fileDialogEx___boolean_java_lang_String_java_lang_String_java_lang_String_java_lang_String_java_lang_String_1ARRAY_int_boolean_boolean_boolean_boolean_java_lang_String_1ARRAY_R_java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_BOOLEAN save, JAVA_OBJECT title, JAVA_OBJECT acceptLabel, JAVA_OBJECT initialDir, JAVA_OBJECT suggestedName, JAVA_OBJECT filters, JAVA_INT selectedFilter, JAVA_BOOLEAN multi, JAVA_BOOLEAN dirs, JAVA_BOOLEAN confirmOverwrite, JAVA_BOOLEAN showHidden, JAVA_OBJECT options) { + (void) save; (void) title; (void) acceptLabel; (void) initialDir; (void) suggestedName; (void) filters; (void) selectedFilter; (void) multi; (void) dirs; (void) confirmOverwrite; (void) showHidden; (void) options; + cn1LinuxStubOnce("fileDialogEx"); + return JAVA_NULL; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_locationGetCurrent___double_1ARRAY_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT out) { + (void) out; + cn1LinuxStubOnce("locationGetCurrent"); + return JAVA_FALSE; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_shareText___java_lang_String_java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT text, JAVA_OBJECT title) { + (void) text; (void) title; + cn1LinuxStubOnce("shareText"); + return JAVA_FALSE; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_contactsGetAll___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("contactsGetAll"); + return JAVA_NULL; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_biometricAvailability___R_int(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("biometricAvailability"); + return 0; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_biometricAuthenticate___java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT message) { + (void) message; + cn1LinuxStubOnce("biometricAuthenticate"); + return JAVA_FALSE; +} + +/* ---- cn1_linux_text_input.c ---- */ +JAVA_BOOLEAN com_codename1_impl_linux_LinuxTextInput_startNative___int_int_int_boolean_boolean_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT session, JAVA_INT slot, JAVA_INT constraint, JAVA_BOOLEAN correct, JAVA_BOOLEAN capitalize) { + (void) session; (void) slot; (void) constraint; (void) correct; (void) capitalize; + cn1LinuxStubOnce("LinuxTextInput_startNative"); + return JAVA_FALSE; +} +JAVA_VOID com_codename1_impl_linux_LinuxTextInput_stopNative___int(CODENAME_ONE_THREAD_STATE, JAVA_INT session) { + (void) session; + cn1LinuxStubOnce("LinuxTextInput_stopNative"); +} +JAVA_VOID com_codename1_impl_linux_LinuxTextInput_updateNative___int_int_java_lang_String_int_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_INT session, JAVA_INT sequence, JAVA_OBJECT text, JAVA_INT caret, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height) { + (void) session; (void) sequence; (void) text; (void) caret; (void) x; (void) y; (void) width; (void) height; + cn1LinuxStubOnce("LinuxTextInput_updateNative"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxTextInput_pollNative___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return JAVA_NULL; +} + +/* ---- cn1_linux_widgets.c ---- */ +JAVA_LONG com_codename1_impl_linux_LinuxNative_widgetCreate___int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT w, JAVA_INT h) { + (void) w; (void) h; + cn1LinuxStubOnce("widgetCreate"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_widgetUpdatePixels___long_int_1ARRAY_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT argb, JAVA_INT w, JAVA_INT h) { + (void) peer; (void) argb; (void) w; (void) h; + cn1LinuxStubOnce("widgetUpdatePixels"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_widgetSetPosition___long_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT x, JAVA_INT y) { + (void) peer; (void) x; (void) y; + cn1LinuxStubOnce("widgetSetPosition"); +} +JAVA_INT com_codename1_impl_linux_LinuxNative_widgetGetX___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("widgetGetX"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_widgetGetY___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("widgetGetY"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_widgetSetHitRects___long_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT rects) { + (void) peer; (void) rects; + cn1LinuxStubOnce("widgetSetHitRects"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_widgetDestroy___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("widgetDestroy"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_widgetFocusApp__(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("widgetFocusApp"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_widgetPollEvent___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + return JAVA_NULL; +} + +/* ---- cn1_linux_gl.c ---- */ +#ifndef CN1_EMBEDDED_HAS_GL +JAVA_LONG com_codename1_impl_linux_LinuxNative_gl3dCreateContext___R_long(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("gl3dCreateContext"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dDestroyContext___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer) { + (void) contextPeer; + cn1LinuxStubOnce("gl3dDestroyContext"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dBeginFrame___long_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer, JAVA_INT width, JAVA_INT height) { + (void) contextPeer; (void) width; (void) height; + cn1LinuxStubOnce("gl3dBeginFrame"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_gl3dCaptureFrame___long_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer) { + (void) contextPeer; + cn1LinuxStubOnce("gl3dCaptureFrame"); + return JAVA_NULL; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_gl3dCaptureToFile___long_java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer, JAVA_OBJECT path) { + (void) contextPeer; (void) path; + cn1LinuxStubOnce("gl3dCaptureToFile"); + return JAVA_FALSE; +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_gl3dCreateFloatBuffer___float_1ARRAY_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT data, JAVA_INT floatCount) { + (void) data; (void) floatCount; + cn1LinuxStubOnce("gl3dCreateFloatBuffer"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dUpdateFloatBuffer___long_float_1ARRAY_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG bufferPeer, JAVA_OBJECT data, JAVA_INT floatCount) { + (void) bufferPeer; (void) data; (void) floatCount; + cn1LinuxStubOnce("gl3dUpdateFloatBuffer"); +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_gl3dCreateShortBuffer___short_1ARRAY_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT data, JAVA_INT indexCount) { + (void) data; (void) indexCount; + cn1LinuxStubOnce("gl3dCreateShortBuffer"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dUpdateShortBuffer___long_short_1ARRAY_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG bufferPeer, JAVA_OBJECT data, JAVA_INT indexCount) { + (void) bufferPeer; (void) data; (void) indexCount; + cn1LinuxStubOnce("gl3dUpdateShortBuffer"); +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_gl3dCreateTexture___int_1ARRAY_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT argb, JAVA_INT width, JAVA_INT height) { + (void) argb; (void) width; (void) height; + cn1LinuxStubOnce("gl3dCreateTexture"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dDisposeBuffer___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG bufferPeer) { + (void) bufferPeer; + cn1LinuxStubOnce("gl3dDisposeBuffer"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dDisposeTexture___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG texturePeer) { + (void) texturePeer; + cn1LinuxStubOnce("gl3dDisposeTexture"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dDisposePipeline___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG pipelinePeer) { + (void) pipelinePeer; + cn1LinuxStubOnce("gl3dDisposePipeline"); +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_gl3dGetOrCreatePipeline___long_java_lang_String_java_lang_String_int_int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer, JAVA_OBJECT key, JAVA_OBJECT glslSource, JAVA_INT blendMode, JAVA_INT cullMode, JAVA_INT depthTest, JAVA_INT depthWrite) { + (void) contextPeer; (void) key; (void) glslSource; (void) blendMode; (void) cullMode; (void) depthTest; (void) depthWrite; + cn1LinuxStubOnce("gl3dGetOrCreatePipeline"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dClear___long_int_boolean_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer, JAVA_INT argbColor, JAVA_BOOLEAN clearColor, JAVA_BOOLEAN clearDepth) { + (void) contextPeer; (void) argbColor; (void) clearColor; (void) clearDepth; + cn1LinuxStubOnce("gl3dClear"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dSetViewport___long_int_int_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer, JAVA_INT x, JAVA_INT y, JAVA_INT width, JAVA_INT height) { + (void) contextPeer; (void) x; (void) y; (void) width; (void) height; + cn1LinuxStubOnce("gl3dSetViewport"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dDrawIndexed___long_long_long_int_long_int_int_float_1ARRAY_int_long_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer, JAVA_LONG pipelinePeer, JAVA_LONG vboPeer, JAVA_INT strideBytes, JAVA_LONG iboPeer, JAVA_INT indexCount, JAVA_INT primitive, JAVA_OBJECT uniforms, JAVA_INT uniformFloats, JAVA_LONG texturePeer, JAVA_INT texFilter, JAVA_INT texWrap) { + (void) contextPeer; (void) pipelinePeer; (void) vboPeer; (void) strideBytes; (void) iboPeer; (void) indexCount; (void) primitive; (void) uniforms; (void) uniformFloats; (void) texturePeer; (void) texFilter; (void) texWrap; + cn1LinuxStubOnce("gl3dDrawIndexed"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_gl3dDrawArrays___long_long_long_int_int_int_float_1ARRAY_int_long_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG contextPeer, JAVA_LONG pipelinePeer, JAVA_LONG vboPeer, JAVA_INT strideBytes, JAVA_INT vertexCount, JAVA_INT primitive, JAVA_OBJECT uniforms, JAVA_INT uniformFloats, JAVA_LONG texturePeer, JAVA_INT texFilter, JAVA_INT texWrap) { + (void) contextPeer; (void) pipelinePeer; (void) vboPeer; (void) strideBytes; (void) vertexCount; (void) primitive; (void) uniforms; (void) uniformFloats; (void) texturePeer; (void) texFilter; (void) texWrap; + cn1LinuxStubOnce("gl3dDrawArrays"); +} +#endif /* CN1_EMBEDDED_HAS_GL */ + +/* ---- cn1_linux_media.c ---- */ +#ifndef CN1_EMBEDDED_HAS_GST +JAVA_LONG com_codename1_impl_linux_LinuxNative_mediaCreate___byte_1ARRAY_int_java_lang_String_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT data, JAVA_INT length, JAVA_OBJECT mimeType) { + (void) data; (void) length; (void) mimeType; + cn1LinuxStubOnce("mediaCreate"); + return 0; +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_mediaCreateUri___java_lang_String_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT uri) { + (void) uri; + cn1LinuxStubOnce("mediaCreateUri"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_mediaPlay___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("mediaPlay"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_mediaPause___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("mediaPause"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_mediaSetTime___long_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT millis) { + (void) peer; (void) millis; + cn1LinuxStubOnce("mediaSetTime"); +} +JAVA_INT com_codename1_impl_linux_LinuxNative_mediaGetTime___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("mediaGetTime"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_mediaGetDuration___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("mediaGetDuration"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_mediaSetVolume___long_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_INT volume) { + (void) peer; (void) volume; + cn1LinuxStubOnce("mediaSetVolume"); +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_mediaIsPlaying___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("mediaIsPlaying"); + return JAVA_FALSE; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_mediaIsEnded___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("mediaIsEnded"); + return JAVA_FALSE; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_mediaIsVideo___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("mediaIsVideo"); + return JAVA_FALSE; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_mediaDestroy___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("mediaDestroy"); +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_audioRecStart___java_lang_String_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT path, JAVA_INT sampleRate, JAVA_INT channels) { + (void) path; (void) sampleRate; (void) channels; + cn1LinuxStubOnce("audioRecStart"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_audioRecStop___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("audioRecStop"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_cameraCaptureFrame___int_1ARRAY_R_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT outDims) { + (void) outDims; + cn1LinuxStubOnce("cameraCaptureFrame"); + return JAVA_NULL; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_cameraEnumerate___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("cameraEnumerate"); + return JAVA_NULL; +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_cameraSessionStart___int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT deviceIndex, JAVA_INT reqW, JAVA_INT reqH) { + (void) deviceIndex; (void) reqW; (void) reqH; + cn1LinuxStubOnce("cameraSessionStart"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_cameraSessionStop___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { + (void) handle; + cn1LinuxStubOnce("cameraSessionStop"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_cameraSessionSetPaused___long_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_BOOLEAN paused) { + (void) handle; (void) paused; + cn1LinuxStubOnce("cameraSessionSetPaused"); +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_cameraSessionLatestFrame___long_int_1ARRAY_R_int_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_OBJECT outDims) { + (void) handle; (void) outDims; + cn1LinuxStubOnce("cameraSessionLatestFrame"); + return JAVA_NULL; +} +#endif /* CN1_EMBEDDED_HAS_GST */ + +/* ---- cn1_linux_video.c ---- */ +#ifndef CN1_EMBEDDED_HAS_GST +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_videoBackendAvailable___R_boolean(CODENAME_ONE_THREAD_STATE) { + cn1LinuxStubOnce("videoBackendAvailable"); + return JAVA_FALSE; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_videoFactoryAvailable___java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT nameObj) { + (void) nameObj; + cn1LinuxStubOnce("videoFactoryAvailable"); + return JAVA_FALSE; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_videoDecoderAvailable___java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT capsObj) { + (void) capsObj; + cn1LinuxStubOnce("videoDecoderAvailable"); + return JAVA_FALSE; +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_videoReaderOpen___java_lang_String_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT pathObj) { + (void) pathObj; + cn1LinuxStubOnce("videoReaderOpen"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_videoReaderWidth___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderWidth"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_videoReaderHeight___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderHeight"); + return 0; +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_videoReaderDuration___long_R_long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderDuration"); + return 0; +} +JAVA_FLOAT com_codename1_impl_linux_LinuxNative_videoReaderFrameRate___long_R_float(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderFrameRate"); + return 0; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_videoReaderHasVideo___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderHasVideo"); + return JAVA_FALSE; +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_videoReaderHasAudio___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderHasAudio"); + return JAVA_FALSE; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_videoReaderAudioSampleRate___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderAudioSampleRate"); + return 0; +} +JAVA_INT com_codename1_impl_linux_LinuxNative_videoReaderAudioChannels___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderAudioChannels"); + return 0; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_videoReaderFrameAt___long_long_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_LONG ms) { + (void) peer; (void) ms; + cn1LinuxStubOnce("videoReaderFrameAt"); + return JAVA_NULL; +} +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_videoReaderReadAudio___long_R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderReadAudio"); + return JAVA_NULL; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_videoReaderClose___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoReaderClose"); +} +JAVA_LONG com_codename1_impl_linux_LinuxNative_videoWriterOpen___java_lang_String_boolean_int_int_float_int_int_boolean_int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT pathObj, JAVA_BOOLEAN hevc, JAVA_INT width, JAVA_INT height, JAVA_FLOAT fps, JAVA_INT videoBitRate, JAVA_INT gop, JAVA_BOOLEAN hasAudio, JAVA_INT audioBitRate, JAVA_INT sampleRate, JAVA_INT channels) { + (void) pathObj; (void) hevc; (void) width; (void) height; (void) fps; (void) videoBitRate; (void) gop; (void) hasAudio; (void) audioBitRate; (void) sampleRate; (void) channels; + cn1LinuxStubOnce("videoWriterOpen"); + return 0; +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_videoWriterFrame___long_byte_1ARRAY_int_int_long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT rgba, JAVA_INT w, JAVA_INT h, JAVA_LONG ptsMs) { + (void) peer; (void) rgba; (void) w; (void) h; (void) ptsMs; + cn1LinuxStubOnce("videoWriterFrame"); +} +JAVA_VOID com_codename1_impl_linux_LinuxNative_videoWriterAudio___long_byte_1ARRAY_int_int_long(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer, JAVA_OBJECT pcm, JAVA_INT sampleRate, JAVA_INT channels, JAVA_LONG ptsMs) { + (void) peer; (void) pcm; (void) sampleRate; (void) channels; (void) ptsMs; + cn1LinuxStubOnce("videoWriterAudio"); +} +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_videoWriterClose___long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG peer) { + (void) peer; + cn1LinuxStubOnce("videoWriterClose"); + return JAVA_FALSE; +} +#endif /* CN1_EMBEDDED_HAS_GST */ + +#endif /* CN1_LINUX_EMBEDDED */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_events.c b/Ports/LinuxPort/nativeSources/cn1_linux_events.c new file mode 100644 index 00000000000..ede891c5fd2 --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_events.c @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The input ring buffer every Linux display backend feeds and the EDT drains + * through the pollEvent bridge: the GTK window (cn1_linux_window.c) and the + * embedded DRM/KMS, framebuffer and headless display (cn1_linux_embedded.c). + * GTK-free on purpose, so both targets compile it unchanged. + * + * scripts/test_native_hover_queue.py and scripts/test_linux_event_pump.py + * compile the ring standalone by slicing this file from CN1_EVENT_RING to the + * end marker below; keep anything the slice needs inside it. + */ + +#include "cn1_linux.h" +#include + +/* ----------------------------------------------------------- event ring */ + +#define CN1_EVENT_RING 1024 +typedef struct { + int type, x, y, key; + /* Which window the event came from. Zero is the application's main window, + * which is every event this port produced before desktop windows existed. */ + int windowId; +} CN1Event; +static CN1Event cn1EventRing[CN1_EVENT_RING]; +static int cn1EventHead = 0; +static int cn1EventTail = 0; +static pthread_mutex_t cn1EventLock = PTHREAD_MUTEX_INITIALIZER; + + +void cn1LinuxPushEvent(int type, int x, int y, int keyCode) { + cn1LinuxPushWindowEvent(0, type, x, y, keyCode); +} + +/* Events the framework cannot reconstruct if they are lost, of which there are two + * kinds. + * + * Lifecycle: a lost hide leaves a window the framework believes is on screen, painting + * and animating until something else happens to it, and a lost close leaves it + * registered with no native window behind it. + * + * Terminations: a release ends something a press started. Lose it and the component + * the press went to stays in that state for good -- the key goes on repeating, the + * button stays down, the drag never finishes -- and the focus change that would + * otherwise cancel a held gesture is no use as a backstop if it is droppable too. + * + * Note the asymmetry with presses, which stay droppable: a release that arrives with + * no press behind it finds no recorded target and is discarded harmlessly, so when + * something has to go it must never be the release. */ +/* Unlike hover motion, leave has no later motion outside the window to repair + * a dropped notification. Protect only the terminal sentinel, not the motion stream. */ +static int cn1LinuxIsProtectedEvent(int type, int x, int y) { + return type == CN1_EVENT_WINDOW_SHOWN || type == CN1_EVENT_WINDOW_HIDDEN + || type == CN1_EVENT_WINDOW_CLOSE + || type == CN1_EVENT_KEY_RELEASED + || type == CN1_EVENT_POINTER_RELEASED + || type == CN1_EVENT_WINDOW_FOCUS + || type == CN1_EVENT_SIZE_CHANGED + || (type == CN1_EVENT_POINTER_HOVER && x == -1 && y == -1); +} + +/* Visibility only. A close request is protected from eviction like any other + * lifecycle event, but it is not a state that a later one supersedes: the delete + * signal does not destroy the window, so a close that a subsequent minimize overwrote + * would take the close listener and the close operation with it. */ +static int cn1LinuxStateClass(int type) { + if (type == CN1_EVENT_WINDOW_SHOWN || type == CN1_EVENT_WINDOW_HIDDEN) { + return 1; + } + if (type == CN1_EVENT_SIZE_CHANGED) { + return 2; + } + return 0; +} + +/* Replaces a queued visibility event for the same window with this newer one. The + * latest state is the one that matters -- a hide followed by a show leaves the window + * shown -- so superseding costs nothing and needs no room. */ +static int cn1LinuxCoalesceLifecycleLocked(int windowId, int type, int x, int y, + int keyCode) { + int idx = cn1EventHead; + int newest = -1; + int cls = cn1LinuxStateClass(type); + if (cls == 0) { + return 0; + } + /* The *newest* match, not the first one found. A window can already have more than + * one transition queued -- a hide then a show -- and replacing the older of the two + * leaves the newer one as the last word, so the framework would end up believing a + * window that is natively hidden is on screen, and go on painting it. */ + while (idx != cn1EventTail) { + if (cn1EventRing[idx].windowId == windowId + && cn1LinuxStateClass(cn1EventRing[idx].type) == cls) { + newest = idx; + } + idx = (idx + 1) % CN1_EVENT_RING; + } + if (newest < 0) { + return 0; + } + cn1EventRing[newest].type = type; + cn1EventRing[newest].x = x; + cn1EventRing[newest].y = y; + cn1EventRing[newest].key = keyCode; + return 1; +} + +/* Removes the oldest droppable event, closing the gap. Used to make room for a + * protected one: advancing the head instead would evict whatever is oldest, and that + * can be a protected event itself -- which is the very thing being kept. */ +static void cn1LinuxRemoveAtLocked(int idx) { + int cur = idx; + int follow = (cur + 1) % CN1_EVENT_RING; + while (follow != cn1EventTail) { + cn1EventRing[cur] = cn1EventRing[follow]; + cur = follow; + follow = (follow + 1) % CN1_EVENT_RING; + } + cn1EventTail = cur; +} + +static int cn1LinuxEvictInputLocked(void) { + int idx = cn1EventHead; + while (idx != cn1EventTail) { + if (!cn1LinuxIsProtectedEvent(cn1EventRing[idx].type, + cn1EventRing[idx].x, cn1EventRing[idx].y)) { + cn1LinuxRemoveAtLocked(idx); + return 1; + } + idx = (idx + 1) % CN1_EVENT_RING; + } + return 0; +} + +/* Last resort when the ring holds nothing but lifecycle events and so has no input to + * give up. A window that toggled visibility several times before the framework drained + * anything has more than one transition queued, and every one but its last is already + * superseded, so dropping the oldest of them frees a slot without changing what any + * window ends up as. Without this a close arriving for a *different* window has nowhere + * to go and is dropped, which is the one outcome this whole path exists to prevent. */ +static int cn1LinuxEvictSupersededVisibilityLocked(void) { + int idx = cn1EventHead; + while (idx != cn1EventTail) { + int cls = cn1LinuxStateClass(cn1EventRing[idx].type); + if (cls != 0) { + int scan = (idx + 1) % CN1_EVENT_RING; + while (scan != cn1EventTail) { + if (cn1EventRing[scan].windowId == cn1EventRing[idx].windowId + && cn1LinuxStateClass(cn1EventRing[scan].type) == cls) { + cn1LinuxRemoveAtLocked(idx); + return 1; + } + scan = (scan + 1) % CN1_EVENT_RING; + } + } + idx = (idx + 1) % CN1_EVENT_RING; + } + return 0; +} + +/* Last resort before giving up an entry outright: drop the oldest *termination*. + * + * When the queue cannot grow, the question is only which loss costs least, and the + * order is droppable input, then a state a later event already supersedes, then a + * termination, then a lifecycle event. A lost release latches one component; a lost + * close or hide loses a whole window -- the close operation never runs, or the + * framework goes on painting a window that is not on screen. So a queued close or + * visibility transition outranks any number of releases behind it. */ +static int cn1LinuxEvictOldestTerminationLocked(void) { + int idx = cn1EventHead; + while (idx != cn1EventTail) { + int t = cn1EventRing[idx].type; + if (t == CN1_EVENT_KEY_RELEASED || t == CN1_EVENT_POINTER_RELEASED + || t == CN1_EVENT_WINDOW_FOCUS + || (t == CN1_EVENT_POINTER_HOVER && cn1EventRing[idx].x == -1 + && cn1EventRing[idx].y == -1)) { + cn1LinuxRemoveAtLocked(idx); + return 1; + } + idx = (idx + 1) % CN1_EVENT_RING; + } + return 0; +} + +void cn1LinuxPushWindowEvent(int windowId, int type, int x, int y, int keyCode) { + pthread_mutex_lock(&cn1EventLock); + int next = (cn1EventTail + 1) % CN1_EVENT_RING; + if (next == cn1EventHead && cn1LinuxIsProtectedEvent(type, x, y)) { + /* Full, and this one must not be the casualty. Supersede this window's own + * queued transition if it has one, otherwise take the room from an input event, + * and failing that from a transition that a later one already supersedes. Never + * from a transition that is still some window's last word. */ + if (cn1LinuxCoalesceLifecycleLocked(windowId, type, x, y, keyCode)) { + pthread_mutex_unlock(&cn1EventLock); + return; + } + if (cn1LinuxEvictInputLocked() || cn1LinuxEvictSupersededVisibilityLocked() + || cn1LinuxEvictOldestTerminationLocked()) { + next = (cn1EventTail + 1) % CN1_EVENT_RING; + } else { + /* Nothing left but lifecycle events -- closes and visibility transitions + * for more distinct windows than the ring can hold, which needs more windows + * open than any application has. Giving up the oldest is all that remains, + * and the newer event at least describes the more recent state. */ + cn1EventHead = (cn1EventHead + 1) % CN1_EVENT_RING; + next = (cn1EventTail + 1) % CN1_EVENT_RING; + } + } + if (next != cn1EventHead) { + cn1EventRing[cn1EventTail].type = type; + cn1EventRing[cn1EventTail].x = x; + cn1EventRing[cn1EventTail].y = y; + cn1EventRing[cn1EventTail].key = keyCode; + cn1EventRing[cn1EventTail].windowId = windowId; + cn1EventTail = next; + } + pthread_mutex_unlock(&cn1EventLock); +} + +int cn1LinuxPopEvent(int* out) { + int has = 0; + pthread_mutex_lock(&cn1EventLock); + if (cn1EventHead != cn1EventTail) { + out[0] = cn1EventRing[cn1EventHead].type; + out[1] = cn1EventRing[cn1EventHead].x; + out[2] = cn1EventRing[cn1EventHead].y; + out[3] = cn1EventRing[cn1EventHead].key; + out[4] = cn1EventRing[cn1EventHead].windowId; + cn1EventHead = (cn1EventHead + 1) % CN1_EVENT_RING; + has = 1; + } + pthread_mutex_unlock(&cn1EventLock); + return has; +} + +/* ------------------------------------------------------------- globals */ + +/* Converts a stream of fractional scroll notches into whole ones. + * + * Smooth scroll deltas are fractions of a notch, so they cannot be forwarded one + * for one: wheelUnits() on the Java side floors any sub-notch delta to a whole + * notch, and a touchpad emits deltas continuously. Whole notches are returned and + * the remainder is carried in *residue until it adds up. Truncation is toward + * zero, so the residue always keeps the sign of the travel. */ +int cn1LinuxTakeWholeNotches(double delta, double* residue) { + double total = *residue + delta; + int notches = (int) total; + *residue = total - notches; + return notches; +} diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_faults.c b/Ports/LinuxPort/nativeSources/cn1_linux_faults.c new file mode 100644 index 00000000000..0e7074a040f --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_faults.c @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE /* pthread_getattr_np + ucontext REG_* gregs; before any libc include */ +#endif +#include "cn1_linux.h" +#include +#include +#include +#include +#include +#ifdef __GLIBC__ +#include /* backtrace() -- glibc only; musl has no execinfo.h */ +#endif +#include + +/* ---- hardware fault -> Java exception (POSIX analog of the iOS SignalHandler + * and the Win32 cn1WinFaultToException) ------------------------------------- + * + * ParparVM's clean C target only NULL-checks a method's `this` at entry. A null + * *argument* deref, a virtual call on a null receiver, or an operand-stack slot + * that legitimately reads back null all fault as a raw SIGSEGV in generated C + * rather than a catchable NullPointerException -- which hard-kills the process + * (and, on CI, the whole screenshot suite) with no Java stack trace. The iOS port + * converts the identical fault into an NPE via a SIGSEGV handler that calls + * throwException(); this is the Linux analog, hardened with the Win32 port's + * "wild faulting address == real corruption, leave it diagnosable" guard. + * + * The handler runs on the faulting thread's own stack for a synchronous signal, + * so longjmp-ing out of it via throwException() to the nearest CN1 try/catch + * behaves exactly as a normally thrown Java exception (this is the documented + * iOS technique). SA_NODEFER keeps the signal unblocked so faults remain + * catchable on subsequent tests after we leave the handler. + * + * NOTE: like every such handler this interferes with a native debugger (gdb sees + * the fault first-chance). It is a release/CI resilience mechanism, not a debug + * aid -- set CN1_LINUX_NO_FAULT_HANDLER=1 to disable it when debugging under gdb. + */ +/* Dump the faulting thread's native backtrace to stderr (the app-output tee). Async- + * signal-safe (write + backtrace). With the per-thread alternate signal stack this runs + * even on a STACK OVERFLOW; the repeating frames are the recursion. Called from both the + * SIGABRT path and the wild-address SIGSEGV path (stack-overflow guard-page faults land + * there), so an overflow is finally legible instead of a bare, corrupt-unwind core. */ +/* async-signal-safe "KEY=0xHEX\n" */ +static void cn1LinuxWriteHexKV(const char* key, unsigned long v) { + char buf[64]; int i = 0; + while (key[i]) { buf[i] = key[i]; i++; } + buf[i++] = '='; buf[i++] = '0'; buf[i++] = 'x'; + char hx[16]; int h = 0; + if (v == 0) hx[h++] = '0'; + while (v) { int d = v & 0xf; hx[h++] = (char)(d < 10 ? '0' + d : 'a' + d - 10); v >>= 4; } + while (h > 0) buf[i++] = hx[--h]; + buf[i++] = '\n'; + write(2, buf, i); +} + +static void cn1LinuxDumpNativeBacktrace(const char* label, siginfo_t* si, void* ucv) { + signal(SIGSEGV, SIG_DFL); + signal(SIGBUS, SIG_DFL); + write(2, "\n=====CN1 ", 10); + write(2, label, strlen(label)); + write(2, " BACKTRACE=====\n", 16); + if (si != NULL) cn1LinuxWriteHexKV("faultAddr", (unsigned long)(uintptr_t)si->si_addr); + /* faulting PC + SP from the ucontext, and this thread's real stack bounds -- if the + * used span (top - sp) is a fraction of a MB, this is a SMALL-STACK thread overflow + * (e.g. CN1 rendering on a WebKit/Gallium/GLib worker thread), not deep recursion. */ + if (ucv != NULL) { + ucontext_t* uc = (ucontext_t*)ucv; + unsigned long sp = 0, pc = 0; +#if defined(__aarch64__) + sp = (unsigned long)uc->uc_mcontext.sp; + pc = (unsigned long)uc->uc_mcontext.pc; +#elif defined(__x86_64__) + sp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP]; + pc = (unsigned long)uc->uc_mcontext.gregs[REG_RIP]; +#endif + if (pc) cn1LinuxWriteHexKV("faultPC", pc); + if (sp) cn1LinuxWriteHexKV("faultSP", sp); + /* The integer registers. Reading them costs nothing and touches no memory, and + * they are the only place the faulting POINTER survives: a wild address tells you + * a read went wrong, the register file tells you what the value WAS and which + * other register held the object it should have come from. gdb cannot supply this + * from the core -- the suite is built -O3 with LTO, so `info args` and + * `info locals` both answer "No locals" and every global reports "unknown type". + * The one 0x100000028 fault that reached a core was diagnosed this far only + * because faultAddr was printed here rather than recovered afterwards. */ +#if defined(__x86_64__) + { + static const char* const rn[16] = { + "rax","rbx","rcx","rdx","rsi","rdi","rbp","rsp", + "r8","r9","r10","r11","r12","r13","r14","r15" }; + static const int ri[16] = { + REG_RAX,REG_RBX,REG_RCX,REG_RDX,REG_RSI,REG_RDI,REG_RBP,REG_RSP, + REG_R8,REG_R9,REG_R10,REG_R11,REG_R12,REG_R13,REG_R14,REG_R15 }; + int r; + for (r = 0; r < 16; r++) { + cn1LinuxWriteHexKV(rn[r], (unsigned long)uc->uc_mcontext.gregs[ri[r]]); + } + } +#elif defined(__aarch64__) + { + char nm[8]; int r; + for (r = 0; r <= 30; r++) { + nm[0] = 'x'; + if (r < 10) { nm[1] = (char)('0' + r); nm[2] = 0; } + else { nm[1] = (char)('0' + r / 10); nm[2] = (char)('0' + r % 10); nm[3] = 0; } + cn1LinuxWriteHexKV(nm, (unsigned long)uc->uc_mcontext.regs[r]); + } + } +#endif + pthread_attr_t at; + if (pthread_getattr_np(pthread_self(), &at) == 0) { + void* base = 0; size_t sz = 0; + if (pthread_attr_getstack(&at, &base, &sz) == 0) { + cn1LinuxWriteHexKV("stackLo", (unsigned long)(uintptr_t)base); + cn1LinuxWriteHexKV("stackSz", (unsigned long)sz); + if (sp) cn1LinuxWriteHexKV("stackUsed", (unsigned long)(((uintptr_t)base + sz) - sp)); + /* The top of the faulting frame. Bounded by the real stack top, so this + * reads only mapped memory and cannot fault a second time -- the faulting + * ADDRESS is unmapped by definition and is deliberately not read. + * + * What it is for: a pointer-shaped fault whose low 32 bits are zero (the + * 0x100000000 case) is either a 64-bit value read as an object or a 32-bit + * write into a 64-bit slot, and the two look identical from the address + * alone. The neighbouring words separate them -- an adjacent half of the + * same 64-bit value is the first, an intact pointer beside a clobbered one + * is the second. */ + unsigned long hi = (unsigned long)(uintptr_t)base + (unsigned long)sz; + if (sp && sp >= (unsigned long)(uintptr_t)base && sp < hi) { + unsigned long lim = sp + 32 * sizeof(unsigned long); + if (lim > hi) { lim = hi; } + unsigned long a; + int slot = 0; + char nm[16]; + for (a = sp; a + sizeof(unsigned long) <= lim; a += sizeof(unsigned long)) { + nm[0]='s'; nm[1]='p'; nm[2]='['; + nm[3]=(char)('0' + slot / 10); nm[4]=(char)('0' + slot % 10); + nm[5]=']'; nm[6]=0; + cn1LinuxWriteHexKV(nm, *(unsigned long*)(uintptr_t)a); + slot++; + } + } + } + pthread_attr_destroy(&at); + } + } +#ifdef __GLIBC__ + void* bt[96]; + int n = backtrace(bt, 96); + backtrace_symbols_fd(bt, n, 2); +#else + const char* na = "(native backtrace unavailable on this libc)\n"; + write(2, na, strlen(na)); +#endif + const char* ftr = "=====END CN1 BACKTRACE=====\n"; + write(2, ftr, strlen(ftr)); +} + +static void cn1LinuxFaultToException(int sig, siginfo_t* si, void* ucv) { + (void) ucv; + /* Only a genuine null-ish deref (null + a small field/vtable/array offset) + * is converted to an NPE. A wild faulting address is real memory corruption OR a + * stack overflow (the guard-page address is high): dump a backtrace so the failing + * frame / recursion is identifiable from the CI log, then restore the default + * disposition and return so the re-executed instruction faults again into a core + * dump, instead of being masked as a recoverable NPE that silently corrupts state. */ + if ((sig == SIGSEGV || sig == SIGBUS) && si != NULL && + (uintptr_t) si->si_addr >= 0x10000) { + cn1LinuxDumpNativeBacktrace("FAULT", si, ucv); + signal(sig, SIG_DFL); + return; + } + struct ThreadLocalData* t = getThreadLocalData(); + if (t == NULL || t->tryBlockOffset <= 0) { + signal(sig, SIG_DFL); + return; + } + throwException(t, __NEW_INSTANCE_java_lang_NullPointerException(t)); +} + +/* Diagnostic: when the process aborts (e.g. __stack_chk_fail / a glib g_error), + * dump the native call stack so the failing frame is identifiable from the log + * even where an interactive debugger is unavailable (CI containers). Enabled only + * when CN1_LINUX_ABORT_BACKTRACE is set so it never interferes with normal runs. */ +static void cn1LinuxAbortBacktrace(int sig) { + cn1LinuxDumpNativeBacktrace("ABORT", NULL, NULL); + signal(sig, SIG_DFL); + raise(sig); +} + +void cn1LinuxInstallFaultHandlers(void) { +#ifdef __GLIBC__ + { + /* The first backtrace() loads libgcc and allocates; done here, so the + * one in the fault dump runs without malloc inside a signal handler. */ + void* warm[1]; + (void) backtrace(warm, 1); + } +#endif + static int installed = 0; + if (installed || getenv("CN1_LINUX_NO_FAULT_HANDLER") != NULL) { + return; + } + installed = 1; + /* Alternate signal stack for THIS (main) thread so SA_ONSTACK works here too. The CN1 + * worker/EDT/GC threads register their own in threadRunner/gcMarkWorkerMain. */ + { + stack_t ss; + ss.ss_sp = malloc(512 * 1024); + if (ss.ss_sp != NULL) { ss.ss_size = 512 * 1024; ss.ss_flags = 0; sigaltstack(&ss, NULL); } + } + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = cn1LinuxFaultToException; + /* SA_ONSTACK: run the handler on the per-thread alternate signal stack (registered in + * threadRunner / gcMarkWorkerMain / crash-protection install) so it can execute even + * on a STACK OVERFLOW, when the faulting thread's own stack is exhausted -- otherwise + * the handler re-faults and the process dies silently with no backtrace. */ + sa.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK; + sigemptyset(&sa.sa_mask); + sigaction(SIGSEGV, &sa, NULL); + sigaction(SIGBUS, &sa, NULL); + /* Dump a native backtrace if the process aborts (e.g. __stack_chk_fail or a + * glib g_error) so the failing frame is identifiable from the CI log without a + * debugger. Silent unless an abort actually fires. */ + signal(SIGABRT, cn1LinuxAbortBacktrace); +} + diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_gfx.h b/Ports/LinuxPort/nativeSources/cn1_linux_gfx.h index 3f2699ca3e9..1a61a84b318 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_gfx.h +++ b/Ports/LinuxPort/nativeSources/cn1_linux_gfx.h @@ -25,7 +25,8 @@ /* * Render-stack internals for the GTK3 translation units (window / graphics / - * text / image). Pulls in GTK + Cairo + Pango + GdkPixbuf, so only the rendering + * text / image). Pulls in GTK + Cairo + Pango + GdkPixbuf (on the embedded + * target, the port's own renderer instead), so only the rendering * units include it (the POSIX io/net/socket units include the GTK-free * cn1_linux.h instead). * @@ -37,17 +38,26 @@ */ #include "cn1_linux.h" +#ifdef CN1_LINUX_EMBEDDED +/* The embedded target draws with its own renderer (cn1_linux_raster.c) and + * FreeType (cn1_linux_text_ft.c), and decodes with libpng and libjpeg + * (cn1_linux_codec.c): no Cairo, Pango, GdkPixbuf or GLib. */ +#include "cn1_linux_raster.h" +#else #include #include #include #include +#endif #ifdef __cplusplus extern "C" { #endif +#ifndef CN1_LINUX_EMBEDDED /* Shared source decoding for main and secondary window pointer events. */ int cn1LinuxPointerSourceFlag(GdkEvent* event); +#endif /* * A drawing target: a Cairo context over a backing image surface. The on-screen @@ -86,6 +96,11 @@ typedef struct CN1Graphics { int flushX, flushY, flushW, flushH; } CN1Graphics; +#ifdef CN1_LINUX_EMBEDDED +/* A FreeType face at a size; defined in cn1_linux_text_ft.c, the only unit that + * looks inside it. */ +typedef struct CN1Font CN1Font; +#else /* A resolved Pango font description + a metrics snapshot for fast char widths. */ typedef struct CN1Font { PangoFontDescription* desc; @@ -93,6 +108,7 @@ typedef struct CN1Font { int ascent; int height; } CN1Font; +#endif /* A decoded image: an ARGB32 Cairo surface; getImageGraphics wraps it lazily. */ typedef struct CN1Image { @@ -102,8 +118,14 @@ typedef struct CN1Image { CN1Graphics* mutableGraphics; /* non-NULL once getImageGraphics was called */ } CN1Image; +#ifdef CN1_LINUX_EMBEDDED +/* Decodes PNG or JPEG bytes into a new ARGB32 surface; NULL when the bytes are + * neither or are damaged (cn1_linux_codec.c). */ +cairo_surface_t* cn1LinuxDecodeImage(const unsigned char* bytes, size_t length); +#else /* The shared PangoContext (created once over the default font map). */ PangoContext* cn1LinuxPangoContext(void); +#endif /* Applies g->color/alpha as the current Cairo source. */ void cn1LinuxApplySource(CN1Graphics* g); @@ -118,6 +140,7 @@ CN1Graphics* cn1LinuxWindowGraphics(void); /* Encodes an ARGB32 Cairo surface to PNG bytes; caller g_free()s *outData. */ int cn1LinuxSurfaceToPng(cairo_surface_t* surface, unsigned char** outData, int* outLen); +#ifndef CN1_LINUX_EMBEDDED /* Native-peer overlay management (defined in cn1_linux_window.c). The GtkFixed * sits in a pass-through GtkOverlay over the Cairo drawing area; these place / * move / remove a native widget tracking a lightweight PeerComponent. Must run @@ -143,6 +166,8 @@ CN1Graphics* cn1LinuxDesktopGraphics(int slot); GtkWidget* cn1LinuxDesktopWidget(int slot); GtkWidget* cn1LinuxDesktopFixed(int slot); +#endif /* !CN1_LINUX_EMBEDDED: the embedded target has no GTK widgets */ + /* Runs fn(arg) on the GTK main loop and blocks the caller until done (inline in * headless mode). For GTK calls the EDT must not make directly. */ void cn1LinuxRunOnMainAndWait(void (*fn)(void*), void* arg); diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_gl.c b/Ports/LinuxPort/nativeSources/cn1_linux_gl.c index 4ed27f68e09..1538f725a89 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_gl.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_gl.c @@ -41,7 +41,11 @@ #include #include #include +#ifdef CN1_LINUX_EMBEDDED +#include "cn1_linux_raster.h" +#else #include +#endif #include #include #include diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_hardware.c b/Ports/LinuxPort/nativeSources/cn1_linux_hardware.c new file mode 100644 index 00000000000..87224385aca --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_hardware.c @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The Linux port's hardware natives (com.codename1.impl.linux.LinuxNative). + * Each forwards to the shared body in cn1_hardware_impl.h, which the headless + * backend uses too; see that header for what they do. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include "cn1_hardware_impl.h" + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwGpioOpen___java_lang_String_int_boolean_boolean_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT chip, JAVA_INT line, JAVA_BOOLEAN output, JAVA_BOOLEAN high, JAVA_INT bias, JAVA_INT edges) { + return cn1HwImpl_hwGpioOpen(threadStateData, chip, line, output, high, bias, edges); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwGpioRead___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return cn1HwImpl_hwGpioRead(threadStateData, h); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwGpioWrite___int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_BOOLEAN high) { + return cn1HwImpl_hwGpioWrite(threadStateData, h, high); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwGpioWaitEdge___int_int_long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { + return cn1HwImpl_hwGpioWaitEdge(threadStateData, h, timeoutMs, tsOut); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwI2cOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, JAVA_INT address) { + return cn1HwImpl_hwI2cOpen(threadStateData, bus, address); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwI2cTransfer___int_byte_1ARRAY_int_byte_1ARRAY_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT w, JAVA_INT wl, JAVA_OBJECT rd, JAVA_INT rl) { + return cn1HwImpl_hwI2cTransfer(threadStateData, h, w, wl, rd, rl); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwSpiOpen___int_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, JAVA_INT cs, JAVA_INT mode, JAVA_INT speed, JAVA_INT bits) { + return cn1HwImpl_hwSpiOpen(threadStateData, bus, cs, mode, speed, bits); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwSpiTransfer___int_byte_1ARRAY_byte_1ARRAY_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT tx, JAVA_OBJECT rx, JAVA_INT len) { + return cn1HwImpl_hwSpiTransfer(threadStateData, h, tx, rx, len); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwSerialOpen___java_lang_String_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT dev, JAVA_INT baud, JAVA_INT dataBits, JAVA_INT parity, JAVA_INT stopBits) { + return cn1HwImpl_hwSerialOpen(threadStateData, dev, baud, dataBits, parity, stopBits); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwSerialRead___int_byte_1ARRAY_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len, JAVA_INT timeoutMs) { + return cn1HwImpl_hwSerialRead(threadStateData, h, buf, off, len, timeoutMs); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwSerialWrite___int_byte_1ARRAY_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len) { + return cn1HwImpl_hwSerialWrite(threadStateData, h, buf, off, len); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwPwmOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT chip, JAVA_INT channel) { + return cn1HwImpl_hwPwmOpen(threadStateData, chip, channel); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwPwmConfigure___int_long_long_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_LONG period, JAVA_LONG duty, JAVA_BOOLEAN enabled) { + return cn1HwImpl_hwPwmConfigure(threadStateData, h, period, duty, enabled); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwCanOpen___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT iface) { + return cn1HwImpl_hwCanOpen(threadStateData, iface); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwCanSend___int_int_boolean_byte_1ARRAY_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT id, JAVA_BOOLEAN ext, JAVA_OBJECT data, JAVA_INT len) { + return cn1HwImpl_hwCanSend(threadStateData, h, id, ext, data, len); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwCanReceive___int_int_1ARRAY_byte_1ARRAY_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT idOut, JAVA_OBJECT data, JAVA_INT timeoutMs) { + return cn1HwImpl_hwCanReceive(threadStateData, h, idOut, data, timeoutMs); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwWatchdogOpen___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT timeoutSeconds) { + return cn1HwImpl_hwWatchdogOpen(threadStateData, timeoutSeconds); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwWatchdogKick___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return cn1HwImpl_hwWatchdogKick(threadStateData, h); +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_hwClose___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_BOOLEAN disarm) { + cn1HwImpl_hwClose(threadStateData, h, disarm); +} + +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_hwList___int_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_INT kind) { + return cn1HwImpl_hwList(threadStateData, kind); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_hwBacklight___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT op, JAVA_INT value) { + return cn1HwImpl_hwBacklight(threadStateData, op, value); +} diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_image.c b/Ports/LinuxPort/nativeSources/cn1_linux_image.c index cff7cd2feba..822188264a1 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_image.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_image.c @@ -143,6 +143,64 @@ JAVA_LONG com_codename1_impl_linux_LinuxNative_createImageFromARGB___int_1ARRAY_ return (JAVA_LONG) (intptr_t) cn1WrapSurface(cn1SurfaceFromArgb(px, width, height)); } +#ifdef CN1_LINUX_EMBEDDED +/* Reads a whole file; the decoder works on bytes. */ +static unsigned char* cn1ReadFile(const char* path, size_t* length) { + FILE* f = fopen(path, "rb"); + unsigned char* data = 0; + size_t size = 0, capacity = 0, n; + if (f == 0) { + return 0; + } + for (;;) { + if (size == capacity) { + size_t grown = capacity == 0 ? 65536 : capacity * 2; + unsigned char* more = (unsigned char*) realloc(data, grown); + if (more == 0) { + free(data); + fclose(f); + return 0; + } + data = more; + capacity = grown; + } + n = fread(data + size, 1, capacity - size, f); + size += n; + if (n == 0) { + break; + } + } + fclose(f); + *length = size; + return data; +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_createImageFromFile___java_lang_String_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT path) { + const char* p = path == JAVA_NULL ? 0 : stringToUTF8(threadStateData, path); + unsigned char* bytes; + size_t length = 0; + cairo_surface_t* s; + if (!p || (bytes = cn1ReadFile(p, &length)) == 0) { + return 0; + } + s = cn1LinuxDecodeImage(bytes, length); + free(bytes); + return s ? (JAVA_LONG) (intptr_t) cn1WrapSurface(s) : 0; +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_createImageFromBytes___byte_1ARRAY_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT data, JAVA_INT offset, JAVA_INT length) { + cairo_surface_t* s; + if (data == JAVA_NULL || length <= 0) { + return 0; + } + /* Never read outside the array, whatever the caller passed. */ + if (offset < 0 || length > ((JAVA_ARRAY) data)->length - offset) { + return 0; + } + s = cn1LinuxDecodeImage((unsigned char*) CN1_ARRAY_DATA(data) + offset, (size_t) length); + return s ? (JAVA_LONG) (intptr_t) cn1WrapSurface(s) : 0; +} +#else static CN1Image* cn1ImageFromPixbuf(GdkPixbuf* pixbuf) { cairo_surface_t* s; cairo_t* cr; @@ -200,6 +258,8 @@ JAVA_LONG com_codename1_impl_linux_LinuxNative_createImageFromBytes___byte_1ARRA return (JAVA_LONG) (intptr_t) img; } +#endif /* CN1_LINUX_EMBEDDED */ + JAVA_LONG com_codename1_impl_linux_LinuxNative_createMutableImage___int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT width, JAVA_INT height, JAVA_INT fillColor) { cairo_surface_t* s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width > 0 ? width : 1, height > 0 ? height : 1); cairo_t* cr = cairo_create(s); diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_raster.c b/Ports/LinuxPort/nativeSources/cn1_linux_raster.c new file mode 100644 index 00000000000..b3231b2bce1 --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_raster.c @@ -0,0 +1,1602 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The embedded target's 2D renderer; cn1_linux_raster.h says what it covers and + * why it exists. + * + * Coverage: every pixel row is sampled on CN1R_SAMPLES horizontal lines, and on + * each line the inside intervals (by the fill rule) are added with their exact + * fractional ends. That is exact area coverage horizontally and 1/16 steps + * vertically, and because the winding is counted per sample line, overlapping + * pieces of one path -- a stroke is built as overlapping quads -- union exactly + * rather than adding up. + * + * Shared by the drawing thread and whichever thread decodes or scales an image, + * so the scratch buffers are per thread and nothing else is global. + */ +#ifdef CN1_LINUX_EMBEDDED + +#include "cn1_linux_raster.h" +#include +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#define CN1R_SAMPLES 16 +#define CN1R_TOLERANCE 0.1 +#define CN1R_MITER_LIMIT 10.0 +/* Coordinates past this are clamped: nothing on a screen is that far out, and an + * unclamped NaN or 1e300 would otherwise walk a loop for ever. */ +#define CN1R_FAR 1.0e6 + +struct cn1r_surface { + int width; + int height; + int stride; + uint32_t* data; +}; + +enum { CN1R_SOLID = 0, CN1R_SURFACE = 1 }; + +struct cn1r_pattern { + int refs; + int kind; + uint32_t pixel; /* solid: premultiplied ARGB */ + cairo_surface_t* surface; /* surface: not owned (Cairo references it; the port + * never frees a source while it is set) */ + cairo_matrix_t toSource; /* device space -> surface pixels */ + double bx0, by0, bx1, by1; /* the surface's extent in device space */ + int filter; +}; + +typedef struct { + int refs; + int x0, y0, x1, y1; /* bounds, device pixels, x1/y1 exclusive */ + unsigned char* mask; /* NULL: the whole rectangle; else (x1-x0)*(y1-y0) */ +} CN1RClip; + +typedef struct CN1RState { + cairo_matrix_t matrix; + cairo_pattern_t* source; + int op; + double lineWidth; + int fillRule; + CN1RClip* clip; /* NULL: no clip */ + struct CN1RState* next; +} CN1RState; + +typedef struct { + int start; + int count; + int closed; +} CN1RSub; + +struct cn1r_context { + cairo_surface_t* target; + CN1RState state; + CN1RState* saved; + double* pts; /* device space x,y pairs */ + int ptCount; + int ptCapacity; + CN1RSub* subs; + int subCount; + int subCapacity; + int hasCurrent; + int reopen; /* close_path ran: the next segment starts a subpath */ + double curX, curY; /* current point, device space */ + double startX, startY; /* start of the current subpath, device space */ +}; + +typedef struct { + double x0, y0, x1, y1; /* y0 < y1 */ + double dxdy; + int dir; +} CN1REdge; + +typedef struct { + CN1REdge* edges; + int count; + int capacity; + double minX, minY, maxX, maxY; +} CN1REdges; + +/* ------------------------------------------------------------------ scratch */ + +static __thread float* cn1rCov = 0; +static __thread float* cn1rRun = 0; +static __thread int cn1rCovCapacity = 0; +static __thread double* cn1rCross = 0; +static __thread int* cn1rCrossDir = 0; +static __thread int cn1rCrossCapacity = 0; +static __thread CN1REdge** cn1rActive = 0; +static __thread int* cn1rDirty = 0; +static __thread int cn1rDirtyCount = 0; +static __thread int cn1rDirtyCapacity = 0; + +static __thread unsigned char* cn1rFlag = 0; + +/* Records that cell i of the row holds a partial coverage or a run delta. Every + * sample line crosses the same few cells, so each is listed once. */ +static inline void cn1rMarkDirty(int i) { + if (cn1rFlag[i]) { + return; + } + if (cn1rDirtyCount == cn1rDirtyCapacity) { + int capacity = cn1rDirtyCapacity == 0 ? 256 : cn1rDirtyCapacity * 2; + int* grown = (int*) realloc(cn1rDirty, sizeof(int) * (size_t) capacity); + if (grown == 0) { + return; + } + cn1rDirty = grown; + cn1rDirtyCapacity = capacity; + } + cn1rFlag[i] = 1; + cn1rDirty[cn1rDirtyCount++] = i; +} + +static int cn1rIntOrder(const void* a, const void* b) { + int x = *(const int*) a, y = *(const int*) b; + return x < y ? -1 : x > y; +} +static __thread int cn1rActiveCapacity = 0; + +static int cn1rEnsureCov(int width) { + if (width + 2 > cn1rCovCapacity) { + int capacity = width + 64; + float* cov = (float*) realloc(cn1rCov, sizeof(float) * (size_t) capacity); + float* run; + unsigned char* flag; + if (cov == 0) { + return 0; + } + cn1rCov = cov; + run = (float*) realloc(cn1rRun, sizeof(float) * (size_t) capacity); + if (run == 0) { + return 0; + } + cn1rRun = run; + flag = (unsigned char*) realloc(cn1rFlag, (size_t) capacity); + if (flag == 0) { + return 0; + } + cn1rFlag = flag; + cn1rCovCapacity = capacity; + } + return 1; +} + +static int cn1rEnsureCross(int count) { + if (count > cn1rCrossCapacity) { + int capacity = count + 64; + double* cross = (double*) realloc(cn1rCross, sizeof(double) * (size_t) capacity); + int* dir; + CN1REdge** active; + if (cross == 0) { + return 0; + } + cn1rCross = cross; + dir = (int*) realloc(cn1rCrossDir, sizeof(int) * (size_t) capacity); + if (dir == 0) { + return 0; + } + cn1rCrossDir = dir; + active = (CN1REdge**) realloc(cn1rActive, sizeof(CN1REdge*) * (size_t) capacity); + if (active == 0) { + return 0; + } + cn1rActive = active; + cn1rCrossCapacity = capacity; + cn1rActiveCapacity = capacity; + } + return 1; +} + +/* ------------------------------------------------------------------- matrix */ + +void cn1r_matrix_init(cairo_matrix_t* m, double xx, double yx, double xy, double yy, double x0, double y0) { + m->xx = xx; + m->yx = yx; + m->xy = xy; + m->yy = yy; + m->x0 = x0; + m->y0 = y0; +} + +void cn1r_matrix_init_identity(cairo_matrix_t* m) { + cn1r_matrix_init(m, 1, 0, 0, 1, 0, 0); +} + +void cn1r_matrix_transform_point(const cairo_matrix_t* m, double* x, double* y) { + double nx = m->xx * *x + m->xy * *y + m->x0; + double ny = m->yx * *x + m->yy * *y + m->y0; + *x = nx; + *y = ny; +} + +static int cn1rInvert(const cairo_matrix_t* m, cairo_matrix_t* out) { + double det = m->xx * m->yy - m->xy * m->yx; + if (det == 0 || !isfinite(det)) { + return 0; + } + out->xx = m->yy / det; + out->xy = -m->xy / det; + out->yx = -m->yx / det; + out->yy = m->xx / det; + out->x0 = -(out->xx * m->x0 + out->xy * m->y0); + out->y0 = -(out->yx * m->x0 + out->yy * m->y0); + return 1; +} + +/* The largest factor the matrix stretches a length by (for flattening). */ +static double cn1rMaxScale(const cairo_matrix_t* m) { + double a = sqrt(m->xx * m->xx + m->yx * m->yx); + double b = sqrt(m->xy * m->xy + m->yy * m->yy); + return a > b ? a : b; +} + +/* ------------------------------------------------------------------ surface */ + +cairo_surface_t* cn1r_image_surface_create(cairo_format_t format, int width, int height) { + cairo_surface_t* s; + (void) format; + if (width < 1) { + width = 1; + } + if (height < 1) { + height = 1; + } + s = (cairo_surface_t*) calloc(1, sizeof(cairo_surface_t)); + if (s == 0) { + return 0; + } + s->width = width; + s->height = height; + s->stride = width * 4; + /* Zeroed, as Cairo's are: a new surface is transparent. */ + s->data = (uint32_t*) calloc((size_t) width * (size_t) height, 4); + if (s->data == 0) { + free(s); + return 0; + } + return s; +} + +unsigned char* cn1r_image_surface_get_data(cairo_surface_t* s) { + return s ? (unsigned char*) s->data : 0; +} + +int cn1r_image_surface_get_stride(cairo_surface_t* s) { + return s ? s->stride : 0; +} + +int cn1r_image_surface_get_width(cairo_surface_t* s) { + return s ? s->width : 0; +} + +int cn1r_image_surface_get_height(cairo_surface_t* s) { + return s ? s->height : 0; +} + +void cn1r_surface_destroy(cairo_surface_t* s) { + if (s) { + free(s->data); + free(s); + } +} + +void cn1r_surface_flush(cairo_surface_t* s) { + (void) s; +} + +void cn1r_surface_mark_dirty(cairo_surface_t* s) { + (void) s; +} + +cairo_status_t cn1r_surface_status(cairo_surface_t* s) { + return s ? CAIRO_STATUS_SUCCESS : CAIRO_STATUS_NO_MEMORY; +} + +/* ---------------------------------------------------------------- png write */ + +typedef struct { + cairo_write_func_t write; + void* closure; + int failed; +} CN1RPngSink; + +static void cn1rPngWriteData(png_structp png, png_bytep data, png_size_t length) { + CN1RPngSink* sink = (CN1RPngSink*) png_get_io_ptr(png); + if (!sink->failed && sink->write(sink->closure, data, (unsigned int) length) != CAIRO_STATUS_SUCCESS) { + sink->failed = 1; + } +} + +static void cn1rPngFlush(png_structp png) { + (void) png; +} + +cairo_status_t cn1r_surface_write_to_png_stream(cairo_surface_t* s, cairo_write_func_t write, void* closure) { + png_structp png; + png_infop info; + unsigned char* row; + CN1RPngSink sink; + int x, y; + if (s == 0 || write == 0) { + return CAIRO_STATUS_WRITE_ERROR; + } + row = (unsigned char*) malloc((size_t) s->width * 4); + if (row == 0) { + return CAIRO_STATUS_NO_MEMORY; + } + png = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); + info = png ? png_create_info_struct(png) : 0; + if (info == 0) { + png_destroy_write_struct(&png, 0); + free(row); + return CAIRO_STATUS_NO_MEMORY; + } + if (setjmp(png_jmpbuf(png))) { + png_destroy_write_struct(&png, &info); + free(row); + return CAIRO_STATUS_WRITE_ERROR; + } + sink.write = write; + sink.closure = closure; + sink.failed = 0; + png_set_write_fn(png, &sink, cn1rPngWriteData, cn1rPngFlush); + png_set_IHDR(png, info, (png_uint_32) s->width, (png_uint_32) s->height, 8, PNG_COLOR_TYPE_RGB_ALPHA, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + for (y = 0; y < s->height; y++) { + const uint32_t* src = s->data + (size_t) y * (size_t) s->width; + for (x = 0; x < s->width; x++) { + uint32_t p = src[x]; + unsigned a = p >> 24, r = (p >> 16) & 0xff, g = (p >> 8) & 0xff, b = p & 0xff; + if (a != 0 && a != 255) { + r = (r * 255 + a / 2) / a; + g = (g * 255 + a / 2) / a; + b = (b * 255 + a / 2) / a; + if (r > 255) r = 255; + if (g > 255) g = 255; + if (b > 255) b = 255; + } + row[x * 4] = (unsigned char) r; + row[x * 4 + 1] = (unsigned char) g; + row[x * 4 + 2] = (unsigned char) b; + row[x * 4 + 3] = (unsigned char) a; + } + png_write_row(png, row); + } + png_write_end(png, info); + png_destroy_write_struct(&png, &info); + free(row); + return sink.failed ? CAIRO_STATUS_WRITE_ERROR : CAIRO_STATUS_SUCCESS; +} + +static cairo_status_t cn1rFileWrite(void* closure, const unsigned char* data, unsigned int length) { + return fwrite(data, 1, length, (FILE*) closure) == length ? CAIRO_STATUS_SUCCESS : CAIRO_STATUS_WRITE_ERROR; +} + +cairo_status_t cn1r_surface_write_to_png(cairo_surface_t* s, const char* filename) { + FILE* f = filename ? fopen(filename, "wb") : 0; + cairo_status_t st; + if (f == 0) { + return CAIRO_STATUS_WRITE_ERROR; + } + st = cn1r_surface_write_to_png_stream(s, cn1rFileWrite, f); + if (fclose(f) != 0 && st == CAIRO_STATUS_SUCCESS) { + st = CAIRO_STATUS_WRITE_ERROR; + } + return st; +} + +/* --------------------------------------------------------- pattern and clip */ + +static cairo_pattern_t* cn1rSolid(double r, double g, double b, double a) { + cairo_pattern_t* p = (cairo_pattern_t*) calloc(1, sizeof(cairo_pattern_t)); + unsigned ia, ir, ig, ib; + if (p == 0) { + return 0; + } + if (a < 0) a = 0; + if (a > 1) a = 1; + if (r < 0) r = 0; + if (r > 1) r = 1; + if (g < 0) g = 0; + if (g > 1) g = 1; + if (b < 0) b = 0; + if (b > 1) b = 1; + ia = (unsigned) (a * 255 + 0.5); + ir = (unsigned) (r * a * 255 + 0.5); + ig = (unsigned) (g * a * 255 + 0.5); + ib = (unsigned) (b * a * 255 + 0.5); + p->refs = 1; + p->kind = CN1R_SOLID; + p->pixel = (ia << 24) | (ir << 16) | (ig << 8) | ib; + return p; +} + +static void cn1rPatternRelease(cairo_pattern_t* p) { + if (p != 0 && --p->refs == 0) { + free(p); + } +} + +static void cn1rClipRelease(CN1RClip* c) { + if (c != 0 && --c->refs == 0) { + free(c->mask); + free(c); + } +} + +void cn1r_pattern_set_filter(cairo_pattern_t* p, cairo_filter_t filter) { + if (p) { + p->filter = (int) filter; + } +} + +/* ------------------------------------------------------------------ context */ + +cairo_t* cn1r_create(cairo_surface_t* target) { + cairo_t* cr = (cairo_t*) calloc(1, sizeof(cairo_t)); + if (cr == 0) { + return 0; + } + cr->target = target; + cn1r_matrix_init_identity(&cr->state.matrix); + cr->state.source = cn1rSolid(0, 0, 0, 1); + cr->state.op = CAIRO_OPERATOR_OVER; + cr->state.lineWidth = 2.0; + cr->state.fillRule = CAIRO_FILL_RULE_WINDING; + return cr; +} + +void cn1r_destroy(cairo_t* cr) { + if (cr == 0) { + return; + } + while (cr->saved != 0) { + cn1r_restore(cr); + } + cn1rPatternRelease(cr->state.source); + cn1rClipRelease(cr->state.clip); + free(cr->pts); + free(cr->subs); + free(cr); +} + +void cn1r_save(cairo_t* cr) { + CN1RState* s = (CN1RState*) malloc(sizeof(CN1RState)); + if (s == 0) { + return; + } + *s = cr->state; + if (s->source) s->source->refs++; + if (s->clip) s->clip->refs++; + s->next = cr->saved; + cr->saved = s; +} + +void cn1r_restore(cairo_t* cr) { + CN1RState* s = cr->saved; + if (s == 0) { + return; + } + cn1rPatternRelease(cr->state.source); + cn1rClipRelease(cr->state.clip); + cr->saved = s->next; + cr->state = *s; + cr->state.next = 0; + free(s); +} + +static void cn1rSetSource(cairo_t* cr, cairo_pattern_t* p) { + if (p == 0) { + return; + } + cn1rPatternRelease(cr->state.source); + cr->state.source = p; +} + +void cn1r_set_source_rgb(cairo_t* cr, double r, double g, double b) { + cn1rSetSource(cr, cn1rSolid(r, g, b, 1)); +} + +void cn1r_set_source_rgba(cairo_t* cr, double r, double g, double b, double a) { + cn1rSetSource(cr, cn1rSolid(r, g, b, a)); +} + +void cn1r_set_source_surface(cairo_t* cr, cairo_surface_t* surface, double x, double y) { + cairo_pattern_t* p; + cairo_matrix_t toUser; + double cx[4], cy[4]; + int i; + if (surface == 0) { + return; + } + p = (cairo_pattern_t*) calloc(1, sizeof(cairo_pattern_t)); + if (p == 0) { + return; + } + p->refs = 1; + p->kind = CN1R_SURFACE; + p->surface = surface; + p->filter = CAIRO_FILTER_GOOD; + if (!cn1rInvert(&cr->state.matrix, &toUser)) { + /* A singular matrix draws nothing; an empty extent says so. */ + p->bx0 = p->by0 = p->bx1 = p->by1 = 0; + cn1r_matrix_init_identity(&p->toSource); + cn1rSetSource(cr, p); + return; + } + /* Device -> user, then user -> surface: the surface's origin is at (x, y). */ + p->toSource = toUser; + p->toSource.x0 -= x; + p->toSource.y0 -= y; + cx[0] = x; cy[0] = y; + cx[1] = x + surface->width; cy[1] = y; + cx[2] = x; cy[2] = y + surface->height; + cx[3] = x + surface->width; cy[3] = y + surface->height; + p->bx0 = p->by0 = 1e30; + p->bx1 = p->by1 = -1e30; + for (i = 0; i < 4; i++) { + cn1r_matrix_transform_point(&cr->state.matrix, &cx[i], &cy[i]); + if (cx[i] < p->bx0) p->bx0 = cx[i]; + if (cx[i] > p->bx1) p->bx1 = cx[i]; + if (cy[i] < p->by0) p->by0 = cy[i]; + if (cy[i] > p->by1) p->by1 = cy[i]; + } + cn1rSetSource(cr, p); +} + +cairo_pattern_t* cn1r_get_source(cairo_t* cr) { + return cr->state.source; +} + +void cn1r_set_operator(cairo_t* cr, cairo_operator_t op) { + cr->state.op = (int) op; +} + +void cn1r_set_line_width(cairo_t* cr, double width) { + cr->state.lineWidth = width; +} + +void cn1r_set_fill_rule(cairo_t* cr, cairo_fill_rule_t rule) { + cr->state.fillRule = (int) rule; +} + +void cn1r_set_matrix(cairo_t* cr, const cairo_matrix_t* m) { + cr->state.matrix = *m; +} + +void cn1r_get_matrix(cairo_t* cr, cairo_matrix_t* m) { + *m = cr->state.matrix; +} + +void cn1r_identity_matrix(cairo_t* cr) { + cn1r_matrix_init_identity(&cr->state.matrix); +} + +void cn1r_translate(cairo_t* cr, double tx, double ty) { + cairo_matrix_t* m = &cr->state.matrix; + m->x0 += m->xx * tx + m->xy * ty; + m->y0 += m->yx * tx + m->yy * ty; +} + +void cn1r_scale(cairo_t* cr, double sx, double sy) { + cairo_matrix_t* m = &cr->state.matrix; + m->xx *= sx; + m->yx *= sx; + m->xy *= sy; + m->yy *= sy; +} + +/* --------------------------------------------------------------------- path */ + +static double cn1rClamp(double v) { + if (!(v > -CN1R_FAR)) { + return -CN1R_FAR; + } + return v < CN1R_FAR ? v : CN1R_FAR; +} + +static void cn1rAddPoint(cairo_t* cr, double x, double y) { + if (cr->ptCount == cr->ptCapacity) { + int capacity = cr->ptCapacity == 0 ? 64 : cr->ptCapacity * 2; + double* pts = (double*) realloc(cr->pts, sizeof(double) * 2 * (size_t) capacity); + if (pts == 0) { + return; + } + cr->pts = pts; + cr->ptCapacity = capacity; + } + cr->pts[cr->ptCount * 2] = cn1rClamp(x); + cr->pts[cr->ptCount * 2 + 1] = cn1rClamp(y); + cr->ptCount++; + cr->subs[cr->subCount - 1].count++; + cr->curX = x; + cr->curY = y; +} + +static void cn1rBeginSub(cairo_t* cr, double x, double y) { + if (cr->subCount == cr->subCapacity) { + int capacity = cr->subCapacity == 0 ? 8 : cr->subCapacity * 2; + CN1RSub* subs = (CN1RSub*) realloc(cr->subs, sizeof(CN1RSub) * (size_t) capacity); + if (subs == 0) { + return; + } + cr->subs = subs; + cr->subCapacity = capacity; + } + cr->subs[cr->subCount].start = cr->ptCount; + cr->subs[cr->subCount].count = 0; + cr->subs[cr->subCount].closed = 0; + cr->subCount++; + cr->hasCurrent = 1; + cr->reopen = 0; + cr->startX = x; + cr->startY = y; + cn1rAddPoint(cr, x, y); +} + +/* A segment ending at device (x, y): starts a subpath at the current point when + * the previous one was closed, as Cairo's implicit move_to does. */ +static void cn1rLineDevice(cairo_t* cr, double x, double y) { + if (!cr->hasCurrent) { + cn1rBeginSub(cr, x, y); + return; + } + if (cr->reopen) { + cn1rBeginSub(cr, cr->curX, cr->curY); + } + cn1rAddPoint(cr, x, y); +} + +void cn1r_new_path(cairo_t* cr) { + cr->ptCount = 0; + cr->subCount = 0; + cr->hasCurrent = 0; + cr->reopen = 0; +} + +void cn1r_new_sub_path(cairo_t* cr) { + cr->hasCurrent = 0; +} + +void cn1r_move_to(cairo_t* cr, double x, double y) { + cn1r_matrix_transform_point(&cr->state.matrix, &x, &y); + cn1rBeginSub(cr, x, y); +} + +void cn1r_line_to(cairo_t* cr, double x, double y) { + cn1r_matrix_transform_point(&cr->state.matrix, &x, &y); + cn1rLineDevice(cr, x, y); +} + +void cn1r_curve_to(cairo_t* cr, double x1, double y1, double x2, double y2, double x3, double y3) { + double p0x, p0y, ddx, ddy, dd, d2; + int n, i; + if (!cr->hasCurrent) { + cn1r_move_to(cr, x1, y1); + } + cn1r_matrix_transform_point(&cr->state.matrix, &x1, &y1); + cn1r_matrix_transform_point(&cr->state.matrix, &x2, &y2); + cn1r_matrix_transform_point(&cr->state.matrix, &x3, &y3); + p0x = cr->curX; + p0y = cr->curY; + /* An affine map keeps a Bezier a Bezier, so it is flattened in device space; + * the second differences bound how far a chord strays from the curve. */ + ddx = fabs(p0x - 2 * x1 + x2); + ddy = fabs(p0y - 2 * y1 + y2); + dd = ddx > ddy ? ddx : ddy; + ddx = fabs(x1 - 2 * x2 + x3); + ddy = fabs(y1 - 2 * y2 + y3); + d2 = ddx > ddy ? ddx : ddy; + if (d2 > dd) { + dd = d2; + } + n = (int) ceil(sqrt(0.75 * dd / CN1R_TOLERANCE)); + if (n < 1 || !isfinite(dd)) { + n = 1; + } + if (n > 200) { + n = 200; + } + for (i = 1; i <= n; i++) { + double t = (double) i / n, u = 1 - t; + double a = u * u * u, b = 3 * u * u * t, c = 3 * u * t * t, d = t * t * t; + cn1rLineDevice(cr, a * p0x + b * x1 + c * x2 + d * x3, a * p0y + b * y1 + c * y2 + d * y3); + } +} + +static void cn1rArc(cairo_t* cr, double xc, double yc, double r, double a1, double a2) { + double sweep = a2 - a1; + double rDevice = fabs(r) * cn1rMaxScale(&cr->state.matrix); + double step; + int n, i; + if (!isfinite(sweep) || !isfinite(rDevice)) { + return; + } + if (rDevice > CN1R_TOLERANCE) { + step = 2 * acos(1 - CN1R_TOLERANCE / rDevice); + } else { + step = M_PI / 2; + } + n = (int) ceil(fabs(sweep) / step); + if (n < 1) { + n = 1; + } + if (n > 4096) { + n = 4096; + } + for (i = 0; i <= n; i++) { + double a = a1 + sweep * i / n; + double x = xc + r * cos(a), y = yc + r * sin(a); + cn1r_matrix_transform_point(&cr->state.matrix, &x, &y); + /* Cairo joins an arc to the current point with a line. */ + cn1rLineDevice(cr, x, y); + } +} + +void cn1r_arc(cairo_t* cr, double xc, double yc, double r, double a1, double a2) { + if (!(a2 >= a1)) { + /* Cairo's rule: the end angle is advanced by whole turns past the start. */ + double turns = ceil((a1 - a2) / (2 * M_PI)); + if (!isfinite(turns)) { + return; + } + a2 += turns * 2 * M_PI; + } + cn1rArc(cr, xc, yc, r, a1, a2); +} + +void cn1r_arc_negative(cairo_t* cr, double xc, double yc, double r, double a1, double a2) { + if (!(a2 <= a1)) { + double turns = ceil((a2 - a1) / (2 * M_PI)); + if (!isfinite(turns)) { + return; + } + a2 -= turns * 2 * M_PI; + } + cn1rArc(cr, xc, yc, r, a1, a2); +} + +void cn1r_close_path(cairo_t* cr) { + if (!cr->hasCurrent || cr->subCount == 0) { + return; + } + cr->subs[cr->subCount - 1].closed = 1; + cr->curX = cr->startX; + cr->curY = cr->startY; + cr->reopen = 1; +} + +void cn1r_rectangle(cairo_t* cr, double x, double y, double w, double h) { + cn1r_move_to(cr, x, y); + cn1r_line_to(cr, x + w, y); + cn1r_line_to(cr, x + w, y + h); + cn1r_line_to(cr, x, y + h); + cn1r_close_path(cr); +} + +/* -------------------------------------------------------------------- edges */ + +static void cn1rEdge(CN1REdges* e, double x0, double y0, double x1, double y1) { + CN1REdge* edge; + int dir = 1; + if (y0 == y1) { + return; + } + if (y0 > y1) { + double t = x0; + x0 = x1; + x1 = t; + t = y0; + y0 = y1; + y1 = t; + dir = -1; + } + if (e->count == e->capacity) { + int capacity = e->capacity == 0 ? 64 : e->capacity * 2; + CN1REdge* edges = (CN1REdge*) realloc(e->edges, sizeof(CN1REdge) * (size_t) capacity); + if (edges == 0) { + return; + } + e->edges = edges; + e->capacity = capacity; + } + edge = &e->edges[e->count++]; + edge->x0 = x0; + edge->y0 = y0; + edge->x1 = x1; + edge->y1 = y1; + edge->dxdy = (x1 - x0) / (y1 - y0); + edge->dir = dir; + if (x0 < e->minX) e->minX = x0; + if (x1 < e->minX) e->minX = x1; + if (x0 > e->maxX) e->maxX = x0; + if (x1 > e->maxX) e->maxX = x1; + if (y0 < e->minY) e->minY = y0; + if (y1 > e->maxY) e->maxY = y1; +} + +static void cn1rEdgesInit(CN1REdges* e) { + memset(e, 0, sizeof(*e)); + e->minX = e->minY = 1e30; + e->maxX = e->maxY = -1e30; +} + +static void cn1rPolygon(CN1REdges* e, const double* p, int n) { + int i; + for (i = 0; i < n; i++) { + int j = i + 1 == n ? 0 : i + 1; + cn1rEdge(e, p[i * 2], p[i * 2 + 1], p[j * 2], p[j * 2 + 1]); + } +} + +/* Every subpath, closed whether or not close_path ran (a fill closes them). */ +static void cn1rPathEdges(cairo_t* cr, CN1REdges* e) { + int s; + for (s = 0; s < cr->subCount; s++) { + if (cr->subs[s].count >= 2) { + cn1rPolygon(e, cr->pts + cr->subs[s].start * 2, cr->subs[s].count); + } + } +} + +static int cn1rEdgeOrder(const void* a, const void* b) { + double d = ((const CN1REdge*) a)->y0 - ((const CN1REdge*) b)->y0; + return d < 0 ? -1 : d > 0 ? 1 : 0; +} + +/* ------------------------------------------------------------------ stroker */ + +static double cn1rSignedArea(const double* p, int n) { + double a = 0; + int i; + for (i = 0; i < n; i++) { + int j = i + 1 == n ? 0 : i + 1; + a += p[i * 2] * p[j * 2 + 1] - p[j * 2] * p[i * 2 + 1]; + } + return a; +} + +/* Adds a piece of a stroke wound the same way as every other piece, so the + * nonzero rule unions them instead of letting opposite windings cancel. */ +static void cn1rStrokePiece(CN1REdges* e, double* p, int n) { + if (cn1rSignedArea(p, n) < 0) { + int i; + for (i = 0; i < n / 2; i++) { + double tx = p[i * 2], ty = p[i * 2 + 1]; + p[i * 2] = p[(n - 1 - i) * 2]; + p[i * 2 + 1] = p[(n - 1 - i) * 2 + 1]; + p[(n - 1 - i) * 2] = tx; + p[(n - 1 - i) * 2 + 1] = ty; + } + } + cn1rPolygon(e, p, n); +} + +static void cn1rJoin(CN1REdges* e, double px, double py, double ux1, double uy1, double ux2, double uy2, double hw) { + double cross = ux1 * uy2 - uy1 * ux2; + double dot = ux1 * ux2 + uy1 * uy2; + double o, ax, ay, bx, by, q[8]; + if (fabs(cross) < 1e-9 && dot > 0) { + return; + } + /* The gap opens on the side away from the turn. */ + o = cross > 0 ? -1 : 1; + ax = px + o * -uy1 * hw; + ay = py + o * ux1 * hw; + bx = px + o * -uy2 * hw; + by = py + o * ux2 * hw; + q[0] = px; + q[1] = py; + q[2] = ax; + q[3] = ay; + if ((1 + dot) / 2 >= 1 / (CN1R_MITER_LIMIT * CN1R_MITER_LIMIT)) { + /* The miter tip: where the two outer edges meet. */ + double mx = -uy1 - uy2, my = ux1 + ux2; + double k = o * hw / (1 + dot); + q[4] = px + mx * k; + q[5] = py + my * k; + q[6] = bx; + q[7] = by; + cn1rStrokePiece(e, q, 4); + } else { + q[4] = bx; + q[5] = by; + cn1rStrokePiece(e, q, 3); + } +} + +static void cn1rStrokeEdges(cairo_t* cr, CN1REdges* e) { + const cairo_matrix_t* m = &cr->state.matrix; + double hw = cr->state.lineWidth * sqrt(fabs(m->xx * m->yy - m->xy * m->yx)) / 2; + int s; + if (!(hw > 0)) { + return; + } + for (s = 0; s < cr->subCount; s++) { + const double* src = cr->pts + cr->subs[s].start * 2; + int count = cr->subs[s].count; + int closed = cr->subs[s].closed; + double* p; + int n = 0, i, segs; + if (count < 2) { + continue; + } + p = (double*) malloc(sizeof(double) * 2 * (size_t) count); + if (p == 0) { + continue; + } + /* Drop repeated points; they have no direction. */ + for (i = 0; i < count; i++) { + if (n == 0 || fabs(src[i * 2] - p[(n - 1) * 2]) > 1e-9 || fabs(src[i * 2 + 1] - p[(n - 1) * 2 + 1]) > 1e-9) { + p[n * 2] = src[i * 2]; + p[n * 2 + 1] = src[i * 2 + 1]; + n++; + } + } + if (closed && n > 2 && fabs(p[0] - p[(n - 1) * 2]) < 1e-9 && fabs(p[1] - p[(n - 1) * 2 + 1]) < 1e-9) { + n--; + } + if (n < 2) { + free(p); + continue; + } + segs = closed ? n : n - 1; + for (i = 0; i < segs; i++) { + int j = i + 1 == n ? 0 : i + 1; + double dx = p[j * 2] - p[i * 2], dy = p[j * 2 + 1] - p[i * 2 + 1]; + double len = sqrt(dx * dx + dy * dy); + double nx, ny, q[8]; + if (len <= 0) { + continue; + } + nx = -dy / len * hw; + ny = dx / len * hw; + q[0] = p[i * 2] + nx; + q[1] = p[i * 2 + 1] + ny; + q[2] = p[j * 2] + nx; + q[3] = p[j * 2 + 1] + ny; + q[4] = p[j * 2] - nx; + q[5] = p[j * 2 + 1] - ny; + q[6] = p[i * 2] - nx; + q[7] = p[i * 2 + 1] - ny; + cn1rStrokePiece(e, q, 4); + /* The join at the segment's end, when another segment follows. */ + if (i + 1 < segs || closed) { + int k = j + 1 == n ? 0 : j + 1; + double ex = p[k * 2] - p[j * 2], ey = p[k * 2 + 1] - p[j * 2 + 1]; + double elen = sqrt(ex * ex + ey * ey); + if (elen > 0) { + cn1rJoin(e, p[j * 2], p[j * 2 + 1], dx / len, dy / len, ex / elen, ey / elen, hw); + } + } + } + free(p); + } +} + +/* -------------------------------------------------------------- compositing */ + +static inline uint32_t cn1rScale(uint32_t p, unsigned c) { + /* Each channel times c/255, rounded: two channels per multiply. */ + uint32_t rb = (p & 0x00ff00ffu) * c + 0x00800080u; + uint32_t ag = ((p >> 8) & 0x00ff00ffu) * c + 0x00800080u; + rb = ((rb + ((rb >> 8) & 0x00ff00ffu)) >> 8) & 0x00ff00ffu; + ag = (ag + ((ag >> 8) & 0x00ff00ffu)) & 0xff00ff00u; + return rb | ag; +} + +static inline uint32_t cn1rOver(uint32_t s, uint32_t d) { + unsigned sa = s >> 24; + if (sa == 255) { + return s; + } + if (sa == 0 && s == 0) { + return d; + } + return s + cn1rScale(d, 255 - sa); +} + +static inline uint32_t cn1rBlend(int op, uint32_t s, uint32_t d, unsigned cov) { + if (op == CAIRO_OPERATOR_OVER) { + return cn1rOver(cov == 255 ? s : cn1rScale(s, cov), d); + } + if (op == CAIRO_OPERATOR_SOURCE) { + return cov == 255 ? s : cn1rScale(s, cov) + cn1rScale(d, 255 - cov); + } + /* CLEAR */ + return cov == 255 ? 0 : cn1rScale(d, 255 - cov); +} + +static inline uint32_t cn1rTexel(const cairo_surface_t* s, int x, int y) { + if (x < 0 || y < 0 || x >= s->width || y >= s->height) { + return 0; + } + return s->data[(size_t) y * (size_t) s->width + (size_t) x]; +} + +static uint32_t cn1rBilinear(const cairo_surface_t* s, double sx, double sy) { + double fx = sx - 0.5, fy = sy - 0.5; + int x0 = (int) floor(fx), y0 = (int) floor(fy); + unsigned wx = (unsigned) ((fx - x0) * 256), wy = (unsigned) ((fy - y0) * 256); + uint32_t a = cn1rTexel(s, x0, y0), b = cn1rTexel(s, x0 + 1, y0); + uint32_t c = cn1rTexel(s, x0, y0 + 1), d = cn1rTexel(s, x0 + 1, y0 + 1); + uint32_t out = 0; + int shift; + if (wx > 255) wx = 255; + if (wy > 255) wy = 255; + for (shift = 0; shift < 32; shift += 8) { + unsigned top = ((a >> shift) & 0xff) * (256 - wx) + ((b >> shift) & 0xff) * wx; + unsigned bottom = ((c >> shift) & 0xff) * (256 - wx) + ((d >> shift) & 0xff) * wx; + unsigned v = (top * (256 - wy) + bottom * wy + 32768) >> 16; + out |= (v > 255 ? 255 : v) << shift; + } + return out; +} + +/* Composites one device row [x0, x1) of the source, weighted by cov (NULL: full + * coverage), the clip's mask and alpha (0..255). */ +static void cn1rRow(cairo_t* cr, int y, int x0, int x1, const float* cov, unsigned alpha) { + cairo_surface_t* dst = cr->target; + cairo_pattern_t* src = cr->state.source; + CN1RClip* clip = cr->state.clip; + const unsigned char* mask = 0; + uint32_t* row = dst->data + (size_t) y * (size_t) dst->width; + int op = cr->state.op; + int x; + if (clip != 0 && clip->mask != 0) { + mask = clip->mask + (size_t) (y - clip->y0) * (size_t) (clip->x1 - clip->x0) - clip->x0; + } + if (src->kind == CN1R_SOLID) { + uint32_t pixel = alpha == 255 ? src->pixel : cn1rScale(src->pixel, alpha); + if (cov == 0 && mask == 0 && (op == CAIRO_OPERATOR_SOURCE || (pixel >> 24) == 255)) { + for (x = x0; x < x1; x++) { + row[x] = pixel; + } + return; + } + int store = mask == 0 && (op == CAIRO_OPERATOR_SOURCE || (pixel >> 24) == 255); + for (x = x0; x < x1; x++) { + unsigned c = 255; + if (cov != 0) { + float f = cov[x - x0]; + if (f <= 0) { + continue; + } + if (f >= 1 && store) { + /* The inside of a shape: a run of whole pixels, stored. Most of + * what a fill covers is here, and only its edges blend. */ + int end = x + 1; + while (end < x1 && cov[end - x0] >= 1) { + end++; + } + for (; x < end; x++) { + row[x] = pixel; + } + x--; + continue; + } + c = f >= 1 ? 255 : (unsigned) (f * 255 + 0.5f); + } + if (mask != 0) { + c = (c * mask[x] + 127) / 255; + } + if (c != 0) { + row[x] = cn1rBlend(op, pixel, row[x], c); + } + } + return; + } + { + const cairo_matrix_t* t = &src->toSource; + const cairo_surface_t* s = src->surface; + double px = x0 + 0.5, py = y + 0.5; + double sx = t->xx * px + t->xy * py + t->x0; + double sy = t->yx * px + t->yy * py + t->y0; + /* Whole pixels moved by whole pixels: the sample IS a texel, whatever the + * filter, so copy it. Every unscaled image the UI draws comes here. */ + int exact = t->xx == 1 && t->yy == 1 && t->xy == 0 && t->yx == 0 + && t->x0 == floor(t->x0) && t->y0 == floor(t->y0); + int nearest = src->filter == CAIRO_FILTER_NEAREST || src->filter == CAIRO_FILTER_FAST; + for (x = x0; x < x1; x++, sx += t->xx, sy += t->yx) { + unsigned c = 255; + uint32_t texel; + if (cov != 0) { + float f = cov[x - x0]; + if (f <= 0) { + continue; + } + c = f >= 1 ? 255 : (unsigned) (f * 255 + 0.5f); + } + if (mask != 0) { + c = (c * mask[x] + 127) / 255; + if (c == 0) { + continue; + } + } + if (exact || nearest) { + texel = cn1rTexel(s, (int) floor(sx), (int) floor(sy)); + } else { + texel = cn1rBilinear(s, sx, sy); + } + if (alpha != 255) { + texel = cn1rScale(texel, alpha); + } + if (texel != 0 || op != CAIRO_OPERATOR_OVER) { + row[x] = cn1rBlend(op, texel, row[x], c); + } + } + } +} + +/* The device rectangle drawing may touch: the surface, narrowed by the clip. */ +static void cn1rBounds(cairo_t* cr, int* x0, int* y0, int* x1, int* y1) { + *x0 = 0; + *y0 = 0; + *x1 = cr->target->width; + *y1 = cr->target->height; + if (cr->state.clip != 0) { + CN1RClip* c = cr->state.clip; + if (c->x0 > *x0) *x0 = c->x0; + if (c->y0 > *y0) *y0 = c->y0; + if (c->x1 < *x1) *x1 = c->x1; + if (c->y1 < *y1) *y1 = c->y1; + } +} + +static void cn1rNarrow(int* x0, int* y0, int* x1, int* y1, double fx0, double fy0, double fx1, double fy1) { + int a = (int) floor(fx0), b = (int) floor(fy0), c = (int) ceil(fx1), d = (int) ceil(fy1); + if (a > *x0) *x0 = a; + if (b > *y0) *y0 = b; + if (c < *x1) *x1 = c; + if (d < *y1) *y1 = d; +} + +/* + * Scan converts the edges over the device rectangle [x0,x1) x [y0,y1) and hands + * every covered span of every row to emit: either the coverage of each pixel of + * [rx0, rx1), or, with cov NULL, one coverage for all of them. + * + * Only the cells an edge crosses get a coverage of their own. Between them the + * coverage is constant, so the inside of a shape is one span whatever its width, + * and nothing walks the row pixel by pixel. + */ +typedef void (*CN1RRowSink)(void* ctx, int y, int rx0, int rx1, const float* cov, float constant); + +static void cn1rScan(CN1REdges* e, int rule, int x0, int y0, int x1, int y1, CN1RRowSink emit, void* ctx) { + int width = x1 - x0; + int next = 0, activeCount = 0; + int y; + if (e->count == 0 || width <= 0 || y1 <= y0) { + return; + } + if (!cn1rEnsureCov(width) || !cn1rEnsureCross(e->count)) { + return; + } + qsort(e->edges, (size_t) e->count, sizeof(CN1REdge), cn1rEdgeOrder); + { + double top = floor(e->minY); + if (top > y0) { + y0 = (int) top; + } + if (ceil(e->maxY) < y1) { + y1 = (int) ceil(e->maxY); + } + } + /* Both rows start clear, and each row clears the cells it dirtied. */ + memset(cn1rCov, 0, sizeof(float) * (size_t) (width + 2)); + memset(cn1rRun, 0, sizeof(float) * (size_t) (width + 2)); + memset(cn1rFlag, 0, (size_t) (width + 2)); + for (y = y0; y < y1; y++) { + int s, i; + cn1rDirtyCount = 0; + for (s = 0; s < CN1R_SAMPLES; s++) { + double sy = y + (s + 0.5) / CN1R_SAMPLES; + int n = 0, winding = 0, inside = 0; + double start = 0; + while (next < e->count && e->edges[next].y0 <= sy) { + cn1rActive[activeCount++] = &e->edges[next++]; + } + for (i = 0; i < activeCount;) { + CN1REdge* edge = cn1rActive[i]; + if (edge->y1 <= sy) { + cn1rActive[i] = cn1rActive[--activeCount]; + continue; + } + if (edge->y0 <= sy) { + double cx = edge->x0 + (sy - edge->y0) * edge->dxdy; + int j = n; + /* Insertion sort: a row crosses a handful of edges. */ + while (j > 0 && cn1rCross[j - 1] > cx) { + cn1rCross[j] = cn1rCross[j - 1]; + cn1rCrossDir[j] = cn1rCrossDir[j - 1]; + j--; + } + cn1rCross[j] = cx; + cn1rCrossDir[j] = edge->dir; + n++; + } + i++; + } + for (i = 0; i < n; i++) { + int now; + winding += cn1rCrossDir[i]; + now = rule == CAIRO_FILL_RULE_EVEN_ODD ? (winding & 1) : winding != 0; + if (now && !inside) { + start = cn1rCross[i]; + } else if (!now && inside) { + double a = start - x0, b = cn1rCross[i] - x0; + const float w = 1.0f / CN1R_SAMPLES; + if (a < 0) a = 0; + if (b > width) b = width; + if (b > a) { + int ia = (int) a, ib = (int) b; + if (ia == ib) { + cn1rCov[ia] += (float) (b - a) * w; + cn1rMarkDirty(ia); + } else { + cn1rCov[ia] += (float) (ia + 1 - a) * w; + cn1rCov[ib] += (float) (b - ib) * w; + cn1rMarkDirty(ia); + cn1rMarkDirty(ib); + if (ib > ia + 1) { + /* The run starts after cell ia and stops at ib. */ + cn1rRun[ia + 1] += w; + cn1rRun[ib] -= w; + cn1rMarkDirty(ia + 1); + } + } + } + } + inside = now; + } + } + if (cn1rDirtyCount > 0) { + float run = 0; + int pos = 0, k = 0, n = 0; + /* Sorted and unique; a row has a handful, so insertion sort. */ + if (cn1rDirtyCount > 64) { + qsort(cn1rDirty, (size_t) cn1rDirtyCount, sizeof(int), cn1rIntOrder); + } else { + for (i = 1; i < cn1rDirtyCount; i++) { + int v = cn1rDirty[i], j = i; + while (j > 0 && cn1rDirty[j - 1] > v) { + cn1rDirty[j] = cn1rDirty[j - 1]; + j--; + } + cn1rDirty[j] = v; + } + } + for (i = 0; i < cn1rDirtyCount; i++) { + cn1rFlag[cn1rDirty[i]] = 0; + } + n = cn1rDirtyCount; + while (k < n) { + int start = cn1rDirty[k], end = start; + if (start > pos && run > 0 && pos < width) { + emit(ctx, y, x0 + pos, x0 + (start < width ? start : width), 0, run); + } + /* A group of adjacent dirty cells, each with its own coverage. */ + while (1) { + run += cn1rRun[end]; + cn1rCov[end] += run; + cn1rRun[end] = 0; + if (k + 1 < n && cn1rDirty[k + 1] == end + 1) { + k++; + end++; + } else { + break; + } + } + if (start < width) { + emit(ctx, y, x0 + start, x0 + (end < width ? end + 1 : width), cn1rCov + start, 0); + } + memset(cn1rCov + start, 0, sizeof(float) * (size_t) (end - start + 1)); + pos = end + 1; + k++; + } + } + if (next >= e->count && activeCount == 0) { + break; + } + } +} + +typedef struct { + cairo_t* cr; + unsigned alpha; +} CN1RFillCtx; + +static void cn1rFillRow(void* ctx, int y, int rx0, int rx1, const float* cov, float constant) { + CN1RFillCtx* f = (CN1RFillCtx*) ctx; + if (cov == 0 && constant < 1 && f->cr->state.op != CAIRO_OPERATOR_OVER) { + /* SOURCE and CLEAR mix with what is there by the coverage, which a + * scaled source cannot express: hand the span over as coverage. */ + float buf[64]; + int i; + for (i = 0; i < 64; i++) { + buf[i] = constant; + } + for (; rx0 < rx1; rx0 += 64) { + cn1rRow(f->cr, y, rx0, rx1 - rx0 < 64 ? rx1 : rx0 + 64, buf, f->alpha); + } + return; + } + if (cov == 0 && constant < 1) { + unsigned a = (unsigned) (constant * f->alpha + 0.5f); + if (a != 0) { + cn1rRow(f->cr, y, rx0, rx1, 0, a); + } + return; + } + cn1rRow(f->cr, y, rx0, rx1, cov, f->alpha); +} + +/* An integer, axis-aligned rectangle in device space, which needs no scan. */ +static int cn1rPixelRect(cairo_t* cr, int* x0, int* y0, int* x1, int* y1) { + const double* p; + double ax, ay, bx, by; + int i; + if (cr->subCount != 1 || cr->subs[0].count < 4 || cr->subs[0].count > 5) { + return 0; + } + p = cr->pts + cr->subs[0].start * 2; + if (cr->subs[0].count == 5 && (p[8] != p[0] || p[9] != p[1])) { + return 0; + } + ax = bx = p[0]; + ay = by = p[1]; + for (i = 0; i < 4; i++) { + double x = p[i * 2], y = p[i * 2 + 1]; + if (x != floor(x) || y != floor(y)) { + return 0; + } + if (x < ax) ax = x; + if (x > bx) bx = x; + if (y < ay) ay = y; + if (y > by) by = y; + } + for (i = 0; i < 4; i++) { + int j = (i + 1) % 4; + if (p[i * 2] != p[j * 2] && p[i * 2 + 1] != p[j * 2 + 1]) { + return 0; + } + } + *x0 = (int) ax; + *y0 = (int) ay; + *x1 = (int) bx; + *y1 = (int) by; + return 1; +} + +static void cn1rFillEdges(cairo_t* cr, CN1REdges* e, int rule, unsigned alpha) { + int x0, y0, x1, y1; + CN1RFillCtx ctx; + cn1rBounds(cr, &x0, &y0, &x1, &y1); + cn1rNarrow(&x0, &y0, &x1, &y1, e->minX, e->minY, e->maxX, e->maxY); + if (cr->state.source->kind == CN1R_SURFACE) { + cairo_pattern_t* p = cr->state.source; + cn1rNarrow(&x0, &y0, &x1, &y1, p->bx0, p->by0, p->bx1, p->by1); + } + ctx.cr = cr; + ctx.alpha = alpha; + cn1rScan(e, rule, x0, y0, x1, y1, cn1rFillRow, &ctx); +} + +void cn1r_fill(cairo_t* cr) { + int rx0, ry0, rx1, ry1; + if (cr->target == 0 || cr->state.source == 0) { + cn1r_new_path(cr); + return; + } + if (cn1rPixelRect(cr, &rx0, &ry0, &rx1, &ry1)) { + int x0, y0, x1, y1, y; + cn1rBounds(cr, &x0, &y0, &x1, &y1); + if (rx0 > x0) x0 = rx0; + if (ry0 > y0) y0 = ry0; + if (rx1 < x1) x1 = rx1; + if (ry1 < y1) y1 = ry1; + if (cr->state.source->kind == CN1R_SURFACE) { + cairo_pattern_t* p = cr->state.source; + cn1rNarrow(&x0, &y0, &x1, &y1, p->bx0, p->by0, p->bx1, p->by1); + } + for (y = y0; y < y1; y++) { + if (x1 > x0) { + cn1rRow(cr, y, x0, x1, 0, 255); + } + } + } else { + CN1REdges e; + cn1rEdgesInit(&e); + cn1rPathEdges(cr, &e); + cn1rFillEdges(cr, &e, cr->state.fillRule, 255); + free(e.edges); + } + cn1r_new_path(cr); +} + +void cn1r_stroke(cairo_t* cr) { + CN1REdges e; + if (cr->target == 0 || cr->state.source == 0) { + cn1r_new_path(cr); + return; + } + cn1rEdgesInit(&e); + cn1rStrokeEdges(cr, &e); + cn1rFillEdges(cr, &e, CAIRO_FILL_RULE_WINDING, 255); + free(e.edges); + cn1r_new_path(cr); +} + +void cn1r_paint_with_alpha(cairo_t* cr, double alpha) { + int x0, y0, x1, y1, y; + unsigned a; + if (cr->target == 0 || cr->state.source == 0 || !(alpha > 0)) { + return; + } + a = alpha >= 1 ? 255 : (unsigned) (alpha * 255 + 0.5); + cn1rBounds(cr, &x0, &y0, &x1, &y1); + if (cr->state.source->kind == CN1R_SURFACE) { + cairo_pattern_t* p = cr->state.source; + cn1rNarrow(&x0, &y0, &x1, &y1, p->bx0, p->by0, p->bx1, p->by1); + } + for (y = y0; y < y1; y++) { + if (x1 > x0) { + cn1rRow(cr, y, x0, x1, 0, a); + } + } +} + +void cn1r_paint(cairo_t* cr) { + cn1r_paint_with_alpha(cr, 1.0); +} + +/* ---------------------------------------------------------------------- clip */ + +typedef struct { + CN1RClip* clip; + const CN1RClip* old; +} CN1RClipCtx; + +static void cn1rClipRow(void* ctx, int y, int rx0, int rx1, const float* cov, float constant) { + CN1RClipCtx* c = (CN1RClipCtx*) ctx; + int w = c->clip->x1 - c->clip->x0; + unsigned char* row = c->clip->mask + (size_t) (y - c->clip->y0) * (size_t) w - c->clip->x0; + const unsigned char* old = 0; + int x; + if (c->old != 0 && c->old->mask != 0) { + old = c->old->mask + (size_t) (y - c->old->y0) * (size_t) (c->old->x1 - c->old->x0) - c->old->x0; + } + for (x = rx0; x < rx1; x++) { + float f = cov != 0 ? cov[x - rx0] : constant; + unsigned v = f <= 0 ? 0 : f >= 1 ? 255 : (unsigned) (f * 255 + 0.5f); + if (old != 0) { + v = (v * old[x] + 127) / 255; + } + row[x] = (unsigned char) v; + } +} + +void cn1r_clip(cairo_t* cr) { + const CN1RClip* old = cr->state.clip; + CN1RClip* clip; + int x0, y0, x1, y1, rx0, ry0, rx1, ry1; + if (cr->target == 0) { + cn1r_new_path(cr); + return; + } + clip = (CN1RClip*) calloc(1, sizeof(CN1RClip)); + if (clip == 0) { + cn1r_new_path(cr); + return; + } + clip->refs = 1; + cn1rBounds(cr, &x0, &y0, &x1, &y1); + if (cn1rPixelRect(cr, &rx0, &ry0, &rx1, &ry1)) { + if (rx0 > x0) x0 = rx0; + if (ry0 > y0) y0 = ry0; + if (rx1 < x1) x1 = rx1; + if (ry1 < y1) y1 = ry1; + if (x1 < x0) x1 = x0; + if (y1 < y0) y1 = y0; + clip->x0 = x0; + clip->y0 = y0; + clip->x1 = x1; + clip->y1 = y1; + if (old != 0 && old->mask != 0 && x1 > x0 && y1 > y0) { + /* A rectangle inside a shaped clip keeps the shape's mask. */ + int y, w = x1 - x0; + clip->mask = (unsigned char*) malloc((size_t) w * (size_t) (y1 - y0)); + if (clip->mask != 0) { + for (y = y0; y < y1; y++) { + memcpy(clip->mask + (size_t) (y - y0) * (size_t) w, + old->mask + (size_t) (y - old->y0) * (size_t) (old->x1 - old->x0) + (x0 - old->x0), (size_t) w); + } + } + } + } else { + CN1REdges e; + CN1RClipCtx ctx; + cn1rEdgesInit(&e); + cn1rPathEdges(cr, &e); + cn1rNarrow(&x0, &y0, &x1, &y1, e.minX, e.minY, e.maxX, e.maxY); + if (x1 < x0) x1 = x0; + if (y1 < y0) y1 = y0; + clip->x0 = x0; + clip->y0 = y0; + clip->x1 = x1; + clip->y1 = y1; + if (x1 > x0 && y1 > y0) { + clip->mask = (unsigned char*) calloc((size_t) (x1 - x0) * (size_t) (y1 - y0), 1); + if (clip->mask != 0) { + ctx.clip = clip; + ctx.old = old; + cn1rScan(&e, cr->state.fillRule, x0, y0, x1, y1, cn1rClipRow, &ctx); + } + } + free(e.edges); + } + cn1rClipRelease(cr->state.clip); + cr->state.clip = clip; + cn1r_new_path(cr); +} + +/* ---------------------------------------------------------------- a8 masks */ + +void cn1r_mask_a8(cairo_t* cr, const unsigned char* mask, int pitch, int width, int height, int x, int y) { + int x0, y0, x1, y1, row; + if (cr->target == 0 || cr->state.source == 0 || mask == 0) { + return; + } + cn1rBounds(cr, &x0, &y0, &x1, &y1); + if (x > x0) x0 = x; + if (y > y0) y0 = y; + if (x + width < x1) x1 = x + width; + if (y + height < y1) y1 = y + height; + if (x1 <= x0 || !cn1rEnsureCov(x1 - x0)) { + return; + } + for (row = y0; row < y1; row++) { + const unsigned char* m = mask + (size_t) (row - y) * (size_t) pitch + (x0 - x); + int i, any = 0; + for (i = 0; i < x1 - x0; i++) { + cn1rCov[i] = m[i] * (1.0f / 255); + any |= m[i]; + } + if (any) { + cn1rRow(cr, row, x0, x1, cn1rCov, 255); + } + } +} + +#endif /* CN1_LINUX_EMBEDDED */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_raster.h b/Ports/LinuxPort/nativeSources/cn1_linux_raster.h new file mode 100644 index 00000000000..9e9c8526218 --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_raster.h @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +#ifndef CN1_LINUX_RASTER_H +#define CN1_LINUX_RASTER_H + +/* + * The embedded target's 2D renderer: the part of Cairo's API the port's drawing + * units call, implemented in cn1_linux_raster.c with nothing under it but libc, + * libm and (for writing PNG) libpng. + * + * Why not Cairo itself: a distribution's libcairo is built for a desktop and + * links X11, xcb, epoxy and pixman, and Pango, HarfBuzz, FontConfig, GdkPixbuf + * and GLib/GIO came with it. That was 23 MB of shared libraries mapped by an + * application that draws into a framebuffer, read from storage on every cold + * start, for the fifty-odd functions listed here. + * + * The real names are macros over cn1r_ symbols. A library the application loads + * later (a GStreamer plugin, say) may bring the real libcairo into the process, + * and symbols of our own under Cairo's names would then be bound in its place. + * + * Semantics follow Cairo's where the port relies on them: ARGB32 is premultiplied + * and native endian, a path is held in device space as it is built (so a save, + * transform and restore around cairo_arc shapes the arc), the clip, matrix, + * source and operator are saved and restored, and fill, stroke and clip consume + * the path. What the port never asks for is not there: other formats, patterns + * other than a colour or a surface, dashes, line caps other than butt, joins + * other than miter. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { CAIRO_FORMAT_ARGB32 = 0 } cairo_format_t; + +typedef enum { + CAIRO_STATUS_SUCCESS = 0, + CAIRO_STATUS_NO_MEMORY = 1, + CAIRO_STATUS_INVALID_MATRIX = 5, + CAIRO_STATUS_WRITE_ERROR = 11 +} cairo_status_t; + +typedef enum { + CAIRO_OPERATOR_CLEAR = 0, + CAIRO_OPERATOR_SOURCE = 1, + CAIRO_OPERATOR_OVER = 2 +} cairo_operator_t; + +typedef enum { + CAIRO_FILL_RULE_WINDING = 0, + CAIRO_FILL_RULE_EVEN_ODD = 1 +} cairo_fill_rule_t; + +typedef enum { + CAIRO_FILTER_FAST = 0, + CAIRO_FILTER_GOOD = 1, + CAIRO_FILTER_BEST = 2, + CAIRO_FILTER_NEAREST = 3, + CAIRO_FILTER_BILINEAR = 4 +} cairo_filter_t; + +typedef struct { + double xx, yx, xy, yy, x0, y0; +} cairo_matrix_t; + +typedef struct cn1r_surface cairo_surface_t; +typedef struct cn1r_context cairo_t; +typedef struct cn1r_pattern cairo_pattern_t; +typedef cairo_status_t (*cairo_write_func_t)(void* closure, const unsigned char* data, unsigned int length); + +#define cairo_image_surface_create cn1r_image_surface_create +#define cairo_image_surface_get_data cn1r_image_surface_get_data +#define cairo_image_surface_get_stride cn1r_image_surface_get_stride +#define cairo_image_surface_get_width cn1r_image_surface_get_width +#define cairo_image_surface_get_height cn1r_image_surface_get_height +#define cairo_surface_destroy cn1r_surface_destroy +#define cairo_surface_flush cn1r_surface_flush +#define cairo_surface_mark_dirty cn1r_surface_mark_dirty +#define cairo_surface_status cn1r_surface_status +#define cairo_surface_write_to_png cn1r_surface_write_to_png +#define cairo_surface_write_to_png_stream cn1r_surface_write_to_png_stream +#define cairo_create cn1r_create +#define cairo_destroy cn1r_destroy +#define cairo_save cn1r_save +#define cairo_restore cn1r_restore +#define cairo_set_source_rgb cn1r_set_source_rgb +#define cairo_set_source_rgba cn1r_set_source_rgba +#define cairo_set_source_surface cn1r_set_source_surface +#define cairo_get_source cn1r_get_source +#define cairo_pattern_set_filter cn1r_pattern_set_filter +#define cairo_set_operator cn1r_set_operator +#define cairo_set_line_width cn1r_set_line_width +#define cairo_set_fill_rule cn1r_set_fill_rule +#define cairo_paint cn1r_paint +#define cairo_paint_with_alpha cn1r_paint_with_alpha +#define cairo_fill cn1r_fill +#define cairo_stroke cn1r_stroke +#define cairo_clip cn1r_clip +#define cairo_new_path cn1r_new_path +#define cairo_new_sub_path cn1r_new_sub_path +#define cairo_move_to cn1r_move_to +#define cairo_line_to cn1r_line_to +#define cairo_curve_to cn1r_curve_to +#define cairo_arc cn1r_arc +#define cairo_arc_negative cn1r_arc_negative +#define cairo_rectangle cn1r_rectangle +#define cairo_close_path cn1r_close_path +#define cairo_set_matrix cn1r_set_matrix +#define cairo_get_matrix cn1r_get_matrix +#define cairo_identity_matrix cn1r_identity_matrix +#define cairo_translate cn1r_translate +#define cairo_scale cn1r_scale +#define cairo_matrix_init cn1r_matrix_init +#define cairo_matrix_init_identity cn1r_matrix_init_identity +#define cairo_matrix_transform_point cn1r_matrix_transform_point + +cairo_surface_t* cn1r_image_surface_create(cairo_format_t format, int width, int height); +unsigned char* cn1r_image_surface_get_data(cairo_surface_t* surface); +int cn1r_image_surface_get_stride(cairo_surface_t* surface); +int cn1r_image_surface_get_width(cairo_surface_t* surface); +int cn1r_image_surface_get_height(cairo_surface_t* surface); +void cn1r_surface_destroy(cairo_surface_t* surface); +void cn1r_surface_flush(cairo_surface_t* surface); +void cn1r_surface_mark_dirty(cairo_surface_t* surface); +cairo_status_t cn1r_surface_status(cairo_surface_t* surface); +cairo_status_t cn1r_surface_write_to_png(cairo_surface_t* surface, const char* filename); +cairo_status_t cn1r_surface_write_to_png_stream(cairo_surface_t* surface, cairo_write_func_t write, void* closure); + +cairo_t* cn1r_create(cairo_surface_t* target); +void cn1r_destroy(cairo_t* cr); +void cn1r_save(cairo_t* cr); +void cn1r_restore(cairo_t* cr); +void cn1r_set_source_rgb(cairo_t* cr, double red, double green, double blue); +void cn1r_set_source_rgba(cairo_t* cr, double red, double green, double blue, double alpha); +void cn1r_set_source_surface(cairo_t* cr, cairo_surface_t* surface, double x, double y); +cairo_pattern_t* cn1r_get_source(cairo_t* cr); +void cn1r_pattern_set_filter(cairo_pattern_t* pattern, cairo_filter_t filter); +void cn1r_set_operator(cairo_t* cr, cairo_operator_t op); +void cn1r_set_line_width(cairo_t* cr, double width); +void cn1r_set_fill_rule(cairo_t* cr, cairo_fill_rule_t rule); +void cn1r_paint(cairo_t* cr); +void cn1r_paint_with_alpha(cairo_t* cr, double alpha); +void cn1r_fill(cairo_t* cr); +void cn1r_stroke(cairo_t* cr); +void cn1r_clip(cairo_t* cr); +void cn1r_new_path(cairo_t* cr); +void cn1r_new_sub_path(cairo_t* cr); +void cn1r_move_to(cairo_t* cr, double x, double y); +void cn1r_line_to(cairo_t* cr, double x, double y); +void cn1r_curve_to(cairo_t* cr, double x1, double y1, double x2, double y2, double x3, double y3); +void cn1r_arc(cairo_t* cr, double xc, double yc, double radius, double angle1, double angle2); +void cn1r_arc_negative(cairo_t* cr, double xc, double yc, double radius, double angle1, double angle2); +void cn1r_rectangle(cairo_t* cr, double x, double y, double width, double height); +void cn1r_close_path(cairo_t* cr); +void cn1r_set_matrix(cairo_t* cr, const cairo_matrix_t* matrix); +void cn1r_get_matrix(cairo_t* cr, cairo_matrix_t* matrix); +void cn1r_identity_matrix(cairo_t* cr); +void cn1r_translate(cairo_t* cr, double tx, double ty); +void cn1r_scale(cairo_t* cr, double sx, double sy); +void cn1r_matrix_init(cairo_matrix_t* matrix, double xx, double yx, double xy, double yy, double x0, double y0); +void cn1r_matrix_init_identity(cairo_matrix_t* matrix); +void cn1r_matrix_transform_point(const cairo_matrix_t* matrix, double* x, double* y); + +/* + * Not Cairo: composites the current source through an 8-bit coverage mask whose + * top-left pixel lands on device pixel (x, y), under the current clip and + * operator. The text unit draws its glyphs with it. + */ +void cn1r_mask_a8(cairo_t* cr, const unsigned char* mask, int pitch, int width, int height, int x, int y); + +#ifdef __cplusplus +} +#endif + +#endif /* CN1_LINUX_RASTER_H */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_surface.c b/Ports/LinuxPort/nativeSources/cn1_linux_surface.c new file mode 100644 index 00000000000..8cc3a9512ae --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_surface.c @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * Offscreen render targets and PNG encoding, shared by the GTK window + * (cn1_linux_window.c) and the embedded display (cn1_linux_embedded.c). Pure + * Cairo, so it is compiled into both Linux targets unchanged. + */ + +#include "cn1_linux_gfx.h" +#include +#include + +JAVA_LONG com_codename1_impl_linux_LinuxNative_createOffscreenGraphics___int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT width, JAVA_INT height) { + CN1Graphics* g = (CN1Graphics*) calloc(1, sizeof(CN1Graphics)); + int w = width > 0 ? width : 1; + int h = height > 0 ? height : 1; + g->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + g->cr = cairo_create(g->surface); + g->width = w; + g->height = h; + g->alpha = 255; + g->clipW = w; + g->clipH = h; + cairo_matrix_init_identity(&g->transform); + return (JAVA_LONG) (intptr_t) g; +} + +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_saveGraphicsToPng___long_java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG graphics, JAVA_OBJECT path) { + extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT); + CN1Graphics* g = (CN1Graphics*) (intptr_t) graphics; + const char* p = path == JAVA_NULL ? 0 : stringToUTF8(threadStateData, path); + if (!g || !g->surface || !p) { + return JAVA_FALSE; + } + cairo_surface_flush(g->surface); + return cairo_surface_write_to_png(g->surface, p) == CAIRO_STATUS_SUCCESS ? JAVA_TRUE : JAVA_FALSE; +} + +/* A growable byte buffer used as the cairo PNG write closure. */ +struct CN1PngBuf { unsigned char* data; int len; int cap; }; + +static cairo_status_t cn1PngWrite(void* closure, const unsigned char* data, unsigned int length) { + struct CN1PngBuf* b = (struct CN1PngBuf*) closure; + if (b->len + (int) length > b->cap) { + int cap = b->cap > 0 ? b->cap * 2 : 8192; + while (cap < b->len + (int) length) cap *= 2; + b->data = (unsigned char*) realloc(b->data, cap); + b->cap = cap; + } + memcpy(b->data + b->len, data, length); + b->len += (int) length; + return CAIRO_STATUS_SUCCESS; +} + +/* PNG-encodes an ARGB32 surface into a freshly malloc'd buffer; caller free()s + * *outData. Returns 1 on success. Shared by image.c (encodeArgbToPng). */ +int cn1LinuxSurfaceToPng(cairo_surface_t* surface, unsigned char** outData, int* outLen) { + struct CN1PngBuf buf = { 0, 0, 0 }; + cairo_status_t st; + cairo_surface_flush(surface); + st = cairo_surface_write_to_png_stream(surface, cn1PngWrite, &buf); + if (st != CAIRO_STATUS_SUCCESS) { + free(buf.data); + *outData = 0; + *outLen = 0; + return 0; + } + *outData = buf.data; + *outLen = buf.len; + return 1; +} + diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_text_ft.c b/Ports/LinuxPort/nativeSources/cn1_linux_text_ft.c new file mode 100644 index 00000000000..dbae8a4e29e --- /dev/null +++ b/Ports/LinuxPort/nativeSources/cn1_linux_text_ft.c @@ -0,0 +1,900 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * Fonts and text for the embedded target, on FreeType alone. The desktop target + * uses Pango over FontConfig (cn1_linux_text.c, which the embedded project does + * not compile); this unit defines the same natives. + * + * Finding a font. FontConfig answers "Sans, bold" by reading its configuration + * -- dozens of XML files on a stock image -- and a cache, on every start. Here + * a family is found by FILE NAME: the directories in CN1_FONT_PATH (colon + * separated), a "fonts" directory beside the executable, then /usr/share/fonts + * and /usr/local/share/fonts, are listed once, and the generic families map to + * the file names the common free families ship under (DejaVu, Liberation, Noto, + * FreeFont, Roboto, Open Sans). A named family matches a file whose name starts + * with it. Nothing is parsed until a face is used. A face the application + * registers from memory (its own TrueType fonts, the material icon font) is + * matched by the family name inside it, before any file. + * + * Bold and italic come from the family's own faces when it has them and are + * synthesized otherwise (FreeType emboldens and slants the outline). + * + * Shaping is per character: advances, no kerning and no ligatures. That is what + * a UI's labels need in the scripts DejaVu covers; complex scripts need the + * desktop target's HarfBuzz, and are not what the embedded target is for. + * + * A character the face lacks is drawn from the Sans face, so a symbol in a label + * set in an icon font, or an accent the icon font lacks, still shows. + */ +#ifdef CN1_LINUX_EMBEDDED + +#define _GNU_SOURCE +#include "cn1_linux_gfx.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include FT_FREETYPE_H +#include FT_SIZES_H +#include FT_SYNTHESIS_H + +extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT str); + +/* CN1 font constants (com.codename1.ui.Font). */ +#define CN1_FACE_MONOSPACE 32 +#define CN1_FACE_PROPORTIONAL 64 +#define CN1_STYLE_BOLD 1 +#define CN1_STYLE_ITALIC 2 +#define CN1_SIZE_SMALL 8 +#define CN1_SIZE_LARGE 16 + +/* --------------------------------------------------------------- the faces */ + +typedef struct CN1FtFace { + FT_Face face; + char family[128]; + char path[PATH_MAX]; /* empty for a face registered from memory */ + int bold; + int italic; + unsigned char* memory; /* owned bytes of a face registered from memory */ + struct CN1FtFace* next; +} CN1FtFace; + +typedef struct { + uint32_t cp; /* 0: empty slot */ + int advance; /* 26.6 */ + int left, top, width, height; + unsigned char* bits; /* width*height coverage, NULL for a blank glyph */ +} CN1FtGlyph; + +typedef struct CN1FtSized { + CN1FtFace* face; + int px; + int synthBold; + int synthItalic; + FT_Size size; + int ascent; + int height; + CN1FtGlyph* glyphs; + int glyphCapacity; + int glyphCount; + struct CN1FtSized* fallback; /* the Sans face at this size; NULL for Sans itself */ + int fallbackResolved; + struct CN1FtSized* next; +} CN1FtSized; + +struct CN1Font { + CN1FtSized* sized; /* NULL when no face could be found at all */ + char family[128]; + int bold; + int italic; + int pixelSize; + int ascent; + int height; +}; + +static FT_Library cn1Ft = 0; +static CN1FtFace* cn1Faces = 0; +static CN1FtSized* cn1Sized = 0; + +typedef struct { + char* path; + char* key; /* lower case, no extension, letters and digits only */ +} CN1FontFile; + +static CN1FontFile* cn1FontFiles = 0; +static int cn1FontFileCount = 0; +static int cn1FontFileCapacity = 0; +static int cn1FontFilesListed = 0; + +static FT_Library cn1FtLibrary(void) { + if (cn1Ft == 0 && FT_Init_FreeType(&cn1Ft) != 0) { + cn1Ft = 0; + } + return cn1Ft; +} + +/* "DejaVuSans-Bold.ttf" -> "dejavusansbold": the form names are compared in. */ +static void cn1FontKey(const char* name, char* out, size_t room) { + size_t n = 0; + const char* dot = strrchr(name, '.'); + const char* end = dot ? dot : name + strlen(name); + for (; name < end && n + 1 < room; name++) { + char c = *name; + if (c >= 'A' && c <= 'Z') { + out[n++] = (char) (c - 'A' + 'a'); + } else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { + out[n++] = c; + } + } + out[n] = 0; +} + +static int cn1IsFontFile(const char* name) { + const char* dot = strrchr(name, '.'); + return dot != 0 && (strcasecmp(dot, ".ttf") == 0 || strcasecmp(dot, ".otf") == 0 || strcasecmp(dot, ".ttc") == 0); +} + +static void cn1AddFontFile(const char* path, const char* name) { + char key[256]; + if (cn1FontFileCount == cn1FontFileCapacity) { + int capacity = cn1FontFileCapacity == 0 ? 64 : cn1FontFileCapacity * 2; + CN1FontFile* files = (CN1FontFile*) realloc(cn1FontFiles, sizeof(CN1FontFile) * (size_t) capacity); + if (files == 0) { + return; + } + cn1FontFiles = files; + cn1FontFileCapacity = capacity; + } + cn1FontKey(name, key, sizeof(key)); + cn1FontFiles[cn1FontFileCount].path = strdup(path); + cn1FontFiles[cn1FontFileCount].key = strdup(key); + if (cn1FontFiles[cn1FontFileCount].path != 0 && cn1FontFiles[cn1FontFileCount].key != 0) { + cn1FontFileCount++; + } +} + +static void cn1ListFontDir(const char* dir, int depth) { + DIR* d; + struct dirent* entry; + if (depth > 4 || (d = opendir(dir)) == 0) { + return; + } + while ((entry = readdir(d)) != 0) { + char path[PATH_MAX]; + struct stat st; + if (entry->d_name[0] == '.') { + continue; + } + if (snprintf(path, sizeof(path), "%s/%s", dir, entry->d_name) >= (int) sizeof(path)) { + continue; + } + if (cn1IsFontFile(entry->d_name)) { + cn1AddFontFile(path, entry->d_name); + } else if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + cn1ListFontDir(path, depth + 1); + } + } + closedir(d); +} + +static void cn1ListFontFiles(void) { + const char* env; + char exe[PATH_MAX]; + ssize_t n; + if (cn1FontFilesListed) { + return; + } + cn1FontFilesListed = 1; + env = getenv("CN1_FONT_PATH"); + if (env != 0) { + char* copy = strdup(env); + char* save = 0; + char* part = copy ? strtok_r(copy, ":", &save) : 0; + while (part != 0) { + cn1ListFontDir(part, 0); + part = strtok_r(0, ":", &save); + } + free(copy); + } + n = readlink("/proc/self/exe", exe, sizeof(exe) - 7); + if (n > 0) { + char* slash; + exe[n] = 0; + slash = strrchr(exe, '/'); + if (slash != 0) { + strcpy(slash + 1, "fonts"); + cn1ListFontDir(exe, 0); + } + } + cn1ListFontDir("/usr/share/fonts", 0); + cn1ListFontDir("/usr/local/share/fonts", 0); +} + +static const char* cn1FindFontFile(const char* key) { + int i; + for (i = 0; i < cn1FontFileCount; i++) { + if (strcmp(cn1FontFiles[i].key, key) == 0) { + return cn1FontFiles[i].path; + } + } + return 0; +} + +static CN1FtFace* cn1FaceFromFile(const char* path) { + CN1FtFace* f; + FT_Library lib = cn1FtLibrary(); + for (f = cn1Faces; f != 0; f = f->next) { + if (strcmp(f->path, path) == 0) { + return f; + } + } + if (lib == 0) { + return 0; + } + f = (CN1FtFace*) calloc(1, sizeof(CN1FtFace)); + if (f == 0) { + return 0; + } + if (FT_New_Face(lib, path, 0, &f->face) != 0) { + free(f); + return 0; + } + strncpy(f->path, path, sizeof(f->path) - 1); + strncpy(f->family, f->face->family_name ? f->face->family_name : "", sizeof(f->family) - 1); + f->bold = (f->face->style_flags & FT_STYLE_FLAG_BOLD) != 0; + f->italic = (f->face->style_flags & FT_STYLE_FLAG_ITALIC) != 0; + f->next = cn1Faces; + cn1Faces = f; + return f; +} + +/* File names of the generic families, regular / bold / italic / bold italic. */ +static const char* const cn1SansNames[4][7] = { + { "dejavusans", "liberationsansregular", "notosansregular", "freesans", "robotoregular", "opensansregular", "arial" }, + { "dejavusansbold", "liberationsansbold", "notosansbold", "freesansbold", "robotobold", "opensansbold", "arialbd" }, + { "dejavusansoblique", "liberationsansitalic", "notosansitalic", "freesansoblique", "robotoitalic", "opensansitalic", "ariali" }, + { "dejavusansboldoblique", "liberationsansbolditalic", "notosansbolditalic", "freesansboldoblique", "robotobolditalic", "opensansbolditalic", "arialbi" } +}; +static const char* const cn1SerifNames[4][7] = { + { "dejavuserif", "liberationserifregular", "notoserifregular", "freeserif", 0, 0, 0 }, + { "dejavuserifbold", "liberationserifbold", "notoserifbold", "freeserifbold", 0, 0, 0 }, + { "dejavuserifitalic", "liberationserifitalic", "notoserifitalic", "freeserifitalic", 0, 0, 0 }, + { "dejavuserifbolditalic", "liberationserifbolditalic", "notoserifbolditalic", "freeserifbolditalic", 0, 0, 0 } +}; +static const char* const cn1MonoNames[4][7] = { + { "dejavusansmono", "liberationmonoregular", "notosansmonoregular", "freemono", 0, 0, 0 }, + { "dejavusansmonobold", "liberationmonobold", "notosansmonobold", "freemonobold", 0, 0, 0 }, + { "dejavusansmonooblique", "liberationmonoitalic", "notosansmonoitalic", "freemonooblique", 0, 0, 0 }, + { "dejavusansmonoboldoblique", "liberationmonobolditalic", "notosansmonobolditalic", "freemonoboldoblique", 0, 0, 0 } +}; + +static CN1FtFace* cn1GenericFace(const char* const names[4][7], int style) { + int i; + for (i = 0; i < 7; i++) { + const char* path = names[style][i] ? cn1FindFontFile(names[style][i]) : 0; + if (path != 0) { + CN1FtFace* f = cn1FaceFromFile(path); + if (f != 0) { + return f; + } + } + } + return 0; +} + +/* The Sans regular face: the default, and the fallback for missing glyphs. */ +static CN1FtFace* cn1SansFace(void) { + CN1FtFace* f; + cn1ListFontFiles(); + f = cn1GenericFace(cn1SansNames, 0); + if (f == 0 && cn1FontFileCount > 0) { + /* No family this unit knows: any font beats no text. */ + f = cn1FaceFromFile(cn1FontFiles[0].path); + } + return f; +} + +/* + * The face for a family and style, and whether bold or italic still has to be + * synthesized on top of it. + */ +static CN1FtFace* cn1ResolveFace(const char* family, int bold, int italic, int* synthBold, int* synthItalic) { + const char* const (*generic)[7] = 0; + CN1FtFace* best = 0; + CN1FtFace* f; + char key[256]; + int style = (bold ? 1 : 0) + (italic ? 2 : 0); + int i; + *synthBold = 0; + *synthItalic = 0; + /* A face the application registered from memory, by its own family name. */ + for (f = cn1Faces; f != 0; f = f->next) { + if (f->memory != 0 && strcasecmp(f->family, family) == 0) { + if (f->bold == bold && f->italic == italic) { + best = f; + break; + } + if (best == 0) { + best = f; + } + } + } + if (best == 0) { + cn1ListFontFiles(); + if (strcasecmp(family, "Serif") == 0) { + generic = cn1SerifNames; + } else if (strcasecmp(family, "Monospace") == 0) { + generic = cn1MonoNames; + } else if (strcasecmp(family, "Sans") == 0 || strcasecmp(family, "sans-serif") == 0 + || strcasecmp(family, "System") == 0) { + generic = cn1SansNames; + } + if (generic != 0) { + best = cn1GenericFace(generic, style); + if (best == 0 && style != 0) { + best = cn1GenericFace(generic, 0); + } + } else { + /* A named family: its files start with its name. Prefer the one whose + * name says the style asked for. */ + static const char* const suffixes[4][3] = { + { "regular", "", "book" }, + { "bold", "bd", "semibold" }, + { "italic", "oblique", "it" }, + { "bolditalic", "boldoblique", "bi" } + }; + char want[300]; + cn1FontKey(family, key, sizeof(key)); + for (i = 0; i < 3 && best == 0; i++) { + const char* path; + snprintf(want, sizeof(want), "%s%s", key, suffixes[style][i]); + path = cn1FindFontFile(want); + if (path != 0) { + best = cn1FaceFromFile(path); + } + } + for (i = 0; i < 3 && best == 0 && style != 0; i++) { + const char* path; + snprintf(want, sizeof(want), "%s%s", key, suffixes[0][i]); + path = cn1FindFontFile(want); + if (path != 0) { + best = cn1FaceFromFile(path); + } + } + } + if (best == 0) { + best = cn1GenericFace(cn1SansNames, style); + if (best == 0) { + best = cn1SansFace(); + } + } + } + if (best != 0) { + *synthBold = bold && !best->bold; + *synthItalic = italic && !best->italic; + } + return best; +} + +/* ------------------------------------------------------------- face, sized */ + +static CN1FtSized* cn1SizedFace(CN1FtFace* face, int px, int synthBold, int synthItalic) { + CN1FtSized* s; + if (face == 0) { + return 0; + } + if (px < 1) { + px = 1; + } + for (s = cn1Sized; s != 0; s = s->next) { + if (s->face == face && s->px == px && s->synthBold == synthBold && s->synthItalic == synthItalic) { + return s; + } + } + s = (CN1FtSized*) calloc(1, sizeof(CN1FtSized)); + if (s == 0) { + return 0; + } + if (FT_New_Size(face->face, &s->size) != 0) { + free(s); + return 0; + } + FT_Activate_Size(s->size); + FT_Set_Pixel_Sizes(face->face, 0, (FT_UInt) px); + s->face = face; + s->px = px; + s->synthBold = synthBold; + s->synthItalic = synthItalic; + s->ascent = (int) ((s->size->metrics.ascender + 32) >> 6); + s->height = s->ascent + (int) ((-s->size->metrics.descender + 32) >> 6); + if (s->height <= 0) { + s->ascent = px; + s->height = px + px / 4; + } + s->next = cn1Sized; + cn1Sized = s; + return s; +} + +/* Loads glyph index `index` of `s` into its face's slot, styled; 0 on success. */ +static int cn1LoadGlyph(CN1FtSized* s, FT_UInt index) { + FT_Face face = s->face->face; + FT_Activate_Size(s->size); + if (FT_Load_Glyph(face, index, FT_LOAD_TARGET_LIGHT | FT_LOAD_NO_BITMAP) != 0) { + return -1; + } + if (s->synthItalic) { + FT_GlyphSlot_Oblique(face->glyph); + } + if (s->synthBold) { + FT_GlyphSlot_Embolden(face->glyph); + } + return 0; +} + +static CN1FtSized* cn1Fallback(CN1FtSized* s) { + if (!s->fallbackResolved) { + CN1FtFace* sans = cn1SansFace(); + s->fallbackResolved = 1; + if (sans != 0 && sans != s->face) { + s->fallback = cn1SizedFace(sans, s->px, s->synthBold, s->synthItalic); + } + } + return s->fallback; +} + +/* The face (this one or the fallback) and index that draw code point cp. */ +static CN1FtSized* cn1GlyphSource(CN1FtSized* s, uint32_t cp, FT_UInt* index) { + *index = FT_Get_Char_Index(s->face->face, cp); + if (*index == 0) { + CN1FtSized* fb = cn1Fallback(s); + if (fb != 0) { + FT_UInt other = FT_Get_Char_Index(fb->face->face, cp); + if (other != 0) { + *index = other; + return fb; + } + } + } + return s; +} + +static CN1FtGlyph* cn1GlyphSlot(CN1FtSized* s, uint32_t cp) { + uint32_t mask = (uint32_t) s->glyphCapacity - 1; + uint32_t i = (cp * 2654435761u) & mask; + while (s->glyphs[i].cp != 0 && s->glyphs[i].cp != cp) { + i = (i + 1) & mask; + } + return &s->glyphs[i]; +} + +static int cn1GrowGlyphs(CN1FtSized* s) { + CN1FtGlyph* old = s->glyphs; + int oldCapacity = s->glyphCapacity, i; + int capacity = oldCapacity == 0 ? 256 : oldCapacity * 2; + CN1FtGlyph* grown = (CN1FtGlyph*) calloc((size_t) capacity, sizeof(CN1FtGlyph)); + if (grown == 0) { + return 0; + } + s->glyphs = grown; + s->glyphCapacity = capacity; + for (i = 0; i < oldCapacity; i++) { + if (old[i].cp != 0) { + *cn1GlyphSlot(s, old[i].cp) = old[i]; + } + } + free(old); + return 1; +} + +/* The rendered, cached glyph for code point cp (never 0: callers map NUL). */ +static CN1FtGlyph* cn1Glyph(CN1FtSized* s, uint32_t cp) { + CN1FtGlyph* g; + CN1FtSized* src; + FT_UInt index; + FT_GlyphSlot slot; + if ((s->glyphCount + 1) * 10 >= s->glyphCapacity * 7 && !cn1GrowGlyphs(s)) { + return 0; + } + g = cn1GlyphSlot(s, cp); + if (g->cp == cp) { + return g; + } + memset(g, 0, sizeof(*g)); + g->cp = cp; + s->glyphCount++; + src = cn1GlyphSource(s, cp, &index); + if (cn1LoadGlyph(src, index) != 0) { + return g; + } + slot = src->face->face->glyph; + g->advance = (int) slot->advance.x; + if (FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL) == 0 && slot->bitmap.width > 0 && slot->bitmap.rows > 0) { + int y; + g->left = slot->bitmap_left; + g->top = slot->bitmap_top; + g->width = (int) slot->bitmap.width; + g->height = (int) slot->bitmap.rows; + g->bits = (unsigned char*) malloc((size_t) g->width * (size_t) g->height); + /* A negative pitch puts the top row last in memory; the normal render + * mode never produces one, and a glyph that has it is drawn blank. */ + if (g->bits != 0 && slot->bitmap.pitch < g->width) { + memset(g->bits, 0, (size_t) g->width * (size_t) g->height); + } else if (g->bits != 0) { + for (y = 0; y < g->height; y++) { + memcpy(g->bits + (size_t) y * (size_t) g->width, + slot->bitmap.buffer + (ptrdiff_t) y * slot->bitmap.pitch, (size_t) g->width); + } + } + } + return g; +} + +/* ------------------------------------------------------------------- fonts */ + +static CN1Font* cn1MakeFont(const char* family, int bold, int italic, int pixelSize) { + CN1Font* f = (CN1Font*) calloc(1, sizeof(CN1Font)); + int synthBold = 0, synthItalic = 0; + CN1FtFace* face; + if (f == 0) { + return 0; + } + if (pixelSize < 1) { + pixelSize = 1; + } + strncpy(f->family, family, sizeof(f->family) - 1); + f->bold = bold; + f->italic = italic; + f->pixelSize = pixelSize; + face = cn1ResolveFace(family, bold, italic, &synthBold, &synthItalic); + f->sized = cn1SizedFace(face, pixelSize, synthBold, synthItalic); + if (f->sized != 0) { + f->ascent = f->sized->ascent; + f->height = f->sized->height; + } else { + f->ascent = pixelSize; + f->height = pixelSize + pixelSize / 4; + } + return f; +} + +static CN1Font* cn1DefaultFont = 0; + +static CN1Font* cn1FontOrDefault(JAVA_LONG font) { + CN1Font* f = (CN1Font*) (intptr_t) font; + if (f == 0) { + if (cn1DefaultFont == 0) { + cn1DefaultFont = cn1MakeFont("Sans", 0, 0, 18); + } + f = cn1DefaultFont; + } + return f; +} + +/* The next code point of UTF-8 text; malformed bytes read as U+FFFD. */ +static uint32_t cn1NextCodePoint(const unsigned char** p, const unsigned char* end) { + const unsigned char* s = *p; + uint32_t c = *s++; + int extra = 0; + if (c >= 0xf0 && c < 0xf8) { + c &= 0x07; + extra = 3; + } else if (c >= 0xe0) { + c &= 0x0f; + extra = 2; + } else if (c >= 0xc0) { + c &= 0x1f; + extra = 1; + } else if (c >= 0x80) { + *p = s; + return 0xfffd; + } + while (extra-- > 0) { + if (s >= end || (*s & 0xc0) != 0x80) { + *p = s; + return 0xfffd; + } + c = (c << 6) | (*s++ & 0x3f); + } + *p = s; + return c == 0 ? 0xfffd : c; +} + +static int cn1MeasureUtf8(CN1Font* f, const char* utf8, int len) { + const unsigned char* p = (const unsigned char*) utf8; + const unsigned char* end; + long total = 0; + if (utf8 == 0 || f->sized == 0) { + return 0; + } + end = p + (len < 0 ? (int) strlen(utf8) : len); + while (p < end) { + CN1FtGlyph* g = cn1Glyph(f->sized, cn1NextCodePoint(&p, end)); + if (g != 0) { + total += g->advance; + } + } + return (int) ((total + 32) >> 6); +} + +/* Transformed text (a rotation or a scale on the graphics): glyphs are rendered + * through the matrix as they are drawn and not cached; the UI rarely does it. */ +static void cn1DrawTransformed(CN1Graphics* g, CN1Font* f, const char* s, double x, double baseline) { + const unsigned char* p = (const unsigned char*) s; + const unsigned char* end = p + strlen(s); + const cairo_matrix_t* m = &g->transform; + FT_Matrix ft; + double pen = x; + /* FreeType's y axis points up, the screen's down. */ + ft.xx = (FT_Fixed) (m->xx * 65536); + ft.xy = (FT_Fixed) (-m->xy * 65536); + ft.yx = (FT_Fixed) (-m->yx * 65536); + ft.yy = (FT_Fixed) (m->yy * 65536); + while (p < end) { + uint32_t cp = cn1NextCodePoint(&p, end); + FT_UInt index; + CN1FtSized* src = cn1GlyphSource(f->sized, cp, &index); + FT_Face face = src->face->face; + FT_GlyphSlot slot; + double dx = pen, dy = baseline; + FT_Vector delta; + cairo_matrix_transform_point(m, &dx, &dy); + delta.x = (FT_Pos) ((dx - floor(dx)) * 64); + delta.y = (FT_Pos) (-(dy - floor(dy)) * 64); + FT_Set_Transform(face, &ft, &delta); + if (cn1LoadGlyph(src, index) == 0) { + slot = face->glyph; + if (FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL) == 0 && slot->bitmap.width > 0 && slot->bitmap.pitch > 0) { + cn1r_mask_a8(g->cr, slot->bitmap.buffer, slot->bitmap.pitch, (int) slot->bitmap.width, + (int) slot->bitmap.rows, (int) floor(dx) + slot->bitmap_left, (int) floor(dy) - slot->bitmap_top); + } + } + FT_Set_Transform(face, 0, 0); + { + /* The advance in user space: the untransformed glyph's. */ + CN1FtGlyph* cached = cn1Glyph(f->sized, cp); + pen += cached != 0 ? cached->advance / 64.0 : 0; + } + } +} + +/* ------------------------------------------------------------------ natives */ + +JAVA_LONG com_codename1_impl_linux_LinuxNative_createFont___int_int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT face, JAVA_INT style, JAVA_INT size) { + const char* family = "Sans"; + int pixelSize = 18; + if ((face & CN1_FACE_MONOSPACE) != 0) { + family = "Monospace"; + } else if ((face & CN1_FACE_PROPORTIONAL) != 0) { + family = "Serif"; + } + if ((size & CN1_SIZE_SMALL) != 0) { + pixelSize = 14; + } else if ((size & CN1_SIZE_LARGE) != 0) { + pixelSize = 24; + } + return (JAVA_LONG) (intptr_t) cn1MakeFont(family, (style & CN1_STYLE_BOLD) != 0, (style & CN1_STYLE_ITALIC) != 0, pixelSize); +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_getDefaultFont___R_long(CODENAME_ONE_THREAD_STATE) { + return (JAVA_LONG) (intptr_t) cn1FontOrDefault(0); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_stringWidth___long_java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG font, JAVA_OBJECT str) { + if (str == JAVA_NULL) { + return 0; + } + return cn1MeasureUtf8(cn1FontOrDefault(font), stringToUTF8(threadStateData, str), -1); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_charWidth___long_char_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG font, JAVA_CHAR c) { + CN1Font* f = cn1FontOrDefault(font); + CN1FtGlyph* g; + if (f->sized == 0) { + return 0; + } + g = cn1Glyph(f->sized, c == 0 ? 0xfffd : (uint32_t) (c & 0xffff)); + return g != 0 ? (g->advance + 32) >> 6 : 0; +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_charsWidth___long_char_1ARRAY_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG font, JAVA_OBJECT chars, JAVA_INT offset, JAVA_INT length) { + /* A Java char[] holds 2-byte JAVA_ARRAY_CHAR elements, not 4-byte JAVA_CHAR. */ + JAVA_ARRAY_CHAR* data; + CN1Font* f = cn1FontOrDefault(font); + long total = 0; + int i; + if (chars == JAVA_NULL || length <= 0 || f->sized == 0) { + return 0; + } + if (offset < 0 || length > ((JAVA_ARRAY) chars)->length - offset) { + return 0; + } + data = (JAVA_ARRAY_CHAR*) CN1_ARRAY_DATA(chars); + for (i = 0; i < length; i++) { + uint32_t cp = (uint32_t) data[offset + i] & 0xffff; + CN1FtGlyph* g; + /* A surrogate pair is one character. */ + if (cp >= 0xd800 && cp < 0xdc00 && i + 1 < length) { + uint32_t low = (uint32_t) data[offset + i + 1] & 0xffff; + if (low >= 0xdc00 && low < 0xe000) { + cp = 0x10000 + ((cp - 0xd800) << 10) + (low - 0xdc00); + i++; + } + } + g = cn1Glyph(f->sized, cp == 0 ? 0xfffd : cp); + if (g != 0) { + total += g->advance; + } + } + return (int) ((total + 32) >> 6); +} + +JAVA_INT com_codename1_impl_linux_LinuxNative_fontHeight___long_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG font) { + return cn1FontOrDefault(font)->height; +} + +/* The Font.STYLE_* bits a "native:" name asks for (see cn1_linux_text.c). */ +static int cn1NativeFontStyle(const char* name) { + int style = 0; + if (name == 0 || strncmp(name, "native:", 7) != 0) { + return 0; + } + if (strstr(name + 7, "Bold") != 0 || strstr(name + 7, "Black") != 0) { + style |= CN1_STYLE_BOLD; + } + if (strstr(name + 7, "Italic") != 0) { + style |= CN1_STYLE_ITALIC; + } + return style; +} + +static CN1Font* cn1NamedFont(const char* name) { + if (strncmp(name, "native:", 7) == 0) { + int style = cn1NativeFontStyle(name); + return cn1MakeFont("Sans", (style & CN1_STYLE_BOLD) != 0, (style & CN1_STYLE_ITALIC) != 0, 18); + } + return cn1MakeFont(name, 0, 0, 18); +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_loadTrueTypeFont___java_lang_String_java_lang_String_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT fontName, JAVA_OBJECT fileName) { + char name[256]; + const char* s = fontName == JAVA_NULL ? "Sans" : stringToUTF8(threadStateData, fontName); + strncpy(name, s ? s : "Sans", sizeof(name) - 1); + name[sizeof(name) - 1] = 0; + if (fileName != JAVA_NULL && strncmp(name, "native:", 7) != 0) { + /* A file the font directories have under that name is the family. */ + const char* file = stringToUTF8(threadStateData, fileName); + const char* slash = file ? strrchr(file, '/') : 0; + char key[256]; + const char* path; + cn1ListFontFiles(); + cn1FontKey(slash ? slash + 1 : (file ? file : ""), key, sizeof(key)); + path = key[0] ? cn1FindFontFile(key) : 0; + if (path != 0) { + CN1FtFace* face = cn1FaceFromFile(path); + if (face != 0) { + return (JAVA_LONG) (intptr_t) cn1MakeFont(face->family, face->bold, face->italic, 18); + } + } + } + return (JAVA_LONG) (intptr_t) cn1NamedFont(name); +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_loadTrueTypeFontFromMemory___java_lang_String_byte_1ARRAY_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT fontName, JAVA_OBJECT data) { + char name[256]; + const char* s = fontName == JAVA_NULL ? "Sans" : stringToUTF8(threadStateData, fontName); + CN1FtFace* f; + FT_Library lib; + int len; + strncpy(name, s ? s : "Sans", sizeof(name) - 1); + name[sizeof(name) - 1] = 0; + if (data == JAVA_NULL || (len = (int) ((JAVA_ARRAY) data)->length) <= 0 || (lib = cn1FtLibrary()) == 0) { + return (JAVA_LONG) (intptr_t) cn1NamedFont(name); + } + f = (CN1FtFace*) calloc(1, sizeof(CN1FtFace)); + if (f == 0) { + return (JAVA_LONG) (intptr_t) cn1NamedFont(name); + } + /* FreeType reads the face from these bytes for as long as it lives. */ + f->memory = (unsigned char*) malloc((size_t) len); + if (f->memory == 0) { + free(f); + return (JAVA_LONG) (intptr_t) cn1NamedFont(name); + } + memcpy(f->memory, CN1_ARRAY_DATA(data), (size_t) len); + if (FT_New_Memory_Face(lib, f->memory, len, 0, &f->face) != 0) { + free(f->memory); + free(f); + return (JAVA_LONG) (intptr_t) cn1NamedFont(name); + } + strncpy(f->family, f->face->family_name ? f->face->family_name + : (strncmp(name, "native:", 7) == 0 ? name + 7 : name), sizeof(f->family) - 1); + f->bold = (f->face->style_flags & FT_STYLE_FLAG_BOLD) != 0; + f->italic = (f->face->style_flags & FT_STYLE_FLAG_ITALIC) != 0; + f->next = cn1Faces; + cn1Faces = f; + return (JAVA_LONG) (intptr_t) cn1MakeFont(f->family, f->bold, f->italic, 18); +} + +JAVA_LONG com_codename1_impl_linux_LinuxNative_deriveTrueTypeFont___long_float_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_LONG font, JAVA_FLOAT size, JAVA_INT weight) { + CN1Font* src = cn1FontOrDefault(font); + /* A non-positive size is the documented "unset" sentinel (see cn1_linux_text.c). */ + if (size <= 0.0f) { + size = 15.0f; + } + return (JAVA_LONG) (intptr_t) cn1MakeFont(src->family, src->bold || (weight & 1) != 0, + src->italic || (weight & 2) != 0, (int) (size + 0.5f)); +} + +JAVA_VOID com_codename1_impl_linux_LinuxNative_drawString___long_java_lang_String_int_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG graphics, JAVA_OBJECT str, JAVA_INT x, JAVA_INT y) { + CN1Graphics* g = (CN1Graphics*) (intptr_t) graphics; + CN1Font* f; + const char* s; + const cairo_matrix_t* m; + if (str == JAVA_NULL || g == 0) { + return; + } + f = cn1FontOrDefault(g->font ? (JAVA_LONG) (intptr_t) g->font : 0); + if (f->sized == 0) { + return; + } + s = stringToUTF8(threadStateData, str); + if (s == 0 || *s == 0) { + return; + } + cairo_save(g->cr); + cn1LinuxApplyClip(g); + cairo_set_matrix(g->cr, &g->transform); + cn1LinuxApplySource(g); + m = &g->transform; + if (m->xx == 1 && m->yy == 1 && m->xy == 0 && m->yx == 0) { + /* (x, y) is the top of the line, as with Pango; the baseline is an ascent + * below it. Glyphs land on whole pixels, where they were hinted. */ + const unsigned char* p = (const unsigned char*) s; + const unsigned char* end = p + strlen(s); + long pen = (long) ((x + m->x0) * 64); + int baseline = (int) floor(y + m->y0 + 0.5) + f->ascent; + while (p < end) { + CN1FtGlyph* glyph = cn1Glyph(f->sized, cn1NextCodePoint(&p, end)); + if (glyph == 0) { + continue; + } + if (glyph->bits != 0) { + cn1r_mask_a8(g->cr, glyph->bits, glyph->width, glyph->width, glyph->height, + (int) ((pen + 32) >> 6) + glyph->left, baseline - glyph->top); + } + pen += glyph->advance; + } + } else { + cn1DrawTransformed(g, f, s, x, y + f->ascent); + } + cairo_restore(g->cr); +} + +#endif /* CN1_LINUX_EMBEDDED */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_window.c b/Ports/LinuxPort/nativeSources/cn1_linux_window.c index 48078776ad6..14f84d87406 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_window.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_window.c @@ -50,225 +50,8 @@ #endif #include -/* ----------------------------------------------------------- event ring */ - -#define CN1_EVENT_RING 1024 -typedef struct { - int type, x, y, key; - /* Which window the event came from. Zero is the application's main window, - * which is every event this port produced before desktop windows existed. */ - int windowId; -} CN1Event; -static CN1Event cn1EventRing[CN1_EVENT_RING]; -static int cn1EventHead = 0; -static int cn1EventTail = 0; -static pthread_mutex_t cn1EventLock = PTHREAD_MUTEX_INITIALIZER; - - -void cn1LinuxPushEvent(int type, int x, int y, int keyCode) { - cn1LinuxPushWindowEvent(0, type, x, y, keyCode); -} - -/* Events the framework cannot reconstruct if they are lost, of which there are two - * kinds. - * - * Lifecycle: a lost hide leaves a window the framework believes is on screen, painting - * and animating until something else happens to it, and a lost close leaves it - * registered with no native window behind it. - * - * Terminations: a release ends something a press started. Lose it and the component - * the press went to stays in that state for good -- the key goes on repeating, the - * button stays down, the drag never finishes -- and the focus change that would - * otherwise cancel a held gesture is no use as a backstop if it is droppable too. - * - * Note the asymmetry with presses, which stay droppable: a release that arrives with - * no press behind it finds no recorded target and is discarded harmlessly, so when - * something has to go it must never be the release. */ -/* Unlike hover motion, leave has no later motion outside the window to repair - * a dropped notification. Protect only the terminal sentinel, not the motion stream. */ -static int cn1LinuxIsProtectedEvent(int type, int x, int y) { - return type == CN1_EVENT_WINDOW_SHOWN || type == CN1_EVENT_WINDOW_HIDDEN - || type == CN1_EVENT_WINDOW_CLOSE - || type == CN1_EVENT_KEY_RELEASED - || type == CN1_EVENT_POINTER_RELEASED - || type == CN1_EVENT_WINDOW_FOCUS - || type == CN1_EVENT_SIZE_CHANGED - || (type == CN1_EVENT_POINTER_HOVER && x == -1 && y == -1); -} - -/* Visibility only. A close request is protected from eviction like any other - * lifecycle event, but it is not a state that a later one supersedes: the delete - * signal does not destroy the window, so a close that a subsequent minimize overwrote - * would take the close listener and the close operation with it. */ -static int cn1LinuxStateClass(int type) { - if (type == CN1_EVENT_WINDOW_SHOWN || type == CN1_EVENT_WINDOW_HIDDEN) { - return 1; - } - if (type == CN1_EVENT_SIZE_CHANGED) { - return 2; - } - return 0; -} - -/* Replaces a queued visibility event for the same window with this newer one. The - * latest state is the one that matters -- a hide followed by a show leaves the window - * shown -- so superseding costs nothing and needs no room. */ -static int cn1LinuxCoalesceLifecycleLocked(int windowId, int type, int x, int y, - int keyCode) { - int idx = cn1EventHead; - int newest = -1; - int cls = cn1LinuxStateClass(type); - if (cls == 0) { - return 0; - } - /* The *newest* match, not the first one found. A window can already have more than - * one transition queued -- a hide then a show -- and replacing the older of the two - * leaves the newer one as the last word, so the framework would end up believing a - * window that is natively hidden is on screen, and go on painting it. */ - while (idx != cn1EventTail) { - if (cn1EventRing[idx].windowId == windowId - && cn1LinuxStateClass(cn1EventRing[idx].type) == cls) { - newest = idx; - } - idx = (idx + 1) % CN1_EVENT_RING; - } - if (newest < 0) { - return 0; - } - cn1EventRing[newest].type = type; - cn1EventRing[newest].x = x; - cn1EventRing[newest].y = y; - cn1EventRing[newest].key = keyCode; - return 1; -} - -/* Removes the oldest droppable event, closing the gap. Used to make room for a - * protected one: advancing the head instead would evict whatever is oldest, and that - * can be a protected event itself -- which is the very thing being kept. */ -static void cn1LinuxRemoveAtLocked(int idx) { - int cur = idx; - int follow = (cur + 1) % CN1_EVENT_RING; - while (follow != cn1EventTail) { - cn1EventRing[cur] = cn1EventRing[follow]; - cur = follow; - follow = (follow + 1) % CN1_EVENT_RING; - } - cn1EventTail = cur; -} - -static int cn1LinuxEvictInputLocked(void) { - int idx = cn1EventHead; - while (idx != cn1EventTail) { - if (!cn1LinuxIsProtectedEvent(cn1EventRing[idx].type, - cn1EventRing[idx].x, cn1EventRing[idx].y)) { - cn1LinuxRemoveAtLocked(idx); - return 1; - } - idx = (idx + 1) % CN1_EVENT_RING; - } - return 0; -} - -/* Last resort when the ring holds nothing but lifecycle events and so has no input to - * give up. A window that toggled visibility several times before the framework drained - * anything has more than one transition queued, and every one but its last is already - * superseded, so dropping the oldest of them frees a slot without changing what any - * window ends up as. Without this a close arriving for a *different* window has nowhere - * to go and is dropped, which is the one outcome this whole path exists to prevent. */ -static int cn1LinuxEvictSupersededVisibilityLocked(void) { - int idx = cn1EventHead; - while (idx != cn1EventTail) { - int cls = cn1LinuxStateClass(cn1EventRing[idx].type); - if (cls != 0) { - int scan = (idx + 1) % CN1_EVENT_RING; - while (scan != cn1EventTail) { - if (cn1EventRing[scan].windowId == cn1EventRing[idx].windowId - && cn1LinuxStateClass(cn1EventRing[scan].type) == cls) { - cn1LinuxRemoveAtLocked(idx); - return 1; - } - scan = (scan + 1) % CN1_EVENT_RING; - } - } - idx = (idx + 1) % CN1_EVENT_RING; - } - return 0; -} - -/* Last resort before giving up an entry outright: drop the oldest *termination*. - * - * When the queue cannot grow, the question is only which loss costs least, and the - * order is droppable input, then a state a later event already supersedes, then a - * termination, then a lifecycle event. A lost release latches one component; a lost - * close or hide loses a whole window -- the close operation never runs, or the - * framework goes on painting a window that is not on screen. So a queued close or - * visibility transition outranks any number of releases behind it. */ -static int cn1LinuxEvictOldestTerminationLocked(void) { - int idx = cn1EventHead; - while (idx != cn1EventTail) { - int t = cn1EventRing[idx].type; - if (t == CN1_EVENT_KEY_RELEASED || t == CN1_EVENT_POINTER_RELEASED - || t == CN1_EVENT_WINDOW_FOCUS - || (t == CN1_EVENT_POINTER_HOVER && cn1EventRing[idx].x == -1 - && cn1EventRing[idx].y == -1)) { - cn1LinuxRemoveAtLocked(idx); - return 1; - } - idx = (idx + 1) % CN1_EVENT_RING; - } - return 0; -} - -void cn1LinuxPushWindowEvent(int windowId, int type, int x, int y, int keyCode) { - pthread_mutex_lock(&cn1EventLock); - int next = (cn1EventTail + 1) % CN1_EVENT_RING; - if (next == cn1EventHead && cn1LinuxIsProtectedEvent(type, x, y)) { - /* Full, and this one must not be the casualty. Supersede this window's own - * queued transition if it has one, otherwise take the room from an input event, - * and failing that from a transition that a later one already supersedes. Never - * from a transition that is still some window's last word. */ - if (cn1LinuxCoalesceLifecycleLocked(windowId, type, x, y, keyCode)) { - pthread_mutex_unlock(&cn1EventLock); - return; - } - if (cn1LinuxEvictInputLocked() || cn1LinuxEvictSupersededVisibilityLocked() - || cn1LinuxEvictOldestTerminationLocked()) { - next = (cn1EventTail + 1) % CN1_EVENT_RING; - } else { - /* Nothing left but lifecycle events -- closes and visibility transitions - * for more distinct windows than the ring can hold, which needs more windows - * open than any application has. Giving up the oldest is all that remains, - * and the newer event at least describes the more recent state. */ - cn1EventHead = (cn1EventHead + 1) % CN1_EVENT_RING; - next = (cn1EventTail + 1) % CN1_EVENT_RING; - } - } - if (next != cn1EventHead) { - cn1EventRing[cn1EventTail].type = type; - cn1EventRing[cn1EventTail].x = x; - cn1EventRing[cn1EventTail].y = y; - cn1EventRing[cn1EventTail].key = keyCode; - cn1EventRing[cn1EventTail].windowId = windowId; - cn1EventTail = next; - } - pthread_mutex_unlock(&cn1EventLock); -} - -int cn1LinuxPopEvent(int* out) { - int has = 0; - pthread_mutex_lock(&cn1EventLock); - if (cn1EventHead != cn1EventTail) { - out[0] = cn1EventRing[cn1EventHead].type; - out[1] = cn1EventRing[cn1EventHead].x; - out[2] = cn1EventRing[cn1EventHead].y; - out[3] = cn1EventRing[cn1EventHead].key; - out[4] = cn1EventRing[cn1EventHead].windowId; - cn1EventHead = (cn1EventHead + 1) % CN1_EVENT_RING; - has = 1; - } - pthread_mutex_unlock(&cn1EventLock); - return has; -} +/* The event ring and cn1LinuxTakeWholeNotches live in cn1_linux_events.c, + * which the GTK-free embedded target shares. */ /* ------------------------------------------------------------- globals */ @@ -799,19 +582,6 @@ static gboolean cn1OnGenericEvent(GtkWidget* widget, GdkEvent* e, gpointer data) return TRUE; } -/* Converts a stream of fractional scroll notches into whole ones. - * - * Smooth scroll deltas are fractions of a notch, so they cannot be forwarded one - * for one: wheelUnits() on the Java side floors any sub-notch delta to a whole - * notch, and a touchpad emits deltas continuously. Whole notches are returned and - * the remainder is carried in *residue until it adds up. Truncation is toward - * zero, so the residue always keeps the sign of the travel. */ -int cn1LinuxTakeWholeNotches(double delta, double* residue) { - double total = *residue + delta; - int notches = (int) total; - *residue = total - notches; - return notches; -} /* The main window's smooth-scroll residue. Secondary windows keep their own in * CN1LinuxWindow, so two windows cannot consume each other's partial notches. */ @@ -870,209 +640,8 @@ static gboolean cn1OnDelete(GtkWidget* widget, GdkEvent* e, gpointer data) { /* --------------------------------------------------------- lifecycle */ -/* ---- hardware fault -> Java exception (POSIX analog of the iOS SignalHandler - * and the Win32 cn1WinFaultToException) ------------------------------------- - * - * ParparVM's clean C target only NULL-checks a method's `this` at entry. A null - * *argument* deref, a virtual call on a null receiver, or an operand-stack slot - * that legitimately reads back null all fault as a raw SIGSEGV in generated C - * rather than a catchable NullPointerException -- which hard-kills the process - * (and, on CI, the whole screenshot suite) with no Java stack trace. The iOS port - * converts the identical fault into an NPE via a SIGSEGV handler that calls - * throwException(); this is the Linux analog, hardened with the Win32 port's - * "wild faulting address == real corruption, leave it diagnosable" guard. - * - * The handler runs on the faulting thread's own stack for a synchronous signal, - * so longjmp-ing out of it via throwException() to the nearest CN1 try/catch - * behaves exactly as a normally thrown Java exception (this is the documented - * iOS technique). SA_NODEFER keeps the signal unblocked so faults remain - * catchable on subsequent tests after we leave the handler. - * - * NOTE: like every such handler this interferes with a native debugger (gdb sees - * the fault first-chance). It is a release/CI resilience mechanism, not a debug - * aid -- set CN1_LINUX_NO_FAULT_HANDLER=1 to disable it when debugging under gdb. - */ -/* Dump the faulting thread's native backtrace to stderr (the app-output tee). Async- - * signal-safe (write + backtrace). With the per-thread alternate signal stack this runs - * even on a STACK OVERFLOW; the repeating frames are the recursion. Called from both the - * SIGABRT path and the wild-address SIGSEGV path (stack-overflow guard-page faults land - * there), so an overflow is finally legible instead of a bare, corrupt-unwind core. */ -/* async-signal-safe "KEY=0xHEX\n" */ -static void cn1LinuxWriteHexKV(const char* key, unsigned long v) { - char buf[64]; int i = 0; - while (key[i]) { buf[i] = key[i]; i++; } - buf[i++] = '='; buf[i++] = '0'; buf[i++] = 'x'; - char hx[16]; int h = 0; - if (v == 0) hx[h++] = '0'; - while (v) { int d = v & 0xf; hx[h++] = (char)(d < 10 ? '0' + d : 'a' + d - 10); v >>= 4; } - while (h > 0) buf[i++] = hx[--h]; - buf[i++] = '\n'; - write(2, buf, i); -} - -static void cn1LinuxDumpNativeBacktrace(const char* label, siginfo_t* si, void* ucv) { - signal(SIGSEGV, SIG_DFL); - signal(SIGBUS, SIG_DFL); - write(2, "\n=====CN1 ", 10); - write(2, label, strlen(label)); - write(2, " BACKTRACE=====\n", 16); - if (si != NULL) cn1LinuxWriteHexKV("faultAddr", (unsigned long)(uintptr_t)si->si_addr); - /* faulting PC + SP from the ucontext, and this thread's real stack bounds -- if the - * used span (top - sp) is a fraction of a MB, this is a SMALL-STACK thread overflow - * (e.g. CN1 rendering on a WebKit/Gallium/GLib worker thread), not deep recursion. */ - if (ucv != NULL) { - ucontext_t* uc = (ucontext_t*)ucv; - unsigned long sp = 0, pc = 0; -#if defined(__aarch64__) - sp = (unsigned long)uc->uc_mcontext.sp; - pc = (unsigned long)uc->uc_mcontext.pc; -#elif defined(__x86_64__) - sp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP]; - pc = (unsigned long)uc->uc_mcontext.gregs[REG_RIP]; -#endif - if (pc) cn1LinuxWriteHexKV("faultPC", pc); - if (sp) cn1LinuxWriteHexKV("faultSP", sp); - /* The integer registers. Reading them costs nothing and touches no memory, and - * they are the only place the faulting POINTER survives: a wild address tells you - * a read went wrong, the register file tells you what the value WAS and which - * other register held the object it should have come from. gdb cannot supply this - * from the core -- the suite is built -O3 with LTO, so `info args` and - * `info locals` both answer "No locals" and every global reports "unknown type". - * The one 0x100000028 fault that reached a core was diagnosed this far only - * because faultAddr was printed here rather than recovered afterwards. */ -#if defined(__x86_64__) - { - static const char* const rn[16] = { - "rax","rbx","rcx","rdx","rsi","rdi","rbp","rsp", - "r8","r9","r10","r11","r12","r13","r14","r15" }; - static const int ri[16] = { - REG_RAX,REG_RBX,REG_RCX,REG_RDX,REG_RSI,REG_RDI,REG_RBP,REG_RSP, - REG_R8,REG_R9,REG_R10,REG_R11,REG_R12,REG_R13,REG_R14,REG_R15 }; - int r; - for (r = 0; r < 16; r++) { - cn1LinuxWriteHexKV(rn[r], (unsigned long)uc->uc_mcontext.gregs[ri[r]]); - } - } -#elif defined(__aarch64__) - { - char nm[8]; int r; - for (r = 0; r <= 30; r++) { - nm[0] = 'x'; - if (r < 10) { nm[1] = (char)('0' + r); nm[2] = 0; } - else { nm[1] = (char)('0' + r / 10); nm[2] = (char)('0' + r % 10); nm[3] = 0; } - cn1LinuxWriteHexKV(nm, (unsigned long)uc->uc_mcontext.regs[r]); - } - } -#endif - pthread_attr_t at; - if (pthread_getattr_np(pthread_self(), &at) == 0) { - void* base = 0; size_t sz = 0; - if (pthread_attr_getstack(&at, &base, &sz) == 0) { - cn1LinuxWriteHexKV("stackLo", (unsigned long)(uintptr_t)base); - cn1LinuxWriteHexKV("stackSz", (unsigned long)sz); - if (sp) cn1LinuxWriteHexKV("stackUsed", (unsigned long)(((uintptr_t)base + sz) - sp)); - /* The top of the faulting frame. Bounded by the real stack top, so this - * reads only mapped memory and cannot fault a second time -- the faulting - * ADDRESS is unmapped by definition and is deliberately not read. - * - * What it is for: a pointer-shaped fault whose low 32 bits are zero (the - * 0x100000000 case) is either a 64-bit value read as an object or a 32-bit - * write into a 64-bit slot, and the two look identical from the address - * alone. The neighbouring words separate them -- an adjacent half of the - * same 64-bit value is the first, an intact pointer beside a clobbered one - * is the second. */ - unsigned long hi = (unsigned long)(uintptr_t)base + (unsigned long)sz; - if (sp && sp >= (unsigned long)(uintptr_t)base && sp < hi) { - unsigned long lim = sp + 32 * sizeof(unsigned long); - if (lim > hi) { lim = hi; } - unsigned long a; - int slot = 0; - char nm[16]; - for (a = sp; a + sizeof(unsigned long) <= lim; a += sizeof(unsigned long)) { - nm[0]='s'; nm[1]='p'; nm[2]='['; - nm[3]=(char)('0' + slot / 10); nm[4]=(char)('0' + slot % 10); - nm[5]=']'; nm[6]=0; - cn1LinuxWriteHexKV(nm, *(unsigned long*)(uintptr_t)a); - slot++; - } - } - } - pthread_attr_destroy(&at); - } - } -#ifdef __GLIBC__ - void* bt[96]; - int n = backtrace(bt, 96); - backtrace_symbols_fd(bt, n, 2); -#else - const char* na = "(native backtrace unavailable on this libc)\n"; - write(2, na, strlen(na)); -#endif - const char* ftr = "=====END CN1 BACKTRACE=====\n"; - write(2, ftr, strlen(ftr)); -} - -static void cn1LinuxFaultToException(int sig, siginfo_t* si, void* ucv) { - (void) ucv; - /* Only a genuine null-ish deref (null + a small field/vtable/array offset) - * is converted to an NPE. A wild faulting address is real memory corruption OR a - * stack overflow (the guard-page address is high): dump a backtrace so the failing - * frame / recursion is identifiable from the CI log, then restore the default - * disposition and return so the re-executed instruction faults again into a core - * dump, instead of being masked as a recoverable NPE that silently corrupts state. */ - if ((sig == SIGSEGV || sig == SIGBUS) && si != NULL && - (uintptr_t) si->si_addr >= 0x10000) { - cn1LinuxDumpNativeBacktrace("FAULT", si, ucv); - signal(sig, SIG_DFL); - return; - } - struct ThreadLocalData* t = getThreadLocalData(); - if (t == NULL || t->tryBlockOffset <= 0) { - signal(sig, SIG_DFL); - return; - } - throwException(t, __NEW_INSTANCE_java_lang_NullPointerException(t)); -} - -/* Diagnostic: when the process aborts (e.g. __stack_chk_fail / a glib g_error), - * dump the native call stack so the failing frame is identifiable from the log - * even where an interactive debugger is unavailable (CI containers). Enabled only - * when CN1_LINUX_ABORT_BACKTRACE is set so it never interferes with normal runs. */ -static void cn1LinuxAbortBacktrace(int sig) { - cn1LinuxDumpNativeBacktrace("ABORT", NULL, NULL); - signal(sig, SIG_DFL); - raise(sig); -} - -static void cn1LinuxInstallFaultHandlers() { - static int installed = 0; - if (installed || getenv("CN1_LINUX_NO_FAULT_HANDLER") != NULL) { - return; - } - installed = 1; - /* Alternate signal stack for THIS (main) thread so SA_ONSTACK works here too. The CN1 - * worker/EDT/GC threads register their own in threadRunner/gcMarkWorkerMain. */ - { - stack_t ss; - ss.ss_sp = malloc(512 * 1024); - if (ss.ss_sp != NULL) { ss.ss_size = 512 * 1024; ss.ss_flags = 0; sigaltstack(&ss, NULL); } - } - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_sigaction = cn1LinuxFaultToException; - /* SA_ONSTACK: run the handler on the per-thread alternate signal stack (registered in - * threadRunner / gcMarkWorkerMain / crash-protection install) so it can execute even - * on a STACK OVERFLOW, when the faulting thread's own stack is exhausted -- otherwise - * the handler re-faults and the process dies silently with no backtrace. */ - sa.sa_flags = SA_SIGINFO | SA_NODEFER | SA_ONSTACK; - sigemptyset(&sa.sa_mask); - sigaction(SIGSEGV, &sa, NULL); - sigaction(SIGBUS, &sa, NULL); - /* Dump a native backtrace if the process aborts (e.g. __stack_chk_fail or a - * glib g_error) so the failing frame is identifiable from the CI log without a - * debugger. Silent unless an abort actually fires. */ - signal(SIGABRT, cn1LinuxAbortBacktrace); -} +/* The fault -> exception handlers live in cn1_linux_faults.c, shared with the + * embedded target. */ /* The main window's title, after initDisplay has already set it once. * @@ -1524,6 +1093,11 @@ JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_isScreenReaderEnabled___R_bool /* True when the default seat has a touchscreen pointing device attached, so the * framework reports a touch device (Display.isTouchScreen()). */ +/* The desktop target; cn1_linux_embedded.c answers true. */ +JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_isEmbeddedDisplay___R_boolean(CODENAME_ONE_THREAD_STATE) { + return JAVA_FALSE; +} + JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_isTouchDevice___R_boolean(CODENAME_ONE_THREAD_STATE) { GdkDisplay* display = gdk_display_get_default(); if (display == NULL) { @@ -2055,32 +1629,6 @@ JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_pumpMessages___R_boolean(CODEN /* ----------------------------------------------------- offscreen / headless */ -JAVA_LONG com_codename1_impl_linux_LinuxNative_createOffscreenGraphics___int_int_R_long(CODENAME_ONE_THREAD_STATE, JAVA_INT width, JAVA_INT height) { - CN1Graphics* g = (CN1Graphics*) calloc(1, sizeof(CN1Graphics)); - int w = width > 0 ? width : 1; - int h = height > 0 ? height : 1; - g->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); - g->cr = cairo_create(g->surface); - g->width = w; - g->height = h; - g->alpha = 255; - g->clipW = w; - g->clipH = h; - cairo_matrix_init_identity(&g->transform); - return (JAVA_LONG) (intptr_t) g; -} - -JAVA_BOOLEAN com_codename1_impl_linux_LinuxNative_saveGraphicsToPng___long_java_lang_String_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG graphics, JAVA_OBJECT path) { - extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT); - CN1Graphics* g = (CN1Graphics*) (intptr_t) graphics; - const char* p = path == JAVA_NULL ? 0 : stringToUTF8(threadStateData, path); - if (!g || !g->surface || !p) { - return JAVA_FALSE; - } - cairo_surface_flush(g->surface); - return cairo_surface_write_to_png(g->surface, p) == CAIRO_STATUS_SUCCESS ? JAVA_TRUE : JAVA_FALSE; -} - JAVA_VOID com_codename1_impl_linux_LinuxNative_enableHeadlessScreenshot___java_lang_String_int_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT path, JAVA_INT width, JAVA_INT height) { extern const char* stringToUTF8(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT); const char* p = path == JAVA_NULL ? 0 : stringToUTF8(threadStateData, path); @@ -2093,40 +1641,6 @@ JAVA_VOID com_codename1_impl_linux_LinuxNative_enableHeadlessScreenshot___java_l } } -/* A growable byte buffer used as the cairo PNG write closure. */ -struct CN1PngBuf { unsigned char* data; int len; int cap; }; - -static cairo_status_t cn1PngWrite(void* closure, const unsigned char* data, unsigned int length) { - struct CN1PngBuf* b = (struct CN1PngBuf*) closure; - if (b->len + (int) length > b->cap) { - int cap = b->cap > 0 ? b->cap * 2 : 8192; - while (cap < b->len + (int) length) cap *= 2; - b->data = (unsigned char*) realloc(b->data, cap); - b->cap = cap; - } - memcpy(b->data + b->len, data, length); - b->len += (int) length; - return CAIRO_STATUS_SUCCESS; -} - -/* PNG-encodes an ARGB32 surface into a freshly malloc'd buffer; caller free()s - * *outData. Returns 1 on success. Shared by image.c (encodeArgbToPng). */ -int cn1LinuxSurfaceToPng(cairo_surface_t* surface, unsigned char** outData, int* outLen) { - struct CN1PngBuf buf = { 0, 0, 0 }; - cairo_status_t st; - cairo_surface_flush(surface); - st = cairo_surface_write_to_png_stream(surface, cn1PngWrite, &buf); - if (st != CAIRO_STATUS_SUCCESS) { - free(buf.data); - *outData = 0; - *outLen = 0; - return 0; - } - *outData = buf.data; - *outLen = buf.len; - return 1; -} - JAVA_OBJECT com_codename1_impl_linux_LinuxNative_captureWindowToPngBytes___R_byte_1ARRAY(CODENAME_ONE_THREAD_STATE) { unsigned char* data = 0; int len = 0; diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java new file mode 100644 index 00000000000..1bdf0073f68 --- /dev/null +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.linux; + +import com.codename1.ui.Button; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Dialog; +import com.codename1.ui.Display; +import com.codename1.ui.TextArea; +import com.codename1.ui.TextField; +import com.codename1.ui.events.ActionEvent; +import com.codename1.ui.events.ActionListener; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.GridLayout; + +/// Text editing on the embedded Linux target, where there is no native text +/// widget and usually no physical keyboard. A field being edited opens a +/// dialog holding a lightweight copy of it and an on-screen keyboard; a +/// physical keyboard types into the same field, so a panel with one works the +/// same way. Done delivers the text through `Display.onEditingComplete`, +/// Cancel restores the original. +final class LinuxEmbeddedEditor { + private static final String[][] LETTERS = { + {"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}, + {"a", "s", "d", "f", "g", "h", "j", "k", "l", "'"}, + {"z", "x", "c", "v", "b", "n", "m", ",", ".", "-"} + }; + private static final String[][] SYMBOLS = { + {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}, + {"@", "#", "$", "%", "&", "*", "(", ")", "/", ":"}, + {"!", "?", ";", "\"", "+", "=", "_", "<", ">", "~"} + }; + + private LinuxEmbeddedEditor() { + } + + static void edit(final Component cmp, int maxSize, int constraint, final String text) { + final boolean multiLine = cmp instanceof TextArea && !((TextArea) cmp).isSingleLineTextArea(); + final TextField field = new TextField(text == null ? "" : text); + field.setConstraint(constraint); + if (maxSize > 0) { + field.setMaxSize(maxSize); + } + if (multiLine) { + field.setSingleLineTextArea(false); + field.setRows(3); + } + final Dialog dlg = new Dialog(new BorderLayout()); + final boolean[] done = new boolean[1]; + final Container keys = new Container(BoxLayout.y()); + final boolean[] shift = new boolean[1]; + final boolean[] symbols = new boolean[1]; + final Runnable[] rebuild = new Runnable[1]; + rebuild[0] = new Runnable() { + @Override + public void run() { + keys.removeAll(); + String[][] rows = symbols[0] ? SYMBOLS : LETTERS; + for (int r = 0; r < rows.length; r++) { + Container row = new Container(new GridLayout(1, rows[r].length)); + for (int k = 0; k < rows[r].length; k++) { + String label = rows[r][k]; + if (shift[0] && !symbols[0]) { + label = upper(label); + } + row.add(key(label, field, null)); + } + keys.add(row); + } + Container last = new Container(new GridLayout(1, 5)); + last.add(key(shift[0] ? "SHIFT*" : "Shift", field, new Runnable() { + @Override + public void run() { + shift[0] = !shift[0]; + rebuild[0].run(); + } + })); + last.add(key(symbols[0] ? "abc" : "?123", field, new Runnable() { + @Override + public void run() { + symbols[0] = !symbols[0]; + rebuild[0].run(); + } + })); + last.add(key("Space", field, null)); + last.add(key("Del", field, null)); + last.add(key(multiLine ? "Enter" : "Done", field, multiLine ? null : new Runnable() { + @Override + public void run() { + done[0] = true; + dlg.dispose(); + } + })); + keys.add(last); + keys.revalidate(); + } + }; + rebuild[0].run(); + Container top = new Container(new BorderLayout()); + top.add(BorderLayout.CENTER, field); + Button ok = new Button("OK"); + ok.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + done[0] = true; + dlg.dispose(); + } + }); + Button cancel = new Button("Cancel"); + cancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + dlg.dispose(); + } + }); + Container buttons = new Container(new GridLayout(1, 2)); + buttons.add(cancel); + buttons.add(ok); + top.add(BorderLayout.EAST, buttons); + dlg.add(BorderLayout.NORTH, top); + dlg.add(BorderLayout.CENTER, keys); + field.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (!multiLine) { + done[0] = true; + dlg.dispose(); + } + } + }); + dlg.setDisposeWhenPointerOutOfBounds(false); + dlg.setScrollable(false); + dlg.getContentPane().setPreferredH(Display.getInstance().getDisplayHeight() / 2); + // Modal: the port is in synchronous edit mode, so this blocks the caller + // (through invokeAndBlock, the EDT keeps running) until the dialog closes. + dlg.showPacked(BorderLayout.SOUTH, true); + Display.getInstance().onEditingComplete(cmp, done[0] ? field.getText() : text); + } + + private static Button key(final String label, final TextField field, final Runnable action) { + Button b = new Button(label); + b.setUIID("VKBKey"); + b.setFocusable(false); + b.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) { + if (action != null) { + action.run(); + return; + } + String t = field.getText(); + int caret = field.getCursorPosition(); + if (caret < 0 || caret > t.length()) { + caret = t.length(); + } + String insert; + if ("Del".equals(label)) { + if (caret == 0) { + return; + } + field.setText(t.substring(0, caret - 1) + t.substring(caret)); + field.setCursorPosition(caret - 1); + return; + } else if ("Space".equals(label)) { + insert = " "; + } else if ("Enter".equals(label)) { + insert = "\n"; + } else { + insert = label; + } + if (field.getMaxSize() > 0 && t.length() + insert.length() > field.getMaxSize()) { + return; + } + field.setText(t.substring(0, caret) + insert + t.substring(caret)); + field.setCursorPosition(caret + insert.length()); + } + }); + return b; + } + + private static String upper(String s) { + char c = s.charAt(0); + if (c >= 'a' && c <= 'z') { + return String.valueOf((char) (c - 'a' + 'A')); + } + return s; + } +} diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxHardware.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxHardware.java new file mode 100644 index 00000000000..5629279c4ba --- /dev/null +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxHardware.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.linux; + +import com.codename1.impl.KernelHardware; + +/// The Linux port's [KernelHardware], shared by the desktop and the embedded +/// Linux targets: a desktop has no GPIO controller, but a USB serial adapter or +/// a USB-to-I2C bridge works the same on either. The natives are in +/// `cn1_linux_hardware.c`. +class LinuxHardware extends KernelHardware { + + /// Listeners run on the event dispatch thread. + @Override + protected void deliver(Runnable callback) { + com.codename1.ui.Display.getInstance().callSerially(callback); + } + + @Override + protected String rawList(int kind) { + return LinuxNative.hwList(kind); + } + + @Override + protected int rawBacklight(int op, int value) { + return LinuxNative.hwBacklight(op, value); + } + + @Override + protected int rawGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges) { + // C reads the name up to its first NUL; a name holding one would open + // a device other than the one named. -22 is EINVAL. + if (chip == null || chip.indexOf('\0') >= 0) { + return -22; + } + return LinuxNative.hwGpioOpen(chip, line, output, high, bias, edges); + } + + @Override + protected int rawGpioRead(int handle) { + return LinuxNative.hwGpioRead(handle); + } + + @Override + protected int rawGpioWrite(int handle, boolean high) { + return LinuxNative.hwGpioWrite(handle, high); + } + + @Override + protected int rawGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) { + return LinuxNative.hwGpioWaitEdge(handle, timeoutMs, timestampOut); + } + + @Override + protected int rawI2cOpen(int bus, int address) { + return LinuxNative.hwI2cOpen(bus, address); + } + + @Override + protected int rawI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) { + return LinuxNative.hwI2cTransfer(handle, write, writeLen, read, readLen); + } + + @Override + protected int rawSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) { + return LinuxNative.hwSpiOpen(bus, chipSelect, mode, speedHz, bitsPerWord); + } + + @Override + protected int rawSpiTransfer(int handle, byte[] tx, byte[] rx, int len) { + return LinuxNative.hwSpiTransfer(handle, tx, rx, len); + } + + @Override + protected int rawSerialOpen(String device, int baud, int dataBits, int parity, int stopBits) { + // C reads the name up to its first NUL; a name holding one would open + // a device other than the one named. -22 is EINVAL. + if (device == null || device.indexOf('\0') >= 0) { + return -22; + } + return LinuxNative.hwSerialOpen(device, baud, dataBits, parity, stopBits); + } + + @Override + protected int rawSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs) { + return LinuxNative.hwSerialRead(handle, buf, off, len, timeoutMs); + } + + @Override + protected int rawSerialWrite(int handle, byte[] buf, int off, int len) { + return LinuxNative.hwSerialWrite(handle, buf, off, len); + } + + @Override + protected int rawPwmOpen(int chip, int channel) { + return LinuxNative.hwPwmOpen(chip, channel); + } + + @Override + protected int rawPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) { + return LinuxNative.hwPwmConfigure(handle, periodNanos, dutyNanos, enabled); + } + + @Override + protected int rawCanOpen(String iface) { + // C reads the name up to its first NUL; a name holding one would open + // a device other than the one named. -22 is EINVAL. + if (iface == null || iface.indexOf('\0') >= 0) { + return -22; + } + return LinuxNative.hwCanOpen(iface); + } + + @Override + protected int rawCanSend(int handle, int id, boolean extended, byte[] data, int len) { + return LinuxNative.hwCanSend(handle, id, extended, data, len); + } + + @Override + protected int rawCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs) { + return LinuxNative.hwCanReceive(handle, idOut, data, timeoutMs); + } + + @Override + protected int rawWatchdogOpen(int timeoutSeconds) { + return LinuxNative.hwWatchdogOpen(timeoutSeconds); + } + + @Override + protected int rawWatchdogKick(int handle) { + return LinuxNative.hwWatchdogKick(handle); + } + + @Override + protected void rawClose(int handle, boolean disarm) { + LinuxNative.hwClose(handle, disarm); + } +} diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java index 344463ff8b7..7e04273f831 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java @@ -345,6 +345,10 @@ public static void setDefaultWindowSize(int width, int height) { } } + /// True on the embedded target (DRM/KMS or fbdev, no window system). Fixed at + /// build time: the native is a constant in each of the two display units. + static final boolean EMBEDDED = LinuxNative.isEmbeddedDisplay(); + @Override public void init(Object m) { // The class library (vm/JavaAPI) reaches the EDT, the log, secure @@ -572,6 +576,16 @@ public com.codename1.util.Simd createSimd() { // networking layer to read API keys / tokens at rest; see LinuxSecureStorage. private com.codename1.security.SecureStorage secureStorage; + private LinuxHardware hardware; + + @Override + public com.codename1.hardware.Hardware getHardware() { + if (hardware == null) { + hardware = new LinuxHardware(); + } + return hardware; + } + @Override public com.codename1.security.SecureStorage getSecureStorage() { if (secureStorage == null) { @@ -2419,14 +2433,14 @@ public void getRGB(Object nativeImage, int[] arr, int offset, int x, int y, int */ @Override public boolean isNativeInputSupported() { - return true; + return !EMBEDDED; } private final LinuxTextInput lowLevelTextInput = new LinuxTextInput(); @Override public boolean isTextInputSupported() { - return true; + return !EMBEDDED; } @Override @@ -2458,7 +2472,7 @@ public void stopTextInput(Object handle) { */ @Override public boolean isAsyncEditMode() { - return true; + return !EMBEDDED; } /** @@ -2471,6 +2485,10 @@ public boolean isAsyncEditMode() { */ @Override public void editString(final Component cmp, int maxSize, int constraint, String text, int initiatingKeycode) { + if (EMBEDDED) { + LinuxEmbeddedEditor.edit(cmp, maxSize, constraint, text); + return; + } lowLevelTextInput.stopCurrent(); // Tapping straight from one field into another: finish the first cleanly // before overlaying the new one, so two controls never float at once. @@ -3813,9 +3831,13 @@ public String getPlatformName() { /// /// The list in that paragraph is now shorter by one: @desktopTitleBarMode is read here, /// because this port has a native menu bar to hand the commands to. + /// + /// The embedded target is the exception: an appliance panel has no window, + /// menu bar or title bar, and is driven by touch or a rotary encoder, so it + /// takes the mobile resource and theme layers. @Override public boolean isDesktop() { - return true; + return !EMBEDDED; } diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java index 6a8aeef89d8..9c837592f88 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java @@ -306,6 +306,11 @@ public static native void accessibilityNode(long id, long parentId, String role, */ public static native boolean isTouchDevice(); + /// True on the embedded target, where the application owns the whole screen + /// through DRM/KMS or fbdev with no window system: there are no native text + /// editors, menus or extra windows, and text is edited in place. + public static native boolean isEmbeddedDisplay(); + /** * Parks the calling (main) thread to keep the process alive in headless * screenshot mode, where there is no window message loop. The EDT exits the @@ -1171,4 +1176,28 @@ public static native void gl3dDrawArrays(long contextPeer, long pipelinePeer, lo public static native byte[] sqlColBlob(long stmtPeer, int col) throws java.io.IOException; public static native double sqlColDouble(long stmtPeer, int col) throws java.io.IOException; public static native long sqlColLong(long stmtPeer, int col) throws java.io.IOException; + + // ---- com.codename1.hardware (cn1_linux_hardware.c). Each returns a value + // >= 0 or -errno; -ETIMEDOUT reports a timeout. + public static native int hwGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges); + public static native int hwGpioRead(int handle); + public static native int hwGpioWrite(int handle, boolean high); + public static native int hwGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut); + public static native int hwI2cOpen(int bus, int address); + public static native int hwI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen); + public static native int hwSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord); + public static native int hwSpiTransfer(int handle, byte[] tx, byte[] rx, int len); + public static native int hwSerialOpen(String device, int baud, int dataBits, int parity, int stopBits); + public static native int hwSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs); + public static native int hwSerialWrite(int handle, byte[] buf, int off, int len); + public static native int hwPwmOpen(int chip, int channel); + public static native int hwPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled); + public static native int hwCanOpen(String iface); + public static native int hwCanSend(int handle, int id, boolean extended, byte[] data, int len); + public static native int hwCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs); + public static native int hwWatchdogOpen(int timeoutSeconds); + public static native int hwWatchdogKick(int handle); + public static native void hwClose(int handle, boolean disarm); + public static native String hwList(int kind); + public static native int hwBacklight(int op, int value); } diff --git a/docs/developer-guide/Embedded-Linux.asciidoc b/docs/developer-guide/Embedded-Linux.asciidoc new file mode 100644 index 00000000000..093d84e35af --- /dev/null +++ b/docs/developer-guide/Embedded-Linux.asciidoc @@ -0,0 +1,178 @@ +== Embedded Linux devices + +Codename One runs on embedded Linux boards -- Raspberry Pi, NXP i.MX, Rockchip, +NVIDIA Jetson, BeagleBoard, RISC-V boards -- as the same single native binary the +<<_working_with_the_native_linux_port,native Linux port>> builds, with the desktop +layer swapped out. There is no X11, no Wayland compositor, no GTK and no JVM on the +device: the application takes the screen directly through the kernel, reads touch, +keys and rotary encoders straight from the input devices, and starts from boot as a +service. + +The same source is also the phone, tablet and desktop companion application. A +form, a theme, an entity model or a REST client written for the device builds for +iOS and Android unchanged, and the simulator runs it on the desktop with a virtual +board attached. + +=== Why use Codename One on a device + +* *One native executable.* ParparVM translates the application, Codename One and + the Java runtime it uses to C, and the board's compiler builds it. Nothing is + interpreted and nothing is compiled at start-up, so the first frame arrives in + tens of milliseconds and memory use stays flat. +* *Java or Kotlin for the whole product.* The device UI, the companion application + and, with the <<_backend,backend>>, the server or the headless gateway share one + language, one build and one set of classes. +* *A renderer that owns every pixel.* Codename One draws its own components, so the + user interface looks the same on a 3.5 inch SPI panel, a 10 inch HMI and a phone, + and it needs nothing from the board but a frame buffer. +* *Hardware access in the core API.* GPIO, I2C, SPI, serial ports, PWM, CAN, the + watchdog and the backlight are `com.codename1.hardware`, implemented in the port, + with virtual devices in the simulator. + +=== Displays and the software fallback + +At start-up the port picks the first display back end that works, in this order, +and every board renders through at least the last two: + +. *DRM/KMS* (`/dev/dri/card*`): a mode is set on the first connected output and + frames are flipped between two buffers, so there is no tearing. This is the path + on Raspberry Pi 4 and 5, i.MX 8, Rockchip and any board with a modern display + driver. +. *fbdev* (`/dev/fb0`): the legacy frame buffer, at 32 or 16 bits per pixel, for + SPI panels and older kernels. +. *Memory*: renders without a display, for tests, screenshots and benchmarks. + +Rendering is in software, by a renderer built into the port, which needs no GPU +driver and is fast enough for a full-screen HMI at 60 frames per second on a +Raspberry Pi class board. Text is drawn with FreeType and images are decoded with +libpng and libjpeg; nothing of a desktop stack (X11, GLib, Cairo, Pango or +FontConfig) is loaded, so the application starts in a few tens of milliseconds. +Images are PNG or JPEG. + +Fonts are found by file name, without FontConfig: in the directories listed in +`CN1_FONT_PATH`, then in a `fonts` directory beside the executable, then under +`/usr/share/fonts` and `/usr/local/share/fonts`. The default face is DejaVu Sans, +or Liberation, Noto, FreeFont, Roboto or Open Sans when DejaVu is absent. Fonts an +application bundles are used as they are on every other platform. +The `CN1_DISPLAY` environment variable forces a back end (`drm`, `fbdev`, `memory` +or `headless`); the others are listed in the table below. + +.Environment variables read by the embedded port +[cols="<1,<3",options="header"] +|=== +|Variable |Effect + +|`CN1_DISPLAY` |`drm`, `fbdev`, `memory` or `headless`; by default the first that works +|`CN1_DRM_DEVICE`, `CN1_FB_DEVICE` |The device node to use instead of the first found +|`CN1_ROTATION` |`0`, `90`, `180` or `270`, for a panel mounted on its side +|`CN1_DPI` |The panel's density, when the display does not report its size +|`CN1_SIZE` |`800x480`: the size of a `memory` display +|`CN1_CURSOR` |`1` draws a pointer for a mouse; touch panels have none by default +|`CN1_TOUCH_SWAP_XY`, `CN1_TOUCH_INVERT_X`, `CN1_TOUCH_INVERT_Y` |Correct a touch controller mounted differently from its panel +|`CN1_INPUT_NO_GRAB` |`1` leaves the input devices shared with other processes +|`CN1_FONT_PATH` |Directories, separated by colons, searched for fonts before the system's +|=== + +Input comes from every `/dev/input/event*` device, including devices plugged in +later: multi-touch panels (with pinch), mice, keyboards and rotary encoders, which +move the focus like the arrow keys. Text fields open an on-screen keyboard, since +a device seldom has a physical one. `SIGTERM` closes the application cleanly and the +console is restored on exit. + +=== Building for a board + +Add the `linux.embedded` build hint to a native Linux build and name the board, and +the builder picks the architecture, the display defaults and the libraries to link: + +[source,properties] +---- +codename1.arg.linux.embedded=true +codename1.arg.linux.embedded.board=rpi5 +---- + +.Board profiles +[cols="<1,<1,<3",options="header"] +|=== +|`linux.embedded.board` |Architecture |Boards + +|`rpi5`, `rpi4`, `rpi3`, `rpi-zero2` |arm64 |Raspberry Pi 5, 4, 3 and Zero 2 W on a 64-bit OS +|`rpi-armv7` |armv7 |Raspberry Pi on a 32-bit OS +|`rpi-touch7` |arm64 |Raspberry Pi with the official 7 inch touch display +|`imx8`, `imx6` |arm64, armv7 |NXP i.MX 8 and i.MX 6 +|`rk3588` |arm64 |Rockchip RK3588 boards (Orange Pi 5, Rock 5) +|`jetson` |arm64 |NVIDIA Jetson +|`beagleplay` |arm64 |BeagleBoard BeaglePlay +|`visionfive2` |riscv64 |StarFive VisionFive 2 +|`generic-arm64`, `generic-armv7`, `generic-x64`, `generic-riscv64` |as named |Any other board +|=== + +`linux.embedded.display`, `linux.embedded.rotation` and `linux.embedded.dpi` set +the defaults the binary starts with; the environment variables override them on the +device. `linux.sysroot` points a cross build at a Yocto or Buildroot SDK sysroot, so +the binary links against exactly the libraries the image carries. + +The device needs FreeType, libpng, libjpeg, zlib, a font and, for DRM/KMS, libdrm. +OpenSSL 3 and libcurl are loaded only when the application first uses cryptography +or the network. Raspberry Pi OS Lite has most of them; the +`scripts/embedded/pios/setup-device.sh` script installs the rest and prepares the +device to start the application at boot. + +=== Deploying to a device + +The `deploy-device` goal copies the binary to the device over SSH, installs it as a +systemd service that starts at boot and restarts if it exits, and restarts it: + +[source,bash] +---- +mvn cn1:deploy-device -Dcn1.device.host=pi.local -Dcn1.device.user=pi +gradle cn1DeployDevice -Pcn1.device.host=pi.local -Pcn1.device.user=pi +---- + +It uses the system's `ssh` and `scp`, so keys, agents and `~/.ssh/config` aliases +work as they do in a terminal. `cn1.device.env` passes environment variables to the +service (`CN1_ROTATION=90,CN1_DISPLAY=fbdev`), and `cn1.device.logs=true` follows its +log after the restart. + +For a production image, `scripts/embedded/yocto/meta-codenameone` is a Yocto layer +and `scripts/embedded/buildroot/package/codenameone-app` a Buildroot package. Both +build the translated application with the image's own toolchain and start it at +boot. + +=== Hardware access + +`com.codename1.hardware.Hardware` is the entry point. On a device it drives the +kernel's own interfaces -- the GPIO character device, i2c-dev, spidev, termios, sysfs +PWM and backlight, SocketCAN and `/dev/watchdog` -- with no extra library. On a phone +it reports no support, so the same code runs everywhere: + +[source,java] +---- +Hardware hw = Hardware.getInstance(); +if (hw.isSupported(Hardware.FEATURE_GPIO)) { + GpioPin led = hw.openGpioOutput("gpiochip0", 17, false); + led.setHigh(true); + GpioPin button = hw.openGpioInput("gpiochip0", 27, GpioPin.BIAS_PULL_UP, GpioPin.EDGE_FALLING); + button.setListener((pin, rising, nanos) -> led.setHigh(!led.isHigh())); +} +I2cDevice sensor = hw.openI2c(1, 0x48); +int raw = sensor.readRegister(0); +---- + +Listeners run on the event dispatch thread. Reads that wait for data -- a serial +port, a CAN frame -- take a timeout and belong off the event dispatch thread. + +The simulator attaches a virtual board: `gpiochip0` with 32 lines, a TMP102 +temperature sensor at `0x48` and an EEPROM at `0x50` on I2C bus 1, loopback SPI, +serial and CAN devices, two PWM channels, a watchdog and a backlight. Hardware code +therefore runs, and can be unit tested, before a board is on the desk. + +=== Headless devices: the backend on a board + +A gateway, a sensor hub or a controller with no screen runs the +<<_backend,backend>> instead: a native REST and WebSocket server, built the same way +and as small. `Hardware.getInstance()` works there too, through the same kernel code +as the UI port, and listener callbacks run on the pin's own thread. The device then +serves its API, the phone application is the client, and both share the entity +classes. + +include::embedded-benchmarks.adoc[] diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 21a24930daf..afbfb2a5c53 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -282,6 +282,8 @@ include::Working-With-Windows.asciidoc[] include::Working-With-Linux.asciidoc[] +include::Embedded-Linux.asciidoc[] + include::Desktop-Integration.asciidoc[] include::Desktop-Windows.asciidoc[] diff --git a/docs/developer-guide/embedded-benchmarks.adoc b/docs/developer-guide/embedded-benchmarks.adoc new file mode 100644 index 00000000000..3e0c66113f2 --- /dev/null +++ b/docs/developer-guide/embedded-benchmarks.adoc @@ -0,0 +1,130 @@ +// Measured with scripts/embedded/bench (run.sh); Embedded-Linux.asciidoc includes this file once, whole. +// Replace the numbers only with a new run of that harness, and keep the host note in step. + +=== Compared with the alternatives + +The same workload ran in each framework: an 800x480 gauge (a filled arc that +sweeps, a needle and a bold percentage) redrawn for every frame, capped at 60 frames +per second, rendered in software into memory. Each ran three times for ten seconds, +from a cold start and then a warm one, in the same Debian bookworm arm64 container on +an Apple silicon host. The table shows the median. A board is slower than that host +and its storage is slower still, so read the columns against each other rather than +as a board's figures. + +.The gauge workload, software rendering, 800x480 (median of three runs) +[cols="<2,>1,>1,>1,>1,>1,>1,>1",options="header"] +|=== +|Framework |First frame, cold (ms) |First frame, warm (ms) |Frames per second |CPU (%) |Memory, PSS (MB) |Memory, private (MB) |Files mapped (MB) + +|Codename One |46 |53 |57 |6 |18 |17 |15 (15 files) +|Slint 1.8, software renderer |45 |103 |51 |2 |23 |22 |4.3 (6 files) +|Qt 6.4 Widgets, raster engine |73 |100 |62 |6 |23 |22 |73 (51 files) +|JavaFX 26, software pipeline |327 |468 |62 |32 to 42 |116 |115 |102 (44 files) +|Chromium kiosk, canvas |717 |419 |60 |50 or more |398 |224 |378 (149 files) +|=== + +* *First frame* is the wall-clock time from starting the process to the end of the + first frame drawn. A *cold* start follows dropping the kernel's page cache, so the + executable and every library, plugin and font it uses are read from storage again, + as on the first start after boot. A *warm* start follows straight after a cold one. + Below about 100 ms the two differ by less than the run-to-run noise. +* *Memory, PSS* (proportional set size) counts every page the process maps, shared + library pages divided among the processes sharing them, so shared libraries are + included. *Private* counts only the pages no other process shares: heap, stacks + and written data. Both are read from `/proc//smaps_rollup` mid-run, and for + Chromium summed over its whole tree of processes. +* *Files mapped* is every distinct file the process has mapped mid-run, counted once + at its full size: the executable, every shared library including the C library, + plugins loaded at run time, fonts and locale data. It is what has to be on the + device for the application to run, whether or not the board image already carries + it. Codename One's executable is 7.7 MB of it; the rest is the C library, FreeType, + libpng, libjpeg, libdrm, libepoxy and the font. OpenSSL and libcurl are loaded only + when an application first uses them, and this one does not. Qt's executable is 0.1 MB of 73; + the rest is Qt Core, Gui and Widgets, their dependencies and the platform plugins. +* *Codename One draws more*: its form has a title bar, a button, a slider and a + label around the gauge; the others draw only the gauge. +* *JavaFX* runs on a JDK 25 runtime cut down with `jlink` to the modules it uses, with + the Headless glass platform JavaFX 26 introduced and Prism's software pipeline. That + is the same rendering path as a board without a GPU, minus the display. Gluon does + not publish its embedded (Monocle) builds, so this is the closest public build. + Its CPU includes the JIT compiler warming up, which is why it varies between runs. +* *Chromium's* CPU covers its start-up as well, so it is an upper bound. +* *Slint* missed frames under the harness's 16 ms pacing loop, not for lack of CPU. + +Codename One and Slint start fastest, Codename One uses the least memory, Slint +needs the least on disk, and the three native toolkits land close together in frame +rate and CPU. Codename One and Slint need far less on disk than Qt. JavaFX, the other +way to put a Java UI on a board, needs about six times Codename One's memory and +seven times its disk, and a kiosk browser about twenty times both. + +image::img/embedded-gui-fps.svg["Bar chart of frames per second on the gauge workload: Codename One 57.5, Slint 51, Qt 62, JavaFX 61.5, Chromium 60",scaledwidth=90%] + +image::img/embedded-gui-first-frame.svg["Bar chart of milliseconds to the first frame from a cold start: Codename One 46, Slint 45, Qt 73, JavaFX 327, Chromium 717",scaledwidth=90%] + +image::img/embedded-gui-memory.svg["Bar chart of memory while animating, PSS in MB: Codename One 18, Slint 23, Qt 23, JavaFX 118, Chromium 409",scaledwidth=90%] + +flutter-pi and Android (AOSP) are not in the table: flutter-pi needs an OpenGL ES +GPU and AOSP needs a board support package, so neither runs in this harness. + +.What each option offers +[cols="<2,<2,<2,<2,<2,<2,<2",options="header"] +|=== +| |Codename One |Qt / Boot2Qt |Slint |flutter-pi |Chromium kiosk |AOSP + +|Language |Java, Kotlin |C++, QML |Rust, C++, JavaScript, Python, `.slint` |Dart |HTML, JavaScript |Java, Kotlin +|Runtime on the device |None: one native executable |Qt libraries |Small native library |Flutter engine |Browser |Android runtime +|Needs a GPU |No |No (raster), yes for Qt Quick |No |Yes |No, but slow without one |Yes +|Display without X11 or Wayland |DRM/KMS, fbdev |`eglfs`, `linuxfb` |`linuxkms` |DRM/KMS |Through Ozone |SurfaceFlinger +|Same code on iOS and Android |Yes |Yes |Not as a primary target |Yes |As a web application |Android only +|GPIO, I2C, SPI, serial, CAN in the API |Yes (`com.codename1.hardware`) |Serial and CAN modules; GPIO through the OS |No |Through plugins |No |Through vendor HALs +|Simulator with virtual hardware |Yes |No |No |No |No |Emulator +|Headless server on the same codebase |Yes (the backend) |Qt Core, HTTP server module |No |No |No |No +|Yocto and Buildroot integration |Layer and package |Yes (meta-qt6, Boot2Qt) |Yes (meta-slint) |Community recipes |Yes |Not applicable +|Licence |GPL 2 with Classpath exception |LGPL 3, GPL or commercial |GPL 3, royalty-free or commercial |BSD 3-clause |BSD 3-clause |Apache 2.0 +|=== + +=== Without a display + +A gateway or sensor hub has no screen, so the GUI comparison says little about it. +The same small JSON service ran in each runtime: `GET /json` answering a short object. +The server was pinned to two cores, a small board's share, and the load generator +(`oha`) ran on the remaining cores. Each runtime ran four rounds, from a cold start +and then a warm one, in the same Debian bookworm arm64 container. The first round +pages the container image in and is left out, so the table shows the median of the +other six runs. + +* *Start* is the time from launch to the first answered request. +* *Idle* is the proportional set size (PSS) two seconds later, with no requests. +* *Steady* is a constant 200 requests per second for ten seconds. It gives the CPU + used over that time and the memory at its end. +* *Peak* is as many requests as 32 connections can drive in ten seconds. + +.A JSON service, two cores (median of six runs) +[cols="<2,>1,>1,>1,>1,>1,>1,>1",options="header"] +|=== +|Runtime |Start (ms) |Idle PSS (MB) |Steady CPU (%) |Steady PSS (MB) |Peak (requests/s) |Peak p99 (ms) |Disk mapped (MB) + +|Codename One backend |40 |9.2 |3.1 |9.2 |379,900 |0.22 |21 (34 files) +|Rust, axum |35 |1.6 |3.2 |1.6 |290,200 |0.25 |3.3 (5 files) +|Go, net/http |48 |5.2 |8.3 |10.2 |178,900 |0.59 |5.2 (1 file) +|Node.js, http |147 |48 |11 |58 |63,500 |1.1 |90 (19 files) +|Python, FastAPI and uvicorn |270 |42 |22 |42 |6,700 |11 |21 (30 files) +|Spring Boot 3.5, JDK 21 |1,340 |164 |16 |181 |33,300 |3.3 |183 (20 files) +|=== + +Codename One starts as fast as the compiled runtimes and serves the most requests of +any of them: 1.3 times Rust's peak and 2.1 times Go's, at the lowest p99, with a +steady-load CPU the same as Rust's. Like Rust's, the server makes two system calls +per request, one read and one write, and a request allocates nothing but the object +the handler returns. Rust and Go use less memory: a Codename One process grows to +about 23 MB at peak load, against Go's 12 MB and Rust's 2 MB. Against the managed +runtimes, it starts 33 times faster than Spring Boot, uses an eighteenth of its +memory and serves over 11 times its peak. + +image::img/embedded-server-throughput.svg["Bar chart of peak JSON requests per second on two cores: Codename One 379,930, Rust 290,204, Go 178,930, Node.js 63,488, Spring Boot 33,322, Python 6,684",scaledwidth=90%] + +image::img/embedded-server-start.svg["Bar chart of milliseconds from launch to the first answered request: Codename One 40, Rust 35, Go 48, Node.js 147, Python 270, Spring Boot 1,340",scaledwidth=90%] + +image::img/embedded-server-memory.svg["Bar chart of memory idle, at 200 requests per second and at peak load, in MB, for each runtime",scaledwidth=90%] + +Every change to the backend runtime or the translator reruns this comparison in continuous integration, against Rust and Go on an arm64 machine. The build fails if Codename One's peak throughput falls below Rust's, if its latency, start or CPU falls too far behind, if its memory passes 12 MB idle or 32 MB at peak, or if a request allocates anything beyond the object its handler returns. diff --git a/docs/developer-guide/img/embedded-gui-first-frame.svg b/docs/developer-guide/img/embedded-gui-first-frame.svg new file mode 100644 index 00000000000..386b1ded8a7 --- /dev/null +++ b/docs/developer-guide/img/embedded-gui-first-frame.svg @@ -0,0 +1,33 @@ + + + +First frame, cold start +Milliseconds from launch to the first frame, lower is better + +0 + +160 + +320 + +480 + +640 + +800 +Codename One + +46 ms +Slint + +45 ms +Qt Widgets + +73 ms +JavaFX + +327 ms +Chromium kiosk + +717 ms + diff --git a/docs/developer-guide/img/embedded-gui-fps.svg b/docs/developer-guide/img/embedded-gui-fps.svg new file mode 100644 index 00000000000..42c67a4524c --- /dev/null +++ b/docs/developer-guide/img/embedded-gui-fps.svg @@ -0,0 +1,33 @@ + + + +Frames per second +The same animated screen, software rendering, higher is better + +0 + +16 + +32 + +48 + +64 + +80 +Codename One + +57.5 +Slint + +51 +Qt Widgets + +62 +JavaFX + +61.5 +Chromium kiosk + +60 + diff --git a/docs/developer-guide/img/embedded-gui-memory.svg b/docs/developer-guide/img/embedded-gui-memory.svg new file mode 100644 index 00000000000..eb0b42159d8 --- /dev/null +++ b/docs/developer-guide/img/embedded-gui-memory.svg @@ -0,0 +1,33 @@ + + + +Memory while animating +Proportional set size in MB, lower is better + +0 + +100 + +200 + +300 + +400 + +500 +Codename One + +18.2 MB +Slint + +23.3 MB +Qt Widgets + +23 MB +JavaFX + +118 MB +Chromium kiosk + +409 MB + diff --git a/docs/developer-guide/img/embedded-server-memory.svg b/docs/developer-guide/img/embedded-server-memory.svg new file mode 100644 index 00000000000..8588cd819f5 --- /dev/null +++ b/docs/developer-guide/img/embedded-server-memory.svg @@ -0,0 +1,66 @@ + + + +Memory +Proportional set size in MB, lower is better + +Idle + +Steady, 200 requests/s + +Peak load + +0 + +50 + +100 + +150 + +200 + +250 +Codename One + +9.2 MB + +9.2 MB + +23 MB +Rust, axum + +1.6 MB + +1.6 MB + +2.2 MB +Go, net/http + +5.2 MB + +10.2 MB + +11.6 MB +Node.js + +47.6 MB + +58.1 MB + +70.6 MB +Python, FastAPI + +42.4 MB + +42.4 MB + +43.9 MB +Spring Boot + +164 MB + +181 MB + +246 MB + diff --git a/docs/developer-guide/img/embedded-server-start.svg b/docs/developer-guide/img/embedded-server-start.svg new file mode 100644 index 00000000000..854b7fecbe7 --- /dev/null +++ b/docs/developer-guide/img/embedded-server-start.svg @@ -0,0 +1,36 @@ + + + +Start +Milliseconds from launch to the first answered request, lower is better + +0 + +300 + +600 + +900 + +1,200 + +1,500 +Codename One + +40 ms +Rust, axum + +35 ms +Go, net/http + +48 ms +Node.js + +147 ms +Python, FastAPI + +270 ms +Spring Boot + +1,340 ms + diff --git a/docs/developer-guide/img/embedded-server-throughput.svg b/docs/developer-guide/img/embedded-server-throughput.svg new file mode 100644 index 00000000000..38633fb27d7 --- /dev/null +++ b/docs/developer-guide/img/embedded-server-throughput.svg @@ -0,0 +1,36 @@ + + + +Peak throughput +JSON requests per second on two cores, 32 connections, higher is better + +0 + +80,000 + +160,000 + +240,000 + +320,000 + +400,000 +Codename One + +379,930 +Rust, axum + +290,204 +Go, net/http + +178,930 +Node.js + +63,488 +Python, FastAPI + +6,684 +Spring Boot + +33,322 + diff --git a/maven/build-engine/src/main/java/com/codename1/builders/EmbeddedBoardProfile.java b/maven/build-engine/src/main/java/com/codename1/builders/EmbeddedBoardProfile.java new file mode 100644 index 00000000000..949917d1667 --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/builders/EmbeddedBoardProfile.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import java.util.LinkedHashMap; +import java.util.Map; + +/// What the embedded Linux target needs to know about a board, keyed by the +/// `linux.embedded.board` build hint. A profile only sets defaults: every value +/// can still be overridden by its own hint, and at run time by the matching +/// `CN1_*` environment variable (see `cn1_linux_embedded.c`). +/// +/// The display default is `auto` for every board, which tries KMS and then +/// fbdev, so an unlisted board is never worse off than a listed one -- the +/// profile exists for the architecture, the expected panel density and the +/// boards whose usual panel is mounted rotated. +final class EmbeddedBoardProfile { + static final String DISPLAY_AUTO = "auto"; + + final String name; + final String arch; + final String display; + final int rotation; + final int dpi; + final String description; + + private static final Map PROFILES = new LinkedHashMap(); + + static { + add("rpi5", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "Raspberry Pi 5 (vc4/v3d KMS)"); + add("rpi4", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "Raspberry Pi 4 / 400 / CM4 (vc4/v3d KMS)"); + add("rpi3", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "Raspberry Pi 3 / CM3, 64-bit OS"); + add("rpi-zero2", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "Raspberry Pi Zero 2 W, 64-bit OS"); + add("rpi-armv7", LinuxNativeBuilder.ARCH_ARMV7, 0, 0, "Any Raspberry Pi on a 32-bit OS"); + // The official 7" touch display reports no physical size over DSI. + add("rpi-touch7", LinuxNativeBuilder.ARCH_ARM64, 0, 133, "Raspberry Pi with the official 7\" DSI display"); + add("imx8", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "NXP i.MX 8M family (LCDIF/DCSS KMS)"); + add("imx6", LinuxNativeBuilder.ARCH_ARMV7, 0, 0, "NXP i.MX 6 family (IPUv3 KMS)"); + add("rk3588", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "Rockchip RK3588 / RK3566 boards (VOP2 KMS)"); + add("jetson", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "NVIDIA Jetson Orin / Nano"); + add("beagleplay", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "BeaglePlay / AM62x (tidss KMS)"); + add("visionfive2", LinuxNativeBuilder.ARCH_RISCV64, 0, 0, "StarFive VisionFive 2 / JH7110"); + add("generic-arm64", LinuxNativeBuilder.ARCH_ARM64, 0, 0, "Any 64-bit ARM board"); + add("generic-armv7", LinuxNativeBuilder.ARCH_ARMV7, 0, 0, "Any 32-bit ARM hard-float board"); + add("generic-x64", LinuxNativeBuilder.ARCH_X64, 0, 0, "x86-64 panel PCs and kiosks"); + add("generic-riscv64", LinuxNativeBuilder.ARCH_RISCV64, 0, 0, "Any 64-bit RISC-V board"); + } + + private EmbeddedBoardProfile(String name, String arch, String display, int rotation, int dpi, String description) { + this.name = name; + this.arch = arch; + this.display = display; + this.rotation = rotation; + this.dpi = dpi; + this.description = description; + } + + private static void add(String name, String arch, int rotation, int dpi, String description) { + PROFILES.put(name, new EmbeddedBoardProfile(name, arch, DISPLAY_AUTO, rotation, dpi, description)); + } + + /// The profile for a board name, or `generic-arm64` for an unknown or absent one. + static EmbeddedBoardProfile forBoard(String board) { + EmbeddedBoardProfile p = board == null ? null : PROFILES.get(board.trim()); + return p != null ? p : PROFILES.get("generic-arm64"); + } + + static Map all() { + return PROFILES; + } + + /// Validates a display hint; anything unknown falls back to auto. + static String normalizeDisplay(String display) { + if ("kms".equals(display) || "fbdev".equals(display) || "memory".equals(display) + || "headless".equals(display)) { + return display; + } + return DISPLAY_AUTO; + } + + /// Validates a rotation hint; anything but 90, 180 or 270 is 0. + static int normalizeRotation(int rotation) { + return rotation == 90 || rotation == 180 || rotation == 270 ? rotation : 0; + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java index 233c2c6f914..7e8620147bd 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/LinuxNativeBuilder.java @@ -83,6 +83,9 @@ public class LinuxNativeBuilder extends Executor { /** Supported target architectures for {@code linux.arch}. */ public static final String ARCH_X64 = "x64"; public static final String ARCH_ARM64 = "arm64"; + /// 32-bit ARM, hard-float (armhf). Embedded boards on a 32-bit OS. + public static final String ARCH_ARMV7 = "armv7"; + public static final String ARCH_RISCV64 = "riscv64"; private File resultDir; private File linuxExecutable; @@ -137,6 +140,12 @@ public static String normalizeArch(String arch) { if (a.equals("x64") || a.equals("x86_64") || a.equals("amd64") || a.equals("x86-64")) { return ARCH_X64; } + if (a.equals("armv7") || a.equals("armhf") || a.equals("arm") || a.equals("armv7l")) { + return ARCH_ARMV7; + } + if (a.equals("riscv64") || a.equals("riscv")) { + return ARCH_RISCV64; + } // Unknown values fall back to x64 rather than failing the build. return ARCH_X64; } @@ -149,7 +158,15 @@ public static String normalizeArch(String arch) { * (Alpine). A glibc binary and a musl binary are not interchangeable. */ public static String targetTriple(String arch, boolean musl) { - String a = ARCH_ARM64.equals(normalizeArch(arch)) ? "aarch64" : "x86_64"; + String n = normalizeArch(arch); + if (ARCH_ARMV7.equals(n)) { + return musl ? "arm-linux-musleabihf" : "arm-linux-gnueabihf.2.28"; + } + if (ARCH_RISCV64.equals(n)) { + // glibc gained riscv64 in 2.27; 2.28 is still the floor. + return musl ? "riscv64-linux-musl" : "riscv64-linux-gnu.2.28"; + } + String a = ARCH_ARM64.equals(n) ? "aarch64" : "x86_64"; return musl ? (a + "-linux-musl") : (a + "-linux-gnu.2.28"); } @@ -182,9 +199,46 @@ static String detectHostArch() { if (a.contains("aarch64") || a.contains("arm64")) { return ARCH_ARM64; } + if (a.startsWith("arm")) { + return ARCH_ARMV7; + } + if (a.contains("riscv64")) { + return ARCH_RISCV64; + } return ARCH_X64; } + /// True for `linux.embedded=true`: the application owns the screen through + /// DRM/KMS or fbdev with no window system and no GTK (see + /// `cn1_linux_embedded.c`). + static boolean isEmbeddedRequested(BuildRequest request) { + return "true".equalsIgnoreCase(request.getArg("linux.embedded", "false")); + } + + /// Preprocessor defaults the embedded display unit reads when the matching + /// `CN1_*` environment variable is not set on the device: the board profile's, + /// overridden by the individual hints. + static String embeddedDefines(BuildRequest request, EmbeddedBoardProfile profile) { + String display = EmbeddedBoardProfile.normalizeDisplay( + request.getArg("linux.embedded.display", profile.display)); + int rotation = EmbeddedBoardProfile.normalizeRotation( + parseIntOr(request.getArg("linux.embedded.rotation", null), profile.rotation)); + int dpi = Math.max(0, parseIntOr(request.getArg("linux.embedded.dpi", null), profile.dpi)); + return "CN1_DEFAULT_DISPLAY_NAME=" + display + ";CN1_DEFAULT_ROTATION=" + rotation + + ";CN1_DEFAULT_DPI=" + dpi; + } + + private static int parseIntOr(String value, int def) { + if (value == null || value.trim().isEmpty()) { + return def; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ex) { + return def; + } + } + @Override protected String hardeningPlatform(BuildRequest request) { return "linux"; @@ -192,7 +246,13 @@ protected String hardeningPlatform(BuildRequest request) { @Override public boolean build(File sourceZip, BuildRequest request) throws BuildException { - String arch = normalizeArch(request.getArg("linux.arch", ARCH_X64)); + boolean embedded = isEmbeddedRequested(request); + EmbeddedBoardProfile board = EmbeddedBoardProfile.forBoard(request.getArg("linux.embedded.board", null)); + // A board implies its architecture; linux.arch still wins when given. + String arch = normalizeArch(request.getArg("linux.arch", embedded ? board.arch : ARCH_X64)); + if (embedded) { + log("Embedded Linux target, board profile " + board.name + " (" + board.description + ")"); + } log("Building native Linux app for arch=" + arch + " (triple " + targetTriple(arch, isMuslRequested(request)) + ")"); File tmpFile = getBuildDirectory(); @@ -363,6 +423,7 @@ public void usesClassMethod(String cls, String method) { } parparCmd.add("-Dcn1.sqlite=" + usesDatabase); parparCmd.add("-Dcn1.sqlcipher=" + usesDatabaseCipher); + parparCmd.add("-Dcn1.linux.embedded=" + embedded); NativeVerifyOption.addTo(parparCmd, request, "linux"); parparCmd.add("-jar"); parparCmd.add(parparVMCompilerJar.getAbsolutePath()); @@ -439,6 +500,15 @@ public void usesClassMethod(String cls, String method) { // Drive the .incbin ASM unit through the same compiler so a zig cc wrapper // links the whole binary. configure.add("-DCMAKE_ASM_COMPILER=" + cc); + if (embedded) { + configure.add("-DCN1_EXTRA_DEFINES=" + embeddedDefines(request, board)); + } + // A board's own sysroot (Yocto SDK, Buildroot staging, a Pi OS image's + // root): headers and pkg-config data for the target architecture. + String sysroot = request.getArg("linux.sysroot", null); + if (sysroot != null && !sysroot.trim().isEmpty()) { + configure.add("-DCMAKE_TOOLCHAIN_FILE=" + writeSysrootToolchain(sysroot.trim(), arch).getAbsolutePath()); + } try { if (!exec(cmakeRoot, 1800000, configure.toArray(new String[0]))) { @@ -534,6 +604,35 @@ private String resolveCCompiler(BuildRequest request, String arch) { return "cc"; } + /// A CMake toolchain file that points the compiler and pkg-config at a + /// target sysroot, so the cairo/pango/drm the embedded build links are the + /// board's and not the build host's. + private File writeSysrootToolchain(String sysroot, String arch) { + String n = normalizeArch(arch); + String multiarch = ARCH_ARM64.equals(n) ? "aarch64-linux-gnu" + : ARCH_ARMV7.equals(n) ? "arm-linux-gnueabihf" + : ARCH_RISCV64.equals(n) ? "riscv64-linux-gnu" : "x86_64-linux-gnu"; + File file = new File(getBuildDirectory(), "sysroot-toolchain.cmake"); + String root = sysroot.replace("\\", "/"); + StringBuilder sb = new StringBuilder(); + sb.append("set(CMAKE_SYSTEM_NAME Linux)\n"); + sb.append("set(CMAKE_SYSROOT \"").append(root).append("\")\n"); + sb.append("set(CMAKE_FIND_ROOT_PATH \"").append(root).append("\")\n"); + sb.append("set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n"); + sb.append("set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n"); + sb.append("set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\n"); + sb.append("set(ENV{PKG_CONFIG_SYSROOT_DIR} \"").append(root).append("\")\n"); + sb.append("set(ENV{PKG_CONFIG_LIBDIR} \"").append(root).append("/usr/lib/pkgconfig:") + .append(root).append("/usr/share/pkgconfig:") + .append(root).append("/usr/lib/").append(multiarch).append("/pkgconfig\")\n"); + try (OutputStream out = new FileOutputStream(file)) { + out.write(sb.toString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException ex) { + throw new BuildException("Failed to write the sysroot toolchain file", ex); + } + return file; + } + /** * Writes an executable wrapper script that invokes {@code zig cc -target * } for the selected arch/libc, so CMake can treat it as a single C diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java new file mode 100644 index 00000000000..bb0c438f0d0 --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.build.Log; +import com.codename1.builders.BuildException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/// Pushes a native Linux build to a device over SSH and runs it there as a +/// systemd service: the Maven `deploy-device` goal and the Gradle +/// `cn1DeployDevice` task are both this class. +/// +/// It drives the system's own `ssh` and `scp`, so keys, agents, `~/.ssh/config` +/// host aliases and jump hosts work exactly as they do in a terminal; nothing +/// here stores a credential. The binary goes to `remoteDir`, a unit named after +/// the service is written to `/etc/systemd/system` and the service is +/// restarted, so a redeploy is one command. The unit starts the application on +/// boot, restarts it if it exits, and runs it on the console's virtual +/// terminal, which is what the embedded display needs to take the screen. +public final class DeviceDeployer { + private final Log log; + private File binary; + private String host; + private String user; + private int port = 22; + private String remoteDir = "/opt/codenameone"; + private String service; + private String environment = ""; + private boolean followLogs; + + public DeviceDeployer(Log log) { + this.log = log; + } + + public DeviceDeployer binary(File binary) { + this.binary = binary; + return this; + } + + public DeviceDeployer host(String host) { + this.host = host; + return this; + } + + public DeviceDeployer user(String user) { + this.user = user; + return this; + } + + public DeviceDeployer port(int port) { + this.port = port; + return this; + } + + public DeviceDeployer remoteDir(String remoteDir) { + this.remoteDir = remoteDir; + return this; + } + + /// The systemd unit name, without `.service`. Defaults to the binary's name. + public DeviceDeployer service(String service) { + this.service = service; + return this; + } + + /// `KEY=value` pairs, separated by spaces or commas, for the unit's + /// `Environment=` line: `CN1_ROTATION=90`, `CN1_DISPLAY=fbdev`, ... + public DeviceDeployer environment(String environment) { + this.environment = environment == null ? "" : environment; + return this; + } + + /// Streams the service's journal after the restart, until interrupted. + public DeviceDeployer followLogs(boolean followLogs) { + this.followLogs = followLogs; + return this; + } + + public void deploy() throws BuildException { + if (host == null || host.trim().isEmpty()) { + throw new BuildException("No device host: set cn1.device.host (an address or an ~/.ssh/config alias)"); + } + if (binary == null || !binary.isFile()) { + throw new BuildException("No native Linux binary to deploy" + (binary == null ? "" : " at " + binary) + + ": build the linux-native target locally or set cn1.device.binary"); + } + if (!isElf(binary)) { + throw new BuildException(binary + " is not a Linux executable (no ELF header)"); + } + String name = service != null && !service.trim().isEmpty() ? service.trim() : binary.getName(); + // The first character is a letter or digit so that nothing here can reach + // ssh, install or systemctl as an option ("-oProxyCommand=..." runs a + // local command), and a name cannot be "." or "..". + if (!name.matches("[A-Za-z0-9][A-Za-z0-9_.@-]*") || !remoteDir.matches("/[A-Za-z0-9_./-]*") + || remoteDir.contains("..")) { + throw new BuildException("The service name may only use letters, digits and _.@- and must start with a" + + " letter or digit; the remote directory is an absolute path of the same characters and /"); + } + String h = host.trim(); + String u = user == null ? "" : user.trim(); + if (!h.matches("[A-Za-z0-9_\\[][A-Za-z0-9_.:%\\[\\]-]*") || !u.matches("([A-Za-z0-9_][A-Za-z0-9_.-]*)?")) { + throw new BuildException("cn1.device.host and cn1.device.user must start with a letter or digit and use" + + " only letters, digits and _.-: (" + h + ")"); + } + String target = u.isEmpty() ? h : u + "@" + h; + String remoteBinary = remoteDir + "/" + name; + + log.info("Copying " + binary.getName() + " (" + (binary.length() / 1024) + " KB) to " + target + + " and installing the " + name + " service"); + run(ssh(target, false, installScript(name, remoteBinary)), binary); + log.info("Deployed " + remoteBinary + "; it starts on boot and restarts if it exits"); + if (followLogs) { + run(ssh(target, true, "sudo journalctl -f -n 50 -u " + name + ".service")); + } + } + + /// The shell run on the device: read the binary from standard input into a + /// private directory, move it into place, write the unit only when it + /// changed, and restart. The staging directory comes from `mktemp -d`, so + /// it is mode 0700 and named unpredictably: another account on the device + /// cannot plant or swap the files root is about to install. `sudo` because both destinations are + /// root's; a device whose deploy user has no passwordless sudo fails here + /// with sudo's own message. + String installScript(String name, String remoteBinary) { + StringBuilder unit = new StringBuilder(); + unit.append("[Unit]\n") + .append("Description=Codename One application ").append(name).append('\n') + .append("After=systemd-udev-settle.service network-online.target\n") + .append("Wants=network-online.target\n") + .append("Conflicts=getty@tty1.service\n\n") + .append("[Service]\n") + .append("ExecStart=").append(remoteBinary).append('\n') + .append("WorkingDirectory=").append(remoteDir).append('\n') + .append("Restart=always\nRestartSec=1\n") + .append("StandardInput=tty\nTTYPath=/dev/tty1\nTTYReset=yes\nTTYVHangup=yes\n") + .append("StandardOutput=journal\nStandardError=journal\n"); + for (String pair : environment.split("[,\\s]+")) { + if (pair.contains("=")) { + // Quoted, with systemd's specifier and escape characters doubled, + // so a value reaches the program exactly as written. + unit.append("Environment=\"").append(pair.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("%", "%%")).append("\"\n"); + } + } + unit.append("\n[Install]\nWantedBy=multi-user.target\n"); + String unitPath = "/etc/systemd/system/" + name + ".service"; + return "set -e\n" + + "d=$(mktemp -d)\n" + + "trap 'rm -rf \"$d\"' EXIT\n" + + "cat > \"$d/upload\"\n" + + "sudo mkdir -p -- " + remoteDir + "\n" + + "sudo install -m 0755 -- \"$d/upload\" " + remoteBinary + "\n" + + "printf '%s' '" + unit.toString().replace("'", "'\\''") + "' > \"$d/unit\"\n" + + "if ! sudo cmp -s -- \"$d/unit\" " + unitPath + "; then\n" + + " sudo install -m 0644 -- \"$d/unit\" " + unitPath + "\n" + + " sudo systemctl daemon-reload\n" + + " sudo systemctl enable -- " + name + ".service\n" + + "fi\n" + + "sudo systemctl restart -- " + name + ".service\n"; + } + + private List ssh(String target, boolean tty, String script) { + List cmd = new ArrayList(Arrays.asList("ssh", "-p", String.valueOf(port))); + if (tty) { + cmd.add("-t"); + } + // "--" ends ssh's options, so the target can never be read as one. + cmd.add("--"); + cmd.add(target); + cmd.add(script); + return cmd; + } + + private void run(List command) throws BuildException { + run(command, null); + } + + /// Runs command with the console attached, or with input as its standard + /// input when it is not null. + private void run(List command, File input) throws BuildException { + try { + ProcessBuilder pb = new ProcessBuilder(command).inheritIO(); + if (input != null) { + pb.redirectInput(input); + } + Process p = pb.start(); + int rc = p.waitFor(); + if (rc != 0) { + throw new BuildException(command.get(0) + " exited with " + rc); + } + } catch (IOException ex) { + throw new BuildException("Could not run " + command.get(0) + "; is OpenSSH installed?", ex); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new BuildException("Interrupted", ex); + } + } + + static boolean isElf(File f) { + byte[] magic = new byte[4]; + try (InputStream in = new FileInputStream(f)) { + return in.read(magic) == 4 && magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; + } catch (IOException ex) { + return false; + } + } + + /// The newest ELF executable directly under one of `dirs`, or `null`: what a + /// local linux-native build left behind. + public static File findBinary(File... dirs) { + File best = null; + for (File dir : dirs) { + File[] files = dir == null ? null : dir.listFiles(); + if (files == null) { + continue; + } + for (File f : files) { + if (f.isFile() && f.canExecute() && isElf(f) && (best == null || f.lastModified() > best.lastModified())) { + best = f; + } + } + } + return best; + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/maven/processors/RestControllerAnnotationProcessor.java b/maven/build-engine/src/main/java/com/codename1/maven/processors/RestControllerAnnotationProcessor.java index a7fcd73a662..5be5093aee7 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/processors/RestControllerAnnotationProcessor.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/processors/RestControllerAnnotationProcessor.java @@ -1802,6 +1802,9 @@ public int compare(Route a, Route b) { .append("String[] bound) throws Exception {\n"); emitRoute(sb, route, i, c); sb.append(" }\n\n"); + if (usesEncoder(route)) { + emitEncoder(sb, route, i); + } } if (local) { sb.append(" public com.codename1.backend.HttpServer.Response resolve(\n") @@ -1932,11 +1935,7 @@ private static void emitRoute(StringBuilder sb, Route route, int index, Controll .append(";\n"); sb.append(pad).append("return result == null ? request.respond(404, \"text/plain\", EMPTY)\n"); sb.append(pad).append(" : request.respondJson(").append(route.status) - .append(", new com.codename1.backend.Json.Writable() {\n"); - sb.append(pad).append(" public void writeTo(com.codename1.backend.ByteSink out) {\n"); - appendIndented(sb, route.codecWrite, pad + " "); - sb.append(pad).append(" }\n"); - sb.append(pad).append(" });\n"); + .append(", result, CN1_ENCODER").append(index).append(");\n"); } else { // Everything else is JSON. respondJson writes into the connection's own // Response, so a route that returns a value still allocates only that value. @@ -2741,6 +2740,33 @@ private static void appendIndented(StringBuilder sb, String code, String pad) { } } + /// True exactly when emitRoute answers through CN1_ENCODER: the same + /// tests in the same order as its branches, so the field and its one use + /// cannot disagree. + private static boolean usesEncoder(Route route) { + return !route.view && !"void".equals(route.returnJavaType) + && !isResponseType(route.returnJavaType) + && !"java.lang.String".equals(route.returnJavaType) + && route.codecWrite != null; + } + + /// The route's codec as a static `Json.Encoder`. It used to be an anonymous + /// Writable built around the result on every request: one object per + /// response, which with the result itself was all a small JSON route + /// allocated. The encoder is handed the value instead, so it is made once. + private static void emitEncoder(StringBuilder sb, Route route, int index) { + String type = BackendJsonCodecs.source(route.returnJavaType); + sb.append(" private static final com.codename1.backend.Json.Encoder CN1_ENCODER") + .append(index).append(" =\n"); + sb.append(" new com.codename1.backend.Json.Encoder() {\n"); + sb.append(" @SuppressWarnings(\"unchecked\")\n"); + sb.append(" public void write(Object cn1Value, com.codename1.backend.ByteSink out) {\n"); + sb.append(" final ").append(type).append(" result = (").append(type) + .append(") cn1Value;\n"); + appendIndented(sb, route.codecWrite, " "); + sb.append(" }\n };\n\n"); + } + private static void emitRouterHelpers(StringBuilder sb) { sb.append(" private static final byte[] EMPTY = new byte[0];\n\n"); // The multipart helpers. Unused ones go with the dead-code pass. diff --git a/maven/build-engine/src/test/java/com/codename1/maven/DeviceDeployerTest.java b/maven/build-engine/src/test/java/com/codename1/maven/DeviceDeployerTest.java new file mode 100644 index 00000000000..b93a516cd66 --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/maven/DeviceDeployerTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.builders.BuildException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DeviceDeployerTest { + + @TempDir + File dir; + + private File write(String name, byte[] content, boolean executable) throws IOException { + File f = new File(dir, name); + try (OutputStream out = new FileOutputStream(f)) { + out.write(content); + } + assertTrue(f.setExecutable(executable)); + return f; + } + + @Test + void findsTheNewestElfExecutableOnly() throws IOException { + byte[] elf = {0x7f, 'E', 'L', 'F', 2, 1, 1}; + write("script.sh", "#!/bin/sh\n".getBytes(StandardCharsets.US_ASCII), true); + File old = write("old", elf, true); + assertTrue(old.setLastModified(1000L)); + File app = write("app", elf, true); + write("lib.so", elf, false); + assertEquals(app, DeviceDeployer.findBinary(dir, new File(dir, "missing"))); + assertNull(DeviceDeployer.findBinary(new File(dir, "missing"))); + } + + @Test + void refusesWhatItCannotDeploy() throws IOException { + File script = write("script.sh", "#!/bin/sh\n".getBytes(StandardCharsets.US_ASCII), true); + DeviceDeployer d = new DeviceDeployer(new NullLog()).binary(script); + assertThrows(BuildException.class, d::deploy, "no host"); + d.host("pi.local"); + BuildException e = assertThrows(BuildException.class, d::deploy); + assertTrue(e.getMessage().contains("ELF"), e.getMessage()); + File elf = write("app", new byte[] {0x7f, 'E', 'L', 'F'}, true); + d.binary(elf).service("app; reboot"); + assertThrows(BuildException.class, d::deploy); + } + + @Test + void namesThatCouldBeReadAsOptionsAreRefused() throws Exception { + File elf = write("app", new byte[] {0x7f, 'E', 'L', 'F'}, true); + for (String host : new String[] {"-oProxyCommand=touch x", "a b", "-p"}) { + DeviceDeployer d = new DeviceDeployer(new NullLog()).host(host).binary(elf); + assertThrows(BuildException.class, d::deploy, host); + } + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("pi").user("-x") + .binary(elf)::deploy); + for (String service : new String[] {"-h", "..", "."}) { + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("pi").binary(elf) + .service(service)::deploy, service); + } + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("pi").binary(elf) + .remoteDir("/opt/../etc")::deploy); + } + + @Test + void theInstallScriptIsValidShellAndCarriesTheEnvironment() throws Exception { + String script = new DeviceDeployer(new NullLog()).remoteDir("/opt/app") + .environment("CN1_ROTATION=90, CN1_DISPLAY=fbdev NAME=it's P=50%") + .installScript("demo", "/opt/app/demo"); + assertTrue(script.contains("ExecStart=/opt/app/demo")); + assertTrue(script.contains("Environment=\"CN1_ROTATION=90\"\n")); + assertTrue(script.contains("Environment=\"CN1_DISPLAY=fbdev\"\n")); + assertTrue(script.contains("systemctl restart -- demo.service")); + File f = new File(dir, "install.sh"); + Files.write(f.toPath(), script.getBytes(StandardCharsets.UTF_8)); + Process p = new ProcessBuilder("sh", "-n", f.getAbsolutePath()).redirectErrorStream(true).start(); + assertEquals(0, p.waitFor(), "sh -n rejected the script"); + // The unit is printed through printf '%s' '...'; the quote in a value must + // come out as written rather than ending the string. + Process unit = new ProcessBuilder("sh", "-c", script.substring(script.indexOf("printf"), + script.indexOf(" > \"$d/unit\""))).start(); + String text = new String(readAll(unit), StandardCharsets.UTF_8); + assertEquals(0, unit.waitFor()); + assertTrue(text.contains("Environment=\"NAME=it's\"\n") && text.contains("Environment=\"P=50%%\"\n"), text); + assertTrue(text.startsWith("[Unit]\n"), text); + } + + private static byte[] readAll(Process p) throws IOException { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = p.getInputStream().read(buf)) > 0) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + private static final class NullLog implements com.codename1.build.Log { + public boolean isDebugEnabled() { return false; } + public void debug(CharSequence c) { } + public void debug(CharSequence c, Throwable t) { } + public void debug(Throwable t) { } + public boolean isInfoEnabled() { return false; } + public void info(CharSequence c) { } + public void info(CharSequence c, Throwable t) { } + public void info(Throwable t) { } + public boolean isWarnEnabled() { return false; } + public void warn(CharSequence c) { } + public void warn(CharSequence c, Throwable t) { } + public void warn(Throwable t) { } + public boolean isErrorEnabled() { return false; } + public void error(CharSequence c) { } + public void error(CharSequence c, Throwable t) { } + public void error(Throwable t) { } + } +} diff --git a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java index 0fa62565f0c..eb1bc1bc225 100644 --- a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java +++ b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java @@ -198,6 +198,56 @@ static void register(List h) { .type(HintType.STRING) .platform("linux")); + h.add(new Hint("linux.embedded") + .group(HintGroup.LINUX) + .type(HintType.BOOLEAN) + .def("false") + .platform("linux") + .doc("Builds for an embedded Linux device: the application owns the screen through " + + "DRM/KMS or the framebuffer, with no window system and no GTK, and reads " + + "touch, mouse, keyboard and rotary input straight from evdev.")); + + h.add(new Hint("linux.embedded.board") + .group(HintGroup.LINUX) + .type(HintType.STRING) + .def("generic-arm64") + .platform("linux") + .doc("Board profile for `linux.embedded`, which sets the architecture and display " + + "defaults: `rpi5`, `rpi4`, `rpi3`, `rpi-zero2`, `rpi-armv7`, `rpi-touch7`, " + + "`imx8`, `imx6`, `rk3588`, `jetson`, `beagleplay`, `visionfive2`, " + + "`generic-arm64`, `generic-armv7`, `generic-x64` or `generic-riscv64`.")); + + h.add(new Hint("linux.embedded.display") + .group(HintGroup.LINUX) + .type(HintType.STRING) + .def("auto") + .platform("linux") + .doc("Default display backend for `linux.embedded`: `auto` (KMS, then the " + + "framebuffer), `kms`, `fbdev`, `memory` or `headless`. `CN1_DISPLAY` on the " + + "device overrides it.")); + + h.add(new Hint("linux.embedded.rotation") + .group(HintGroup.LINUX) + .type(HintType.INT) + .def("0") + .platform("linux") + .doc("Clockwise panel rotation for `linux.embedded`: 0, 90, 180 or 270.")); + + h.add(new Hint("linux.embedded.dpi") + .group(HintGroup.LINUX) + .type(HintType.INT) + .def("0") + .platform("linux") + .doc("Panel density for `linux.embedded` when the display does not report its " + + "physical size; 0 uses what the panel reports.")); + + h.add(new Hint("linux.sysroot") + .group(HintGroup.LINUX) + .type(HintType.STRING) + .platform("linux") + .doc("Target sysroot (a Yocto SDK, Buildroot staging directory or a mounted device " + + "image) whose headers and libraries a cross build compiles and links against.")); + h.add(new Hint("linux.nativeVerify") .group(HintGroup.LINUX) .type(HintType.STRING) diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/ToolSupport.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/ToolSupport.java index 3b21262fd24..52196697e6d 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/ToolSupport.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/ToolSupport.java @@ -66,6 +66,10 @@ static void register(Project project, ProjectLayout layout, SourceSet main, Code Provider> roots = project.provider(() -> sourceRoots(main)); Provider javaRoot = project.provider(() -> primaryJavaRoot(main, layout).getAbsolutePath()); Provider encoding = AppSupport.javaEncoding(project, main); + project.getTasks().register("cn1DeployDevice", com.codename1.gradle.tasks.DeployDeviceTask.class, t -> { + t.setGroup(AppSupport.GROUP); + t.setDescription("Copies the native Linux build to a device over SSH and runs it as a service."); + }); register(project, layout, ext, userProperties, roots, javaRoot, encoding, "settings", DesktopTool.SETTINGS, "Opens Codename One Settings for this project"); register(project, layout, ext, userProperties, roots, javaRoot, encoding, "guibuilder", diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java new file mode 100644 index 00000000000..bda3c572b29 --- /dev/null +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.gradle.tasks; + +import com.codename1.builders.BuildException; +import com.codename1.gradle.GradleLog; +import com.codename1.maven.DeviceDeployer; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; + +import java.io.File; + +/// Copies a native Linux build to a device over SSH and runs it there as a +/// systemd service: `gradle cn1DeployDevice -Pcn1.device.host=pi.local`. The +/// same [DeviceDeployer] and the same `cn1.device.*` properties as the Maven +/// `deploy-device` goal. +@DisableCachingByDefault(because = "Deploys to a device") +public abstract class DeployDeviceTask extends DefaultTask { + + @TaskAction + public void deploy() { + File buildDir = getProject().getLayout().getBuildDirectory().get().getAsFile(); + String binary = deviceProperty("binary"); + File bin = binary != null ? getProject().file(binary) + : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native")); + String port = deviceProperty("port"); + String dir = deviceProperty("dir"); + try { + new DeviceDeployer(new GradleLog(getLogger())).host(deviceProperty("host")).user(deviceProperty("user")) + .port(port == null ? 22 : Integer.parseInt(port)).binary(bin) + .remoteDir(dir == null ? "/opt/codenameone" : dir).service(deviceProperty("service")) + .environment(deviceProperty("env")).followLogs("true".equals(deviceProperty("logs"))).deploy(); + } catch (BuildException ex) { + throw new GradleException(ex.getMessage(), ex); + } catch (NumberFormatException ex) { + throw new GradleException("cn1.device.port is not a number: " + port, ex); + } + } + + private String deviceProperty(String name) { + Object v = getProject().findProperty("cn1.device." + name); + return v == null ? null : v.toString(); + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java new file mode 100644 index 00000000000..cf92144c824 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.builders.BuildException; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.File; + +/** + * Copies a native Linux build to a device over SSH and runs it there as a + * systemd service that starts on boot: {@code mvn cn1:deploy-device + * -Dcn1.device.host=pi.local}. Redeploying replaces the binary and restarts the + * service. See {@link DeviceDeployer}. + */ +@Mojo(name = "deploy-device", requiresProject = false) +public class DeployDeviceMojo extends AbstractMojo { + + /** The device: an address, a host name or an {@code ~/.ssh/config} alias. */ + @Parameter(property = "cn1.device.host") + private String host; + + /** The SSH user; the default is whatever ssh picks for the host. */ + @Parameter(property = "cn1.device.user") + private String user; + + /** The SSH port. */ + @Parameter(property = "cn1.device.port", defaultValue = "22") + private int port; + + /** + * The executable to deploy. By default the newest Linux executable under + * {@code target}, which is where a local linux-native build leaves it. + */ + @Parameter(property = "cn1.device.binary") + private File binary; + + /** Where the binary is installed on the device. */ + @Parameter(property = "cn1.device.dir", defaultValue = "/opt/codenameone") + private String remoteDir; + + /** The systemd service name; defaults to the binary's file name. */ + @Parameter(property = "cn1.device.service") + private String service; + + /** Environment for the service, e.g. {@code CN1_ROTATION=90,CN1_DISPLAY=fbdev}. */ + @Parameter(property = "cn1.device.env") + private String environment; + + /** Streams the service's log after the restart. */ + @Parameter(property = "cn1.device.logs", defaultValue = "false") + private boolean logs; + + @Parameter(defaultValue = "${project.build.directory}", readonly = true) + private File buildDirectory; + + @Override + public void execute() throws MojoExecutionException { + File bin = binary; + if (bin == null && buildDirectory != null) { + bin = DeviceDeployer.findBinary(buildDirectory, new File(buildDirectory, "linux-native")); + } + try { + new DeviceDeployer(MavenLog.of(getLog())).host(host).user(user).port(port).binary(bin) + .remoteDir(remoteDir).service(service).environment(environment).followLogs(logs).deploy(); + } catch (BuildException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } +} diff --git a/maven/javase/src/test/java/com/codename1/impl/javase/JavaSEHardwareTest.java b/maven/javase/src/test/java/com/codename1/impl/javase/JavaSEHardwareTest.java new file mode 100644 index 00000000000..faa933b93c4 --- /dev/null +++ b/maven/javase/src/test/java/com/codename1/impl/javase/JavaSEHardwareTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.javase; + +import com.codename1.hardware.CanBus; +import com.codename1.hardware.CanFrame; +import com.codename1.hardware.GpioPin; +import com.codename1.hardware.Hardware; +import com.codename1.hardware.HardwareException; +import com.codename1.hardware.I2cDevice; +import com.codename1.hardware.SerialPort; +import com.codename1.hardware.SpiDevice; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// The simulator's virtual board, driven through the public hardware API the +/// way an application drives a real one. +class JavaSEHardwareTest { + private final Hardware hw = new JavaSEHardware(); + + @Test + void reportsEveryFeature() { + assertTrue(hw.isSupported(Hardware.FEATURE_GPIO | Hardware.FEATURE_I2C | Hardware.FEATURE_CAN)); + assertArrayEquals(new String[]{"gpiochip0"}, hw.listGpioChips()); + } + + @Test + void outputReadsBackAndInputSeesSimulatedLevel() throws HardwareException { + GpioPin out = hw.openGpioOutput("gpiochip0", 17, true); + assertTrue(out.isHigh()); + out.setHigh(false); + assertFalse(JavaSEHardware.getLevel(17)); + out.close(); + + GpioPin in = hw.openGpioInput("gpiochip0", 5, GpioPin.BIAS_PULL_UP, GpioPin.EDGE_BOTH); + JavaSEHardware.setInput("gpiochip0", 5, true); + assertTrue(in.isHigh()); + JavaSEHardware.setInput("gpiochip0", 5, false); + assertFalse(in.isHigh()); + in.close(); + assertThrows(HardwareException.class, () -> hw.openGpioOutput("gpiochip9", 1, false)); + } + + @Test + void temperatureSensorAndEeprom() throws HardwareException { + JavaSEHardware.setTemperature(25.0); + I2cDevice tmp = hw.openI2c(1, 0x48); + byte[] raw = tmp.readRegisters(0, 2); + int value = ((raw[0] << 8) | (raw[1] & 0xff)) >> 4; + assertEquals(25.0, value * 0.0625, 0.0001); + tmp.close(); + + I2cDevice eeprom = hw.openI2c(1, 0x50); + eeprom.write(new byte[]{0x10, 1, 2, 3}); + assertArrayEquals(new byte[]{1, 2, 3}, eeprom.writeRead(new byte[]{0x10}, 3)); + eeprom.close(); + assertThrows(HardwareException.class, () -> hw.openI2c(1, 0x20)); + } + + @Test + void loopbacks() throws HardwareException { + SpiDevice spi = hw.openSpi(0, 0, 0, 1000000, 8); + assertArrayEquals(new byte[]{9, 8, 7}, spi.transfer(new byte[]{9, 8, 7})); + spi.close(); + + SerialPort serial = hw.openSerial("/dev/ttySIM0", 115200, 8, SerialPort.PARITY_NONE, 1); + serial.write("hi".getBytes(), 0, 2); + byte[] buf = new byte[8]; + assertEquals(2, serial.read(buf, 0, buf.length, 1000)); + assertEquals("hi", new String(buf, 0, 2)); + assertEquals(0, serial.read(buf, 0, buf.length, 10)); + serial.close(); + + CanBus can = hw.openCan("vcan0"); + can.send(new CanFrame(0x123, false, new byte[]{4, 5})); + CanFrame f = can.receive(1000); + assertEquals(0x123, f.getId()); + assertArrayEquals(new byte[]{4, 5}, f.getData()); + assertNull(can.receive(10)); + can.close(); + } + + @Test + void backlightClamps() throws HardwareException { + hw.setBacklightBrightness(400); + assertEquals(255, hw.getBacklightBrightness()); + hw.setBacklightBrightness(-3); + assertEquals(0, hw.getBacklightBrightness()); + hw.setBacklightBrightness(255); + } +} diff --git a/scripts/embedded/Containerfile.dev b/scripts/embedded/Containerfile.dev new file mode 100644 index 00000000000..40b0633f108 --- /dev/null +++ b/scripts/embedded/Containerfile.dev @@ -0,0 +1,12 @@ +# The build environment for embedded Linux targets: the board's own compiler and +# the libraries the embedded port links (no GTK). build-dist.sh builds it once +# per architecture. Debian bookworm matches Raspberry Pi OS and most vendor +# BSPs' glibc; a binary built here runs on bookworm or newer. +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake pkg-config binutils \ + libfreetype-dev libpng-dev libjpeg-dev libssl-dev libcurl4-openssl-dev \ + libdrm-dev libegl-dev libgles-dev libepoxy-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + libnghttp2-dev zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* diff --git a/scripts/embedded/README.md b/scripts/embedded/README.md new file mode 100644 index 00000000000..a8fd62f3a98 --- /dev/null +++ b/scripts/embedded/README.md @@ -0,0 +1,65 @@ +# Embedded Linux + +Tools for running Codename One applications on embedded Linux boards through the +embedded variant of the native Linux port (`CN1_LINUX_EMBEDDED`: DRM/KMS, fbdev or +memory display, evdev input, no GTK). The user documentation is the developer guide's +"Embedded Linux devices" chapter (`docs/developer-guide/Embedded-Linux.asciidoc`). + +| Path | What it is | +|---|---| +| `translate.sh` | Translates compiled application classes into the CMake project the board builds, from this checkout's own build output, with no build service. | +| `build-dist.sh` | Builds that project for `arm64`, `arm`, `amd64` or `riscv64` in a container (`Containerfile.dev`), strips the binary and keeps its symbols beside it. | +| `gen-embedded-stubs.py` | Generates `Ports/LinuxPort/nativeSources/cn1_linux_embedded_stubs.c`, the natives the embedded build leaves out; `--check` is the CI form. | +| `demo/` | `EmbeddedDemo`, a small HMI that is also the benchmark workload (`CN1_BENCH_SECONDS`). | +| `bench/` | The same workload in Slint, Qt, JavaFX and a Chromium kiosk, and the harness that runs all of them. | +| `pios/setup-device.sh` | Prepares Raspberry Pi OS Lite to start an application at boot. | +| `yocto/meta-codenameone` | Yocto layer: a `codenameone-app` class and an example recipe. | +| `buildroot/package/codenameone-app` | Buildroot package. | + +## Building the demo + +Needs `maven/core`, `maven/linux` and `vm/ByteCodeTranslator` built into this +checkout (`mvn -Dmaven.repo.local=$(pwd)/../.m2-repo -Plocal-dev-javase -pl core,linux,parparvm -am install`). + +```bash +B=/tmp/demo; mkdir -p $B/classes +javac -source 1.8 -target 1.8 -cp maven/core/target/classes:maven/linux/target/classes \ + -d $B/classes scripts/embedded/demo/src/EmbeddedDemo.java +scripts/embedded/translate.sh $B/classes EmbeddedDemo $B/out +scripts/embedded/build-dist.sh $B/out arm64 +CN1_DISPLAY=memory CN1_SIZE=800x480 CN1_BENCH_SECONDS=10 $B/out/build-arm64/EmbeddedDemo # on the board +``` + +`CN1_SNAPSHOT=/tmp/shot.ppm` writes frame 60 (or `CN1_SNAPSHOT_FRAME`) to a file, +which is how the memory display is checked without a screen. + +## Benchmarks + +```bash +docker build --platform linux/arm64 -t cn1-bench -f scripts/embedded/bench/Containerfile scripts/embedded/bench +docker run --rm --privileged --platform linux/arm64 -v $B/out/build-arm64:/cn1 cn1-bench /bench/run.sh /cn1 10 3 +``` + +Each line is `framework cold|warm first_frame_ms fps cpu_pct pss_kb private_kb rss_kb +mapped_kb mapped_files`. `--privileged` is required: a cold start drops the page +cache first. `run.sh` explains each column; `results.txt` holds the last run. The developer guide's +table (`docs/developer-guide/embedded-benchmarks.adoc`) records the numbers and the +host they were taken on. + +`bench/headless/` is the same comparison without a display: one JSON service in the +Codename One backend, Go, Rust, Node.js, Python and Spring Boot, loaded with `oha`. + +```bash +docker build --platform linux/arm64 -t cn1-headless -f scripts/embedded/bench/headless/Containerfile scripts/embedded/bench/headless +docker run --rm --privileged --platform linux/arm64 -v :/dist cn1-headless /bench/run.sh /dist/ 3 +``` + +The backend binary comes from `vm/backend/package.sh` (`CN1_BACKEND_DEMO=demo/bench`). +`run.sh` explains each column, and `results.txt` holds the last run. + +CI holds the backend to these numbers (`.github/workflows/backend-performance.yml`). It runs +the comparison against Rust and Go on an arm64 runner, counts what each request allocates +with `census.sh` (a binary built with `CN1_BACKEND_CFLAGS="-DCN1_ALLOC_CENSUS +-DCN1_HEAP_HISTOGRAM"`), and `gate.py` fails the job when a limit is broken. The limits +are at the top of `gate.py`. The job uploads its results, census and charts, which +`charts.py` draws from the same data as the guide's charts. diff --git a/scripts/embedded/bench/Containerfile b/scripts/embedded/bench/Containerfile new file mode 100644 index 00000000000..2241bd8dd9d --- /dev/null +++ b/scripts/embedded/bench/Containerfile @@ -0,0 +1,31 @@ +# Every competitor's workload, built and run on the same Debian bookworm base +# as the Codename One binary. Native arm64 on an Apple silicon or arm64 Linux +# host; anything else runs under emulation and only the ratios mean anything. +FROM debian:bookworm +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake pkg-config git ca-certificates curl procps \ + qt6-base-dev libgl-dev fonts-dejavu-core fontconfig libfontconfig-dev \ + chromium \ + libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0 \ + libcurl4 libdrm2 \ + && rm -rf /var/lib/apt/lists/* +RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.90.0 +ENV PATH=/root/.cargo/bin:$PATH +# JavaFX: a JDK to build with, and the JavaFX 26 SDK and jmods. 26 is the first +# release with a display-less glass platform (Headless); Gluon's Monocle builds +# for boards are not published. The gauge then runs on a jlink'd runtime holding +# only the modules it uses, which is how a device image ships a Java application. +RUN mkdir -p /opt/jdk /opt/fx && cd /opt \ + && curl -sSL -o jdk.tgz "https://api.adoptium.net/v3/binary/latest/25/ga/linux/aarch64/jdk/hotspot/normal/eclipse" \ + && tar xzf jdk.tgz -C /opt/jdk --strip-components=1 \ + && curl -sSL -o fx.zip https://download2.gluonhq.com/openjfx/26.0.2/openjfx-26.0.2_linux-aarch64_bin-sdk.zip \ + && curl -sSL -o fxmods.zip https://download2.gluonhq.com/openjfx/26.0.2/openjfx-26.0.2_linux-aarch64_bin-jmods.zip \ + && python3 -c "import zipfile; [zipfile.ZipFile(z).extractall('/opt/fx') for z in ('fx.zip', 'fxmods.zip')]" \ + && /opt/jdk/bin/jlink --module-path /opt/fx/javafx-jmods-26.0.2 --add-modules java.base,javafx.graphics,jdk.unsupported \ + --strip-debug --no-header-files --no-man-pages --output /opt/fxrt +COPY . /bench +RUN cd /bench/qt && cmake -S . -B build -DCMAKE_BUILD_TYPE=Release >/dev/null && cmake --build build \ + && strip build/qtgauge +RUN cd /bench/slint && cargo build --release -q +RUN cd /bench/javafx && /opt/jdk/bin/javac --module-path /opt/fx/javafx-sdk-26.0.2/lib \ + --add-modules javafx.graphics -d classes Gauge.java diff --git a/scripts/embedded/bench/charts.py b/scripts/embedded/bench/charts.py new file mode 100644 index 00000000000..1c1bdf34b57 --- /dev/null +++ b/scripts/embedded/bench/charts.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Draws the developer guide's embedded benchmark charts from the raw results. + + scripts/embedded/bench/charts.py [output-dir [server-results]] + +Reads scripts/embedded/bench/results.txt (the GUI comparison, written by run.sh) +and scripts/embedded/bench/headless/results.txt (the server comparison, written by +headless/run.sh), takes the same medians as the tables in +docs/developer-guide/embedded-benchmarks.adoc and writes one SVG per chart into +docs/developer-guide/img (or output-dir). CI's performance gate passes the +server results its own run produced, so its charts show that run. Standard library only: the charts are +plain horizontal bars, and a plotting package is one more thing a contributor +would have to install to regenerate them. +""" + +import os +import statistics +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.normpath(os.path.join(HERE, "..", "..", "..")) + +BACKGROUND = "#0b132b" +GRID = "#2a3350" +TEXT = "#e0e6f0" +MUTED = "#8a96b0" +HIGHLIGHT = "#3fb68b" +OTHER = "#5b7bd5" +SERIES = ["#3fb68b", "#5b7bd5", "#e0a458"] + +GUI_NAMES = [ + ("codenameone", "Codename One"), + ("slint", "Slint"), + ("qt", "Qt Widgets"), + ("javafx", "JavaFX"), + ("chromium", "Chromium kiosk"), +] + +SERVER_NAMES = [ + ("codenameone", "Codename One"), + ("rust", "Rust, axum"), + ("go", "Go, net/http"), + ("node", "Node.js"), + ("python", "Python, FastAPI"), + ("spring", "Spring Boot"), +] + + +def number(token): + """A measurement, with run.sh's "50+" (a lower bound) read as its value and + its "?" (not measured) as None.""" + if token == "?": + return None + return float(token.rstrip("+")) + + +def median(values): + measured = [v for v in values if v is not None] + return statistics.median(measured) if measured else 0.0 + + +def read_rows(path, width): + rows = [] + with open(path) as source: + for line in source: + fields = line.split() + if len(fields) == width and not line.startswith("#"): + rows.append(fields) + return rows + + +def gui_medians(path): + """framework -> column -> median, over every run of that framework and mode. + + Columns: first_frame_ms fps cpu_pct pss_kb private_kb rss_kb mapped_kb files. + """ + runs = {} + for fields in read_rows(path, 10): + key = (fields[0], fields[1]) + runs.setdefault(key, []).append([number(v) for v in fields[2:]]) + medians = {} + for (name, mode), values in runs.items(): + medians.setdefault(name, {})[mode] = [median(c) for c in zip(*values)] + # Everything but the first frame is measured once the application runs, where + # a cold and a warm start are the same thing, so the table pools both. + for (name, mode), values in runs.items(): + pooled = medians[name].setdefault("all", []) + pooled.extend(values) + for name in medians: + medians[name]["all"] = [median(c) for c in zip(*medians[name]["all"])] + return medians + + +def server_medians(path): + """runtime -> columns, the median of every run after the first round. + + Columns: start_ms idle_pss_kb idle_private_kb mapped_kb mapped_files + steady_cpu_pct steady_pss_kb peak_rps peak_p99_ms peak_pss_kb. The first round + (a cold and a warm run per runtime) pages the container image in, so it is left + out, as the table says. + """ + seen = {} + runs = {} + for fields in read_rows(path, 12): + name = fields[0] + seen[name] = seen.get(name, 0) + 1 + if seen[name] <= 2: + continue + runs.setdefault(name, []).append([number(v) for v in fields[2:]]) + return {name: [median(c) for c in zip(*values)] + for name, values in runs.items()} + + +def escape(text): + return text.replace("&", "&").replace("<", "<").replace(">", ">") + + +def label(value, unit): + if value >= 1000: + text = "{:,.0f}".format(value) + elif value >= 100: + text = "{:.0f}".format(value) + else: + text = "{:.1f}".format(value).rstrip("0").rstrip(".") + return text + unit + + +def nice_max(value): + """The axis end: the first round step of a power of ten at or above value, so + the longest bar fills most of the plot.""" + step = 1.0 + while step * 10 <= value: + step *= 10 + for factor in (1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10): + if step * factor >= value: + return step * factor + return step * 10 + + +def bar_chart(path, title, subtitle, rows, unit, series=None): + """Horizontal bars, one group per row. + + rows is a list of (label, [values]); series names the values when there is + more than one per row. The first row is Codename One and is drawn highlighted + when there is a single series. + """ + count = len(rows[0][1]) + left, right, top = 150, 90, 78 + (26 if series else 0) + bar, gap, group_gap = 18, 4, 16 + group = count * bar + (count - 1) * gap + width = 720 + plot = width - left - right + height = top + len(rows) * (group + group_gap) + 40 + peak = nice_max(max(v for _, values in rows for v in values)) + + out = [] + out.append('') + out.append('' + .format(w=width, h=height)) + out.append(''.format(BACKGROUND)) + out.append('{}' + .format(TEXT, escape(title))) + out.append('{}' + .format(MUTED, escape(subtitle))) + if series: + x = left + for index, name in enumerate(series): + out.append('' + .format(x, SERIES[index])) + out.append('{}' + .format(x + 18, TEXT, escape(name))) + x += 34 + int(6.6 * len(name)) + bottom = height - 30 + for tick in range(0, 6): + value = peak * tick / 5 + x = left + plot * tick / 5 + out.append(''.format(x=x, t=top - 8, b=bottom, c=GRID)) + out.append('' + '{}'.format(x, bottom + 16, MUTED, label(value, ""))) + y = top + for row_index, (name, values) in enumerate(rows): + out.append('{}' + .format(left - 10, y + group / 2 + 4.5, TEXT, + "bold" if row_index == 0 else "normal", escape(name))) + for index, value in enumerate(values): + if series: + colour = SERIES[index] + else: + colour = HIGHLIGHT if row_index == 0 else OTHER + length = max(plot * value / peak, 1.5) + by = y + index * (bar + gap) + out.append('' + .format(left, by, length, bar, colour)) + out.append('{}' + .format(left + length + 6, by + bar / 2 + 4, TEXT, label(value, unit))) + y += group + group_gap + out.append('') + with open(path, "w") as target: + target.write("\n".join(out) + "\n") + + +def main(): + target = sys.argv[1] if len(sys.argv) > 1 else os.path.join( + ROOT, "docs", "developer-guide", "img") + gui = gui_medians(os.path.join(HERE, "results.txt")) + server = server_medians(sys.argv[2] if len(sys.argv) > 2 else + os.path.join(HERE, "headless", "results.txt")) + names = dict(GUI_NAMES) + + def gui_rows(column, mode, scale=1.0): + return [(names[k], [gui[k][mode][column] / scale]) for k, _ in GUI_NAMES if k in gui] + + bar_chart(os.path.join(target, "embedded-gui-fps.svg"), + "Frames per second", "The same animated screen, software rendering, higher is better", + gui_rows(1, "all"), "") + bar_chart(os.path.join(target, "embedded-gui-first-frame.svg"), + "First frame, cold start", "Milliseconds from launch to the first frame, lower is better", + gui_rows(0, "cold"), " ms") + bar_chart(os.path.join(target, "embedded-gui-memory.svg"), + "Memory while animating", "Proportional set size in MB, lower is better", + gui_rows(3, "all", 1000.0), " MB") + + labels = dict(SERVER_NAMES) + order = [k for k, _ in SERVER_NAMES if k in server] + bar_chart(os.path.join(target, "embedded-server-throughput.svg"), + "Peak throughput", "JSON requests per second on two cores, 32 connections, higher is better", + [(labels[k], [server[k][7]]) for k in order], "") + bar_chart(os.path.join(target, "embedded-server-start.svg"), + "Start", "Milliseconds from launch to the first answered request, lower is better", + [(labels[k], [server[k][0]]) for k in order], " ms") + bar_chart(os.path.join(target, "embedded-server-memory.svg"), + "Memory", "Proportional set size in MB, lower is better", + [(labels[k], [server[k][1] / 1000.0, server[k][6] / 1000.0, server[k][9] / 1000.0]) + for k in order], " MB", + series=["Idle", "Steady, 200 requests/s", "Peak load"]) + + +if __name__ == "__main__": + main() + diff --git a/scripts/embedded/bench/chromium/gauge.html b/scripts/embedded/bench/chromium/gauge.html new file mode 100644 index 00000000000..a0a5fdc6128 --- /dev/null +++ b/scripts/embedded/bench/chromium/gauge.html @@ -0,0 +1,31 @@ + +gauge + + + diff --git a/scripts/embedded/bench/common.h b/scripts/embedded/bench/common.h new file mode 100644 index 00000000000..b84012395cf --- /dev/null +++ b/scripts/embedded/bench/common.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* Shared by the C and C++ benchmark workloads: the same measurements the + * Codename One demo prints, in the same format. */ +#ifndef CN1_BENCH_COMMON_H +#define CN1_BENCH_COMMON_H +#include +#include +#include +#include +#include + +static long long bench_now_ms(void) { + struct timeval tv; + gettimeofday(&tv, 0); + return (long long) tv.tv_sec * 1000 + tv.tv_usec / 1000; +} + +static long bench_cpu_ticks(void) { + char buf[1024]; + FILE* f = fopen("/proc/self/stat", "r"); + size_t n; + char* p; + long ut = 0, st = 0; + int i; + if (!f) return 0; + n = fread(buf, 1, sizeof(buf) - 1, f); + fclose(f); + buf[n] = 0; + p = strrchr(buf, ')'); + if (!p) return 0; + p += 2; + for (i = 0; i < 11 && p; i++) { p = strchr(p, ' '); if (p) p++; } + if (p) sscanf(p, "%ld %ld", &ut, &st); + return ut + st; +} + +static long bench_rss_kb(void) { + long size = 0, rss = 0; + FILE* f = fopen("/proc/self/statm", "r"); + if (f) { if (fscanf(f, "%ld %ld", &size, &rss) != 2) rss = 0; fclose(f); } + return rss * (sysconf(_SC_PAGESIZE) / 1024); +} + +static int bench_seconds(void) { + const char* s = getenv("CN1_BENCH_SECONDS"); + return s ? atoi(s) : 10; +} + +static void bench_first_frame(void) { + printf("CN1BENCH first_frame_epoch_ms=%lld\n", bench_now_ms()); + fflush(stdout); +} + +static void bench_report(long frames, long long elapsedMs, long cpuTicks) { + if (elapsedMs < 1) elapsedMs = 1; + printf("CN1BENCH fps=%lld frames=%ld rss_kb=%ld cpu_pct=%lld\n", frames * 1000LL / elapsedMs, + frames, bench_rss_kb(), cpuTicks * 1000LL / elapsedMs); + fflush(stdout); +} +#endif diff --git a/scripts/embedded/bench/headless/Containerfile b/scripts/embedded/bench/headless/Containerfile new file mode 100644 index 00000000000..36bc08b17f8 --- /dev/null +++ b/scripts/embedded/bench/headless/Containerfile @@ -0,0 +1,26 @@ +# The headless (GUI-free) comparison: the same two-route HTTP service in each +# runtime, built and run on the same Debian bookworm arm64 base as the Codename +# One backend binary. oha drives the load. +FROM debian:bookworm +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential ca-certificates curl procps util-linux python3 python3-venv \ + nodejs maven \ + && rm -rf /var/lib/apt/lists/* +RUN curl -sSL https://go.dev/dl/go1.25.3.linux-arm64.tar.gz | tar xz -C /usr/local +RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.90.0 +RUN mkdir -p /opt/jdk && curl -sSL "https://api.adoptium.net/v3/binary/latest/21/ga/linux/aarch64/jdk/hotspot/normal/eclipse" \ + | tar xz -C /opt/jdk --strip-components=1 +ENV PATH=/root/.cargo/bin:/usr/local/go/bin:$PATH JAVA_HOME=/opt/jdk +RUN cargo install oha --version 1.10.0 --locked -q +RUN python3 -m venv /opt/py && /opt/py/bin/pip install -q fastapi==0.120.0 uvicorn==0.38.0 +# The servers one directory at a time and the scripts last, so editing a script +# does not rebuild every server (CI caches these layers). +COPY go /bench/go +RUN cd /bench/go && CGO_ENABLED=0 go build -ldflags="-s -w" -o gobench . +COPY rust /bench/rust +RUN cd /bench/rust && cargo build --release -q +COPY spring /bench/spring +RUN cd /bench/spring && mvn -q -B package -DskipTests +COPY node /bench/node +COPY python /bench/python +COPY run.sh census.sh /bench/ diff --git a/scripts/embedded/bench/headless/census.sh b/scripts/embedded/bench/headless/census.sh new file mode 100755 index 00000000000..bc859545b7f --- /dev/null +++ b/scripts/embedded/bench/headless/census.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Prints the heap bytes the backend allocates per request, as one line: +# +# bytes_per_request objects_per_request +# +# census.sh [requests] +# +# The census counts every object the program allocates and prints the totals +# when it exits (CN1_HEAP_REPORT asks for that report). It runs the server twice: once answering a single request and +# once answering that one plus [requests] more (default 200,000) over the same +# 32 connections run.sh's peak load uses. The difference is what the requests +# cost, with startup and connection setup cancelled out, so the figure is exact +# rather than sampled and does not depend on how fast the machine is. +set -uo pipefail +BIN="$1" +REQUESTS="${2:-200000}" +PORT=8080 +URL="http://127.0.0.1:$PORT/json" + +# Prints " " from the census the server writes when it exits. +serve() { + local count="$1" pid line + CN1_HEAP_REPORT=1 PORT=$PORT "$BIN" >/tmp/census.log 2>&1 & + pid=$! + until curl -sf -o /dev/null --max-time 1 "$URL"; do + kill -0 $pid 2>/dev/null || { echo "census.sh: server died: $(tail -3 /tmp/census.log)" >&2; exit 1; } + sleep 0.01 + done + if [ "$count" -gt 0 ]; then + oha -n "$count" -c 32 --no-tui "$URL" >/dev/null 2>&1 \ + || { echo "census.sh: load generator failed" >&2; exit 1; } + fi + kill -TERM $pid + wait $pid 2>/dev/null + line=$(grep -m1 '^\[ALLOC:exit\] ' /tmp/census.log) \ + || { echo "census.sh: no exit census; was the binary built with -DCN1_ALLOC_CENSUS?" >&2; exit 1; } + echo "$line" | awk '{print $2, $4}' +} + +# Plain assignments, not process substitution, so a failed run stops the script. +base=$(serve 0) || exit 1 +loaded=$(serve "$REQUESTS") || exit 1 +read -r base_objects base_bytes <<<"$base" +read -r objects bytes <<<"$loaded" +awk -v b="$((bytes - base_bytes))" -v o="$((objects - base_objects))" -v n="$REQUESTS" \ + 'BEGIN { printf "%.1f %.2f\n", b / n, o / n }' diff --git a/scripts/embedded/bench/headless/gate.py b/scripts/embedded/bench/headless/gate.py new file mode 100644 index 00000000000..344d420e150 --- /dev/null +++ b/scripts/embedded/bench/headless/gate.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Holds the Codename One backend to its headless benchmark results. + + gate.py [summary.md] + +results.txt is what run.sh printed with RUNTIMES including codenameone and rust; +census-line is census.sh's output ("bytes_per_request objects_per_request"). +Prints a table of the medians beside the limits, appends it to summary.md when +given (CI passes $GITHUB_STEP_SUMMARY), and exits 1 if any limit is broken. + +Speed is judged against Rust's axum measured in the same job, never in absolute +requests per second: a CI runner's speed varies from one machine to the next, +while two servers taking turns on the same cores see the same machine. Memory +and allocation are judged absolutely, since neither depends on the CPU. + +The limits sit below what the developer guide reports, with room for runner +noise. When a change moves a number for good, move its limit with it, in the +same change, and refresh the guide from the job's uploaded results. +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +import charts # noqa: E402 + +# Columns of charts.server_medians. +START, IDLE_PSS, STEADY_CPU, PEAK_RPS, PEAK_P99, PEAK_PSS = 0, 1, 5, 7, 8, 9 + +# A request may allocate the one object the demo's handler returns and nothing +# else; the runtime's own share is zero. The counters are not atomic, so a +# busy run can read a little under one object per request, never over it. +MAX_OBJECTS_PER_REQUEST = 1.05 +# The census counts what was asked for: the handler's Message is 16 bytes, +# which the heap rounds up to a 24 byte slot. +MAX_BYTES_PER_REQUEST = 24.0 +# Peak throughput as a share of Rust's; the guide reports about 1.3. +MIN_RPS_VS_RUST = 1.0 +# p99 at peak load, against Rust's plus a fixed allowance for the timer's grain. +MAX_P99_VS_RUST = 1.5 +P99_ALLOWANCE_MS = 0.1 +# Start, against Rust's plus an allowance: both are a few tens of milliseconds. +MAX_START_VS_RUST = 1.5 +START_ALLOWANCE_MS = 20.0 +# CPU at a steady 200 requests per second, against Rust's. +MAX_STEADY_CPU_VS_RUST = 1.5 +STEADY_CPU_ALLOWANCE = 1.0 +# Proportional set size in MB; the guide reports 9.2 idle and about 23 at peak. +MAX_IDLE_MB = 12.0 +MAX_PEAK_MB = 32.0 +# Runs per runtime the medians need after the first round is dropped. +MIN_RUNS = 2 + + +def main(): + if len(sys.argv) < 3: + print(__doc__) + return 2 + results, census = sys.argv[1], sys.argv[2].split() + rows = charts.read_rows(results, 12) + failed_runs = [line.strip() for line in open(results) if " failed:" in line] + medians = charts.server_medians(results) + counts = {} + for fields in rows: + counts[fields[0]] = counts.get(fields[0], 0) + 1 + + checks = [] + + def check(name, value, limit, ok, unit=""): + checks.append((name, value, limit, ok, unit)) + + for line in failed_runs: + check("run failed: " + line, 0, 0, False) + for runtime in ("codenameone", "rust"): + kept = counts.get(runtime, 0) - 2 + check(runtime + " runs measured", kept, MIN_RUNS, kept >= MIN_RUNS) + if all(ok for _, _, _, ok, _ in checks) and len(census) == 2: + cn1, rust = medians["codenameone"], medians["rust"] + bytes_per, objects_per = float(census[0]), float(census[1]) + check("objects allocated per request", objects_per, MAX_OBJECTS_PER_REQUEST, + objects_per <= MAX_OBJECTS_PER_REQUEST) + check("bytes allocated per request", bytes_per, MAX_BYTES_PER_REQUEST, + bytes_per <= MAX_BYTES_PER_REQUEST, " B") + floor = rust[PEAK_RPS] * MIN_RPS_VS_RUST + check("peak requests/s (Rust {:,.0f})".format(rust[PEAK_RPS]), cn1[PEAK_RPS], floor, + cn1[PEAK_RPS] >= floor) + ceiling = rust[PEAK_P99] * MAX_P99_VS_RUST + P99_ALLOWANCE_MS + check("peak p99 (Rust {:.2f} ms)".format(rust[PEAK_P99]), cn1[PEAK_P99], ceiling, + cn1[PEAK_P99] <= ceiling, " ms") + ceiling = rust[START] * MAX_START_VS_RUST + START_ALLOWANCE_MS + check("start (Rust {:.0f} ms)".format(rust[START]), cn1[START], ceiling, + cn1[START] <= ceiling, " ms") + ceiling = rust[STEADY_CPU] * MAX_STEADY_CPU_VS_RUST + STEADY_CPU_ALLOWANCE + check("steady CPU % (Rust {:.1f})".format(rust[STEADY_CPU]), cn1[STEADY_CPU], ceiling, + cn1[STEADY_CPU] <= ceiling) + check("idle memory", cn1[IDLE_PSS] / 1000.0, MAX_IDLE_MB, + cn1[IDLE_PSS] / 1000.0 <= MAX_IDLE_MB, " MB") + check("peak memory", cn1[PEAK_PSS] / 1000.0, MAX_PEAK_MB, + cn1[PEAK_PSS] / 1000.0 <= MAX_PEAK_MB, " MB") + elif len(census) != 2: + check("census line '{}'".format(" ".join(census)), 0, 0, False) + + lines = ["| Check | Codename One | Limit | |", "|---|---|---|---|"] + for name, value, limit, ok, unit in checks: + lines.append("| {} | {:,.2f}{} | {:,.2f}{} | {} |".format( + name, value, unit, limit, unit, "pass" if ok else "**FAIL**")) + report = "\n".join(lines) + print(report) + if len(sys.argv) > 3: + with open(sys.argv[3], "a") as summary: + summary.write("### Backend performance gate\n\n" + report + "\n") + return 0 if all(ok for _, _, _, ok, _ in checks) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/embedded/bench/headless/go/go.mod b/scripts/embedded/bench/headless/go/go.mod new file mode 100644 index 00000000000..24108d1418d --- /dev/null +++ b/scripts/embedded/bench/headless/go/go.mod @@ -0,0 +1,3 @@ +module gobench + +go 1.25 diff --git a/scripts/embedded/bench/headless/go/main.go b/scripts/embedded/bench/headless/go/main.go new file mode 100644 index 00000000000..bfbe6ce2bd9 --- /dev/null +++ b/scripts/embedded/bench/headless/go/main.go @@ -0,0 +1,57 @@ +// The Go half of the comparison: the same two routes as demo/bench, on net/http. +// +// net/http rather than fasthttp, because "Go performance" to most people means +// the standard library, and because it is the honest comparison: fasthttp is +// faster than net/http, so a claim measured against net/http must not be +// restated as a claim about Go in general. +// +// Deliberately plain: no router, no middleware, no logging. Anything added here +// that demo/bench does not do is a difference in the measurement rather than in +// the runtimes. +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "runtime" + "strconv" +) + +type message struct { + Message string `json:"message"` +} + +func main() { + port := envInt("PORT", 8080) + + mux := http.NewServeMux() + mux.HandleFunc("/plaintext", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("Hello, World!")) + }) + mux.HandleFunc("/json", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(message{Message: "Hello, World!"}) + }) + + fmt.Printf("bench listening on port %d with GOMAXPROCS=%d\n", port, runtime.GOMAXPROCS(0)) + server := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: mux} + if err := server.ListenAndServe(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func envInt(name string, fallback int) int { + value := os.Getenv(name) + if value == "" { + return fallback + } + parsed, err := strconv.Atoi(value) + if err != nil { + return fallback + } + return parsed +} diff --git a/scripts/embedded/bench/headless/node/server.js b/scripts/embedded/bench/headless/node/server.js new file mode 100644 index 00000000000..7b5d014452b --- /dev/null +++ b/scripts/embedded/bench/headless/node/server.js @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +// The Node.js half of the headless comparison: the same two routes on the +// built-in http module, no framework. +const http = require('http'); +const port = parseInt(process.env.PORT || '8080', 10); +http.createServer((req, res) => { + if (req.url === '/plaintext') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello, World!'); + } else if (req.url === '/json') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ message: 'Hello, World!' })); + } else { + res.writeHead(404); + res.end(); + } +}).listen(port); diff --git a/scripts/embedded/bench/headless/python/app.py b/scripts/embedded/bench/headless/python/app.py new file mode 100644 index 00000000000..3d22b52e655 --- /dev/null +++ b/scripts/embedded/bench/headless/python/app.py @@ -0,0 +1,16 @@ +# The Python half of the headless comparison: the same two routes on FastAPI, +# served by uvicorn -- the usual way a Python service is written today. +from fastapi import FastAPI +from fastapi.responses import PlainTextResponse + +app = FastAPI() + + +@app.get("/plaintext", response_class=PlainTextResponse) +def plaintext(): + return "Hello, World!" + + +@app.get("/json") +def json(): + return {"message": "Hello, World!"} diff --git a/scripts/embedded/bench/headless/results.txt b/scripts/embedded/bench/headless/results.txt new file mode 100644 index 00000000000..d251b158e6f --- /dev/null +++ b/scripts/embedded/bench/headless/results.txt @@ -0,0 +1,48 @@ +codenameone cold 42 8910 8400 21478 34 3.7 8996 387069 0.21 22462 +go cold 41 5092 5092 5312 1 10.4 10256 183461 0.56 11660 +rust cold 38 1579 1124 3420 5 2.9 1538 302453 0.24 2166 +node cold 141 47610 47108 89521 19 12.0 57524 65230 1.01 71007 +python cold 268 42347 41840 20901 30 21.7 42292 6726 11.45 43878 +spring cold 1397 166378 165852 182740 20 13.4 178398 35397 2.77 241013 +codenameone warm 37 9299 8792 21478 34 3.0 9391 386578 0.22 22951 +go warm 49 5192 5192 5312 1 8.7 10168 179755 0.57 11600 +rust warm 31 1629 1172 3420 5 2.7 1588 294236 0.25 2235 +node warm 174 47667 47160 89521 19 11.4 57800 66896 0.94 70972 +python warm 275 42483 41976 20901 30 23.4 42480 6764 11.38 43996 +spring warm 1188 159696 159168 182740 20 13.5 188228 36302 2.69 265191 +codenameone cold 41 9028 8528 21478 34 3.2 9120 387698 0.21 22523 +go cold 41 5172 5172 5312 1 8.7 10200 177952 0.59 11616 +rust cold 38 1581 1124 3420 5 3.2 1544 296926 0.24 2179 +node cold 144 47630 47124 89521 19 12.2 58619 63351 1.02 70366 +python cold 248 42342 41840 20901 30 21.3 42292 6813 11.37 43879 +spring cold 1358 168023 167492 182740 20 12.2 178306 34228 2.89 246204 +codenameone warm 30 9285 8776 21478 34 3.1 9372 380306 0.21 23223 +go warm 50 5176 5176 5312 1 7.5 10208 184012 0.56 11608 +rust warm 30 1657 1200 3420 5 3.1 1612 299390 0.24 2260 +node warm 171 47619 47112 89521 19 10.4 55916 87119 0.81 70567 +python warm 283 42474 41972 20901 30 22.7 42468 6706 11.31 44003 +spring warm 1322 166987 166456 182740 20 13.1 177490 34177 2.89 242260 +codenameone cold 44 9047 8540 21478 34 2.9 9136 384958 0.21 23103 +go cold 51 5176 5176 5312 1 8.2 10220 179740 0.58 11600 +rust cold 37 1577 1124 3420 5 3.1 1537 294241 0.24 2173 +node cold 150 47603 47096 89521 19 11.4 58213 63625 1.07 71259 +python cold 256 42347 41840 20901 30 24.1 42344 6735 11.53 43895 +spring cold 1320 159878 159348 182740 20 16.1 174261 32740 3.38 230892 +codenameone warm 39 9270 8760 21478 34 3.2 9356 379553 0.22 22837 +go warm 44 5152 5152 5312 1 9.7 10264 180890 0.57 11648 +rust warm 27 1653 1196 3420 5 3.2 1612 279571 0.27 2239 +node warm 139 47670 47164 89521 19 12.2 57891 61173 1.14 70722 +python warm 301 42480 41976 20901 30 19.7 42430 6495 11.80 43977 +spring warm 1272 161643 161112 182740 20 17.3 187338 32763 3.35 260169 +codenameone cold 42 8959 8452 21478 34 3.1 9049 358773 0.25 22448 +go cold 46 5172 5172 5312 1 8.3 10200 170878 0.66 11560 +rust cold 44 1588 1128 3420 5 3.2 1544 284796 0.26 2160 +node cold 142 47574 47072 89521 19 11.5 56088 65648 1.04 70383 +python cold 254 42347 41840 20901 30 24.7 42336 6514 11.98 43894 +spring cold 1501 163387 162856 182740 20 15.8 182718 33034 3.33 247156 +codenameone warm 38 9295 8788 21478 34 3.1 9385 368259 0.23 23511 +go warm 51 5164 5164 5312 1 7.8 10248 178120 0.60 11712 +rust warm 33 1633 1176 3420 5 2.7 1592 286168 0.26 2224 +node warm 153 47647 47140 89521 19 10.8 58749 60504 1.09 70716 +python warm 283 42483 41976 20901 30 19.9 42432 6662 11.44 43982 +spring warm 2042 165603 165072 182740 20 16.9 183759 33609 3.39 245036 diff --git a/scripts/embedded/bench/headless/run.sh b/scripts/embedded/bench/headless/run.sh new file mode 100755 index 00000000000..40042cf3fdf --- /dev/null +++ b/scripts/embedded/bench/headless/run.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Runs the headless comparison and prints one line per run: +# +# start_ms idle_pss_kb idle_private_kb mapped_kb mapped_files \ +# steady_cpu_pct steady_pss_kb peak_rps peak_p99_ms peak_pss_kb +# +# run.sh [runs] +# +# RUNTIMES (default: every one) names the runtimes to run, space separated; they +# always run in the order below. CI's performance gate runs "codenameone rust go". +# +# Needs a privileged container (docker run --privileged) to drop the page cache +# before a cold start. +# +# start process start to the first answered GET /json +# idle memory two seconds after that, with nothing to do +# steady a constant 200 requests per second for 10 s, which is a busy +# gateway: CPU over the window, memory at its end +# peak as many requests as 32 connections can drive for 10 s +# +# Every server is pinned to cores 0-1 (a small board's share) and the load +# generator to the rest, so the two never compete for a core. Memory is PSS and +# private from /proc//smaps_rollup, summed over the process tree; mapped is +# every distinct file the tree maps, counted once at full size. +set -uo pipefail +RUNS="${2:-3}" +RUNTIMES="${RUNTIMES:-codenameone go rust node python spring}" +PORT=8080 +URL="http://127.0.0.1:$PORT/json" +SERVER_CPUS=0-1 +LOAD_CPUS=2-$(( $(nproc) - 1 )) + +# Read from the container's own disk, not a bind mount (see ../run.sh). +mkdir -p /opt/cn1 && cp "$1" /opt/cn1/server + +now() { date +%s%3N; } + +drop_caches() { + sync + echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || { echo "run.sh: cannot drop caches; use --privileged" >&2; exit 2; } +} + +tree() { + local c + echo "$1" + for c in $(pgrep -P "$1"); do tree "$c"; done +} + +memory() { + local files="" p + for p in "$@"; do [ -r /proc/$p/smaps_rollup ] && files="$files /proc/$p/smaps_rollup"; done + [ -n "$files" ] || { echo "0 0"; return; } + awk '/^Pss:/{pss+=$2} /^Private_(Clean|Dirty):/{priv+=$2} END{printf "%d %d", pss, priv}' $files +} + +footprint() { + local p + for p in "$@"; do awk '$6 ~ /^\// {print $6}' /proc/$p/maps 2>/dev/null; done | sort -u \ + | while read -r f; do [ -f "$f" ] && stat -c %s "$f"; done \ + | awk '{s+=$1; n++} END{printf "%d %d", s/1024, n}' +} + +cpu_ticks() { + local p t=0 + for p in "$@"; do t=$((t + $(awk '{print $14 + $15}' /proc/$p/stat 2>/dev/null || echo 0))); done + echo $t +} + +run_one() { + local name="$1" mode="$2"; shift 2 + case " $RUNTIMES " in *" $name "*) ;; *) return ;; esac + local t0 pid start pids idle fp c0 c1 steady peak rps p99 + [ "$mode" = cold ] && drop_caches + t0=$(now) + PORT=$PORT taskset -c $SERVER_CPUS "$@" >/tmp/server.log 2>&1 & + pid=$! + until curl -sf -o /dev/null --max-time 1 "$URL"; do + kill -0 $pid 2>/dev/null || { echo "$name $mode failed: $(tail -3 /tmp/server.log)"; return; } + sleep 0.005 + done + start=$(( $(now) - t0 )) + sleep 2 + pids=$(tree $pid) + idle=$(memory $pids) + fp=$(footprint $pids) + + c0=$(cpu_ticks $(tree $pid)) + taskset -c $LOAD_CPUS oha -q 200 -z 10s --no-tui -c 4 "$URL" >/dev/null 2>&1 + c1=$(cpu_ticks $(tree $pid)) + steady="$(( (c1 - c0) * 100 / 10 / 100 )).$(( (c1 - c0) % 10 ))" + local steadyMem; steadyMem=$(memory $(tree $pid) | cut -d' ' -f1) + + taskset -c $LOAD_CPUS oha -z 10s --no-tui -c 32 --output-format json "$URL" >/tmp/peak.json 2>/dev/null & + local loader=$! + sleep 8 + local peakMem; peakMem=$(memory $(tree $pid) | cut -d' ' -f1) + wait $loader + rps=$(python3 -c "import json;print(int(json.load(open('/tmp/peak.json'))['summary']['requestsPerSec']))" 2>/dev/null || echo '?') + p99=$(python3 -c "import json;print('%.2f' % (json.load(open('/tmp/peak.json'))['latencyPercentiles']['p99'] * 1000))" 2>/dev/null || echo '?') + + for p in $(tree $pid); do kill -TERM $p 2>/dev/null; done + wait $pid 2>/dev/null + while kill -0 $pid 2>/dev/null; do sleep 0.2; done + sleep 2 + echo "$name $mode $start $idle $fp $steady $steadyMem $rps $p99 $peakMem" +} + +for i in $(seq "$RUNS"); do + for mode in cold warm; do + # The demo's handler is written as an application's is, with no switches. + run_one codenameone $mode /opt/cn1/server + run_one go $mode /bench/go/gobench + run_one rust $mode /bench/rust/target/release/axumbench + run_one node $mode node /bench/node/server.js + run_one python $mode /opt/py/bin/uvicorn --app-dir /bench/python --host 127.0.0.1 --port $PORT --log-level warning app:app + run_one spring $mode /opt/jdk/bin/java -jar /bench/spring/target/springbench.jar --server.port=$PORT + done +done diff --git a/scripts/embedded/bench/headless/rust/Cargo.toml b/scripts/embedded/bench/headless/rust/Cargo.toml new file mode 100644 index 00000000000..20deda2c32d --- /dev/null +++ b/scripts/embedded/bench/headless/rust/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "axumbench" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +tokio = { version = "1", features = ["rt-multi-thread", "net", "macros"] } +serde = { version = "1", features = ["derive"] } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true diff --git a/scripts/embedded/bench/headless/rust/src/main.rs b/scripts/embedded/bench/headless/rust/src/main.rs new file mode 100644 index 00000000000..f80a6914535 --- /dev/null +++ b/scripts/embedded/bench/headless/rust/src/main.rs @@ -0,0 +1,19 @@ +// The Rust half of the headless comparison: the same two routes on axum, the +// most used Rust web framework, with tokio's default multi-threaded runtime. +use axum::{routing::get, Json, Router}; +use serde::Serialize; + +#[derive(Serialize)] +struct Message { + message: &'static str, +} + +#[tokio::main] +async fn main() { + let port = std::env::var("PORT").unwrap_or_else(|_| "8080".into()); + let app = Router::new() + .route("/plaintext", get(|| async { "Hello, World!" })) + .route("/json", get(|| async { Json(Message { message: "Hello, World!" }) })); + let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/scripts/embedded/bench/headless/spring/pom.xml b/scripts/embedded/bench/headless/spring/pom.xml new file mode 100644 index 00000000000..4f2a58757f7 --- /dev/null +++ b/scripts/embedded/bench/headless/spring/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.6 + + bench + springbench + 1 + + 21 + + + + org.springframework.boot + spring-boot-starter-web + + + + springbench + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/scripts/embedded/bench/headless/spring/src/main/java/bench/Bench.java b/scripts/embedded/bench/headless/spring/src/main/java/bench/Bench.java new file mode 100644 index 00000000000..ecfe4e7436d --- /dev/null +++ b/scripts/embedded/bench/headless/spring/src/main/java/bench/Bench.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +// The Spring Boot half of the headless comparison: the same two routes on +// spring-boot-starter-web (Tomcat), as a Java team would write the service. +package bench; + +import java.util.Map; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +@RestController +public class Bench { + @GetMapping(value = "/plaintext", produces = "text/plain") + public String plaintext() { + return "Hello, World!"; + } + + @GetMapping("/json") + public Map json() { + return Map.of("message", "Hello, World!"); + } + + public static void main(String[] args) { + SpringApplication.run(Bench.class, args); + } +} diff --git a/scripts/embedded/bench/javafx/Gauge.java b/scripts/embedded/bench/javafx/Gauge.java new file mode 100644 index 00000000000..21a90b172b8 --- /dev/null +++ b/scripts/embedded/bench/javafx/Gauge.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +// The benchmark gauge in JavaFX: a Canvas redrawn on every pulse with Prism's +// software pipeline on a display-less glass platform -- the JavaFX path on a +// board without a GPU. Prints the same CN1BENCH lines as ../common.h. +import java.nio.file.Files; +import java.nio.file.Paths; + +import javafx.animation.AnimationTimer; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.geometry.VPos; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.ArcType; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.TextAlignment; +import javafx.stage.Stage; + +public class Gauge extends Application { + private static final int W = 800; + private static final int H = 480; + private double angle; + private long frames; + private boolean first = true; + + @Override + public void start(Stage stage) { + Canvas canvas = new Canvas(W, H); + GraphicsContext g = canvas.getGraphicsContext2D(); + Font font = Font.font("DejaVu Sans", FontWeight.BOLD, 28); + stage.setScene(new Scene(new Pane(canvas), W, H)); + stage.show(); + long start = System.currentTimeMillis(); + long cpu0 = cpuTicks(); + long end = start + seconds() * 1000L; + new AnimationTimer() { + @Override + public void handle(long now) { + angle += 0.04; + draw(g, font); + frames++; + if (first) { + first = false; + // The first pulse renders the frame drawn here. + Platform.runLater(() -> System.out.println("CN1BENCH first_frame_epoch_ms=" + System.currentTimeMillis())); + } + if (System.currentTimeMillis() >= end) { + stop(); + long elapsed = Math.max(1, System.currentTimeMillis() - start); + System.out.println("CN1BENCH fps=" + frames * 1000 / elapsed + " frames=" + frames + + " rss_kb=0 cpu_pct=" + (cpuTicks() - cpu0) * 1000 / elapsed); + Platform.exit(); + } + } + }.start(); + } + + private void draw(GraphicsContext g, Font font) { + int cx = W / 2, cy = H / 2, r = Math.min(W, H) / 2 - 10; + int sweep = (int) ((Math.sin(angle) + 1) * 135); + g.setFill(Color.rgb(0x10, 0x18, 0x20)); + g.fillRect(0, 0, W, H); + g.setFill(Color.rgb(0x2a, 0x3a, 0x4a)); + g.fillOval(cx - r, cy - r, r * 2, r * 2); + g.setFill(Color.rgb(0x33, 0xaa, 0xff)); + g.fillArc(cx - r, cy - r, r * 2, r * 2, 225 - sweep, sweep, ArcType.ROUND); + g.setFill(Color.rgb(0x10, 0x18, 0x20)); + g.fillOval(cx - r + 24, cy - r + 24, r * 2 - 48, r * 2 - 48); + double a = Math.toRadians(225 - sweep); + g.setStroke(Color.WHITE); + g.setLineWidth(1); + g.strokeLine(cx, cy, cx + (int) (Math.cos(a) * (r - 30)), cy - (int) (Math.sin(a) * (r - 30))); + g.setFill(Color.WHITE); + g.setFont(font); + g.setTextAlign(TextAlignment.CENTER); + g.setTextBaseline(VPos.TOP); + g.fillText((sweep * 100 / 270) + " %", cx, cy + r / 3 - 28); + } + + private static int seconds() { + String s = System.getenv("CN1_BENCH_SECONDS"); + return s == null ? 10 : Integer.parseInt(s); + } + + // utime + stime from /proc/self/stat, in clock ticks (100 per second). + private static long cpuTicks() { + try { + String stat = new String(Files.readAllBytes(Paths.get("/proc/self/stat"))); + String[] f = stat.substring(stat.lastIndexOf(')') + 2).split(" "); + return Long.parseLong(f[11]) + Long.parseLong(f[12]); + } catch (Exception e) { + return 0; + } + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/scripts/embedded/bench/qt/CMakeLists.txt b/scripts/embedded/bench/qt/CMakeLists.txt new file mode 100644 index 00000000000..db8977f3470 --- /dev/null +++ b/scripts/embedded/bench/qt/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) +project(qtgauge CXX) +set(CMAKE_CXX_STANDARD 17) +find_package(Qt6 REQUIRED COMPONENTS Widgets) +add_executable(qtgauge gauge.cpp) +target_link_libraries(qtgauge Qt6::Widgets) diff --git a/scripts/embedded/bench/qt/gauge.cpp b/scripts/embedded/bench/qt/gauge.cpp new file mode 100644 index 00000000000..dc34f6c4d13 --- /dev/null +++ b/scripts/embedded/bench/qt/gauge.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +// The benchmark gauge in Qt 6 Widgets, drawn with QPainter (Qt's software +// raster engine), on the offscreen platform plugin -- the same path as +// linuxfb on a board without a GPU. +#include +#include +#include +#include +#include +extern "C" { +#include "../common.h" +} + +class Gauge : public QWidget { +public: + double angle = 0; + long frames = 0; + bool first = true; +protected: + void paintEvent(QPaintEvent*) override { + frames++; + QPainter g(this); + g.setRenderHint(QPainter::Antialiasing, true); + int w = width(), h = height(), cx = w / 2, cy = h / 2; + int r = std::min(w, h) / 2 - 10; + g.fillRect(0, 0, w, h, QColor(0x10, 0x18, 0x20)); + g.setPen(Qt::NoPen); + g.setBrush(QColor(0x2a, 0x3a, 0x4a)); + g.drawEllipse(cx - r, cy - r, r * 2, r * 2); + int sweep = (int) ((std::sin(angle) + 1) * 135); + g.setBrush(QColor(0x33, 0xaa, 0xff)); + g.drawPie(cx - r, cy - r, r * 2, r * 2, (225 - sweep) * 16, sweep * 16); + g.setBrush(QColor(0x10, 0x18, 0x20)); + g.drawEllipse(cx - r + 24, cy - r + 24, r * 2 - 48, r * 2 - 48); + double a = (225 - sweep) * M_PI / 180.0; + g.setPen(QPen(Qt::white, 1)); + g.drawLine(cx, cy, cx + (int) (std::cos(a) * (r - 30)), cy - (int) (std::sin(a) * (r - 30))); + QFont f = g.font(); + f.setBold(true); + f.setPixelSize(28); + g.setFont(f); + QString v = QString::number(sweep * 100 / 270) + " %"; + g.drawText(QRect(0, cy + r / 3 - 28, w, 40), Qt::AlignHCenter, v); + if (first) { + first = false; + bench_first_frame(); + } + } +}; + +int main(int argc, char** argv) { + QApplication app(argc, argv); + Gauge gauge; + gauge.resize(800, 480); + gauge.show(); + QTimer tick; + QObject::connect(&tick, &QTimer::timeout, [&] { gauge.angle += 0.04; gauge.update(); }); + tick.start(16); + long long start = bench_now_ms(); + long cpu0 = bench_cpu_ticks(); + QTimer::singleShot(bench_seconds() * 1000, [&] { + bench_report(gauge.frames, bench_now_ms() - start, bench_cpu_ticks() - cpu0); + app.quit(); + }); + return app.exec(); +} diff --git a/scripts/embedded/bench/results.txt b/scripts/embedded/bench/results.txt new file mode 100644 index 00000000000..474e875362a --- /dev/null +++ b/scripts/embedded/bench/results.txt @@ -0,0 +1,32 @@ +# framework mode first_frame_ms fps cpu_pct pss_kb private_kb rss_kb mapped_kb mapped_files +# docker run --privileged, Debian bookworm arm64 on Apple silicon, 10 s x 3 runs +codenameone cold 46 57 7 18151 17640 19016 14931 15 +slint cold 45 51 2 23290 22812 24124 4395 6 +qt cold 73 62 7 22852 22276 23780 75146 51 +javafx cold 325 62 40 118710 118120 119672 104419 44 +chromium cold 623 60 42+ 407683 229472 952668 387377 149 +codenameone warm 65 57 6 18198 17692 19056 14931 15 +slint warm 63 51 2 23346 22868 24180 4395 6 +qt warm 81 62 7 23210 22636 24140 75146 51 +javafx warm 352 62 42 118126 117536 119088 104419 44 +chromium warm 419 60 51+ 409145 229260 963900 387377 149 +codenameone cold 46 58 7 18154 17644 19020 14931 15 +slint cold 44 51 1 23289 22808 24120 5088 7 +qt cold 70 62 5 22806 22232 23736 75146 51 +javafx cold 327 61 32 118974 118388 119928 104419 44 +chromium cold 717 60 50+ 408095 229388 953488 387377 149 +codenameone warm 53 57 5 18185 17672 19048 14931 15 +slint warm 239 51 5 23341 22860 24172 4395 6 +qt warm 100 62 5 23197 22620 24124 75146 51 +javafx warm 468 61 33 118070 117480 119032 104419 44 +chromium warm 230 59 54+ 410420 230272 964460 387377 149 +codenameone cold 49 58 6 18150 17640 19016 14931 15 +slint cold 139 51 4 23286 22812 24112 4395 6 +qt cold 324 61 6 22802 22228 23732 75146 51 +javafx cold 1174 62 22 117246 116656 118208 104419 44 +chromium cold 3654 ? 115+ 406203 226552 953960 387341 147 +codenameone warm 52 58 7 18189 17676 19052 14931 15 +slint warm 103 51 4 23330 22852 24164 4395 6 +qt warm 138 62 6 23202 22628 24132 75146 51 +javafx warm 1278 59 58 124878 124288 125840 104419 44 +chromium warm 421 59 65+ 410891 230124 964436 387377 149 diff --git a/scripts/embedded/bench/run.sh b/scripts/embedded/bench/run.sh new file mode 100755 index 00000000000..e0da1d80959 --- /dev/null +++ b/scripts/embedded/bench/run.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Runs every workload and prints one line per run: +# +# first_frame_ms fps cpu_pct pss_kb private_kb rss_kb mapped_kb mapped_files +# +# run.sh [seconds] [runs] +# +# Needs a privileged container (docker run --privileged): a cold start drops the +# page cache first, so every executable, library, plugin and font is read from +# disk again -- which is what the first start after boot costs, and where a large +# set of dynamic libraries shows. A warm start follows straight after. +# +# Memory is sampled from /proc//smaps_rollup half way through the run: +# PSS every page the process maps, shared pages divided among the +# processes sharing them -- the honest "how much RAM is this" number, +# and the only one that sums correctly over a process tree; +# private pages no other process shares (heap, stacks, written data); +# RSS every resident page, shared ones counted in full. +# Footprint is every FILE the process has mapped at that moment (executable, +# shared libraries, dlopen'ed plugins, fonts, locale data), summed by size once +# per file -- what has to be on the device for it to run, whatever it is called. +# For Chromium all of these are taken over its whole process tree. +set -uo pipefail +SECS="${2:-10}" +RUNS="${3:-3}" +# Every other workload is built into the image; read Codename One from the +# container's own disk too, not from a bind mount, whose cold reads go through +# the host's file sharing and cost seconds a device's storage does not. +mkdir -p /opt/cn1 && cp "$1/EmbeddedDemo" /opt/cn1/ +export QT_QPA_PLATFORM=offscreen CN1_DISPLAY=memory CN1_SIZE=800x480 + +now() { date +%s%3N; } + +drop_caches() { + sync + echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || { echo "run.sh: cannot drop caches; use --privileged" >&2; exit 2; } +} + +# smaps_rollup fields summed over the given pids: "pss private rss" in kB. +memory() { + local files="" p + for p in "$@"; do [ -r /proc/$p/smaps_rollup ] && files="$files /proc/$p/smaps_rollup"; done + [ -n "$files" ] || { echo "0 0 0"; return; } + awk '/^Pss:/{pss+=$2} /^Private_(Clean|Dirty):/{priv+=$2} /^Rss:/{rss+=$2} + END{printf "%d %d %d", pss, priv, rss}' $files +} + +# Distinct files mapped by the given pids: "total_kb count". +footprint() { + local p + for p in "$@"; do awk '$6 ~ /^\// {print $6}' /proc/$p/maps 2>/dev/null; done | sort -u \ + | while read -r f; do [ -f "$f" ] && stat -c %s "$f"; done \ + | awk '{s+=$1; n++} END{printf "%d %d", s/1024, n}' +} + +# The pid and every descendant, however deep (Chromium's zygote forks renderers). +tree() { + local c + echo "$1" + for c in $(pgrep -P "$1"); do tree "$c"; done +} + +run_one() { + local name="$1" mode="$2"; shift 2 + local log t0 pid ff line mem fp cpu0 cpu1 fps + log=$(mktemp) + [ "$mode" = cold ] && drop_caches + t0=$(now) + CN1_BENCH_SECONDS=$SECS "$@" >"$log" 2>&1 & + pid=$! + sleep $((SECS / 2)) + local pids; pids=$(tree $pid) + mem=$(memory $pids) + fp=$(footprint $pids) + if [ "$name" = chromium ]; then + # Chromium never exits by itself; CPU is read over the tree here. + cpu0=0; for p in $pids; do cpu0=$((cpu0 + $(awk '{print $14 + $15}' /proc/$p/stat 2>/dev/null || echo 0))); done + sleep $((SECS - SECS / 2 + 2)) + pkill -TERM -f -- '--user-data-dir=/tmp/chr-' 2>/dev/null + fi + wait $pid 2>/dev/null + ff=$(grep -o 'first_frame_epoch_ms=[0-9]*' "$log" | head -1 | cut -d= -f2) + line=$(grep 'CN1BENCH fps' "$log" | tail -1) + fps=$(echo "$line" | grep -o 'fps=[0-9]*' | cut -d= -f2) + local cpu; cpu=$(echo "$line" | grep -o 'cpu_pct=[0-9]*' | cut -d= -f2) + # Chromium's CPU covers its whole life up to the sample, start-up included. + [ "$name" = chromium ] && cpu="$((cpu0 * 100 / (SECS / 2) / 100))+" + # Nothing of this run may still be alive when the next one is timed: + # Chromium's tree takes seconds to unwind and skewed whatever followed it. + pkill -KILL -f -- '--user-data-dir=/tmp/chr-' 2>/dev/null + while pgrep -f -- '--user-data-dir=/tmp/chr-' >/dev/null || kill -0 $pid 2>/dev/null; do sleep 0.2; done + sleep 2 + echo "$name $mode $(( ${ff:-0} - t0 )) ${fps:-?} ${cpu:-?} $mem $fp" + rm -f "$log" +} + +chromium_cmd() { + exec chromium --headless=new --no-sandbox --disable-gpu --window-size=800,480 \ + --enable-logging=stderr --v=0 --user-data-dir=/tmp/chr-$RANDOM \ + "file:///bench/chromium/gauge.html?s=$SECS" +} + +for i in $(seq "$RUNS"); do + for mode in cold warm; do + run_one codenameone $mode /opt/cn1/EmbeddedDemo + run_one slint $mode /bench/slint/target/release/slintgauge + run_one qt $mode /bench/qt/build/qtgauge + run_one javafx $mode /opt/fxrt/bin/java -Dglass.platform=Headless -Dprism.order=sw \ + -cp /bench/javafx/classes Gauge + run_one chromium $mode chromium_cmd + done +done diff --git a/scripts/embedded/bench/slint/Cargo.toml b/scripts/embedded/bench/slint/Cargo.toml new file mode 100644 index 00000000000..cb49ad5b8d7 --- /dev/null +++ b/scripts/embedded/bench/slint/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "slintgauge" +version = "0.1.0" +edition = "2021" + +[dependencies] +slint = { version = "=1.8.0", default-features = false, features = ["compat-1-2", "std", "renderer-software", "software-renderer-systemfonts"] } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true +panic = "abort" diff --git a/scripts/embedded/bench/slint/src/main.rs b/scripts/embedded/bench/slint/src/main.rs new file mode 100644 index 00000000000..db04606ab8e --- /dev/null +++ b/scripts/embedded/bench/slint/src/main.rs @@ -0,0 +1,103 @@ +// The benchmark gauge in Slint, on Slint's software renderer through a minimal +// platform that renders each frame into a full-frame buffer -- what its +// linuxkms backend does with a dumb buffer on a board without a GPU. +use slint::platform::software_renderer::{MinimalSoftwareWindow, PremultipliedRgbaColor, RepaintBufferType}; +use slint::platform::{Platform, WindowAdapter}; +use std::rc::Rc; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +slint::slint! { + export component Gauge inherits Window { + width: 800px; + height: 480px; + in property arc; + in property needle; + in property value; + Rectangle { background: #101820; } + Path { width: 800px; height: 480px; viewbox-width: 800; viewbox-height: 480; + fill: #2a3a4a; commands: "M 630 240 A 230 230 0 1 0 170 240 A 230 230 0 1 0 630 240 Z"; } + Path { width: 800px; height: 480px; viewbox-width: 800; viewbox-height: 480; + fill: #33aaff; commands: root.arc; } + Path { width: 800px; height: 480px; viewbox-width: 800; viewbox-height: 480; + fill: #101820; commands: "M 606 240 A 206 206 0 1 0 194 240 A 206 206 0 1 0 606 240 Z"; } + Path { width: 800px; height: 480px; viewbox-width: 800; viewbox-height: 480; + stroke: white; stroke-width: 1px; commands: root.needle; } + Text { y: 240px + 76px - 28px; width: 800px; horizontal-alignment: center; + text: root.value; color: white; font-size: 28px; font-weight: 700; } + } +} + +struct Bench(Rc, Instant); + +impl Platform for Bench { + fn create_window_adapter(&self) -> Result, slint::PlatformError> { + Ok(self.0.clone()) + } + fn duration_since_start(&self) -> Duration { + self.1.elapsed() + } +} + +fn read(path: &str) -> String { + std::fs::read_to_string(path).unwrap_or_default() +} + +fn cpu_ticks() -> i64 { + let s = read("/proc/self/stat"); + let rest = &s[s.rfind(')').map(|i| i + 2).unwrap_or(0)..]; + let f: Vec<&str> = rest.split(' ').collect(); + f.get(11).and_then(|v| v.parse::().ok()).unwrap_or(0) + f.get(12).and_then(|v| v.parse::().ok()).unwrap_or(0) +} + +fn rss_kb() -> i64 { + read("/proc/self/statm").split(' ').nth(1).and_then(|v| v.parse::().ok()).unwrap_or(0) * 4 +} + +fn epoch_ms() -> u128 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() +} + +fn main() { + let window = MinimalSoftwareWindow::new(RepaintBufferType::NewBuffer); + slint::platform::set_platform(Box::new(Bench(window.clone(), Instant::now()))).unwrap(); + let ui = Gauge::new().unwrap(); + window.set_size(slint::PhysicalSize::new(800, 480)); + ui.show().unwrap(); + let mut buf = vec![PremultipliedRgbaColor::default(); 800 * 480]; + let secs: u64 = std::env::var("CN1_BENCH_SECONDS").ok().and_then(|s| s.parse().ok()).unwrap_or(10); + let (cx, cy, r) = (400.0f64, 240.0f64, 230.0f64); + let mut angle = 0.0f64; + let mut frames = 0i64; + let mut first = true; + let start = Instant::now(); + let cpu0 = cpu_ticks(); + let mut next = Instant::now(); + while start.elapsed() < Duration::from_secs(secs) { + angle += 0.04; + let sweep = ((angle.sin() + 1.0) * 135.0) as i32; + let a0 = (225.0f64).to_radians(); + let a1 = ((225 - sweep) as f64).to_radians(); + let large = if sweep > 180 { 1 } else { 0 }; + ui.set_arc(format!("M {} {} L {} {} A {r} {r} 0 {large} 1 {} {} Z", cx, cy, + cx + a0.cos() * r, cy - a0.sin() * r, cx + a1.cos() * r, cy - a1.sin() * r).into()); + ui.set_needle(format!("M {cx} {cy} L {} {}", cx + a1.cos() * (r - 30.0), cy - a1.sin() * (r - 30.0)).into()); + ui.set_value(format!("{} %", sweep * 100 / 270).into()); + slint::platform::update_timers_and_animations(); + window.draw_if_needed(|renderer| { + renderer.render(&mut buf, 800); + frames += 1; + if first { + first = false; + println!("CN1BENCH first_frame_epoch_ms={}", epoch_ms()); + } + }); + next += Duration::from_millis(16); + let now = Instant::now(); + if next > now { + std::thread::sleep(next - now); + } + } + let elapsed = start.elapsed().as_millis().max(1) as i64; + println!("CN1BENCH fps={} frames={} rss_kb={} cpu_pct={}", frames * 1000 / elapsed, frames, rss_kb(), + (cpu_ticks() - cpu0) * 1000 / elapsed); +} diff --git a/scripts/embedded/build-dist.sh b/scripts/embedded/build-dist.sh new file mode 100755 index 00000000000..aa010c2e597 --- /dev/null +++ b/scripts/embedded/build-dist.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Builds a translated application (the dist/ that translate.sh or the +# linux-native builder wrote) into a native binary for an embedded board, in a +# container so the host needs nothing but Docker or Podman. +# +# scripts/embedded/build-dist.sh [arch] [image] +# +# the directory translate.sh wrote; dist/CMakeLists.txt inside it +# arch arm64 (default), arm (armv7 hard float), amd64 or riscv64 +# image a Debian based image with the build dependencies; the default, +# cn1-embedded-dev:, is built from Containerfile.dev here +# +# The binary lands in /build-/, stripped by the generated +# project, with its symbols in a .debug file beside it. A foreign architecture runs under QEMU user emulation, +# which Docker Desktop provides and a Linux host gets from qemu-user-static: it +# is several times slower than a cross compiler, but it is the board's own +# compiler and the board's own libraries, so what links here links there. +# +# The generated CMakeLists.txt names its sources by absolute path, so the +# directory is mounted at the same path inside the container. +set -euo pipefail + +OUT="$(cd "${1:?usage: build-dist.sh [arch] [image]}" && pwd)" +ARCH="${2:-arm64}" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENGINE="${CN1_CONTAINER_ENGINE:-$(command -v docker || command -v podman)}" +IMAGE="${3:-cn1-embedded-dev:$ARCH}" + +[ -f "$OUT/dist/CMakeLists.txt" ] || { echo "build-dist: $OUT/dist/CMakeLists.txt not found" >&2; exit 2; } +case "$ARCH" in arm64|arm|amd64|riscv64) ;; *) echo "build-dist: unknown arch $ARCH" >&2; exit 2;; esac + +if [ -z "${3:-}" ] && ! "$ENGINE" image inspect "$IMAGE" >/dev/null 2>&1; then + echo "build-dist: building $IMAGE" + "$ENGINE" build --platform "linux/$ARCH" -t "$IMAGE" -f "$HERE/Containerfile.dev" "$HERE" +fi + +BUILD="$OUT/build-$ARCH" +"$ENGINE" run --rm --platform "linux/$ARCH" -v "$OUT:$OUT" "$IMAGE" bash -euc " + cmake -S '$OUT/dist' -B '$BUILD' -DCMAKE_BUILD_TYPE=Release ${CN1_CMAKE_ARGS:-} + cmake --build '$BUILD' -j\$(nproc) + # The generated project already splits the symbols into .debug and + # strips the binary; splitting again would overwrite them with nothing. + find '$BUILD' -maxdepth 1 -type f -perm -u+x ! -name '*.debug' -exec ls -l {} +" diff --git a/scripts/embedded/buildroot/package/codenameone-app/Config.in b/scripts/embedded/buildroot/package/codenameone-app/Config.in new file mode 100644 index 00000000000..44250df9279 --- /dev/null +++ b/scripts/embedded/buildroot/package/codenameone-app/Config.in @@ -0,0 +1,29 @@ +config BR2_PACKAGE_CODENAMEONE_APP + bool "codenameone-app" + depends on BR2_INSTALL_LIBSTDCPP + depends on BR2_TOOLCHAIN_HAS_THREADS + select BR2_PACKAGE_FREETYPE + select BR2_PACKAGE_LIBPNG + select BR2_PACKAGE_JPEG + select BR2_PACKAGE_ZLIB + select BR2_PACKAGE_OPENSSL + select BR2_PACKAGE_LIBCURL + select BR2_PACKAGE_LIBDRM + select BR2_PACKAGE_DEJAVU + help + A Codename One application on the embedded Linux port: DRM/KMS or + fbdev, evdev input, no X11 or Wayland. Builds the project ParparVM + translated (scripts/embedded/translate.sh) and starts it at boot. + +config BR2_PACKAGE_CODENAMEONE_APP_DIST + string "translated dist directory" + depends on BR2_PACKAGE_CODENAMEONE_APP + help + Absolute path of the dist/ directory translate.sh wrote. + +config BR2_PACKAGE_CODENAMEONE_APP_NAME + string "executable name" + default "Main" + depends on BR2_PACKAGE_CODENAMEONE_APP + help + The application's main class name, which is the executable's name. diff --git a/scripts/embedded/buildroot/package/codenameone-app/codenameone-app.mk b/scripts/embedded/buildroot/package/codenameone-app/codenameone-app.mk new file mode 100644 index 00000000000..0fe14d2b68c --- /dev/null +++ b/scripts/embedded/buildroot/package/codenameone-app/codenameone-app.mk @@ -0,0 +1,40 @@ +################################################################################ +# +# codenameone-app +# +################################################################################ + +CODENAMEONE_APP_VERSION = 1.0 +CODENAMEONE_APP_SITE = $(call qstrip,$(BR2_PACKAGE_CODENAMEONE_APP_DIST)) +CODENAMEONE_APP_SITE_METHOD = local +CODENAMEONE_APP_NAME_EXE = $(call qstrip,$(BR2_PACKAGE_CODENAMEONE_APP_NAME)) +CODENAMEONE_APP_DEPENDENCIES = freetype libpng jpeg zlib \ + openssl libcurl libdrm host-pkgconf +CODENAMEONE_APP_CONF_OPTS = -DCN1_LINUX_EMBEDDED=ON + +# The generated CMakeLists.txt records the absolute directory it was +# translated into; point it at Buildroot's copy. +define CODENAMEONE_APP_RELOCATE + $(SED) 's|^set(CN1_APP_SOURCE_ROOT .*|set(CN1_APP_SOURCE_ROOT "$(@D)/$(CODENAMEONE_APP_NAME_EXE)-src")|' \ + $(@D)/CMakeLists.txt +endef +CODENAMEONE_APP_PRE_CONFIGURE_HOOKS += CODENAMEONE_APP_RELOCATE + +define CODENAMEONE_APP_INSTALL_TARGET_CMDS + $(INSTALL) -D -m 0755 $(@D)/$(CODENAMEONE_APP_NAME_EXE) \ + $(TARGET_DIR)/usr/bin/$(CODENAMEONE_APP_NAME_EXE) +endef + +# BusyBox init: started last, restarted when it exits. +define CODENAMEONE_APP_INSTALL_INIT_SYSV + grep -q codenameone-app $(TARGET_DIR)/etc/inittab || \ + echo "tty1::respawn:/usr/bin/$(CODENAMEONE_APP_NAME_EXE) # codenameone-app" >> $(TARGET_DIR)/etc/inittab +endef + +define CODENAMEONE_APP_INSTALL_INIT_SYSTEMD + $(INSTALL) -d $(TARGET_DIR)/usr/lib/systemd/system + printf '[Unit]\nDescription=Codename One application\nConflicts=getty@tty1.service\n\n[Service]\nExecStart=/usr/bin/%s\nRestart=always\nRestartSec=1\nStandardInput=tty\nTTYPath=/dev/tty1\n\n[Install]\nWantedBy=multi-user.target\n' \ + $(CODENAMEONE_APP_NAME_EXE) > $(TARGET_DIR)/usr/lib/systemd/system/codenameone-app.service +endef + +$(eval $(cmake-package)) diff --git a/scripts/embedded/check-demo-run.py b/scripts/embedded/check-demo-run.py new file mode 100755 index 00000000000..3b820179cb0 --- /dev/null +++ b/scripts/embedded/check-demo-run.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Judges one run of the embedded demo on the memory display. + + check-demo-run.py [summary.md] + +The demo was run with CN1_DISPLAY=memory CN1_SIZE=800x480 CN1_BENCH_SECONDS and +CN1_SNAPSHOT, so its log carries the CN1BENCH lines and the snapshot is frame 60 +as a PNG. Fails when the frame is missing, the wrong size or blank (a renderer +that draws nothing still runs at full speed), or when the speed or memory the +developer guide reports has slipped past the limits below. Standard library only. +""" + +import re +import struct +import sys +import zlib + +WIDTH, HEIGHT = 800, 480 +# The demo paces itself to 60 frames a second; the guide reports 57.5. +MIN_FPS = 50 +# Process start to the first frame on the runner's warm disk; about 50 ms on a +# Pi-class board, and an order of magnitude below what this would catch. +MAX_FIRST_FRAME_MS = 500 +# Resident set while animating; the guide reports 18 MB of PSS. +MAX_RSS_MB = 40 +# Distinct colours frame 60 must hold: the gauge, its text and the controls. +MIN_COLOURS = 50 + + +def png_colours(path): + """(width, height, distinct RGB triples) of an 8-bit RGB or RGBA PNG.""" + data = open(path, "rb").read() + if data[:8] != b"\x89PNG\r\n\x1a\n": + raise ValueError("not a PNG") + pos, idat, header = 8, b"", None + while pos < len(data): + length, kind = struct.unpack(">I4s", data[pos:pos + 8]) + body = data[pos + 8:pos + 8 + length] + if kind == b"IHDR": + header = struct.unpack(">IIBBBBB", body) + elif kind == b"IDAT": + idat += body + pos += 12 + length + width, height, depth, colour = header[0], header[1], header[2], header[3] + if depth != 8 or colour not in (2, 6): + raise ValueError("unexpected PNG format depth={} colour={}".format(depth, colour)) + bpp = 3 if colour == 2 else 4 + raw, stride = zlib.decompress(idat), width * bpp + previous, colours, i = bytearray(stride), set(), 0 + for _ in range(height): + kind, line = raw[i], bytearray(raw[i + 1:i + 1 + stride]) + i += 1 + stride + for x in range(stride): + a = line[x - bpp] if x >= bpp else 0 + b = previous[x] + c = previous[x - bpp] if x >= bpp else 0 + if kind == 1: + line[x] = (line[x] + a) & 255 + elif kind == 2: + line[x] = (line[x] + b) & 255 + elif kind == 3: + line[x] = (line[x] + (a + b) // 2) & 255 + elif kind == 4: + p = a + b - c + pa, pb, pc = abs(p - a), abs(p - b), abs(p - c) + line[x] = (line[x] + (a if pa <= pb and pa <= pc else b if pb <= pc else c)) & 255 + for x in range(0, stride, bpp): + colours.add(bytes(line[x:x + 3])) + previous = line + return width, height, len(colours) + + +def main(): + if len(sys.argv) < 3: + print(__doc__) + return 2 + log = open(sys.argv[1], errors="replace").read() + checks = [] + + def check(name, value, limit, ok): + checks.append((name, value, limit, ok)) + + first = re.search(r"CN1BENCH first_frame_epoch_ms=\d+ first_frame_ms=(\d+)", log) + run = re.findall(r"CN1BENCH fps=(\d+) frames=\d+ rss_kb=(\d+)", log) + check("benchmark lines in the log", int(bool(first and run)), 1, bool(first and run)) + if first and run: + fps, rss = int(run[-1][0]), int(run[-1][1]) / 1000.0 + check("first frame (ms)", int(first.group(1)), MAX_FIRST_FRAME_MS, + int(first.group(1)) <= MAX_FIRST_FRAME_MS) + check("frames per second", fps, MIN_FPS, fps >= MIN_FPS) + check("resident memory (MB)", rss, MAX_RSS_MB, rss <= MAX_RSS_MB) + try: + width, height, colours = png_colours(sys.argv[2]) + check("snapshot size", "{}x{}".format(width, height), "{}x{}".format(WIDTH, HEIGHT), + (width, height) == (WIDTH, HEIGHT)) + check("snapshot colours", colours, MIN_COLOURS, colours >= MIN_COLOURS) + except (OSError, ValueError, zlib.error, TypeError) as error: + check("snapshot readable ({})".format(error), 0, 1, False) + + lines = ["| Check | Value | Limit | |", "|---|---|---|---|"] + for name, value, limit, ok in checks: + lines.append("| {} | {} | {} | {} |".format(name, value, limit, "pass" if ok else "**FAIL**")) + report = "\n".join(lines) + print(report) + if len(sys.argv) > 3: + with open(sys.argv[3], "a") as summary: + summary.write("### Embedded demo\n\n" + report + "\n") + return 0 if all(ok for _, _, _, ok in checks) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/embedded/demo/src/EmbeddedDemo.java b/scripts/embedded/demo/src/EmbeddedDemo.java new file mode 100644 index 00000000000..f2585c3ec14 --- /dev/null +++ b/scripts/embedded/demo/src/EmbeddedDemo.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +import com.codename1.hardware.GpioPin; +import com.codename1.hardware.Hardware; +import com.codename1.hardware.HardwareException; +import com.codename1.impl.linux.LinuxImplementation; +import com.codename1.ui.Button; +import com.codename1.ui.Component; +import com.codename1.ui.Container; +import com.codename1.ui.Display; +import com.codename1.ui.Font; +import com.codename1.ui.Form; +import com.codename1.ui.Graphics; +import com.codename1.ui.Label; +import com.codename1.ui.Slider; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; +import com.codename1.ui.layouts.GridLayout; +import com.codename1.ui.plaf.UIManager; + +/// A small appliance HMI: a live gauge, a GPIO toggle, the backlight and the +/// buses the board exposes. It is also the benchmark workload: started with +/// the environment variable `CN1_BENCH_SECONDS`, it prints the time to its +/// first painted frame, animates the gauge for that many seconds, prints the +/// frame rate and the resident memory, and exits. +public class EmbeddedDemo { + + static long startNanos; + static int benchSeconds; + static int frames; + static boolean firstFramePrinted; + + public static void main(String[] args) { + startNanos = System.nanoTime(); + String bench = System.getenv("CN1_BENCH_SECONDS"); + benchSeconds = bench == null ? 0 : Integer.parseInt(bench.trim()); + Display.init(null); + if (Display.getInstance().getResourceAsStream(EmbeddedDemo.class, "/theme.res") != null) { + UIManager.initFirstTheme("/theme"); + } + Display.getInstance().callSerially(new Runnable() { + public void run() { + show(); + } + }); + LinuxImplementation.runMainEventLoop(); + } + + static void show() { + Form f = new Form("Codename One Embedded", new BorderLayout()); + final Gauge gauge = new Gauge(); + f.add(BorderLayout.CENTER, gauge); + + Container controls = new Container(new GridLayout(1, 3)); + final Hardware hw = Hardware.getInstance(); + final Button led = new Button("LED off"); + led.addActionListener(e -> toggleLed(hw, led)); + Slider backlight = new Slider(); + backlight.setEditable(true); + backlight.setMinValue(0); + backlight.setMaxValue(100); + backlight.setProgress(100); + backlight.addDataChangedListener((type, index) -> setBacklight(hw, index)); + Label buses = new Label(busSummary(hw)); + controls.add(led).add(backlight).add(buses); + f.add(BorderLayout.SOUTH, controls); + f.show(); + f.registerAnimated(gauge); + if (benchSeconds > 0) { + final long benchStart = System.currentTimeMillis(); + final long cpuStart = cpuTicks(); + new Thread(new Runnable() { + public void run() { + try { + Thread.sleep(benchSeconds * 1000L); + } catch (InterruptedException ignored) { + return; + } + long elapsed = System.currentTimeMillis() - benchStart; + long cpu = (cpuTicks() - cpuStart) * 10L; + System.out.println("CN1BENCH fps=" + (frames * 1000L / Math.max(1, elapsed)) + + " frames=" + frames + " rss_kb=" + rssKb() + + " cpu_pct=" + (cpu * 100L / Math.max(1, elapsed))); + System.exit(0); + } + }, "bench").start(); + } + } + + static GpioPin ledPin; + + static void toggleLed(Hardware hw, Button led) { + try { + if (ledPin == null) { + ledPin = hw.openGpioOutput("gpiochip0", 17, false); + } + ledPin.setHigh(!ledPin.isHigh()); + led.setText(ledPin.isHigh() ? "LED on" : "LED off"); + } catch (HardwareException e) { + led.setText("No GPIO"); + } + } + + static void setBacklight(Hardware hw, int percent) { + int max = hw.getMaxBacklightBrightness(); + if (max > 0) { + try { + hw.setBacklightBrightness(max * percent / 100); + } catch (HardwareException ignored) { + // A board without a writable backlight keeps its brightness. + } + } + } + + static String busSummary(Hardware hw) { + return "GPIO " + hw.listGpioChips().length + " I2C " + hw.listI2cBuses().length + + " SPI " + hw.listSpiDevices().length + " UART " + hw.listSerialPorts().length; + } + + /// User plus system time of this process in clock ticks (100 per second), + /// fields 14 and 15 of `/proc/self/stat`. + static long cpuTicks() { + String s = readProc("file:///proc/self/stat"); + if (s == null) { + return 0; + } + String[] f = com.codename1.util.StringUtil.tokenize(s.substring(s.lastIndexOf(')') + 2), ' ').toArray(new String[0]); + return Long.parseLong(f[11]) + Long.parseLong(f[12]); + } + + static String readProc(String url) { + try { + java.io.InputStream in = com.codename1.io.FileSystemStorage.getInstance().openInputStream(url); + byte[] b = new byte[1024]; + int n = in.read(b); + in.close(); + return new String(b, 0, Math.max(0, n), "UTF-8").trim(); + } catch (Exception e) { + return null; + } + } + + static long rssKb() { + try { + java.io.InputStream in = com.codename1.io.FileSystemStorage.getInstance().openInputStream("file:///proc/self/statm"); + byte[] b = new byte[128]; + int n = in.read(b); + in.close(); + String s = new String(b, 0, Math.max(0, n), "UTF-8").trim(); + long pages = Long.parseLong(s.substring(s.indexOf(' ') + 1, s.indexOf(' ', s.indexOf(' ') + 1))); + return pages * 4; + } catch (Exception e) { + return -1; + } + } + + /// A needle gauge that sweeps continuously: enough drawing per frame + /// (arcs, text, antialiased lines) to be a fair frame-rate workload. + static class Gauge extends Component { + double angle; + long last; + + public boolean animate() { + long now = System.currentTimeMillis(); + if (now - last < 1) { + return false; + } + last = now; + angle += 0.04; + return true; + } + + public void paint(Graphics g) { + frames++; + if (!firstFramePrinted) { + firstFramePrinted = true; + // Wall-clock, so the harness measures from exec rather than from main: + // the same start every framework in scripts/embedded/bench is held to. + System.out.println("CN1BENCH first_frame_epoch_ms=" + System.currentTimeMillis() + + " first_frame_ms=" + ((System.nanoTime() - startNanos) / 1000000L)); + } + int w = getWidth(); + int h = getHeight(); + int cx = getX() + w / 2; + int cy = getY() + h / 2; + int r = Math.min(w, h) / 2 - 10; + g.setAntiAliased(true); + g.setColor(0x101820); + g.fillRect(getX(), getY(), w, h); + g.setColor(0x2a3a4a); + g.fillArc(cx - r, cy - r, r * 2, r * 2, 0, 360); + g.setColor(0x33aaff); + int sweep = (int) ((Math.sin(angle) + 1) * 135); + g.fillArc(cx - r, cy - r, r * 2, r * 2, 225 - sweep, sweep); + g.setColor(0x101820); + g.fillArc(cx - r + 24, cy - r + 24, r * 2 - 48, r * 2 - 48, 0, 360); + double a = Math.toRadians(225 - sweep); + g.setColor(0xffffff); + g.drawLine(cx, cy, cx + (int) (Math.cos(a) * (r - 30)), cy - (int) (Math.sin(a) * (r - 30))); + g.setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE)); + String v = String.valueOf(sweep * 100 / 270) + " %"; + g.drawString(v, cx - g.getFont().stringWidth(v) / 2, cy + r / 3); + } + } +} diff --git a/scripts/embedded/gen-embedded-stubs.py b/scripts/embedded/gen-embedded-stubs.py new file mode 100755 index 00000000000..986adf4cafc --- /dev/null +++ b/scripts/embedded/gen-embedded-stubs.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Generates Ports/LinuxPort/nativeSources/cn1_linux_embedded_stubs.c. + +The embedded Linux target (CN1_LINUX_EMBEDDED) does not compile the port's +GTK units -- there is no window system on an appliance -- and compiles the +GL and GStreamer units only when the board's sysroot has those libraries. +Every LinuxNative method those units define still needs a symbol, or the +link fails (and, worse, ParparVM's dead-code pass would read a missing +native as unused). This writes one stub per such native that answers +"unsupported": zero, false or null, and logs once. + + scripts/embedded/gen-embedded-stubs.py # rewrite the file + scripts/embedded/gen-embedded-stubs.py --check # CI: fail if stale +""" +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +NATIVE = REPO / 'Ports/LinuxPort/nativeSources' +OUT = NATIVE / 'cn1_linux_embedded_stubs.c' + +# unit -> guard under which its stubs compile (None: always in embedded). +UNITS = [ + ('cn1_linux_desktopwindow.c', None), + ('cn1_linux_edit.c', None), + ('cn1_linux_browser.c', None), + ('cn1_linux_peer.c', None), + ('cn1_linux_print.c', None), + ('cn1_linux_services.c', None), + ('cn1_linux_text_input.c', None), + ('cn1_linux_widgets.c', None), + ('cn1_linux_gl.c', 'CN1_EMBEDDED_HAS_GL'), + ('cn1_linux_media.c', 'CN1_EMBEDDED_HAS_GST'), + ('cn1_linux_video.c', 'CN1_EMBEDDED_HAS_GST'), +] + +# cn1_linux_window.c's natives are implemented for real by cn1_linux_embedded.c. +DEF = re.compile(r'^(JAVA_[A-Z]+)\s+(com_codename1_\w+)\s*\(([^)]*)\)\s*\{', re.M) + +RETURNS = { + 'JAVA_VOID': None, + 'JAVA_OBJECT': 'JAVA_NULL', + 'JAVA_BOOLEAN': 'JAVA_FALSE', +} + +LICENSE = '''/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +''' + +HEADER = '''/* + * GENERATED by scripts/embedded/gen-embedded-stubs.py -- do not edit. + * + * The embedded Linux target does not compile the port's GTK units (and the GL + * and GStreamer units unless the sysroot has those libraries). These stubs give + * every native those units define a symbol that answers "unsupported", so the + * same translated application links on both targets. Each logs once + * (cn1LinuxStubOnce). + */ +#ifdef CN1_LINUX_EMBEDDED +#include "cn1_linux_gfx.h" +''' + + +def collect(): + seen = set() + out = [] + for unit, guard in UNITS: + text = (NATIVE / unit).read_text() + fns = [] + for m in DEF.finditer(text): + ret, name, params = m.group(1), m.group(2), ' '.join(m.group(3).split()) + if name in seen: + continue + seen.add(name) + fns.append((ret, name, params)) + out.append((unit, guard, fns)) + return out + + +def render(): + parts = [LICENSE, HEADER] + for unit, guard, fns in collect(): + if not fns: + continue + parts.append('\n/* ---- %s ---- */\n' % unit) + if guard: + parts.append('#ifndef %s\n' % guard) + for ret, name, params in fns: + method = re.sub(r'^com_codename1_impl_linux_', '', name).split('__', 1)[0] + method = method.replace('LinuxNative_', '') + # Polled every frame or on a timer: a log line would repeat forever in + # spirit and says nothing -- there is simply never anything to report. + silent = re.search(r'(^|_)poll|Poll', method) is not None + args = [p.strip() for p in params.split(',')] + unused = [] + for a in args[1:]: + pname = a.split()[-1].lstrip('*') + unused.append('(void) %s;' % pname) + parts.append('%s %s(%s) {\n' % (ret, name, params)) + if unused: + parts.append(' %s\n' % ' '.join(unused)) + if not silent: + parts.append(' cn1LinuxStubOnce("%s");\n' % method) + value = RETURNS.get(ret, '0') + if value is not None: + parts.append(' return %s;\n' % value) + parts.append('}\n') + if guard: + parts.append('#endif /* %s */\n' % guard) + parts.append('\n#endif /* CN1_LINUX_EMBEDDED */\n') + return ''.join(parts) + + +def main(): + text = render() + if '--check' in sys.argv[1:]: + if not OUT.exists() or OUT.read_text() != text: + print('%s is stale; run scripts/embedded/gen-embedded-stubs.py' % OUT.relative_to(REPO)) + return 1 + return 0 + OUT.write_text(text) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/embedded/pios/setup-device.sh b/scripts/embedded/pios/setup-device.sh new file mode 100755 index 00000000000..e5f4fbb9058 --- /dev/null +++ b/scripts/embedded/pios/setup-device.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Prepares a Raspberry Pi OS Lite (or any Debian bookworm) device to run a +# Codename One embedded application straight from boot, with no desktop: +# +# scp scripts/embedded/pios/setup-device.sh pi@pi.local: && ssh pi@pi.local sudo ./setup-device.sh +# +# Then deploy with `mvn cn1:deploy-device -Dcn1.device.host=pi.local`. +# +# What it does, each step idempotent: +# - installs the shared libraries the embedded port links; +# - lets the deploy user reach the display, input, GPIO, I2C, SPI and serial +# devices (video, render, input, gpio, i2c, spi, dialout groups); +# - enables I2C and SPI where raspi-config exists; +# - frees tty1 for the application (no login prompt), and hides the console +# cursor and boot messages so the screen goes from the firmware splash to +# the application. +set -euo pipefail +[ "$(id -u)" = 0 ] || { echo "setup-device: run as root (sudo)" >&2; exit 1; } +DEPLOY_USER="${SUDO_USER:-${1:-pi}}" + +apt-get update +apt-get install -y --no-install-recommends \ + libfreetype6 libpng16-16 libjpeg62-turbo libssl3 libcurl4 libdrm2 libnghttp2-14 fonts-dejavu-core + +for g in video render input gpio i2c spi dialout; do + getent group "$g" >/dev/null && usermod -aG "$g" "$DEPLOY_USER" +done + +if command -v raspi-config >/dev/null; then + raspi-config nonint do_i2c 0 + raspi-config nonint do_spi 0 +fi + +systemctl disable --now getty@tty1.service 2>/dev/null || true + +CMDLINE=/boot/firmware/cmdline.txt +[ -f "$CMDLINE" ] || CMDLINE=/boot/cmdline.txt +if [ -f "$CMDLINE" ]; then + for opt in quiet loglevel=3 logo.nologo vt.global_cursor_default=0 consoleblank=0; do + grep -qw "$opt" "$CMDLINE" || sed -i "1s/\$/ $opt/" "$CMDLINE" + done +fi +echo "setup-device: done; reboot once so the group and boot changes take effect" diff --git a/scripts/embedded/translate.sh b/scripts/embedded/translate.sh new file mode 100755 index 00000000000..29032fead7b --- /dev/null +++ b/scripts/embedded/translate.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Translates a Codename One application into the CMake project the Linux and +# embedded Linux targets build, without the build service: the same inputs +# LinuxNativeBuilder hands ParparVM (core, the Linux port, JavaAPI and the +# port's native sources), from this checkout's own build output. +# +# scripts/embedded/translate.sh [extra-source-dir ...] +# +# Needs maven/core, maven/linux, vm/JavaAPI and vm/ByteCodeTranslator built +# (see scripts/embedded/README.md). The result is /dist/CMakeLists.txt; +# build it on Linux, or in the cn1-embedded-dev container, with +# scripts/embedded/build-dist.sh. +set -euo pipefail + +if [ $# -lt 3 ]; then + sed -n '2,13p' "$0" + exit 2 +fi +APP_CLASSES=$(cd "$1" && pwd) +MAIN=$2 +OUT=$3 +shift 3 +ROOT=$(cd "$(dirname "$0")/../.." && pwd) + +CORE="$ROOT/maven/core/target/classes" +PORT="$ROOT/maven/linux/target/classes" +JAPI_JAR="$ROOT/vm/JavaAPI/target/JavaAPI-1.0-SNAPSHOT.jar" +TRANSLATOR="$ROOT/vm/ByteCodeTranslator/target/ByteCodeTranslator-1.0-SNAPSHOT.jar" +for f in "$CORE/com/codename1/ui/Form.class" "$PORT/com/codename1/impl/linux/LinuxImplementation.class" "$JAPI_JAR" "$TRANSLATOR"; do + if [ ! -e "$f" ]; then + echo "Missing $f -- build core, the Linux port, JavaAPI and the translator first." >&2 + exit 1 + fi +done + +mkdir -p "$OUT" +OUT=$(cd "$OUT" && pwd) +STAGE="$OUT/stage" +rm -rf "$STAGE" "$OUT/dist" +mkdir -p "$STAGE/japi" "$STAGE/native" "$STAGE/res" +(cd "$STAGE/japi" && unzip -q -o "$JAPI_JAR" -x 'META-INF/*') +cp "$ROOT"/Ports/LinuxPort/nativeSources/* "$STAGE/native/" +if [ -f "$ROOT/CodenameOne/src/material-design-font.ttf" ]; then + cp "$ROOT/CodenameOne/src/material-design-font.ttf" "$STAGE/res/" +fi + +SOURCES="$APP_CLASSES;$CORE;$PORT;$STAGE/japi;$STAGE/native;$STAGE/res" +for extra in "$@"; do + SOURCES="$SOURCES;$(cd "$extra" && pwd)" +done + +JAVA=${JAVA_HOME:+$JAVA_HOME/bin/}java +"$JAVA" -Dcn1.sqlite=true -Dcn1.linux.embedded=${CN1_EMBEDDED:-true} -cp "$TRANSLATOR:$ROOT/vm/ByteCodeTranslator/src" \ + com.codename1.tools.translator.ByteCodeTranslator \ + clean "$SOURCES" "$OUT" "$MAIN" com.codename1.embedded.app "$MAIN" 1.0 linux none +echo "Translated to $OUT/dist" diff --git a/scripts/embedded/yocto/meta-codenameone/README.md b/scripts/embedded/yocto/meta-codenameone/README.md new file mode 100644 index 00000000000..11a1b305f85 --- /dev/null +++ b/scripts/embedded/yocto/meta-codenameone/README.md @@ -0,0 +1,21 @@ +# meta-codenameone + +A Yocto layer that builds Codename One applications into an image, running from +boot as a systemd service on the embedded Linux port (DRM/KMS with a software +fallback, evdev input, no X11, Wayland or GTK). + +``` +bitbake-layers add-layer /path/to/meta-codenameone +``` + +1. Translate the application on a workstation; the C is the same for every + architecture: `scripts/embedded/translate.sh out`. +2. `tar -C out -czf dist.tar.gz dist` and put it beside your recipe + (`files/dist.tar.gz`), or fetch it from wherever your CI publishes it. +3. Write a recipe like `recipes-codenameone/codenameone-app/codenameone-app_1.0.bb` + that `inherit codenameone-app` and sets `CN1_APP` to the main class name. +4. `IMAGE_INSTALL:append = " codenameone-app"` in your image, with + `DISTRO_FEATURES` including `systemd`. + +Set `CN1_GPU = "0"` for a board whose BSP has no EGL/GLES; the application then +renders through DRM dumb buffers or `/dev/fb0` in software. diff --git a/scripts/embedded/yocto/meta-codenameone/classes/codenameone-app.bbclass b/scripts/embedded/yocto/meta-codenameone/classes/codenameone-app.bbclass new file mode 100644 index 00000000000..d2b1ea11eb6 --- /dev/null +++ b/scripts/embedded/yocto/meta-codenameone/classes/codenameone-app.bbclass @@ -0,0 +1,61 @@ +# Builds a Codename One application from the translated project ParparVM +# writes (scripts/embedded/translate.sh, or the linux-native build target with +# the linux.embedded hint), and installs it as a systemd service that owns the +# display from boot. +# +# A recipe sets SRC_URI to that translated directory (a tarball of dist/ or a +# git repository holding it) and CN1_APP to the executable's name. The C is +# architecture independent, so translating once on a workstation is enough for +# every MACHINE. +# +# CN1_APP executable name (the application's main class) +# CN1_ENVIRONMENT space separated KEY=value pairs for the service +# CN1_GPU "1" links DRM/KMS, EGL and GLES when the BSP has them + +inherit cmake pkgconfig systemd + +CN1_APP ?= "${PN}" +CN1_ENVIRONMENT ?= "" +CN1_GPU ?= "1" + +DEPENDS += "freetype libpng jpeg zlib openssl curl" +DEPENDS += "${@'libdrm virtual/egl virtual/libgles2 libepoxy' if d.getVar('CN1_GPU') == '1' else ''}" +RDEPENDS:${PN} += "ttf-dejavu-sans" + +S = "${WORKDIR}/dist" +OECMAKE_SOURCEPATH = "${S}" +EXTRA_OECMAKE += "-DCN1_LINUX_EMBEDDED=ON" + +# The generated CMakeLists.txt records the absolute directory it was translated +# into; point it at the unpacked copy. +do_configure:prepend() { + sed -i 's|^set(CN1_APP_SOURCE_ROOT .*|set(CN1_APP_SOURCE_ROOT "${S}/${CN1_APP}-src")|' ${S}/CMakeLists.txt +} + +do_install() { + install -d ${D}${bindir} ${D}${systemd_system_unitdir} + install -m 0755 ${B}/${CN1_APP} ${D}${bindir}/${CN1_APP} + cat > ${D}${systemd_system_unitdir}/${CN1_APP}.service <bibopCur) +#else +#define CN1_BIBOP_CUR(t) ((t)->bibopCurrent) +#endif + // handles the stack used for print stack trace and GC struct ThreadLocalData { JAVA_LONG threadId; @@ -2085,6 +2103,11 @@ struct ThreadLocalData { /* Per-size-class current BiBOP page. See the note at the fast path: this was * `__thread`, which costs an indirect call per allocation on Darwin. */ struct CN1BibopPage* bibopCurrent[CN1_BIBOP_NUM_CLASSES]; +#ifdef CN1_VIRTUAL_THREADS + /* The pages this thread ALLOCATES into: its own bibopCurrent, except on a virtual + * thread, where it is the carrier's, set at every resume. See CN1_BIBOP_CUR. */ + struct CN1BibopPage** bibopCur; +#endif #ifdef CN1_GC_VERIFY // The thread's whole C stack, [low, high), recorded beside the limit above and @@ -2266,6 +2289,8 @@ struct ThreadLocalData { #include #include #endif +// Cores this process may run on: affinity mask and cgroup quota, not the machine. +int cn1UsableCpuCount(void); static inline void cn1CpuRelax(void) { #if defined(__aarch64__) || defined(__arm__) __asm__ __volatile__("yield" ::: "memory"); @@ -3033,7 +3058,7 @@ static inline __attribute__((always_inline)) JAVA_OBJECT cn1BibopFastAlloc(CODEN // (cn1ClassReady) before it published `initialized`. Every OTHER allocation path // still registers on its own (cn1BibopAlloc, cn1BibopAllocRecycled, the NoZero // entry the String twins use). - CN1BibopPage* p = threadStateData->bibopCurrent[ci]; + CN1BibopPage* p = CN1_BIBOP_CUR(threadStateData)[ci]; if(__builtin_expect(p != (CN1BibopPage*)0 #ifndef CN1_CONSERVATIVE_GC_ROOTS && !threadStateData->nativeAllocationMode @@ -3113,7 +3138,7 @@ static inline __attribute__((always_inline)) JAVA_OBJECT cn1BibopFastAllocNoZero if(!registered) { CN1_CLAZZ_REGISTER(parent); // see cn1BibopFastAlloc: every alloc path registers } - CN1BibopPage* p = threadStateData->bibopCurrent[ci]; + CN1BibopPage* p = CN1_BIBOP_CUR(threadStateData)[ci]; if(__builtin_expect(p != (CN1BibopPage*)0 #ifndef CN1_CONSERVATIVE_GC_ROOTS && !threadStateData->nativeAllocationMode @@ -4813,6 +4838,13 @@ extern struct cn1VirtualThread* cn1SpawnVirtualThread(void (*body)(void*), void* * own body; it frees the stack that body is running on. */ extern void cn1RetireVirtualThread(struct cn1VirtualThread* vt); +/** + * Resumes a virtual thread spawned by cn1SpawnVirtualThread on the calling thread, + * which becomes its carrier: the virtual thread allocates into the carrier's current + * pages until it parks (see CN1_BIBOP_CUR). Use this rather than + * cn1VirtualThreadResume for any virtual thread that runs Java. + */ +extern void cn1ResumeVirtualThreadOn(CODENAME_ONE_THREAD_STATE, struct cn1VirtualThread* vt); #ifdef CN1_CONSERVATIVE_GC_ROOTS // PHASE 3b production conservative-root API. cn1ConservativeResolve maps an diff --git a/vm/ByteCodeTranslator/src/cn1_globals.m b/vm/ByteCodeTranslator/src/cn1_globals.m index 01bf5ed55ad..a63e2e21fd5 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -1153,6 +1153,61 @@ static inline int cn1GcIsOld(JAVA_OBJECT o, int m) { static void cn1GcHybridEndCycle(void); static void cn1GcRsetScan(struct ThreadLocalData* d, JAVA_BOOLEAN trace); #endif +// The cores this process may actually run on -- not the machine's. A server pinned +// with taskset, a container started with --cpus or a cgroup quota sees fewer cores than +// _SC_NPROCESSORS_ONLN reports, and sizing threads by the machine then puts more +// runnable threads on the allowed cores than they can run: the backend's host threads +// and the collector's markers both contend with the mutator they exist to serve. +// Measured on two pinned cores of a five-core host, the backend's JSON route served +// a third fewer requests than Go and Rust, which both read the affinity mask. +// Linux reads the affinity mask, then caps it by a cgroup v2 quota (cpu.max), rounded +// up as Go and Rust round it. Other platforms keep the online count. Cached: the +// answer is read when pools are sized, and a later change would not resize them. +int cn1UsableCpuCount(void) { + static int cached = 0; + int n = cached; + if(n > 0) { + return n; + } +#if defined(_WIN32) + n = cn1_win_cpu_count(); +#else + n = (int)sysconf(_SC_NPROCESSORS_ONLN); +#if defined(__linux__) + { + cpu_set_t set; + CPU_ZERO(&set); + if(sched_getaffinity(0, sizeof(set), &set) == 0) { + int allowed = CPU_COUNT(&set); + if(allowed > 0 && (n <= 0 || allowed < n)) { + n = allowed; + } + } + FILE* f = fopen("/sys/fs/cgroup/cpu.max", "r"); + if(f != NULL) { + char quota[32]; + long period = 0; + if(fscanf(f, "%31s %ld", quota, &period) == 2 && period > 0 && strcmp(quota, "max") != 0) { + long q = atol(quota); + if(q > 0) { + int limit = (int)((q + period - 1) / period); + if(limit > 0 && (n <= 0 || limit < n)) { + n = limit; + } + } + } + fclose(f); + } + } +#endif +#endif + if(n < 1) { + n = 1; + } + cached = n; + return n; +} + static int cn1GcSingleCore(void) { static int cached = -1; if(cached < 0) { @@ -1163,7 +1218,7 @@ static int cn1GcSingleCore(void) { #if defined(_WIN32) cached = 0; // the Win32 shim exposes no CPU count here; stay concurrent #else - cached = sysconf(_SC_NPROCESSORS_ONLN) <= 1; + cached = cn1UsableCpuCount() <= 1; #endif } } @@ -8447,8 +8502,11 @@ static void cn1BibopExitReport(void) { malloc_zone_print(0, 0); } #endif - cn1LiveCensus("exit"); + // The allocation totals before the live walk: they are counters, printed in + // microseconds, while the walk visits every slot and a server's other threads + // can finish exiting the process before it ends. cn1AllocCensus("exit"); + cn1LiveCensus("exit"); #ifdef CN1_ALLOC_CENSUS cn1CapacityHistogram(); #endif @@ -11199,7 +11257,7 @@ static inline void cn1BibopOwnPage(CN1BibopPage* np) { static CN1BibopPage* cn1BibopAcquirePage(CODENAME_ONE_THREAD_STATE, int ci) { pthread_mutex_lock(&bibopMutex); - CN1BibopPage* old = threadStateData->bibopCurrent[ci]; + CN1BibopPage* old = CN1_BIBOP_CUR(threadStateData)[ci]; if(old != 0) { cn1BibopAccountBump(threadStateData, old); old->gcRetiredBy = threadStateData->threadId; @@ -11211,7 +11269,7 @@ static inline void cn1BibopOwnPage(CN1BibopPage* np) { old->nextPool = sh; } while(!atomic_compare_exchange_weak_explicit(&bibopSweepStack, &sh, old, memory_order_release, memory_order_relaxed)); - threadStateData->bibopCurrent[ci] = 0; + CN1_BIBOP_CUR(threadStateData)[ci] = 0; } CN1BibopPage* np = bibopPartialPool[ci]; if(np != 0) { @@ -11274,7 +11332,7 @@ static inline void cn1BibopOwnPage(CN1BibopPage* np) { np->owned = JAVA_TRUE; np->nextPool = 0; cn1BibopOwnPage(np); - threadStateData->bibopCurrent[ci] = np; + CN1_BIBOP_CUR(threadStateData)[ci] = np; return np; } @@ -11943,7 +12001,7 @@ JAVA_OBJECT cn1BibopAllocRecycled(CODENAME_ONE_THREAD_STATE, int size, struct cl return 0; } #endif - CN1BibopPage* p = threadStateData->bibopCurrent[ci]; + CN1BibopPage* p = CN1_BIBOP_CUR(threadStateData)[ci]; if(p == 0 || p->freeList == 0) { return 0; } @@ -11970,7 +12028,7 @@ JAVA_OBJECT cn1BibopAllocRecycled(CODENAME_ONE_THREAD_STATE, int size, struct cl // stays small enough to inline at every allocation site (see the note there). JAVA_OBJECT cn1BibopAllocRecycledNoZero(CODENAME_ONE_THREAD_STATE, int size, struct clazz* parent, int ci) { if(ci < 0) return (JAVA_OBJECT)0; - CN1BibopPage* p = threadStateData->bibopCurrent[ci]; + CN1BibopPage* p = CN1_BIBOP_CUR(threadStateData)[ci]; // A RECYCLED page: take a slot its last sweep freed. This used to fall through to // __NEW_X and the full slow path for EVERY object once pages came back partial -- // which is every allocation in a heap small enough to be reused. Profiled at an 8MB @@ -12022,7 +12080,7 @@ static JAVA_OBJECT cn1BibopAlloc(CODENAME_ONE_THREAD_STATE, int size, struct cla if(ci < 0) { return 0; } - CN1BibopPage* p = threadStateData->bibopCurrent[ci]; + CN1BibopPage* p = CN1_BIBOP_CUR(threadStateData)[ci]; JAVA_OBJECT o = 0; for(;;) { if(p != 0) { @@ -17622,8 +17680,7 @@ static int gcMarkResolveThreadCount() { n = CN1_GC_MARK_THREAD_CAP; } #else - long ncpu = sysconf(_SC_NPROCESSORS_ONLN); - int n = (int)(ncpu - 1); + int n = cn1UsableCpuCount() - 1; if(n > CN1_GC_MARK_THREAD_CAP) { n = CN1_GC_MARK_THREAD_CAP; } diff --git a/vm/ByteCodeTranslator/src/cn1_hardware_impl.h b/vm/ByteCodeTranslator/src/cn1_hardware_impl.h new file mode 100644 index 00000000000..af9ce3d1354 --- /dev/null +++ b/vm/ByteCodeTranslator/src/cn1_hardware_impl.h @@ -0,0 +1,1056 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The bus and pin access behind com.codename1.impl.KernelHardware, shared by + * the Linux port (cn1_linux_hardware.c) and the headless backend + * (vm/backend/native/cn1_backend_hardware.c). Each defines the natives under + * its own class's name and forwards here: ParparVM names a native after its + * class, and the native-signature gate reads literal names. The translator + * emits this header into every Linux, Windows and backend project. + * + * Bus and pin access for com.codename1.hardware on Linux: GPIO through the + * GPIO character device (uapi v2, kernel 5.10+), I2C through i2c-dev, SPI + * through spidev, serial ports through termios, PWM and backlight through + * sysfs, CAN through SocketCAN and the watchdog through /dev/watchdog. + * + * Only kernel uapi headers are used -- no libgpiod, no libi2c -- so the unit + * builds on glibc and musl alike and adds no runtime dependency. GTK-free: it + * is compiled into the desktop and the embedded Linux targets. + * + * Every native returns a handle or a result >= 0, and -errno on failure; the + * Java side (KernelHardware) turns a negative value into a HardwareException + * carrying that errno. A timeout is reported as -ETIMEDOUT. Handles index a + * small table here so a stale or forged handle from Java can never reach a + * random file descriptor. + + */ + +#ifndef CN1_HARDWARE_IMPL_H +#define CN1_HARDWARE_IMPL_H + +#include "cn1_globals.h" +#include +#include +#include + +#if defined(__linux__) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* A malloc'd copy of a Java string; stringToUTF8's buffer is per thread and + * reused by the next call. */ +static char* cn1HwJStrDup(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT value) { + const char* utf8 = value == JAVA_NULL ? 0 : stringToUTF8(threadStateData, value); + char* copy; + size_t length; + if (utf8 == 0) { + return 0; + } + length = strlen(utf8); + copy = (char*) malloc(length + 1); + if (copy != 0) { + memcpy(copy, utf8, length + 1); + } + return copy; +} + +#define CN1_HW_KEEP_ALIVE(obj) __asm__ __volatile__("" : : "r"(obj)) + +enum { HW_FREE = 0, HW_GPIO, HW_I2C, HW_SPI, HW_SERIAL, HW_PWM, HW_CAN, HW_WATCHDOG }; + +#define CN1_HW_SLOTS 256 + +typedef struct { + int kind; + int fd; + int chip; /* PWM: chip number */ + int channel; /* PWM: channel; I2C: address; SPI: bits per word */ + int speed; /* SPI: clock rate */ +} CN1HwSlot; + +static CN1HwSlot cn1HwSlots[CN1_HW_SLOTS]; +static pthread_mutex_t cn1HwLock = PTHREAD_MUTEX_INITIALIZER; + +static int cn1HwAlloc(int kind, int fd) { + int i; + pthread_mutex_lock(&cn1HwLock); + for (i = 1; i < CN1_HW_SLOTS; i++) { + if (cn1HwSlots[i].kind == HW_FREE) { + memset(&cn1HwSlots[i], 0, sizeof(CN1HwSlot)); + cn1HwSlots[i].kind = kind; + cn1HwSlots[i].fd = fd; + pthread_mutex_unlock(&cn1HwLock); + return i; + } + } + pthread_mutex_unlock(&cn1HwLock); + return -EMFILE; +} + +/* Copies the slot out under the lock; returns 0 if the handle is not a live + * slot of the expected kind. */ +static int cn1HwGet(int h, int kind, CN1HwSlot* out) { + int ok = 0; + if (h <= 0 || h >= CN1_HW_SLOTS) { + return 0; + } + pthread_mutex_lock(&cn1HwLock); + if (cn1HwSlots[h].kind == kind) { + *out = cn1HwSlots[h]; + ok = 1; + } + pthread_mutex_unlock(&cn1HwLock); + return ok; +} + +static int cn1HwOpenPath(const char* path, int flags) { + int fd; + do { + fd = open(path, flags | O_CLOEXEC); + } while (fd < 0 && errno == EINTR); + return fd < 0 ? -errno : fd; +} + +static int cn1HwWriteFile(const char* path, const char* value) { + int fd = cn1HwOpenPath(path, O_WRONLY); + ssize_t n; + size_t len = strlen(value); + if (fd < 0) { + return fd; + } + do { + n = write(fd, value, len); + } while (n < 0 && errno == EINTR); + if (n < 0) { + int e = errno; + close(fd); + return -e; + } + close(fd); + return 0; +} + +static long long cn1HwReadNumber(const char* path) { + char buf[64]; + int fd = cn1HwOpenPath(path, O_RDONLY); + ssize_t n; + if (fd < 0) { + return fd; + } + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) { + return -EIO; + } + buf[n] = 0; + return strtoll(buf, 0, 10); +} + +/* Waits for fd to become readable. Returns 1 ready, -ETIMEDOUT, or -errno. */ +static int cn1HwWaitReadable(int fd, int timeoutMs) { + struct pollfd p; + int r; + p.fd = fd; + p.events = POLLIN; + p.revents = 0; + do { + r = poll(&p, 1, timeoutMs < 0 ? -1 : timeoutMs); + } while (r < 0 && errno == EINTR); + if (r < 0) { + return -errno; + } + if (r == 0) { + return -ETIMEDOUT; + } + if (p.revents & (POLLERR | POLLNVAL)) { + return -EIO; + } + return 1; +} + +/* ---------------------------------------------------------------- GPIO */ + +static JAVA_INT cn1HwImpl_hwGpioOpen(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT chip, JAVA_INT line, JAVA_BOOLEAN output, + JAVA_BOOLEAN high, JAVA_INT bias, JAVA_INT edges) { + char path[128]; + char* name = cn1HwJStrDup(threadStateData, chip); + struct gpio_v2_line_request req; + int chipFd, r, h; + if (name == 0 || strchr(name, '/') != 0 || line < 0) { + free(name); + return -EINVAL; + } + snprintf(path, sizeof(path), "/dev/%s", name); + free(name); + chipFd = cn1HwOpenPath(path, O_RDWR); + if (chipFd < 0) { + return chipFd; + } + memset(&req, 0, sizeof(req)); + req.offsets[0] = (unsigned) line; + req.num_lines = 1; + strncpy(req.consumer, "codenameone", sizeof(req.consumer) - 1); + if (output) { + req.config.flags = GPIO_V2_LINE_FLAG_OUTPUT; + req.config.num_attrs = 1; + req.config.attrs[0].attr.id = GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES; + req.config.attrs[0].attr.values = high ? 1 : 0; + req.config.attrs[0].mask = 1; + } else { + req.config.flags = GPIO_V2_LINE_FLAG_INPUT; + if (edges & 1) req.config.flags |= GPIO_V2_LINE_FLAG_EDGE_RISING; + if (edges & 2) req.config.flags |= GPIO_V2_LINE_FLAG_EDGE_FALLING; + } + switch (bias) { + case 1: req.config.flags |= GPIO_V2_LINE_FLAG_BIAS_DISABLED; break; + case 2: req.config.flags |= GPIO_V2_LINE_FLAG_BIAS_PULL_UP; break; + case 3: req.config.flags |= GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN; break; + default: break; + } + r = ioctl(chipFd, GPIO_V2_GET_LINE_IOCTL, &req); + if (r < 0) { + int e = errno; + close(chipFd); + return -e; + } + /* The line fd stays valid without the chip fd. */ + close(chipFd); + h = cn1HwAlloc(HW_GPIO, req.fd); + if (h < 0) { + close(req.fd); + } + return h; +} + +static JAVA_INT cn1HwImpl_hwGpioRead(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + CN1HwSlot s; + struct gpio_v2_line_values v; + if (!cn1HwGet(h, HW_GPIO, &s)) { + return -EBADF; + } + memset(&v, 0, sizeof(v)); + v.mask = 1; + if (ioctl(s.fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &v) < 0) { + return -errno; + } + return (int) (v.bits & 1); +} + +static JAVA_INT cn1HwImpl_hwGpioWrite(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_BOOLEAN high) { + CN1HwSlot s; + struct gpio_v2_line_values v; + if (!cn1HwGet(h, HW_GPIO, &s)) { + return -EBADF; + } + memset(&v, 0, sizeof(v)); + v.mask = 1; + v.bits = high ? 1 : 0; + if (ioctl(s.fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &v) < 0) { + return -errno; + } + return 0; +} + +/* Returns 1 rising, 0 falling, -ETIMEDOUT, or -errno. */ +static JAVA_INT cn1HwImpl_hwGpioWaitEdge(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { + CN1HwSlot s; + struct gpio_v2_line_event ev; + ssize_t n; + int r; + if (!cn1HwGet(h, HW_GPIO, &s)) { + return -EBADF; + } + CN1_YIELD_THREAD; + r = cn1HwWaitReadable(s.fd, timeoutMs); + if (r == 1) { + do { + n = read(s.fd, &ev, sizeof(ev)); + } while (n < 0 && errno == EINTR); + r = n == (ssize_t) sizeof(ev) ? (ev.id == GPIO_V2_LINE_EVENT_RISING_EDGE ? 1 : 0) : (n < 0 ? -errno : -EIO); + } + CN1_RESUME_THREAD; + if (r >= 0 && tsOut != JAVA_NULL && CN1_ARRAY_LENGTH(tsOut) > 0) { + ((JAVA_LONG*) CN1_ARRAY_DATA(tsOut))[0] = (JAVA_LONG) ev.timestamp_ns; + } + return r; +} + +/* ----------------------------------------------------------------- I2C */ + +static JAVA_INT cn1HwImpl_hwI2cOpen(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, JAVA_INT address) { + char path[64]; + int fd, h; + snprintf(path, sizeof(path), "/dev/i2c-%d", bus); + fd = cn1HwOpenPath(path, O_RDWR); + if (fd < 0) { + return fd; + } + h = cn1HwAlloc(HW_I2C, fd); + if (h < 0) { + close(fd); + return h; + } + pthread_mutex_lock(&cn1HwLock); + cn1HwSlots[h].channel = address; + pthread_mutex_unlock(&cn1HwLock); + return h; +} + +static JAVA_INT cn1HwImpl_hwI2cTransfer(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT w, JAVA_INT wl, JAVA_OBJECT rd, JAVA_INT rl) { + CN1HwSlot s; + struct i2c_msg msgs[2]; + struct i2c_rdwr_ioctl_data data; + int count = 0, r; + if (!cn1HwGet(h, HW_I2C, &s)) { + return -EBADF; + } + /* i2c_msg.len is 16 bits: a longer transfer would run short and report success. */ + if (wl < 0 || rl < 0 || wl > 65535 || rl > 65535 + || (wl > 0 && (w == JAVA_NULL || wl > CN1_ARRAY_LENGTH(w))) + || (rl > 0 && (rd == JAVA_NULL || rl > CN1_ARRAY_LENGTH(rd)))) { + return -EINVAL; + } + if (wl > 0) { + msgs[count].addr = (unsigned short) s.channel; + msgs[count].flags = 0; + msgs[count].len = (unsigned short) wl; + msgs[count].buf = (unsigned char*) CN1_ARRAY_DATA(w); + count++; + } + if (rl > 0) { + msgs[count].addr = (unsigned short) s.channel; + msgs[count].flags = I2C_M_RD; + msgs[count].len = (unsigned short) rl; + msgs[count].buf = (unsigned char*) CN1_ARRAY_DATA(rd); + count++; + } + if (count == 0) { + return 0; + } + data.msgs = msgs; + data.nmsgs = (unsigned) count; + r = ioctl(s.fd, I2C_RDWR, &data); + CN1_HW_KEEP_ALIVE(w); + CN1_HW_KEEP_ALIVE(rd); + return r < 0 ? -errno : 0; +} + +/* ----------------------------------------------------------------- SPI */ + +static JAVA_INT cn1HwImpl_hwSpiOpen(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, JAVA_INT cs, JAVA_INT mode, JAVA_INT speed, JAVA_INT bits) { + char path[64]; + unsigned char m = (unsigned char) mode, b = (unsigned char) (bits > 0 ? bits : 8); + unsigned int hz = (unsigned int) speed; + int fd, h; + snprintf(path, sizeof(path), "/dev/spidev%d.%d", bus, cs); + fd = cn1HwOpenPath(path, O_RDWR); + if (fd < 0) { + return fd; + } + if (ioctl(fd, SPI_IOC_WR_MODE, &m) < 0 || ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &b) < 0 + || ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &hz) < 0) { + int e = errno; + close(fd); + return -e; + } + h = cn1HwAlloc(HW_SPI, fd); + if (h < 0) { + close(fd); + return h; + } + pthread_mutex_lock(&cn1HwLock); + cn1HwSlots[h].channel = b; + cn1HwSlots[h].speed = (int) hz; + pthread_mutex_unlock(&cn1HwLock); + return h; +} + +static JAVA_INT cn1HwImpl_hwSpiTransfer(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT tx, JAVA_OBJECT rx, JAVA_INT len) { + CN1HwSlot s; + struct spi_ioc_transfer t; + int r; + if (!cn1HwGet(h, HW_SPI, &s)) { + return -EBADF; + } + if (len < 0 || tx == JAVA_NULL || rx == JAVA_NULL || len > CN1_ARRAY_LENGTH(tx) || len > CN1_ARRAY_LENGTH(rx)) { + return -EINVAL; + } + memset(&t, 0, sizeof(t)); + t.tx_buf = (unsigned long) (uintptr_t) CN1_ARRAY_DATA(tx); + t.rx_buf = (unsigned long) (uintptr_t) CN1_ARRAY_DATA(rx); + t.len = (unsigned) len; + t.speed_hz = (unsigned) s.speed; + t.bits_per_word = (unsigned char) s.channel; + r = ioctl(s.fd, SPI_IOC_MESSAGE(1), &t); + CN1_HW_KEEP_ALIVE(tx); + CN1_HW_KEEP_ALIVE(rx); + return r < 0 ? -errno : 0; +} + +/* -------------------------------------------------------------- serial */ + +static speed_t cn1HwBaud(int baud) { + switch (baud) { + case 1200: return B1200; + case 2400: return B2400; + case 4800: return B4800; + case 9600: return B9600; + case 19200: return B19200; + case 38400: return B38400; + case 57600: return B57600; + case 115200: return B115200; + case 230400: return B230400; +#ifdef B460800 + case 460800: return B460800; +#endif +#ifdef B921600 + case 921600: return B921600; +#endif +#ifdef B1000000 + case 1000000: return B1000000; +#endif + default: return 0; + } +} + +static JAVA_INT cn1HwImpl_hwSerialOpen(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT dev, JAVA_INT baud, JAVA_INT dataBits, JAVA_INT parity, JAVA_INT stopBits) { + char* path = cn1HwJStrDup(threadStateData, dev); + struct termios t; + speed_t sp = cn1HwBaud(baud); + int fd, h; + /* A serial port is a device node: refuse anything else the application + * names (a FIFO, /dev/mem, a path climbing out of /dev). */ + if (path == 0 || strncmp(path, "/dev/", 5) != 0 || strstr(path, "..") != 0) { + free(path); + return -EINVAL; + } + if (sp == 0 || dataBits < 5 || dataBits > 8 || stopBits < 1 || stopBits > 2) { + free(path); + return -EINVAL; + } + fd = cn1HwOpenPath(path, O_RDWR | O_NOCTTY | O_NONBLOCK); + free(path); + if (fd < 0) { + return fd; + } + if (tcgetattr(fd, &t) < 0) { + int e = errno; + close(fd); + return -e; + } + cfmakeraw(&t); + cfsetispeed(&t, sp); + cfsetospeed(&t, sp); + t.c_cflag &= ~(CSIZE | PARENB | PARODD | CSTOPB); + t.c_cflag |= CLOCAL | CREAD; + t.c_cflag |= dataBits == 5 ? CS5 : dataBits == 6 ? CS6 : dataBits == 7 ? CS7 : CS8; + if (parity == 1) t.c_cflag |= PARENB | PARODD; + if (parity == 2) t.c_cflag |= PARENB; + if (stopBits == 2) t.c_cflag |= CSTOPB; + t.c_cc[VMIN] = 0; + t.c_cc[VTIME] = 0; + if (tcsetattr(fd, TCSANOW, &t) < 0) { + int e = errno; + close(fd); + return -e; + } + tcflush(fd, TCIOFLUSH); + h = cn1HwAlloc(HW_SERIAL, fd); + if (h < 0) { + close(fd); + } + return h; +} + +static JAVA_INT cn1HwImpl_hwSerialRead(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len, JAVA_INT timeoutMs) { + CN1HwSlot s; + ssize_t n = 0; + int r; + if (!cn1HwGet(h, HW_SERIAL, &s)) { + return -EBADF; + } + if (buf == JAVA_NULL || off < 0 || len < 0 || off > CN1_ARRAY_LENGTH(buf) - len) { + return -EINVAL; + } + if (len == 0) { + return 0; + } + CN1_YIELD_THREAD; + r = cn1HwWaitReadable(s.fd, timeoutMs); + if (r == 1) { + do { + n = read(s.fd, (char*) CN1_ARRAY_DATA(buf) + off, (size_t) len); + } while (n < 0 && errno == EINTR); + r = n < 0 ? (errno == EAGAIN ? 0 : -errno) : (int) n; + } else if (r == -ETIMEDOUT) { + r = 0; + } + CN1_RESUME_THREAD; + CN1_HW_KEEP_ALIVE(buf); + return r; +} + +static JAVA_INT cn1HwImpl_hwSerialWrite(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len) { + CN1HwSlot s; + int written = 0, err = 0; + if (!cn1HwGet(h, HW_SERIAL, &s)) { + return -EBADF; + } + if (buf == JAVA_NULL || off < 0 || len < 0 || off > CN1_ARRAY_LENGTH(buf) - len) { + return -EINVAL; + } + CN1_YIELD_THREAD; + while (written < len) { + ssize_t n = write(s.fd, (char*) CN1_ARRAY_DATA(buf) + off + written, (size_t) (len - written)); + if (n < 0 && errno == EINTR) { + continue; + } + if (n < 0 && errno == EAGAIN) { + struct pollfd p; + p.fd = s.fd; + p.events = POLLOUT; + p.revents = 0; + poll(&p, 1, 1000); + continue; + } + if (n <= 0) { + err = n < 0 ? errno : EIO; + break; + } + written += (int) n; + } + CN1_RESUME_THREAD; + CN1_HW_KEEP_ALIVE(buf); + return err ? -err : 0; +} + +/* ----------------------------------------------------------------- PWM */ + +static JAVA_INT cn1HwImpl_hwPwmOpen(CODENAME_ONE_THREAD_STATE, JAVA_INT chip, JAVA_INT channel) { + char path[128], num[16]; + int r, h; + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%d/pwm%d", chip, channel); + if (access(path, F_OK) != 0) { + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%d/export", chip); + snprintf(num, sizeof(num), "%d", channel); + r = cn1HwWriteFile(path, num); + if (r < 0 && r != -EBUSY) { + return r; + } + /* udev applies the group permissions asynchronously after export. */ + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%d/pwm%d/period", chip, channel); + for (r = 0; r < 50 && access(path, W_OK) != 0; r++) { + usleep(10000); + } + } + h = cn1HwAlloc(HW_PWM, -1); + if (h < 0) { + return h; + } + pthread_mutex_lock(&cn1HwLock); + cn1HwSlots[h].chip = chip; + cn1HwSlots[h].channel = channel; + pthread_mutex_unlock(&cn1HwLock); + return h; +} + +static int cn1HwPwmAttr(CN1HwSlot* s, const char* attr, long long value) { + char path[128], v[32]; + snprintf(path, sizeof(path), "/sys/class/pwm/pwmchip%d/pwm%d/%s", s->chip, s->channel, attr); + snprintf(v, sizeof(v), "%lld", value); + return cn1HwWriteFile(path, v); +} + +static JAVA_INT cn1HwImpl_hwPwmConfigure(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_LONG period, JAVA_LONG duty, JAVA_BOOLEAN enabled) { + CN1HwSlot s; + int r; + if (!cn1HwGet(h, HW_PWM, &s)) { + return -EBADF; + } + /* The kernel rejects duty > period at every step, so lower the duty + * first when shrinking the period and raise the period first when growing. */ + cn1HwPwmAttr(&s, "duty_cycle", 0); + r = cn1HwPwmAttr(&s, "period", period); + if (r == 0) r = cn1HwPwmAttr(&s, "duty_cycle", duty); + if (r == 0) r = cn1HwPwmAttr(&s, "enable", enabled ? 1 : 0); + return r; +} + +/* ----------------------------------------------------------------- CAN */ + +static JAVA_INT cn1HwImpl_hwCanOpen(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT iface) { + char* name = cn1HwJStrDup(threadStateData, iface); + struct sockaddr_can addr; + unsigned idx; + int fd, h; + if (name == 0) { + return -EINVAL; + } + idx = if_nametoindex(name); + free(name); + if (idx == 0) { + return -ENODEV; + } + fd = socket(PF_CAN, SOCK_RAW | SOCK_CLOEXEC, CAN_RAW); + if (fd < 0) { + return -errno; + } + memset(&addr, 0, sizeof(addr)); + addr.can_family = AF_CAN; + addr.can_ifindex = (int) idx; + if (bind(fd, (struct sockaddr*) &addr, sizeof(addr)) < 0) { + int e = errno; + close(fd); + return -e; + } + h = cn1HwAlloc(HW_CAN, fd); + if (h < 0) { + close(fd); + } + return h; +} + +static JAVA_INT cn1HwImpl_hwCanSend(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT id, JAVA_BOOLEAN ext, JAVA_OBJECT data, JAVA_INT len) { + CN1HwSlot s; + struct can_frame f; + ssize_t n; + if (!cn1HwGet(h, HW_CAN, &s)) { + return -EBADF; + } + if (len < 0 || len > 8 || (len > 0 && (data == JAVA_NULL || len > CN1_ARRAY_LENGTH(data)))) { + return -EINVAL; + } + memset(&f, 0, sizeof(f)); + f.can_id = ext ? (((unsigned) id & CAN_EFF_MASK) | CAN_EFF_FLAG) : ((unsigned) id & CAN_SFF_MASK); + f.can_dlc = (unsigned char) len; + if (len > 0) { + memcpy(f.data, CN1_ARRAY_DATA(data), (size_t) len); + } + do { + n = write(s.fd, &f, sizeof(f)); + } while (n < 0 && errno == EINTR); + return n == (ssize_t) sizeof(f) ? 0 : (n < 0 ? -errno : -EIO); +} + +static JAVA_INT cn1HwImpl_hwCanReceive(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT idOut, JAVA_OBJECT data, JAVA_INT timeoutMs) { + CN1HwSlot s; + struct can_frame f; + ssize_t n = 0; + int r; + if (!cn1HwGet(h, HW_CAN, &s)) { + return -EBADF; + } + if (idOut == JAVA_NULL || CN1_ARRAY_LENGTH(idOut) < 2 || data == JAVA_NULL || CN1_ARRAY_LENGTH(data) < 8) { + return -EINVAL; + } + CN1_YIELD_THREAD; + r = cn1HwWaitReadable(s.fd, timeoutMs); + if (r == 1) { + do { + n = read(s.fd, &f, sizeof(f)); + } while (n < 0 && errno == EINTR); + r = n == (ssize_t) sizeof(f) ? 0 : (n < 0 ? -errno : -EIO); + } + CN1_RESUME_THREAD; + if (r < 0) { + return r; + } + if (f.can_id & CAN_EFF_FLAG) { + ((JAVA_INT*) CN1_ARRAY_DATA(idOut))[0] = (JAVA_INT) (f.can_id & CAN_EFF_MASK); + ((JAVA_INT*) CN1_ARRAY_DATA(idOut))[1] = 1; + } else { + ((JAVA_INT*) CN1_ARRAY_DATA(idOut))[0] = (JAVA_INT) (f.can_id & CAN_SFF_MASK); + ((JAVA_INT*) CN1_ARRAY_DATA(idOut))[1] = 0; + } + memcpy(CN1_ARRAY_DATA(data), f.data, f.can_dlc > 8 ? 8 : f.can_dlc); + return f.can_dlc > 8 ? 8 : f.can_dlc; +} + +/* ------------------------------------------------------------ watchdog */ + +static JAVA_INT cn1HwImpl_hwWatchdogOpen(CODENAME_ONE_THREAD_STATE, JAVA_INT timeoutSeconds) { + int fd = cn1HwOpenPath("/dev/watchdog", O_WRONLY); + int t = timeoutSeconds, h; + if (fd < 0) { + return fd; + } + if (t > 0 && ioctl(fd, WDIOC_SETTIMEOUT, &t) < 0) { + int e = errno; + /* Leave it running: closing without the magic byte keeps the countdown. */ + close(fd); + return -e; + } + h = cn1HwAlloc(HW_WATCHDOG, fd); + if (h < 0) { + close(fd); + } + return h; +} + +static JAVA_INT cn1HwImpl_hwWatchdogKick(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + CN1HwSlot s; + int dummy = 0; + if (!cn1HwGet(h, HW_WATCHDOG, &s)) { + return -EBADF; + } + return ioctl(s.fd, WDIOC_KEEPALIVE, &dummy) < 0 ? -errno : 0; +} + +/* --------------------------------------------------------------- close */ + +static JAVA_VOID cn1HwImpl_hwClose(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_BOOLEAN disarm) { + CN1HwSlot s; + if (h <= 0 || h >= CN1_HW_SLOTS) { + return; + } + pthread_mutex_lock(&cn1HwLock); + s = cn1HwSlots[h]; + cn1HwSlots[h].kind = HW_FREE; + pthread_mutex_unlock(&cn1HwLock); + if (s.kind == HW_FREE) { + return; + } + if (s.kind == HW_WATCHDOG && disarm) { + /* The magic close: drivers without nowayout stop the timer. */ + ssize_t n; + do { + n = write(s.fd, "V", 1); + } while (n < 0 && errno == EINTR); + } + if (s.fd >= 0) { + close(s.fd); + } +} + +/* ----------------------------------------------------------- discovery */ + +/* Lists /dev entries starting with prefix, newline separated. */ +static void cn1HwListDev(const char* prefix, char* out, size_t cap) { + DIR* d = opendir("/dev"); + struct dirent* e; + size_t used = 0; + size_t plen = strlen(prefix); + out[0] = 0; + if (!d) { + return; + } + while ((e = readdir(d)) != 0) { + size_t len = strlen(e->d_name); + if (strncmp(e->d_name, prefix, plen) != 0 || used + len + 7 >= cap) { + continue; + } + { + int n = snprintf(out + used, cap - used, "%s\n", e->d_name); + used = n < 0 ? used : (size_t) n >= cap - used ? cap - 1 : used + (size_t) n; + } + } + closedir(d); +} + +static void cn1HwListCan(char* out, size_t cap) { + DIR* d = opendir("/sys/class/net"); + struct dirent* e; + size_t used = 0; + out[0] = 0; + if (!d) { + return; + } + while ((e = readdir(d)) != 0) { + char path[300]; + if (e->d_name[0] == '.') { + continue; + } + snprintf(path, sizeof(path), "/sys/class/net/%s/type", e->d_name); + /* ARPHRD_CAN */ + if (cn1HwReadNumber(path) == 280 && used + strlen(e->d_name) + 2 < cap) { + { + int n = snprintf(out + used, cap - used, "%s\n", e->d_name); + used = n < 0 ? used : (size_t) n >= cap - used ? cap - 1 : used + (size_t) n; + } + } + } + closedir(d); +} + +/* kind: 0 gpiochips, 1 i2c, 2 spidev, 3 serial, 4 can. Newline separated + * device names, or the empty string. */ +static JAVA_OBJECT cn1HwImpl_hwList(CODENAME_ONE_THREAD_STATE, JAVA_INT kind) { + char buf[8192]; + buf[0] = 0; + switch (kind) { + case 0: cn1HwListDev("gpiochip", buf, sizeof(buf)); break; + case 1: cn1HwListDev("i2c-", buf, sizeof(buf)); break; + case 2: cn1HwListDev("spidev", buf, sizeof(buf)); break; + case 3: { + const char* prefixes[] = { "ttyAMA", "ttyS", "ttyUSB", "ttyACM", "serial", "ttymxc", "ttyO" }; + size_t i, used = 0; + for (i = 0; i < sizeof(prefixes) / sizeof(prefixes[0]) && used < sizeof(buf) - 1; i++) { + cn1HwListDev(prefixes[i], buf + used, sizeof(buf) - used); + used = strlen(buf); + } + break; + } + case 4: cn1HwListCan(buf, sizeof(buf)); break; + default: break; + } + return newStringFromCString(threadStateData, buf); +} + +/* ----------------------------------------------------------- backlight */ + +static int cn1HwBacklightDir(char* out, size_t cap) { + DIR* d = opendir("/sys/class/backlight"); + struct dirent* e; + int found = 0; + if (!d) { + return 0; + } + while ((e = readdir(d)) != 0) { + if (e->d_name[0] != '.') { + snprintf(out, cap, "/sys/class/backlight/%s", e->d_name); + found = 1; + break; + } + } + closedir(d); + return found; +} + +/* op 0: current, 1: max, 2: set value. Returns the value, 0 for a set, or -errno. */ +static JAVA_INT cn1HwImpl_hwBacklight(CODENAME_ONE_THREAD_STATE, JAVA_INT op, JAVA_INT value) { + char dir[280], path[320], v[16]; + long long max; + if (!cn1HwBacklightDir(dir, sizeof(dir))) { + return -ENODEV; + } + snprintf(path, sizeof(path), "%s/max_brightness", dir); + max = cn1HwReadNumber(path); + if (op == 1) { + return (int) max; + } + if (op == 0) { + snprintf(path, sizeof(path), "%s/brightness", dir); + return (int) cn1HwReadNumber(path); + } + if (max < 0) { + return (int) max; + } + if (value < 0) value = 0; + if (value > max) value = (int) max; + snprintf(path, sizeof(path), "%s/brightness", dir); + snprintf(v, sizeof(v), "%d", value); + return cn1HwWriteFile(path, v); +} + +#else /* !__linux__: a backend built for macOS or Windows has no such buses. */ + +static JAVA_INT cn1HwImpl_hwGpioOpen(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT chip, JAVA_INT line, JAVA_BOOLEAN output, JAVA_BOOLEAN high, JAVA_INT bias, JAVA_INT edges) { + (void) threadStateData; + (void) chip; + (void) line; + (void) output; + (void) high; + (void) bias; + (void) edges; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwGpioRead(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + (void) threadStateData; + (void) h; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwGpioWrite(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_BOOLEAN high) { + (void) threadStateData; + (void) h; + (void) high; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwGpioWaitEdge(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { + (void) threadStateData; + (void) h; + (void) timeoutMs; + (void) tsOut; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwI2cOpen(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, JAVA_INT address) { + (void) threadStateData; + (void) bus; + (void) address; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwI2cTransfer(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT w, JAVA_INT wl, JAVA_OBJECT rd, JAVA_INT rl) { + (void) threadStateData; + (void) h; + (void) w; + (void) wl; + (void) rd; + (void) rl; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwSpiOpen(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, JAVA_INT cs, JAVA_INT mode, JAVA_INT speed, JAVA_INT bits) { + (void) threadStateData; + (void) bus; + (void) cs; + (void) mode; + (void) speed; + (void) bits; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwSpiTransfer(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT tx, JAVA_OBJECT rx, JAVA_INT len) { + (void) threadStateData; + (void) h; + (void) tx; + (void) rx; + (void) len; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwSerialOpen(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT dev, JAVA_INT baud, JAVA_INT dataBits, JAVA_INT parity, JAVA_INT stopBits) { + (void) threadStateData; + (void) dev; + (void) baud; + (void) dataBits; + (void) parity; + (void) stopBits; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwSerialRead(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len, JAVA_INT timeoutMs) { + (void) threadStateData; + (void) h; + (void) buf; + (void) off; + (void) len; + (void) timeoutMs; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwSerialWrite(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len) { + (void) threadStateData; + (void) h; + (void) buf; + (void) off; + (void) len; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwPwmOpen(CODENAME_ONE_THREAD_STATE, JAVA_INT chip, JAVA_INT channel) { + (void) threadStateData; + (void) chip; + (void) channel; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwPwmConfigure(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_LONG period, JAVA_LONG duty, JAVA_BOOLEAN enabled) { + (void) threadStateData; + (void) h; + (void) period; + (void) duty; + (void) enabled; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwCanOpen(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT iface) { + (void) threadStateData; + (void) iface; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwCanSend(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT id, JAVA_BOOLEAN ext, JAVA_OBJECT data, JAVA_INT len) { + (void) threadStateData; + (void) h; + (void) id; + (void) ext; + (void) data; + (void) len; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwCanReceive(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT idOut, JAVA_OBJECT data, JAVA_INT timeoutMs) { + (void) threadStateData; + (void) h; + (void) idOut; + (void) data; + (void) timeoutMs; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwWatchdogOpen(CODENAME_ONE_THREAD_STATE, JAVA_INT timeoutSeconds) { + (void) threadStateData; + (void) timeoutSeconds; + return -ENOSYS; +} + +static JAVA_INT cn1HwImpl_hwWatchdogKick(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + (void) threadStateData; + (void) h; + return -ENOSYS; +} + +static JAVA_VOID cn1HwImpl_hwClose(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_BOOLEAN disarm) { + (void) threadStateData; + (void) h; + (void) disarm; +} + +static JAVA_OBJECT cn1HwImpl_hwList(CODENAME_ONE_THREAD_STATE, JAVA_INT kind) { + (void) kind; + return newStringFromCString(threadStateData, ""); +} + +static JAVA_INT cn1HwImpl_hwBacklight(CODENAME_ONE_THREAD_STATE, JAVA_INT op, JAVA_INT value) { + (void) threadStateData; + (void) op; + (void) value; + return -ENOSYS; +} + +#endif /* __linux__ */ + +#endif /* CN1_HARDWARE_IMPL_H */ diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index fd9dac1e388..e95be09cf24 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -549,6 +549,9 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File // the engine is present and as stubs when it is not, so an application that references // com.codename1.db links regardless of how the translator was invoked. copyRuntimeResource(srcRoot, "cn1_db_sqlite_impl.h"); + // The kernel bus and pin access the Linux port and the backend both wrap + // (com.codename1.impl.KernelHardware). Only their wrappers include it. + copyRuntimeResource(srcRoot, "cn1_hardware_impl.h"); emitBundledSqlite(srcRoot); copyRuntimeResource(srcRoot, "xmlvm.h"); @@ -948,6 +951,9 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File // the engine is present and as stubs when it is not, so an application that references // com.codename1.db links regardless of how the translator was invoked. copyRuntimeResource(srcRoot, "cn1_db_sqlite_impl.h"); + // The kernel bus and pin access the Linux port and the backend both wrap + // (com.codename1.impl.KernelHardware). Only their wrappers include it. + copyRuntimeResource(srcRoot, "cn1_hardware_impl.h"); emitBundledSqlite(srcRoot); Parser.writeOutput(srcRoot); @@ -1587,14 +1593,67 @@ private static void writeCmakeProject(File projectRoot, File srcRoot, String app */ private static void writeLinuxLinkSet(Writer writer) throws IOException { writer.append("find_package(PkgConfig REQUIRED)\n"); + // The embedded target owns the screen through DRM/KMS or fbdev with no window + // system (cn1_linux_embedded.c). It drops every GTK unit -- the generated + // cn1_linux_embedded_stubs.c answers "unsupported" for their natives -- and + // links GL and GStreamer only when the sysroot has them. Defaulted from + // -Dcn1.linux.embedded at translation, and still a cache option so the same + // generated project can be configured either way. + writer.append("option(CN1_LINUX_EMBEDDED \"Embedded Linux: DRM/KMS or fbdev, no GTK\" ") + .append(Boolean.getBoolean("cn1.linux.embedded") ? "ON" : "OFF").append(")\n"); + writer.append("if(CN1_LINUX_EMBEDDED)\n"); + // cn1_linux_text.c (Pango) is replaced by cn1_linux_text_ft.c, and Cairo by + // the renderer in cn1_linux_raster.c: the desktop stack mapped 23 MB of + // libraries (X11, GLib, HarfBuzz, FontConfig...) into an application that + // draws into a framebuffer, and every cold start read them from storage. + writer.append(" foreach(cn1_gtk_unit window desktopwindow edit browser peer print services text_input widgets text)\n"); + writer.append(" list(FILTER TRANSLATOR_SOURCES EXCLUDE REGEX \"/cn1_linux_${cn1_gtk_unit}\\\\.c$\")\n"); + writer.append(" endforeach()\n"); + writer.append(" pkg_check_modules(CN1DEPS REQUIRED freetype2 libpng libjpeg zlib)\n"); + // libcrypto is loaded on first use (cn1_linux_crypto.c): headers only. + writer.append(" pkg_check_modules(CN1CRYPTO REQUIRED libcrypto>=3)\n"); + writer.append(" pkg_check_modules(CN1CURL REQUIRED libcurl)\n"); + writer.append(" pkg_check_modules(CN1DRM libdrm)\n"); + writer.append(" pkg_check_modules(CN1GL epoxy egl glesv2)\n"); + writer.append(" pkg_check_modules(CN1GST gstreamer-1.0 gstreamer-app-1.0 gstreamer-video-1.0)\n"); + writer.append(" set_property(TARGET ${PROJECT_NAME} PROPERTY SOURCES ${TRANSLATOR_SOURCES} ${TRANSLATOR_HEADERS} ${TRANSLATOR_ASM_SOURCES})\n"); + writer.append(" target_compile_definitions(${PROJECT_NAME} PRIVATE CN1_LINUX_EMBEDDED)\n"); + writer.append(" set(CN1EMB_LIBS ${CN1DEPS_LIBRARIES})\n"); + writer.append(" if(CN1DRM_FOUND)\n"); + writer.append(" target_compile_definitions(${PROJECT_NAME} PRIVATE CN1_EMBEDDED_HAS_DRM)\n"); + writer.append(" list(APPEND CN1EMB_LIBS ${CN1DRM_LIBRARIES})\n"); + writer.append(" endif()\n"); + writer.append(" if(CN1GL_FOUND)\n"); + writer.append(" target_compile_definitions(${PROJECT_NAME} PRIVATE CN1_EMBEDDED_HAS_GL)\n"); + writer.append(" list(APPEND CN1EMB_LIBS ${CN1GL_LIBRARIES})\n"); + writer.append(" else()\n"); + writer.append(" get_target_property(cn1_srcs ${PROJECT_NAME} SOURCES)\n"); + writer.append(" list(FILTER cn1_srcs EXCLUDE REGEX \"/cn1_linux_gl\\\\.c$\")\n"); + writer.append(" set_property(TARGET ${PROJECT_NAME} PROPERTY SOURCES ${cn1_srcs})\n"); + writer.append(" endif()\n"); + writer.append(" if(CN1GST_FOUND)\n"); + writer.append(" target_compile_definitions(${PROJECT_NAME} PRIVATE CN1_EMBEDDED_HAS_GST)\n"); + writer.append(" else()\n"); + writer.append(" get_target_property(cn1_srcs ${PROJECT_NAME} SOURCES)\n"); + writer.append(" list(FILTER cn1_srcs EXCLUDE REGEX \"/cn1_linux_(media|video)\\\\.c$\")\n"); + writer.append(" set_property(TARGET ${PROJECT_NAME} PROPERTY SOURCES ${cn1_srcs})\n"); + writer.append(" endif()\n"); + writer.append(" target_include_directories(${PROJECT_NAME} PRIVATE ${CN1DEPS_INCLUDE_DIRS} ${CN1CRYPTO_INCLUDE_DIRS} ${CN1CURL_INCLUDE_DIRS} ${CN1DRM_INCLUDE_DIRS} ${CN1GL_INCLUDE_DIRS} ${CN1GST_INCLUDE_DIRS})\n"); + writer.append(" target_compile_options(${PROJECT_NAME} PRIVATE ${CN1DEPS_CFLAGS_OTHER} ${CN1CURL_CFLAGS_OTHER})\n"); + // --as-needed: a library the code ends up not calling is not loaded at + // start. Passed through target_link_libraries, which places it before the + // libraries on the line and works with the project's CMake 3.10 floor. + writer.append(" target_link_libraries(${PROJECT_NAME} -Wl,--as-needed ${CN1EMB_LIBS} m pthread dl)\n"); + writer.append("else()\n"); // Core GTK3 stack -- LINKED. The shipped binary requires only these (plus // libc/libm/pthread/dl) at runtime. Splitting the GLES probe out keeps a // clear error if only the GL bits are missing. writer.append("pkg_check_modules(CN1DEPS REQUIRED\n"); writer.append(" gtk+-3.0 cairo pango pangocairo gdk-pixbuf-2.0 glib-2.0 gobject-2.0 gio-2.0\n"); - writer.append(" fontconfig freetype2\n"); - // libcrypto backs the port's crypto bridge (cn1_linux_crypto.c). - writer.append(" libcrypto)\n"); + writer.append(" fontconfig freetype2)\n"); + // libcrypto backs the port's crypto bridge (cn1_linux_crypto.c), which + // loads it on first use: headers only, like libcurl below. + writer.append("pkg_check_modules(CN1CRYPTO REQUIRED libcrypto>=3)\n"); // HTTP resolves the libcurl.so.4 ABI by name. Direct linking imports the // build distro's CURL_OPENSSL_4 tags, absent on Alma/RHEL's libcurl. // Keep its headers/compiler flags, but do not link CN1CURL_LIBRARIES. @@ -1610,12 +1669,13 @@ private static void writeLinuxLinkSet(Writer writer) throws IOException { writer.append("pkg_check_modules(CN1OPT REQUIRED\n"); writer.append(" gstreamer-1.0 gstreamer-app-1.0 gstreamer-video-1.0\n"); writer.append(" webkit2gtk-4.1 libsecret-1 libnotify libgeoclue-2.0)\n"); - writer.append("target_include_directories(${PROJECT_NAME} PRIVATE ${CN1DEPS_INCLUDE_DIRS} ${CN1CURL_INCLUDE_DIRS} ${CN1GL_INCLUDE_DIRS} ${CN1OPT_INCLUDE_DIRS})\n"); + writer.append("target_include_directories(${PROJECT_NAME} PRIVATE ${CN1DEPS_INCLUDE_DIRS} ${CN1CRYPTO_INCLUDE_DIRS} ${CN1CURL_INCLUDE_DIRS} ${CN1GL_INCLUDE_DIRS} ${CN1OPT_INCLUDE_DIRS})\n"); writer.append("target_compile_options(${PROJECT_NAME} PRIVATE ${CN1DEPS_CFLAGS_OTHER} ${CN1CURL_CFLAGS_OTHER} ${CN1GL_CFLAGS_OTHER} ${CN1OPT_CFLAGS_OTHER})\n"); // libm/pthread back the translated runtime + GC; dl is required for the // dlopen() of the optional libs above. Note: CN1OPT_LIBRARIES is // deliberately NOT linked. writer.append("target_link_libraries(${PROJECT_NAME} ${CN1DEPS_LIBRARIES} ${CN1GL_LIBRARIES} m pthread dl)\n"); + writer.append("endif()\n"); // Dead-strip unreferenced functions/data, matching the Windows port's // /OPT:REF + /OPT:ICF. The translator emits a C function per reachable diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index d57c7d5d8f4..4b58d969d4b 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -2377,6 +2377,10 @@ static void cn1ThreadIdKeyCreate(void) { return NULL; } i->threadId = nativeThreadId; +#ifdef CN1_VIRTUAL_THREADS + // Its own pages until a carrier lends it theirs (cn1ResumeVirtualThreadOn). + i->bibopCur = i->bibopCurrent; +#endif i->tryBlockOffset = 0; i->nativeBuffers = NULL; @@ -2738,6 +2742,26 @@ void cn1RetireVirtualThread(struct cn1VirtualThread* vt) { } cn1VirtualThreadFree(vt); } + +/** + * Lend the carrier's pages, then run. Re-pointed at EVERY resume, not once: a + * virtual thread can be resumed by a different carrier each time, and must never + * allocate into the pages of one that is running something else. The carrier's own + * pointer is taken rather than its array, so a virtual thread resumed from inside + * another shares that one's carrier. + * + * Nothing here needs the collector's cooperation. The carrier is inside a native + * call with threadActive raised for the whole of the run, so it is never held while + * the virtual thread allocates on its behalf; and the virtual thread's own array is + * never written, so the collector, which reads only own arrays, sees nothing move. + */ +void cn1ResumeVirtualThreadOn(CODENAME_ONE_THREAD_STATE, struct cn1VirtualThread* vt) { + struct ThreadLocalData* state = (struct ThreadLocalData*)cn1VirtualThreadState(vt); + if(state != 0) { + state->bibopCur = threadStateData->bibopCur; + } + cn1VirtualThreadResume(vt); +} #endif /* CN1_VIRTUAL_THREADS -- see the capability gate in cn1_virtual_thread.h */ struct ThreadLocalData* getThreadLocalData() { diff --git a/vm/backend/build.sh b/vm/backend/build.sh index d1adf587acc..1d62a897ad4 100755 --- a/vm/backend/build.sh +++ b/vm/backend/build.sh @@ -81,7 +81,7 @@ fi RUNTIME_RESOURCES="$WORK/runtime-resources" mkdir -p "$RUNTIME_RESOURCES" for f in cn1_globals.h cn1_globals.m nativeMethods.m cn1_intrinsics.h cn1_sqlite3.c \ - cn1_process.h cn1_process_vm.h \ + cn1_process.h cn1_process_vm.h cn1_hardware_impl.h \ cn1_virtual_thread.h cn1_virtual_thread.c cn1_virtual_thread_asm.S; do cp "$REPO/vm/ByteCodeTranslator/src/$f" "$RUNTIME_RESOURCES/$f" done diff --git a/vm/backend/demo/bench/com/demo/Bench.java b/vm/backend/demo/bench/com/demo/Bench.java index 4f1a3e845bf..d5589d5e9dc 100644 --- a/vm/backend/demo/bench/com/demo/Bench.java +++ b/vm/backend/demo/bench/com/demo/Bench.java @@ -22,198 +22,64 @@ */ package com.demo; -import java.util.LinkedHashMap; -import java.util.Map; - +import com.codename1.backend.ByteSink; import com.codename1.backend.HttpServer; import com.codename1.backend.Json; import com.codename1.backend.Signals; /** - * The Codename One half of the Go comparison. + * The Codename One half of the Go and Rust comparisons. * * Two routes, deliberately the two TechEmpower framework-benchmark shapes, so the - * numbers here can be read against published ones as well as against the Go - * server beside them: + * numbers here can be read against published ones as well as against the servers + * beside them: * * /plaintext text/plain, a fixed 13-byte body * /json application/json, a small object serialised per request * - * The JSON is built per request rather than served from a constant, because - * serialisation is part of what is being compared; the plaintext route is the one - * that measures the HTTP path with nothing else in it. - * - * bench-server.go is line-for-line the same two handlers on net/http. Anything - * this file does that that one does not -- or the other way round -- is a - * difference in the measurement, not in the runtimes, so keep them matched. + * Both are written the way an application's are, with no switches: /json builds + * its object per request and answers through the shape the annotation processor + * emits for a controller returning a DTO -- the build-time codec writing the + * fields straight into the connection's buffer, on the connection's own Response. + * bench-server.go and the headless comparison's servers do the same work. */ public class Bench { private static final byte[] PLAINTEXT = bytes("Hello, World!"); - private static final int RESPONSE_MODE = envInt("BENCH_REUSE_RESPONSE", 1); - /** - * 0 = build a LinkedHashMap per request (what a hand-written handler does), - * 1 = reuse one map (isolates construction from serialising), - * 2 = write the fields directly (what the annotation processor now emits). - */ - private static final int JSON_MODE = envInt("BENCH_JSON_MODE", 0); + /** The DTO a controller would return. */ + static final class Message { + final String message; + + Message(String message) { + this.message = message; + } + } - /** Reused; the object is immutable and the writer holds no state. */ - private static final com.codename1.backend.Json.Writable MESSAGE_WRITABLE = - new com.codename1.backend.Json.Writable() { - public void writeTo(com.codename1.backend.ByteSink out) { + /// The encoder the build generates for a route returning Message. + private static final Json.Encoder MESSAGE_ENCODER = new Json.Encoder() { + public void write(Object value, ByteSink out) { + final Message result = (Message) value; out.put('{'); out.putAscii("\"message\":"); - com.codename1.backend.Json.writeString("Hello, World!", out); + Json.writeString(result.message, out); out.put('}'); } }; - private static final Map HOISTED = new LinkedHashMap(); - static { - HOISTED.put("message", "Hello, World!"); - } - public static void main(String[] args) throws Exception { Signals.installShutdownHandler(); int port = envInt("PORT", 8080); int workers = envInt("WORKERS", 16); int backlog = envInt("BACKLOG", 1024); - - // DIAGNOSTIC (BENCH_IDLE_THREADS=N): park N Java threads that do nothing. - // - // The collector stops and scans threads ONE AT A TIME, and it rebuilds its - // virtual-thread snapshot inside that per-thread loop -- so the more Java - // threads exist, the longer a snapshot stays live while OTHER threads are - // still running and still free virtual threads. Idle threads therefore - // widen a race they take no part in. This reproduces that without the - // worker pool, which used to supply the threads and no longer exists in - // virtual-thread mode. - int idle = envInt("BENCH_IDLE_THREADS", 0); - for(int iter = 0 ; iter < idle ; iter++) { - Thread parked = new Thread(new Runnable() { - public void run() { - while(true) { - try { - Thread.sleep(3600000); - } catch (InterruptedException err) { - return; - } - } - } - }); - parked.setDaemon(true); - parked.start(); - } - final HttpServer server = HttpServer.start(null, port, backlog, workers, new HttpServer.Handler() { public HttpServer.Response handle(HttpServer.Request request) throws Exception { String target = request.getTarget(); - // startsWith, so BENCH_VARY_TARGETS can drive a DISTINCT target per - // request (/plaintext?u=N) and still be served 200 + keep-alive. - // Matching exactly sends those to the 404 path, which replies - // Connection: close -- and a red-team run of the target cache then - // measures connection teardown rather than the cache: 292 requests - // in 5.1s with 758,381 write errors. Prefix matching keeps the - // adversarial case on the same code path as the normal one. if(target.startsWith("/plaintext")) { - // BENCH_REUSE_RESPONSE=1: hand back one shared Response - // instead of building one per request. - // - // Not a shippable handler -- it measures a CEILING. Response - // is the only per-request allocation left on this route (88 - // bytes), and the server only ever READS it, so sharing one - // is safe here and answers what pooling would be worth before - // any public API is changed to allow it. - // No per-request Response: the connection's own is re-pointed. - // BENCH_REUSE_RESPONSE=0 restores the allocating path, which is - // what the comparison measures against. - // 0 = allocate per request - // 1 = pooled, re-pointed via respond() (nine field writes) - // 2 = pooled but PRE-SET, returned untouched - // - // Mode 2 exists to separate two things mode 1 conflates: the - // saved allocation, and the cost of writing the fields into a - // connection-cold object instead of a bump-allocated one that - // is still warm in cache. Only valid because this route always - // answers with the same status, type and body. - if(RESPONSE_MODE == 2) { - HttpServer.Response r = request.presetResponse(); - if(r != null) { - return r; - } - } - if(RESPONSE_MODE == 1) { - return request.respond(200, "text/plain", PLAINTEXT); - } - return new HttpServer.Response(200, "text/plain", PLAINTEXT); + return request.respond(200, "text/plain", PLAINTEXT); } if("/json".equals(target)) { - // DIAGNOSTIC SPLIT (BENCH_JSON_HOIST=1): reuse one map instead - // of building it per request. - // - // Not a shippable handler -- a real one has different values - // each time -- but it separates the two costs this route pays. - // Go encodes a STRUCT with a cached per-type encoder; we build - // a LinkedHashMap, hash a key, insert, then walk it. Those are - // not the same work, so before concluding "our JSON serialiser - // is slow" it is worth knowing how much of the gap is the - // container rather than the serialising. - // - // Recovers most of the gap -> the fix is a struct-shaped API in - // plain Java. Recovers little -> the cost really is in the byte - // writer, and porting that to C is justified. - // - // ANSWERED, and it is the container. Two pinned cores, 64 - // connections, interleaved with rotating arm order, n=2: - // - // generated DTO (2) 595158 rps 2.619 us/req - // fasthttp 585906 rps 2.525 us/req - // hoisted map (1) 547423 rps 2.849 us/req - // map per request (0) 338167 rps 4.512 us/req - // - // Building the map costs 1.9 us of the 2.0 us that separated - // this route from Go -- hoisting it alone recovers most of that, - // and the struct-shaped writer recovers the rest and passes - // fasthttp. So the byte writer does NOT need porting to C: at - // mode 2 it is already serialising this object for less cpu than - // Go spends on the equivalent, and what looked like a serialiser - // gap was a LinkedHashMap allocated, hashed, inserted into and - // walked once per request. - // - // Mode 0 stays the default because it is the honest cost of a - // handler that hands back a Map, which is what an unannotated - // one does. An annotated DTO gets mode 2's shape from the - // processor without the author writing any of it. - if(JSON_MODE == 1) { - return HttpServer.Response.jsonValue(200, HOISTED); - } - if(JSON_MODE == 3) { - // Mode 2's writer on the connection's pooled Response, so the - // route allocates nothing at all. Mode 2 stays as it was so the - // cost of the Response itself remains measurable against it. - return request.respondJson(200, MESSAGE_WRITABLE); - } - if(JSON_MODE == 2) { - // What the annotation processor now emits for a DTO: no - // map, no key hashing, no walk, no instanceof per value -- - // the field name is a literal and the value takes the - // writer its static type selects. Hand-written here only - // because this benchmark handler is not annotated; the - // generated PetJson.toJson has exactly this shape. - return HttpServer.Response.jsonValue(200, MESSAGE_WRITABLE); - } - // Serialised per request, because the Go side encodes a struct - // per request. Returning a constant string here would compare - // our memcpy against their reflection. - Map out = new LinkedHashMap(); - out.put("message", "Hello, World!"); - // jsonValue, not json(Json.write(...)): the map is serialised - // straight into the connection's write buffer. The Go side - // encodes a struct per request, so this stays a real - // serialisation rather than a hoisted constant. - return HttpServer.Response.jsonValue(200, out); + return request.respondJson(200, new Message("Hello, World!"), MESSAGE_ENCODER); } return HttpServer.Response.text(404, "not found"); } diff --git a/vm/backend/demo/hwcheck/com/demo/HwCheck.java b/vm/backend/demo/hwcheck/com/demo/HwCheck.java new file mode 100644 index 00000000000..d35d9cdab40 --- /dev/null +++ b/vm/backend/demo/hwcheck/com/demo/HwCheck.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.demo; + +import com.codename1.backend.Backend; +import com.codename1.backend.HttpServer; +import com.codename1.hardware.Hardware; +import com.codename1.hardware.HardwareException; + +/// The headless hardware target: a started server installs the kernel +/// implementation, so `Hardware.getInstance()` answers for the board it runs +/// on. On Linux it reports support and lists what the kernel exposes; built +/// for another host every feature is there but every open fails. +public final class HwCheck { + private HwCheck() { + } + + public static void main(String[] args) throws Exception { + Backend backend = Backend.builder().quiet().host("127.0.0.1").port(0) + .handler(new HttpServer.Handler() { + @Override + public HttpServer.Response handle(HttpServer.Request request) { + return null; + } + }).start(); + Hardware hw = Hardware.getInstance(); + System.out.println("HWCHECK supported=" + hw.isSupported(Hardware.FEATURE_GPIO) + + " gpio=" + hw.listGpioChips().length + " i2c=" + hw.listI2cBuses().length + + " serial=" + hw.listSerialPorts().length + " can=" + hw.listCanInterfaces().length); + String open; + try { + hw.openGpioOutput("gpiochip0", 17, false).close(); + open = "opened"; + } catch (HardwareException e) { + open = "errno " + e.getErrno(); + } + backend.stop(); + System.out.println("HWCHECK gpio " + open); + System.out.println(hw.isSupported() ? "HWCHECK OK" : "HWCHECK FAILED"); + } +} diff --git a/vm/backend/docker/Containerfile.musl b/vm/backend/docker/Containerfile.musl index 28126aa22fe..01a525b955e 100644 --- a/vm/backend/docker/Containerfile.musl +++ b/vm/backend/docker/Containerfile.musl @@ -23,7 +23,9 @@ FROM alpine:3.20 ARG CURL_VER=8.7.1 -RUN apk add --no-cache clang lld musl-dev openssl-dev openssl-libs-static \ +# linux-headers: the hardware API (cn1_hardware_impl.h) includes the kernel's +# GPIO, I2C, SPI, CAN and watchdog UAPI headers, which musl-dev does not carry. +RUN apk add --no-cache clang lld musl-dev linux-headers openssl-dev openssl-libs-static \ zlib-dev zlib-static nghttp2-dev nghttp2-static pkgconf curl tar make RUN cd /tmp \ diff --git a/vm/backend/impl/javase/com/codename1/backend/BoardHardware.java b/vm/backend/impl/javase/com/codename1/backend/BoardHardware.java new file mode 100644 index 00000000000..e9fc8b04987 --- /dev/null +++ b/vm/backend/impl/javase/com/codename1/backend/BoardHardware.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.backend; + +/// The Java SE twin of the backend's board access. A JVM cannot reach the +/// kernel's GPIO, I2C or SPI interfaces without native code, so on this arm +/// nothing is installed and `Hardware.getInstance()` keeps answering the +/// unsupported default: a server developed on a laptop runs, and reports no +/// buses, exactly as the client simulator does when it is not a simulator. +public final class BoardHardware { + private BoardHardware() { + } + + /// Does nothing on the JVM; the translated binary installs the kernel + /// implementation here. + public static void install() { + } +} diff --git a/vm/backend/impl/javase/com/codename1/backend/ServerSocket.java b/vm/backend/impl/javase/com/codename1/backend/ServerSocket.java index 782dc46f20f..1321329eb6a 100644 --- a/vm/backend/impl/javase/com/codename1/backend/ServerSocket.java +++ b/vm/backend/impl/javase/com/codename1/backend/ServerSocket.java @@ -183,6 +183,11 @@ public static byte[] threadReadBuffer(int capacity) { private static final ThreadLocal THREAD_READ_BUFFER = new ThreadLocal(); + /// Java SE twin of the probe-free park: a readiness wait with no deadline. + public static boolean parkUntilReadable(int fd) throws IOException { + return awaitReadable(fd, -1); + } + /// Java SE twin of the readiness wait. See the translated version for why the /// shared code asks for this rather than juggling deadlines. /// diff --git a/vm/backend/impl/parparvm/com/codename1/backend/BoardHardware.java b/vm/backend/impl/parparvm/com/codename1/backend/BoardHardware.java new file mode 100644 index 00000000000..618695c4a79 --- /dev/null +++ b/vm/backend/impl/parparvm/com/codename1/backend/BoardHardware.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.backend; + +import com.codename1.hardware.Hardware; +import com.codename1.impl.KernelHardware; + +/// The headless backend's [Hardware]: the same kernel interfaces as the Linux +/// port, through the same C (`cn1_hardware_impl.h`, wrapped in +/// `cn1_backend_hardware.c`). A gateway or sensor hub serves its REST API and +/// drives its buses from one binary. Listener callbacks run on the pin's own +/// watcher thread, since a server has no event dispatch thread. Built for a +/// host other than Linux every primitive answers `-ENOSYS`. +public final class BoardHardware extends KernelHardware { + private static boolean installed; + + private BoardHardware() { + } + + /// Makes this the instance `Hardware.getInstance()` returns. The server + /// calls it as it starts; calling it again does nothing. + public static void install() { + if (!installed) { + installed = true; + Hardware.install(new BoardHardware()); + } + } + + @Override + protected String rawList(int kind) { + return nList(kind); + } + + private static native String nList(int kind); + + @Override + protected int rawBacklight(int op, int value) { + return nBacklight(op, value); + } + + private static native int nBacklight(int op, int value); + + @Override + protected int rawGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges) { + // C reads the name up to its first NUL; a name holding one would open + // a device other than the one named. -22 is EINVAL. + if (chip == null || chip.indexOf('\0') >= 0) { + return -22; + } + return nGpioOpen(chip, line, output, high, bias, edges); + } + + private static native int nGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges); + + @Override + protected int rawGpioRead(int handle) { + return nGpioRead(handle); + } + + private static native int nGpioRead(int handle); + + @Override + protected int rawGpioWrite(int handle, boolean high) { + return nGpioWrite(handle, high); + } + + private static native int nGpioWrite(int handle, boolean high); + + @Override + protected int rawGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) { + return nGpioWaitEdge(handle, timeoutMs, timestampOut); + } + + private static native int nGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut); + + @Override + protected int rawI2cOpen(int bus, int address) { + return nI2cOpen(bus, address); + } + + private static native int nI2cOpen(int bus, int address); + + @Override + protected int rawI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) { + return nI2cTransfer(handle, write, writeLen, read, readLen); + } + + private static native int nI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen); + + @Override + protected int rawSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) { + return nSpiOpen(bus, chipSelect, mode, speedHz, bitsPerWord); + } + + private static native int nSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord); + + @Override + protected int rawSpiTransfer(int handle, byte[] tx, byte[] rx, int len) { + return nSpiTransfer(handle, tx, rx, len); + } + + private static native int nSpiTransfer(int handle, byte[] tx, byte[] rx, int len); + + @Override + protected int rawSerialOpen(String device, int baud, int dataBits, int parity, int stopBits) { + // C reads the name up to its first NUL; a name holding one would open + // a device other than the one named. -22 is EINVAL. + if (device == null || device.indexOf('\0') >= 0) { + return -22; + } + return nSerialOpen(device, baud, dataBits, parity, stopBits); + } + + private static native int nSerialOpen(String device, int baud, int dataBits, int parity, int stopBits); + + @Override + protected int rawSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs) { + return nSerialRead(handle, buf, off, len, timeoutMs); + } + + private static native int nSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs); + + @Override + protected int rawSerialWrite(int handle, byte[] buf, int off, int len) { + return nSerialWrite(handle, buf, off, len); + } + + private static native int nSerialWrite(int handle, byte[] buf, int off, int len); + + @Override + protected int rawPwmOpen(int chip, int channel) { + return nPwmOpen(chip, channel); + } + + private static native int nPwmOpen(int chip, int channel); + + @Override + protected int rawPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) { + return nPwmConfigure(handle, periodNanos, dutyNanos, enabled); + } + + private static native int nPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled); + + @Override + protected int rawCanOpen(String iface) { + // C reads the name up to its first NUL; a name holding one would open + // a device other than the one named. -22 is EINVAL. + if (iface == null || iface.indexOf('\0') >= 0) { + return -22; + } + return nCanOpen(iface); + } + + private static native int nCanOpen(String iface); + + @Override + protected int rawCanSend(int handle, int id, boolean extended, byte[] data, int len) { + return nCanSend(handle, id, extended, data, len); + } + + private static native int nCanSend(int handle, int id, boolean extended, byte[] data, int len); + + @Override + protected int rawCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs) { + return nCanReceive(handle, idOut, data, timeoutMs); + } + + private static native int nCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs); + + @Override + protected int rawWatchdogOpen(int timeoutSeconds) { + return nWatchdogOpen(timeoutSeconds); + } + + private static native int nWatchdogOpen(int timeoutSeconds); + + @Override + protected int rawWatchdogKick(int handle) { + return nWatchdogKick(handle); + } + + private static native int nWatchdogKick(int handle); + + @Override + protected void rawClose(int handle, boolean disarm) { + nClose(handle, disarm); + } + + private static native void nClose(int handle, boolean disarm); +} diff --git a/vm/backend/impl/parparvm/com/codename1/backend/ServerSocket.java b/vm/backend/impl/parparvm/com/codename1/backend/ServerSocket.java index 85ba6b9b6aa..ac4e10de75f 100644 --- a/vm/backend/impl/parparvm/com/codename1/backend/ServerSocket.java +++ b/vm/backend/impl/parparvm/com/codename1/backend/ServerSocket.java @@ -163,6 +163,18 @@ public static boolean awaitReadable(int fd, int timeoutMillis) throws IOExceptio return rc > 0; } + /// Parks the calling virtual thread until the socket is readable, without + /// asking first. For a caller that already knows nothing is there -- its last + /// read came back short or would have blocked -- so a probe could only answer + /// "not yet". Off a virtual thread this is [#awaitReadable] with no deadline. + public static boolean parkUntilReadable(int fd) throws IOException { + return awaitReadable(fd, PARK_WITHOUT_PROBE); + } + + /// The timeout [#parkUntilReadable] hands the native: park on a virtual thread + /// with no probe, wait with no deadline on a platform thread. + private static final int PARK_WITHOUT_PROBE = -2; + /// -1 at end of stream, as InputStream does. public static int read(int fd, byte[] buffer, int offset, int length) throws IOException { checkRange(buffer, offset, length); diff --git a/vm/backend/native/cn1_backend_hardware.c b/vm/backend/native/cn1_backend_hardware.c new file mode 100644 index 00000000000..b022b843d18 --- /dev/null +++ b/vm/backend/native/cn1_backend_hardware.c @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The headless backend's hardware natives (com.codename1.backend.BoardHardware). + * Each forwards to the shared body in cn1_hardware_impl.h, the one the Linux + * port uses; off Linux every call answers -ENOSYS. + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include "cn1_hardware_impl.h" + +JAVA_INT com_codename1_backend_BoardHardware_nGpioOpen___java_lang_String_int_boolean_boolean_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT chip, JAVA_INT line, JAVA_BOOLEAN output, JAVA_BOOLEAN high, JAVA_INT bias, JAVA_INT edges) { + return cn1HwImpl_hwGpioOpen(threadStateData, chip, line, output, high, bias, edges); +} + +JAVA_INT com_codename1_backend_BoardHardware_nGpioRead___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return cn1HwImpl_hwGpioRead(threadStateData, h); +} + +JAVA_INT com_codename1_backend_BoardHardware_nGpioWrite___int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_BOOLEAN high) { + return cn1HwImpl_hwGpioWrite(threadStateData, h, high); +} + +JAVA_INT com_codename1_backend_BoardHardware_nGpioWaitEdge___int_int_long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { + return cn1HwImpl_hwGpioWaitEdge(threadStateData, h, timeoutMs, tsOut); +} + +JAVA_INT com_codename1_backend_BoardHardware_nI2cOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, JAVA_INT address) { + return cn1HwImpl_hwI2cOpen(threadStateData, bus, address); +} + +JAVA_INT com_codename1_backend_BoardHardware_nI2cTransfer___int_byte_1ARRAY_int_byte_1ARRAY_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT w, JAVA_INT wl, JAVA_OBJECT rd, JAVA_INT rl) { + return cn1HwImpl_hwI2cTransfer(threadStateData, h, w, wl, rd, rl); +} + +JAVA_INT com_codename1_backend_BoardHardware_nSpiOpen___int_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, JAVA_INT cs, JAVA_INT mode, JAVA_INT speed, JAVA_INT bits) { + return cn1HwImpl_hwSpiOpen(threadStateData, bus, cs, mode, speed, bits); +} + +JAVA_INT com_codename1_backend_BoardHardware_nSpiTransfer___int_byte_1ARRAY_byte_1ARRAY_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT tx, JAVA_OBJECT rx, JAVA_INT len) { + return cn1HwImpl_hwSpiTransfer(threadStateData, h, tx, rx, len); +} + +JAVA_INT com_codename1_backend_BoardHardware_nSerialOpen___java_lang_String_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT dev, JAVA_INT baud, JAVA_INT dataBits, JAVA_INT parity, JAVA_INT stopBits) { + return cn1HwImpl_hwSerialOpen(threadStateData, dev, baud, dataBits, parity, stopBits); +} + +JAVA_INT com_codename1_backend_BoardHardware_nSerialRead___int_byte_1ARRAY_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len, JAVA_INT timeoutMs) { + return cn1HwImpl_hwSerialRead(threadStateData, h, buf, off, len, timeoutMs); +} + +JAVA_INT com_codename1_backend_BoardHardware_nSerialWrite___int_byte_1ARRAY_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len) { + return cn1HwImpl_hwSerialWrite(threadStateData, h, buf, off, len); +} + +JAVA_INT com_codename1_backend_BoardHardware_nPwmOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT chip, JAVA_INT channel) { + return cn1HwImpl_hwPwmOpen(threadStateData, chip, channel); +} + +JAVA_INT com_codename1_backend_BoardHardware_nPwmConfigure___int_long_long_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_LONG period, JAVA_LONG duty, JAVA_BOOLEAN enabled) { + return cn1HwImpl_hwPwmConfigure(threadStateData, h, period, duty, enabled); +} + +JAVA_INT com_codename1_backend_BoardHardware_nCanOpen___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT iface) { + return cn1HwImpl_hwCanOpen(threadStateData, iface); +} + +JAVA_INT com_codename1_backend_BoardHardware_nCanSend___int_int_boolean_byte_1ARRAY_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT id, JAVA_BOOLEAN ext, JAVA_OBJECT data, JAVA_INT len) { + return cn1HwImpl_hwCanSend(threadStateData, h, id, ext, data, len); +} + +JAVA_INT com_codename1_backend_BoardHardware_nCanReceive___int_int_1ARRAY_byte_1ARRAY_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT idOut, JAVA_OBJECT data, JAVA_INT timeoutMs) { + return cn1HwImpl_hwCanReceive(threadStateData, h, idOut, data, timeoutMs); +} + +JAVA_INT com_codename1_backend_BoardHardware_nWatchdogOpen___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT timeoutSeconds) { + return cn1HwImpl_hwWatchdogOpen(threadStateData, timeoutSeconds); +} + +JAVA_INT com_codename1_backend_BoardHardware_nWatchdogKick___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return cn1HwImpl_hwWatchdogKick(threadStateData, h); +} + +JAVA_VOID com_codename1_backend_BoardHardware_nClose___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_BOOLEAN disarm) { + cn1HwImpl_hwClose(threadStateData, h, disarm); +} + +JAVA_OBJECT com_codename1_backend_BoardHardware_nList___int_R_java_lang_String(CODENAME_ONE_THREAD_STATE, JAVA_INT kind) { + return cn1HwImpl_hwList(threadStateData, kind); +} + +JAVA_INT com_codename1_backend_BoardHardware_nBacklight___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT op, JAVA_INT value) { + return cn1HwImpl_hwBacklight(threadStateData, op, value); +} diff --git a/vm/backend/native/cn1_backend_server.c b/vm/backend/native/cn1_backend_server.c index 6b5b64660c4..a859a314c4e 100644 --- a/vm/backend/native/cn1_backend_server.c +++ b/vm/backend/native/cn1_backend_server.c @@ -271,20 +271,6 @@ static struct JavaArrayPrototype* cn1BackendEnsureWouldBlockArray(void) { return cn1BackendWouldBlockArray; } -/* - * Whether awaitReadable probes with poll() before parking. Read once; see the - * discussion at the call site. 1 (probe) is the shipped default until the A/B - * on an idle host says otherwise. - */ -static int cn1BackendSpeculativePoll(void) { - static int cached = -1; - if(cached < 0) { - const char* v = getenv("CN1_HTTP_SPECULATIVE_POLL"); - cached = (v != 0 && v[0] == '0') ? 0 : 1; - } - return cached; -} - /* * The poller's event array, one per thread, grown on demand and reused. * @@ -598,64 +584,34 @@ JAVA_INT com_codename1_backend_ServerSocket_awaitReadableImpl___int_int_R_int(CO // resumes a parked virtual thread once the poller reports its descriptor // ready, so coming back IS the readiness answer. if(cn1BackendCanPark()) { - // ASK FIRST, and this poll is an optimisation rather than the waste it - // looks like in a syscall census. - // - // It was removed once on the grounds that Go does not do it -- its - // FD.Read calls read() straight away and parks on EAGAIN -- and that the - // census showed 1.9 ppoll per request against Go's zero. Throughput fell - // from 1047890 requests to about 110000 and /json died. The census was - // counting a cheap syscall that PREVENTS an expensive one: when the next - // request has already arrived, which under keep-alive it usually has, - // this answers immediately and the virtual thread never parks. Without - // it every keep-alive wait costs a park, an epoll round trip and a - // resume. - // - // The lesson generalises: syscall COUNT is not cost. Go can afford to - // skip this because its park is a goroutine switch inside a scheduler - // that is already awake; ours goes out to the poller and back. - // Left switchable rather than deleted, because the case against it is - // real and the case for it was measured on an older scheduler. + // -2 is parkUntilReadable: the caller's last read came back short or + // would have blocked, so a probe could only say "not yet". Anything else + // asks once without blocking, so a caller that does not know gets its + // answer for one cheap syscall instead of a park and a resume. // - // Against: the caller's next move is a recv, and on a virtual thread that - // recv already parks on EAGAIN and retries when the poller says the - // descriptor is ready. Polling here first costs one extra syscall on - // EVERY request to learn what the read is about to learn anyway -- a - // corrected census puts it at exactly 1.0 ppoll per request, the only - // syscall in our profile that Go does not make at all. - // - // For: removing it costs almost all of the throughput, and that is still - // true after the host loop learned to drain a local run queue before - // polling -- which was the reason to expect otherwise. RE-MEASURED, four - // arms interleaved on a quiet host, /plaintext at 64 connections, medians - // of four steady-state reps: - // - // go 243,161 req/s - // virtual threads, probe 207,808 0.854 of go - // virtual threads, NO probe 20,134 0.082 of go <-- 12x worse - // - // The syscall census makes the trap explicit: without the probe a request - // costs 2.05 syscalls against Go's 2.64 -- FEWER than Go -- and it is ten - // times slower, because the ppoll it saves is replaced by a park, and a - // park is a poller round trip plus a resume. Syscall COUNT is not cost. - // Do not re-run this experiment expecting a different answer; run it only - // after the PARK itself gets cheaper. - // - // A zero timeout is a genuine probe (a caller asking "is anything there" - // without wanting to wait), so that one still has to ask regardless. - if(!cn1BackendSpeculativePoll() && timeoutMillis != 0) { - return 1; - } - p.revents = 0; - rc = poll(&p, 1, 0); - if(rc > 0) { - return 1; - } - if(rc < 0 && errno != EINTR) { - return -1; - } - if(timeoutMillis == 0) { - return 0; // a pure probe: no data, do not park + // The HTTP keep-alive loop no longer comes through here per request. It + // used to probe before EVERY read, one ppoll per request that neither + // Rust's nor Go's server makes, because the zero-copy read could not park + // when nothing had arrived. That read now parks through the -2 path, and + // only after a short read -- tokio's rule: a read that did not fill the + // buffer drained the socket, so wait for the poller rather than read + // again to learn EAGAIN. Measured under a profiler, two pinned cores, 32 + // connections: 87,551 requests/s with the probe, 162,875 reading first + // (still one EAGAIN recv and one ppoll per request), 175,295 parking + // after a short read -- one recvfrom and one sendto per request, the + // same two syscalls axum makes, which measured 181,815 in that run. + if(timeoutMillis != -2) { + p.revents = 0; + rc = poll(&p, 1, 0); + if(rc > 0) { + return 1; + } + if(rc < 0 && errno != EINTR) { + return -1; + } + if(timeoutMillis == 0) { + return 0; // a pure probe: no data, do not park + } } // YIELD around the park, and this is not optional bookkeeping. // @@ -1434,7 +1390,7 @@ JAVA_INT com_codename1_backend_VirtualThread_resumeImpl___long_R_int(CODENAME_ON return 0; } cn1VirtualThreadSetYieldReason(CN1_VT_YIELD_IO); /* the default for a plain park */ - cn1VirtualThreadResume(vt); + cn1ResumeVirtualThreadOn(threadStateData, vt); if(cn1VirtualThreadFinished(vt)) { atomic_fetch_add(&cn1VtFinished, 1); return 0; @@ -1758,7 +1714,8 @@ JAVA_INT com_codename1_backend_ServerSocket_availableProcessorsImpl___R_int(CODE #ifdef _WIN32 return 1; #else - long n = sysconf(_SC_NPROCESSORS_ONLN); - return n > 0 ? (JAVA_INT)n : 1; + // The affinity mask and cgroup quota, not the machine: host threads past the + // cores the process may use only contend for them. + return cn1UsableCpuCount(); #endif } diff --git a/vm/backend/src/com/codename1/backend/Backend.java b/vm/backend/src/com/codename1/backend/Backend.java index 3dfe6906f87..4acd7f5f7fc 100644 --- a/vm/backend/src/com/codename1/backend/Backend.java +++ b/vm/backend/src/com/codename1/backend/Backend.java @@ -1795,6 +1795,8 @@ public Builder sessionStore(SessionStore store) { /// waiting. Tests want this; a process wants [#run]. public Backend start() throws Exception { claimProcess(); + // Before the application's started() callback, which may open a pin. + BoardHardware.install(); Backend running = null; try { running = startOnce(); diff --git a/vm/backend/src/com/codename1/backend/HttpServer.java b/vm/backend/src/com/codename1/backend/HttpServer.java index cdca56654ae..4de80ae5f51 100644 --- a/vm/backend/src/com/codename1/backend/HttpServer.java +++ b/vm/backend/src/com/codename1/backend/HttpServer.java @@ -548,6 +548,16 @@ public Response respondJson(int status, Object value) { return conn.pooledResponse; } + /// [#respondJson(int, Object)] written by `encoder` rather than by + /// inspecting the value. The form a generated controller uses: the encoder + /// is a static of the route, so answering allocates nothing beyond what the + /// handler returned. `value` must not be null. + public Response respondJson(int status, Object value, Json.Encoder encoder) { + Response r = respondJson(status, value); + r.deferredEncoder = encoder; + return r; + } + /// DIAGNOSTIC: the connection's Response exactly as the last request left /// it, or null the first time. Separates the allocation pooling saves from /// the field writes it adds -- see the bench demo's RESPONSE_MODE. @@ -1403,6 +1413,30 @@ public static final class Response { /// Serialised into the connection buffer at write time; see jsonValue. Object deferredJson; boolean hasDeferredJson; + /// How deferredJson is written, or null for [Json#write(Object, ByteSink)]. + Json.Encoder deferredEncoder; + + /// Writes the deferred value into `out`, through its encoder when it has one. + void writeDeferredJson(ByteSink out) { + if (deferredEncoder != null) { + deferredEncoder.write(deferredJson, out); + } else { + Json.write(deferredJson, out); + } + } + + /// The deferred value as its own array, for the paths with no connection + /// buffer to render into (HTTP/2 frames, serialising before a copy). + byte[] deferredJsonBytes() { + if (deferredEncoder == null) { + return bytes(Json.write(deferredJson)); + } + ByteSink sink = new ByteSink(256); + deferredEncoder.write(deferredJson, sink); + byte[] out = new byte[sink.length()]; + System.arraycopy(sink.bytes(), 0, out, 0, out.length); + return out; + } BodyWriter bodyWriter; /// The length a HEAD answer advertises for a bodiless response, or -1 to /// measure the body as usual: HttpProxy's HEAD carries the upstream's @@ -1419,6 +1453,7 @@ public static final class Response { void releaseRetained() { this.deferredJson = null; this.hasDeferredJson = false; + this.deferredEncoder = null; this.bodyWriter = null; this.describedLength = -1; this.body = EMPTY_BODY; @@ -1454,9 +1489,10 @@ void releaseWriter() { /// object graph may change after this without changing the response. void serializeDeferredJson() { if (hasDeferredJson) { - body = bytes(Json.write(deferredJson)); + body = deferredJsonBytes(); deferredJson = null; hasDeferredJson = false; + deferredEncoder = null; } } @@ -1475,6 +1511,7 @@ void reset(int status, String contentType, byte[] body, int fileFd, this.extraHeaders = extraHeaders; this.deferredJson = null; this.hasDeferredJson = false; + this.deferredEncoder = null; this.bodyWriter = null; this.describedLength = -1; } @@ -1517,6 +1554,7 @@ Response withHeaders(Map headers) { fileLength, headers); copy.deferredJson = deferredJson; copy.hasDeferredJson = hasDeferredJson; + copy.deferredEncoder = deferredEncoder; copy.bodyWriter = bodyWriter; copy.describedLength = describedLength; return copy; @@ -1965,6 +2003,9 @@ static boolean isSpoolable(String contentType, String contentEncoding) { static final int COMBINED_WRITE_LIMIT = 8192; static final byte[] EMPTY_BODY = new byte[0]; + + /// The most one read asks for, zero-copy or copying. + static final int READ_CHUNK = 8192; /// One instance, so the pooled JSON path does not intern a literal per call. static final String JSON_CONTENT_TYPE = "application/json; charset=utf-8"; /// What a Response with no content type is sent as, on either protocol. @@ -5430,9 +5471,38 @@ private static final class Conn { /// answer before it lets go of a borrow, and "is there anything left to /// read" is NOT the same question -- see the comment there. boolean parsedFromBuffer; + /// The last zero-copy read did not fill the buffer, so the socket is very + /// likely empty: a virtual thread waits for the poller before reading again + /// rather than spending a recv to learn EAGAIN. See fill(). + boolean drained; /// Memoised request targets for this connection. See internTarget. private final String[] targetCache = new String[TARGET_CACHE_SLOTS]; + /// The Host value of the previous request on this connection. + private String lastHost; + + /// The Host field as a String, the previous request's when the bytes are + /// the same. A keep-alive client sends one Host for the life of the + /// connection, and building it afresh was three of the five objects a + /// small JSON request allocated (the String, its chars and their copy). + /// One slot, not a table: a connection that alternates hosts is a proxy + /// multiplexing virtual hosts, and it simply misses as it did before. + String internHost(byte[] data, int start, int length) { + String cached = lastHost; + if (cached != null && cached.length() == length) { + int iter = 0; + while (iter < length + && cached.charAt(iter) == (char) (data[start + iter] & 0xff)) { + iter++; + } + if (iter == length) { + return cached; + } + } + String fresh = asciiString(data, start, length); + lastHost = length > MAX_CACHED_TARGET_BYTES ? null : fresh; + return fresh; + } String internTarget(byte[] data, int start, int length) { // NOTHING LONG GOES IN. The cache exists for a route asked for over and @@ -5697,8 +5767,27 @@ int available() { /// not, so detachPreservingOffsets can re-point it. Request liveRequest; + /// The copying read's buffer, created by the first read that copies. + /// + /// A plaintext connection reads straight into its host's buffer and never + /// needs one; TLS and a request spanning reads do. It used to be allocated + /// for every connection up front, and 8KB is over the size the collector + /// serves from its pages, so each one was a zeroed calloc held for the + /// connection's whole life -- the larger part of what an idle keep-alive + /// connection cost. + private byte[] scratch; + + private byte[] scratch() { + byte[] s = scratch; + if (s == null) { + s = new byte[READ_CHUNK]; + scratch = s; + } + return s; + } + /// Reads more. False at end of stream. - boolean fill(byte[] scratch) throws IOException { + boolean fill() throws IOException { if (borrowed && available() == 0 && !parsedFromBuffer) { // Nothing is left unread AND nothing has been parsed out of this // buffer, so this is the START of a new request on a kept-alive @@ -5747,6 +5836,19 @@ boolean fill(byte[] scratch) throws IOException { detachPreservingOffsets(); } if (ZERO_COPY_READ && available() == 0 && session == 0) { + if (drained && !parsedFromBuffer && VirtualThread.isVirtual()) { + // The last read came back short, so the client had sent + // everything it had, and a keep-alive client sends its next + // request only after reading this answer. Park first: a read + // now would almost always answer EAGAIN and cost a syscall to + // say so. The poller is level-triggered, so bytes that did + // arrive meanwhile resume this thread at once. This is how + // tokio reads: a short read clears the socket's readiness. + drained = false; + if (!ServerSocket.parkUntilReadable(fd)) { + return false; + } + } // The common case by far: nothing left over, so the bytes are read // into this thread's reusable buffer and parsed where they land -- // no array allocated, nothing copied. The request is parsed out of @@ -5761,7 +5863,7 @@ boolean fill(byte[] scratch) throws IOException { // // Plaintext only (session == 0): a TLS read decrypts through its // own path and does not hand back a buffer we own. - byte[] direct = ServerSocket.readIntoThreadBuffer(fd, scratch.length); + byte[] direct = ServerSocket.readIntoThreadBuffer(fd, READ_CHUNK); if (direct == null) { return false; } @@ -5788,11 +5890,31 @@ boolean fill(byte[] scratch) throws IOException { // already maintained for fill()'s benefit. Note a SPLIT header // block needs nothing here: after the first partial read // available() is non-zero, so it never takes this branch. - if (!parsedFromBuffer) { + // + // ON A VIRTUAL THREAD the quiet is neither: nothing points into + // the thread buffer yet, so the thread parks until the poller + // reports bytes and reads again. Read first and wait only on + // EAGAIN is what Rust's and Go's servers do; the keep-alive loop + // used to call poll() before EVERY read instead -- one syscall + // per request more than either, measured as a third of ours -- + // because this branch could not park. + if (parsedFromBuffer) { + return fillCopying(); + } + if (!VirtualThread.isVirtual()) { return false; } - return fillCopying(scratch); + do { + if (!ServerSocket.parkUntilReadable(fd)) { + return false; + } + direct = ServerSocket.readIntoThreadBuffer(fd, READ_CHUNK); + if (direct == null) { + return false; + } + } while (direct.length == 0); } + drained = direct.length < READ_CHUNK; if (ZERO_COPY_MODE == 2) { // Diagnostic bisection only -- see ZERO_COPY_MODE. Same read as // mode 1, same heap array as mode 0, so whichever of the two the @@ -5809,7 +5931,7 @@ boolean fill(byte[] scratch) throws IOException { borrowed = true; return true; } - return fillCopying(scratch); + return fillCopying(); } /// The copying read: into this connection's own scratch, then into a buffer @@ -5818,7 +5940,8 @@ boolean fill(byte[] scratch) throws IOException { /// Split out of fill() so the zero-copy path can defer to it when the /// descriptor has nothing ready. readFrom parks on EAGAIN for a virtual /// thread, which is the behaviour the shared-buffer read cannot safely have. - private boolean fillCopying(byte[] scratch) throws IOException { + private boolean fillCopying() throws IOException { + byte[] scratch = scratch(); int n = readFrom(fd, session, scratch, 0, scratch.length); if (n <= 0) { return false; @@ -6205,7 +6328,6 @@ private void serveOneRelease(int fd) { if (virtualThreads) { ownerOf(fd).setConn(fd, conn); } - byte[] scratch = new byte[8192]; int served = 0; // Cleartext HTTP/2 by prior knowledge: a client that already knows the @@ -6227,7 +6349,7 @@ private void serveOneRelease(int fd) { if (session == 0) { try { while (conn.available() < HTTP2_PREFACE.length) { - if (!conn.fill(scratch)) { + if (!conn.fill()) { drop(fd); return; } @@ -6256,7 +6378,7 @@ private void serveOneRelease(int fd) { while (true) { Request request; try { - request = readRequest(conn, scratch); + request = readRequest(conn); } catch (ProtocolException err) { trace("fd=" + fd + " rejected: " + err.getMessage()); writeStatusOnly(conn, err.status, err.getMessage()); @@ -6357,7 +6479,7 @@ private void serveOneRelease(int fd) { if (response.hasDeferredJson) { try { conn.bodySink.reset(); - Json.write(response.deferredJson, conn.bodySink); + response.writeDeferredJson(conn.bodySink); } catch (Throwable err) { rethrowIfFatal(err); System.err.println("handler failed: " + err); @@ -6489,7 +6611,11 @@ private void serveOneRelease(int fd) { // is waiting for still gets the full one when it arrives. // Setting and restoring SO_RCVTIMEO around each wait did work, // and cost four setsockopt per request -- 15% of syscall time. - if (!ServerSocket.awaitReadable(fd, linger)) { + // + // Not on a virtual thread: there fill() reads straight away and + // parks only when nothing has arrived, so asking first would be + // a syscall per request spent learning what the read learns. + if (!virtualThreads && !ServerSocket.awaitReadable(fd, linger)) { break; // quiet client; the poller can have it } // The request this buffer was parsed from has been ANSWERED, @@ -6514,7 +6640,7 @@ private void serveOneRelease(int fd) { // window by construction -- the handler has returned and the // response is on the wire. conn.parsedFromBuffer = false; - more = conn.fill(scratch); + more = conn.fill(); } catch (IOException err) { drop(fd); return; @@ -6539,15 +6665,12 @@ private void serveOneRelease(int fd) { try { // Back to the poller for the next request on this connection. Both // epoll_ctl and kevent are safe to call from this thread. - if (virtualThreads) { - // Reached only when the connection itself is finished: a virtual - // thread does not come back here to wait, it parks where it waits. - // Re-arming now would hand the poller a descriptor nobody owns. - drop(fd); - } else { - ServerSocket.setBlocking(fd, false); - armConnection(fd, false); - } + // + // Never reached on a virtual thread: every break above is taken only + // by pooled workers, and a virtual thread leaves the loop by returning + // once the connection is finished. It parks where it waits instead. + ServerSocket.setBlocking(fd, false); + armConnection(fd, false); } catch (IOException err) { drop(fd); } @@ -7285,7 +7408,7 @@ private byte[] responseBodyFor(Response response, boolean headOnly) throws IOExc // so they are built as their own array. Without this the body is empty, // and the same handler that works over HTTP/1.1 answers HTTP/2 with // nothing at all. - return Response.bytes(Json.write(response.deferredJson)); + return response.deferredJsonBytes(); } return response.body; } @@ -7840,7 +7963,7 @@ private void writeRefusedUpgrade(Conn conn, int fd, long session, Response refus try { if (refused.hasDeferredJson) { conn.bodySink.reset(); - Json.write(refused.deferredJson, conn.bodySink); + refused.writeDeferredJson(conn.bodySink); } writeResponse(conn, fd, session, refused, false, false, true); conn.writtenStatus = status; @@ -7967,7 +8090,7 @@ static String canonicalDeclaredPath(String value) { /// Reads one request. Null when the peer closed; ProtocolException when what /// arrived is not a request this server will act on. - private Request readRequest(Conn conn, byte[] scratch) throws IOException { + private Request readRequest(Conn conn) throws IOException { // Cleared before the header block is read and raised once it has been // parsed, so fill() can tell "start of a request" from "midway through // one" -- which an empty buffer alone cannot say. @@ -7996,7 +8119,7 @@ private Request readRequest(Conn conn, byte[] scratch) throws IOException { if (headDeadline != 0 && System.currentTimeMillis() > headDeadline) { throw new ProtocolException(408, "the request head did not arrive in time"); } - if (!conn.fill(scratch)) { + if (!conn.fill()) { return null; } headerEnd = indexOfHeaderEnd(conn.buffer, conn.pos); @@ -8281,7 +8404,7 @@ private Request readRequest(Conn conn, byte[] scratch) throws IOException { } else if (sliceEqualsIgnoreCase(raw, slices[base], slices[base + 1], HOST_BYTES)) { hostCount++; if (hostValue == null) { - hostValue = asciiString(raw, slices[base + 2], slices[base + 3]); + hostValue = conn.internHost(raw, slices[base + 2], slices[base + 3]); } } } @@ -8411,7 +8534,7 @@ && isSpoolable(request.getHeader("Content-Type"), Spool spool = new Spool(SPOOL_THRESHOLD, MAX_SPOOLED_BODY_BYTES, SPOOL_DIRECTORY); boolean kept = false; try { - boolean complete = chunked ? readChunked(conn, scratch, spool) != null + boolean complete = chunked ? readChunked(conn, spool) != null : conn.spoolTo(spool, declaredLength); if (!complete) { return null; @@ -8429,7 +8552,7 @@ && isSpoolable(request.getHeader("Content-Type"), } } if (chunked) { - byte[] decoded = readChunked(conn, scratch, null); + byte[] decoded = readChunked(conn, null); if (decoded == null) { return null; } @@ -8624,7 +8747,7 @@ static byte[] gunzip(byte[] data, int offset, int length) throws ProtocolExcepti /// chunk as it arrives, never a whole chunk in the connection buffer -- and /// under MAX_SPOOLED_BODY_BYTES; the answer is then an empty array, or null /// for a peer that went away. - private byte[] readChunked(Conn conn, byte[] scratch, Spool spool) throws IOException { + private byte[] readChunked(Conn conn, Spool spool) throws IOException { // Charged against the SAME process-wide budget the fixed-length path uses. // Bounding only that path left this one open: a chunked body is capped per // request at MAX_BODY_BYTES and by nothing at all across requests, so @@ -8668,7 +8791,7 @@ private byte[] readChunked(Conn conn, byte[] scratch, Spool spool) throws IOExce } requireChunkedProgress(started, body.length() + conn.available()); reserveUploadUpTo(charged, body.length() + conn.available()); - if (!conn.fill(scratch)) { + if (!conn.fill()) { return null; } lineEnd = indexOfCrLf(conn.buffer, conn.pos); @@ -8714,7 +8837,7 @@ private byte[] readChunked(Conn conn, byte[] scratch, Spool spool) throws IOExce } requireChunkedProgress(started, body.length() + conn.available()); reserveUploadUpTo(charged, body.length() + conn.available()); - if (!conn.fill(scratch)) { + if (!conn.fill()) { // EOF before the blank line that ends the trailers: the // chunked framing never finished, so this is a truncated // message, not a complete one. Returning the body here @@ -8786,7 +8909,7 @@ private byte[] readChunked(Conn conn, byte[] scratch, Spool spool) throws IOExce requireChunkedProgress(started, (int) Math.min(Integer.MAX_VALUE, spool.length())); reserveUploadUpTo(charged, conn.available()); - if (!conn.fill(scratch)) { + if (!conn.fill()) { return null; } continue; @@ -8801,7 +8924,7 @@ private byte[] readChunked(Conn conn, byte[] scratch, Spool spool) throws IOExce while (conn.available() < 2) { requireChunkedProgress(started, (int) Math.min(Integer.MAX_VALUE, spool.length())); - if (!conn.fill(scratch)) { + if (!conn.fill()) { return null; } } @@ -8818,7 +8941,7 @@ private byte[] readChunked(Conn conn, byte[] scratch, Spool spool) throws IOExce while (conn.available() < size + 2) { requireChunkedProgress(started, body.length() + conn.available()); reserveUploadUpTo(charged, body.length() + conn.available()); - if (!conn.fill(scratch)) { + if (!conn.fill()) { return null; } } diff --git a/vm/backend/src/com/codename1/backend/Json.java b/vm/backend/src/com/codename1/backend/Json.java index f48d5ea0a9b..8977b654e9e 100644 --- a/vm/backend/src/com/codename1/backend/Json.java +++ b/vm/backend/src/com/codename1/backend/Json.java @@ -385,6 +385,19 @@ public interface Writable { void writeTo(ByteSink out); } + /// Writes values of one type as JSON, shared by every request that answers + /// with that type. + /// + /// The stateless counterpart of [Writable]. A Writable carries the value it + /// writes, so a route answering with a codec had to allocate one per request + /// to hold that value; an Encoder is handed the value instead, so a generated + /// route keeps ONE in a static field and a request allocates only what the + /// method returned. See `HttpServer.Request.respondJson(int, Object, Encoder)`. + public interface Encoder { + /// Writes `value`, never null, into `out`. + void write(Object value, ByteSink out); + } + /// One JSON string, escaped, straight into `out`. /// /// Public because generated code calls it. A codec that knows its field types From 3748b1eaa9b23863052f2998993bc7bbcd4565f2 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:29:06 +0200 Subject: [PATCH 02/20] CI round 1: standalone bench build, self-contained hardware snippets, guide snippets by include Co-Authored-By: Claude Opus 5.5 --- .github/workflows/backend-performance.yml | 2 + .../src/com/codename1/hardware/Hardware.java | 26 ++++++--- .../src/com/codename1/hardware/Watchdog.java | 8 ++- .../EmbeddedLinuxJava001Snippet.java | 55 +++++++++++++++++++ .../developer-guide/embedded-linux.properties | 6 ++ .../developer-guide/embedded-linux.sh | 6 ++ docs/developer-guide/Embedded-Linux.asciidoc | 18 ++---- .../com/codename1/maven/DeviceDeployer.java | 2 +- 8 files changed, 97 insertions(+), 26 deletions(-) create mode 100644 docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/EmbeddedLinuxJava001Snippet.java create mode 100644 docs/demos/common/src/main/snippets/developer-guide/embedded-linux.properties create mode 100644 docs/demos/common/src/main/snippets/developer-guide/embedded-linux.sh diff --git a/.github/workflows/backend-performance.yml b/.github/workflows/backend-performance.yml index 2e13b93d970..4a0b033a18d 100644 --- a/.github/workflows/backend-performance.yml +++ b/.github/workflows/backend-performance.yml @@ -76,6 +76,8 @@ jobs: JDK_8_HOME: ${{ env.JAVA_HOME }} CN1_BACKEND_DEMO: demo/bench CN1_BACKEND_ENGINE: docker + # The bench demo needs neither the shared contract nor demo/common. + CN1_BACKEND_STANDALONE_DEMO: "1" run: | mkdir -p "$RUNNER_TEMP/dist" ./package.sh Bench com.demo glibc-arm64 diff --git a/CodenameOne/src/com/codename1/hardware/Hardware.java b/CodenameOne/src/com/codename1/hardware/Hardware.java index 2d6947324a4..2bcdaa5eca5 100644 --- a/CodenameOne/src/com/codename1/hardware/Hardware.java +++ b/CodenameOne/src/com/codename1/hardware/Hardware.java @@ -35,16 +35,24 @@ /// /// ```java /// Hardware hw = Hardware.getInstance(); -/// if (hw.isSupported(Hardware.FEATURE_GPIO)) { -/// final GpioPin led = hw.openGpioOutput("gpiochip0", 17, false); -/// led.setHigh(true); +/// try { +/// if (hw.isSupported(Hardware.FEATURE_GPIO)) { +/// final GpioPin led = hw.openGpioOutput("gpiochip0", 17, false); +/// led.setHigh(true); /// -/// GpioPin button = hw.openGpioInput("gpiochip0", 27, GpioPin.BIAS_PULL_UP, GpioPin.EDGE_FALLING); -/// button.setListener(new GpioListener() { -/// public void edgeDetected(GpioPin pin, boolean rising, long timestampNanos) { -/// led.setHigh(!led.isHigh()); -/// } -/// }); +/// GpioPin button = hw.openGpioInput("gpiochip0", 27, GpioPin.BIAS_PULL_UP, GpioPin.EDGE_FALLING); +/// button.setListener(new GpioListener() { +/// public void edgeDetected(GpioPin pin, boolean rising, long timestampNanos) { +/// try { +/// led.setHigh(!led.isHigh()); +/// } catch (HardwareException e) { +/// Log.e(e); +/// } +/// } +/// }); +/// } +/// } catch (HardwareException e) { +/// Log.e(e); /// } /// ``` /// diff --git a/CodenameOne/src/com/codename1/hardware/Watchdog.java b/CodenameOne/src/com/codename1/hardware/Watchdog.java index 39596babb68..42c17e3576c 100644 --- a/CodenameOne/src/com/codename1/hardware/Watchdog.java +++ b/CodenameOne/src/com/codename1/hardware/Watchdog.java @@ -29,8 +29,12 @@ /// healthy, so a hung event dispatch thread causes a reset: /// /// ```java -/// final Watchdog wd = Hardware.getInstance().openWatchdog(15); -/// UITimer.timer(5000, true, form, () -> wd.kickQuietly()); +/// try { +/// final Watchdog wd = Hardware.getInstance().openWatchdog(15); +/// UITimer.timer(5000, true, Display.getInstance().getCurrent(), () -> wd.kickQuietly()); +/// } catch (HardwareException e) { +/// Log.e(e); +/// } /// ``` @com.codename1.impl.SharedWithBackend public final class Watchdog { diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/EmbeddedLinuxJava001Snippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/EmbeddedLinuxJava001Snippet.java new file mode 100644 index 00000000000..807e7b6d443 --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/EmbeddedLinuxJava001Snippet.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.snippets.generated; + +import com.codename1.hardware.*; +import com.codename1.io.Log; + +class EmbeddedLinuxJava001Snippet { + + void wireBoard() { + // tag::embedded-linux-java-001[] + Hardware hw = Hardware.getInstance(); + try { + if (hw.isSupported(Hardware.FEATURE_GPIO)) { + GpioPin led = hw.openGpioOutput("gpiochip0", 17, false); + led.setHigh(true); + GpioPin button = hw.openGpioInput("gpiochip0", 27, GpioPin.BIAS_PULL_UP, GpioPin.EDGE_FALLING); + button.setListener((pin, rising, nanos) -> { + try { + led.setHigh(!led.isHigh()); + } catch (HardwareException e) { + Log.e(e); + } + }); + } + if (hw.isSupported(Hardware.FEATURE_I2C)) { + I2cDevice sensor = hw.openI2c(1, 0x48); + Log.p("Sensor reads " + sensor.readRegister(0)); + } + } catch (HardwareException e) { + Log.e(e); + } + // end::embedded-linux-java-001[] + } +} diff --git a/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.properties b/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.properties new file mode 100644 index 00000000000..22ffbae639d --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.properties @@ -0,0 +1,6 @@ +// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline. + +// tag::embedded-linux-properties-001[] +codename1.arg.linux.embedded=true +codename1.arg.linux.embedded.board=rpi5 +// end::embedded-linux-properties-001[] diff --git a/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.sh b/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.sh new file mode 100644 index 00000000000..00cb8e068c6 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/embedded-linux.sh @@ -0,0 +1,6 @@ +// Generated from docs/developer-guide/Embedded-Linux.asciidoc source blocks. Edit the guide snippets here, not inline. + +// tag::embedded-linux-bash-001[] +mvn cn1:deploy-device -Dcn1.device.host=pi.local -Dcn1.device.user=pi +gradle cn1DeployDevice -Pcn1.device.host=pi.local -Pcn1.device.user=pi +// end::embedded-linux-bash-001[] diff --git a/docs/developer-guide/Embedded-Linux.asciidoc b/docs/developer-guide/Embedded-Linux.asciidoc index 093d84e35af..7e57ebf1011 100644 --- a/docs/developer-guide/Embedded-Linux.asciidoc +++ b/docs/developer-guide/Embedded-Linux.asciidoc @@ -86,8 +86,7 @@ the builder picks the architecture, the display defaults and the libraries to li [source,properties] ---- -codename1.arg.linux.embedded=true -codename1.arg.linux.embedded.board=rpi5 +include::../demos/common/src/main/snippets/developer-guide/embedded-linux.properties[tag=embedded-linux-properties-001,indent=0] ---- .Board profiles @@ -124,11 +123,10 @@ systemd service that starts at boot and restarts if it exits, and restarts it: [source,bash] ---- -mvn cn1:deploy-device -Dcn1.device.host=pi.local -Dcn1.device.user=pi -gradle cn1DeployDevice -Pcn1.device.host=pi.local -Pcn1.device.user=pi +include::../demos/common/src/main/snippets/developer-guide/embedded-linux.sh[tag=embedded-linux-bash-001,indent=0] ---- -It uses the system's `ssh` and `scp`, so keys, agents and `~/.ssh/config` aliases +It uses the system's `ssh`, so keys, agents and `~/.ssh/config` aliases work as they do in a terminal. `cn1.device.env` passes environment variables to the service (`CN1_ROTATION=90,CN1_DISPLAY=fbdev`), and `cn1.device.logs=true` follows its log after the restart. @@ -147,15 +145,7 @@ it reports no support, so the same code runs everywhere: [source,java] ---- -Hardware hw = Hardware.getInstance(); -if (hw.isSupported(Hardware.FEATURE_GPIO)) { - GpioPin led = hw.openGpioOutput("gpiochip0", 17, false); - led.setHigh(true); - GpioPin button = hw.openGpioInput("gpiochip0", 27, GpioPin.BIAS_PULL_UP, GpioPin.EDGE_FALLING); - button.setListener((pin, rising, nanos) -> led.setHigh(!led.isHigh())); -} -I2cDevice sensor = hw.openI2c(1, 0x48); -int raw = sensor.readRegister(0); +include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/EmbeddedLinuxJava001Snippet.java[tag=embedded-linux-java-001,indent=0] ---- Listeners run on the event dispatch thread. Reads that wait for data -- a serial diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java index bb0c438f0d0..b4d01ae67b4 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java @@ -37,7 +37,7 @@ /// systemd service: the Maven `deploy-device` goal and the Gradle /// `cn1DeployDevice` task are both this class. /// -/// It drives the system's own `ssh` and `scp`, so keys, agents, `~/.ssh/config` +/// It drives the system's own `ssh`, so keys, agents, `~/.ssh/config` /// host aliases and jump hosts work exactly as they do in a terminal; nothing /// here stores a credential. The binary goes to `remoteDir`, a unit named after /// the service is written to `/etc/systemd/system` and the service is From 099487571f6f292120e906d1a90c72014caf3f4e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:07:28 +0200 Subject: [PATCH 03/20] CI round 1: lazy embedded flag for JVM tests, backend chapter anchor Co-Authored-By: Claude Opus 5.5 --- .../impl/linux/LinuxImplementation.java | 24 ++++++++++++++----- docs/developer-guide/Embedded-Linux.asciidoc | 4 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java index 7e04273f831..1f37519968c 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java @@ -347,7 +347,19 @@ public static void setDefaultWindowSize(int width, int height) { /// True on the embedded target (DRM/KMS or fbdev, no window system). Fixed at /// build time: the native is a constant in each of the two display units. - static final boolean EMBEDDED = LinuxNative.isEmbeddedDisplay(); + /// The port's JVM unit tests run with no native library; they exercise the + /// desktop behaviour, so a missing native reads as "not embedded". + private static final class Embedded { + static final boolean VALUE = query(); + + private static boolean query() { + try { + return LinuxNative.isEmbeddedDisplay(); + } catch (UnsatisfiedLinkError notLinked) { + return false; + } + } + } @Override public void init(Object m) { @@ -2433,14 +2445,14 @@ public void getRGB(Object nativeImage, int[] arr, int offset, int x, int y, int */ @Override public boolean isNativeInputSupported() { - return !EMBEDDED; + return !Embedded.VALUE; } private final LinuxTextInput lowLevelTextInput = new LinuxTextInput(); @Override public boolean isTextInputSupported() { - return !EMBEDDED; + return !Embedded.VALUE; } @Override @@ -2472,7 +2484,7 @@ public void stopTextInput(Object handle) { */ @Override public boolean isAsyncEditMode() { - return !EMBEDDED; + return !Embedded.VALUE; } /** @@ -2485,7 +2497,7 @@ public boolean isAsyncEditMode() { */ @Override public void editString(final Component cmp, int maxSize, int constraint, String text, int initiatingKeycode) { - if (EMBEDDED) { + if (Embedded.VALUE) { LinuxEmbeddedEditor.edit(cmp, maxSize, constraint, text); return; } @@ -3837,7 +3849,7 @@ public String getPlatformName() { /// takes the mobile resource and theme layers. @Override public boolean isDesktop() { - return !EMBEDDED; + return !Embedded.VALUE; } diff --git a/docs/developer-guide/Embedded-Linux.asciidoc b/docs/developer-guide/Embedded-Linux.asciidoc index 7e57ebf1011..e31fbd03f9d 100644 --- a/docs/developer-guide/Embedded-Linux.asciidoc +++ b/docs/developer-guide/Embedded-Linux.asciidoc @@ -20,7 +20,7 @@ board attached. interpreted and nothing is compiled at start-up, so the first frame arrives in tens of milliseconds and memory use stays flat. * *Java or Kotlin for the whole product.* The device UI, the companion application - and, with the <<_backend,backend>>, the server or the headless gateway share one + and, with the <<_server_side_backend,backend>>, the server or the headless gateway share one language, one build and one set of classes. * *A renderer that owns every pixel.* Codename One draws its own components, so the user interface looks the same on a 3.5 inch SPI panel, a 10 inch HMI and a phone, @@ -159,7 +159,7 @@ therefore runs, and can be unit tested, before a board is on the desk. === Headless devices: the backend on a board A gateway, a sensor hub or a controller with no screen runs the -<<_backend,backend>> instead: a native REST and WebSocket server, built the same way +<<_server_side_backend,backend>> instead: a native REST and WebSocket server, built the same way and as small. `Hardware.getInstance()` works there too, through the same kernel code as the UI port, and listener callbacks run on the pin's own thread. The device then serves its API, the phone application is the client, and both share the entity From bd4c93d5c6f3a5042f7f4d8a8396bbcb08fa5bc4 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:09:18 +0200 Subject: [PATCH 04/20] Embedded round 2: GPU renderer, mDNS discovery, QR codes, CI fixes - Optional GLES2 compositor for the embedded port (CN1_EMBEDDED_GPU, the linux.embedded.renderer hint, board defaults), with the software raster as fallback and a CI check that both draw the same pixels. The software raster now applies a SOURCE/CLEAR operation alpha as coverage, as Cairo does. - Multicast DNS in Java (com.codename1.impl.MdnsEngine, shared with the backend) over a libc UDP socket (cn1_mdns_impl.h): Bonjour on the Linux ports with no Avahi, and com.codename1.backend.Discovery on the backend. Interoperates with Apple's mDNSResponder (dns-sd browses and resolves it). - com.codename1.qrcode: QR generation (all versions and levels), shared with the backend for SVG; QrCodeImage on the client. - Catch LinkageError in LinuxImplementation: the device class library has no UnsatisfiedLinkError, which broke every translated Linux build. - Print the allocation census before the exit walks, so a server's racing exit no longer loses it (backend-performance gate). - Developer guide prose fixes. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/embedded-linux.yml | 28 + .github/workflows/parparvm-tests.yml | 4 + .../src/com/codename1/impl/MdnsEngine.java | 1094 ++++++++++++++++ .../src/com/codename1/qrcode/QrCode.java | 538 ++++++++ .../src/com/codename1/qrcode/QrCodeImage.java | 60 + .../com/codename1/qrcode/package-info.java | 30 + .../nativeSources/cn1_linux_embedded.c | 95 +- Ports/LinuxPort/nativeSources/cn1_linux_gpu.c | 1158 +++++++++++++++++ Ports/LinuxPort/nativeSources/cn1_linux_gpu.h | 110 ++ .../LinuxPort/nativeSources/cn1_linux_mdns.c | 50 + .../nativeSources/cn1_linux_raster.c | 241 +++- .../nativeSources/cn1_linux_raster.h | 19 + .../impl/linux/LinuxBonjourPlatform.java | 94 ++ .../impl/linux/LinuxImplementation.java | 10 +- .../com/codename1/impl/linux/LinuxMdns.java | 56 + .../com/codename1/impl/linux/LinuxNative.java | 8 + .../backend/DiscoverySnippets.java | 51 + docs/developer-guide/Embedded-Linux.asciidoc | 48 +- .../Network-Connectivity.asciidoc | 7 + docs/developer-guide/embedded-benchmarks.adoc | 24 +- .../builders/EmbeddedBoardProfile.java | 47 +- .../builders/LinuxNativeBuilder.java | 11 +- .../builders/EmbeddedRendererTest.java | 77 ++ .../build/shared/BuildHintsDesktop.java | 10 + .../com/codename1/impl/MdnsEngineTest.java | 214 +++ .../java/com/codename1/qrcode/QrCodeTest.java | 105 ++ scripts/embedded/Containerfile.dev | 6 +- scripts/embedded/README.md | 5 + scripts/embedded/check-demo-run.py | 41 +- scripts/embedded/gpu-check/check-gpu.sh | 25 + scripts/embedded/gpu-check/gpu_check.c | 300 +++++ vm/ByteCodeTranslator/src/cn1_globals.m | 11 +- vm/ByteCodeTranslator/src/cn1_mdns_impl.h | 270 ++++ .../tools/translator/ByteCodeTranslator.java | 21 + vm/backend/build.sh | 2 +- .../com/codename1/backend/BackendMdns.java | 155 +++ .../com/codename1/backend/BackendMdns.java | 67 + vm/backend/native/cn1_backend_mdns.c | 49 + .../com/codename1/qrcode/package-info.java | 30 + .../src/com/codename1/backend/Discovery.java | 203 +++ 40 files changed, 5311 insertions(+), 63 deletions(-) create mode 100644 CodenameOne/src/com/codename1/impl/MdnsEngine.java create mode 100644 CodenameOne/src/com/codename1/qrcode/QrCode.java create mode 100644 CodenameOne/src/com/codename1/qrcode/QrCodeImage.java create mode 100644 CodenameOne/src/com/codename1/qrcode/package-info.java create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_gpu.c create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_gpu.h create mode 100644 Ports/LinuxPort/nativeSources/cn1_linux_mdns.c create mode 100644 Ports/LinuxPort/src/com/codename1/impl/linux/LinuxBonjourPlatform.java create mode 100644 Ports/LinuxPort/src/com/codename1/impl/linux/LinuxMdns.java create mode 100644 docs/demos/backend/src/main/java/com/codenameone/developerguide/backend/DiscoverySnippets.java create mode 100644 maven/build-engine/src/test/java/com/codename1/builders/EmbeddedRendererTest.java create mode 100644 maven/core-unittests/src/test/java/com/codename1/impl/MdnsEngineTest.java create mode 100644 maven/core-unittests/src/test/java/com/codename1/qrcode/QrCodeTest.java create mode 100755 scripts/embedded/gpu-check/check-gpu.sh create mode 100644 scripts/embedded/gpu-check/gpu_check.c create mode 100644 vm/ByteCodeTranslator/src/cn1_mdns_impl.h create mode 100644 vm/backend/impl/javase/com/codename1/backend/BackendMdns.java create mode 100644 vm/backend/impl/parparvm/com/codename1/backend/BackendMdns.java create mode 100644 vm/backend/native/cn1_backend_mdns.c create mode 100644 vm/backend/package-docs/com/codename1/qrcode/package-info.java create mode 100644 vm/backend/src/com/codename1/backend/Discovery.java diff --git a/.github/workflows/embedded-linux.yml b/.github/workflows/embedded-linux.yml index 8da6e2b4606..f53a68338d5 100644 --- a/.github/workflows/embedded-linux.yml +++ b/.github/workflows/embedded-linux.yml @@ -7,6 +7,12 @@ # or when the speed and memory the developer guide reports have slipped. The # generated stub file is checked against the natives too, since a native added to # LinuxNative without a stub breaks only the embedded link. +# +# The GPU renderer (CN1_EMBEDDED_GPU, GLES2 through GBM/EGL) runs on Mesa's +# llvmpipe, since the runner has no GPU: gpu-check draws every kind of operation +# both ways and fails on a differing pixel, and the demo is built a second time +# with the GPU renderer, whose frame 60 must match the software run's exactly. +# llvmpipe says nothing about a board's GPU speed; it holds correctness. name: Embedded Linux on: @@ -91,6 +97,28 @@ jobs: python3 scripts/embedded/check-demo-run.py "$B/result/demo.log" "$B/result/frame60.png" \ "$GITHUB_STEP_SUMMARY" + - name: The GPU renderer draws what the software one draws + run: scripts/embedded/gpu-check/check-gpu.sh arm64 "$RUNNER_TEMP/demo/result/gpu-check" + + # Same translation, rebuilt with the GPU renderer into the same build + # directory; the software run above has already been judged. + - name: Run the demo with the GPU renderer + env: + CN1_CMAKE_ARGS: -DCN1_EMBEDDED_GPU=ON + run: | + B="$RUNNER_TEMP/demo" + scripts/embedded/build-dist.sh "$B/out" arm64 + mkdir -p "$B/result/gpu" + docker run --rm -e LIBGL_ALWAYS_SOFTWARE=1 -v "$B/out/build-arm64:/cn1:ro" \ + -v "$B/result/gpu:/result" cn1-embedded-dev:arm64 \ + bash -c 'cd /tmp && CN1_DISPLAY=memory CN1_SIZE=800x480 CN1_BENCH_SECONDS=10 \ + CN1_SNAPSHOT=/result/frame60.png timeout 120 /cn1/EmbeddedDemo > /result/demo.log 2>&1' + grep -q "renderer gpu" "$B/result/gpu/demo.log" \ + || { echo "the GPU build did not render with the GPU"; tail -20 "$B/result/gpu/demo.log"; exit 1; } + python3 scripts/embedded/check-demo-run.py --max-rss-mb 160 \ + --reference "$B/result/frame60.png" "$B/result/gpu/demo.log" "$B/result/gpu/frame60.png" \ + "$GITHUB_STEP_SUMMARY" + - name: Upload the run if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index 63981a5d24c..fa94b5e628d 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -44,6 +44,8 @@ on: - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' - 'CodenameOne/src/com/codename1/hardware/**' + - 'CodenameOne/src/com/codename1/qrcode/QrCode.java' + - 'CodenameOne/src/com/codename1/impl/MdnsEngine.java' - 'CodenameOne/src/com/codename1/orm/session/**' - 'CodenameOne/src/com/codename1/orm/repository/**' - 'CodenameOne/src/com/codename1/annotations/db/**' @@ -109,6 +111,8 @@ on: - 'CodenameOne/src/com/codename1/impl/SharedWithBackend.java' - 'CodenameOne/src/com/codename1/impl/orm/**' - 'CodenameOne/src/com/codename1/hardware/**' + - 'CodenameOne/src/com/codename1/qrcode/QrCode.java' + - 'CodenameOne/src/com/codename1/impl/MdnsEngine.java' - 'CodenameOne/src/com/codename1/orm/session/**' - 'CodenameOne/src/com/codename1/orm/repository/**' - 'CodenameOne/src/com/codename1/annotations/db/**' diff --git a/CodenameOne/src/com/codename1/impl/MdnsEngine.java b/CodenameOne/src/com/codename1/impl/MdnsEngine.java new file mode 100644 index 00000000000..8374ea010b5 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/MdnsEngine.java @@ -0,0 +1,1094 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/// Multicast DNS service discovery (RFC 6762 and RFC 6763) written in Java over +/// a UDP socket, for platforms without a system responder to call: the Linux +/// port, embedded boards and the headless backend. Each supplies only the +/// `raw` socket primitives, so the protocol is one implementation everywhere. +/// +/// It publishes services (probing for a unique name, announcing, answering +/// queries, saying goodbye) and browses for them (querying with back-off, +/// resolving each instance's SRV, TXT and address, and reporting it lost on a +/// goodbye or when its records expire). It answers for the host's own name +/// too, so a client can reach `.local` without another responder. +/// +/// One thread, started with the first publish or browse, owns the socket and +/// every mutable structure. Callers on other threads only replace the +/// immutable arrays of what is published and browsed, so nothing is locked. +/// Listener callbacks run on that thread; the client platform hands them to +/// the EDT, the backend calls them as they are. +/// +/// Part of the framework's implementation, not intended for application use: +/// applications use `com.codename1.io.bonjour` or the backend's `Discovery`. +@SharedWithBackend +public abstract class MdnsEngine { + + /// Receives what a browse finds. Called on the engine's thread. + public interface Listener { + /// An instance of the browsed type was resolved, or its address, port + /// or TXT record changed. + void resolved(Object browse, String name, String type, String host, int port, + Map txt); + + /// An instance said goodbye or its records expired. + void lost(Object browse, String name, String type); + + /// The engine could not start or its socket failed. + void failed(Object browse, String message); + } + + static final int TYPE_A = 1; + static final int TYPE_PTR = 12; + static final int TYPE_TXT = 16; + static final int TYPE_SRV = 33; + static final int TYPE_ANY = 255; + private static final int CLASS_IN = 1; + private static final int CACHE_FLUSH = 0x8000; + private static final int TTL_HOST = 120; + private static final int TTL_OTHER = 4500; + private static final int MAX_PACKET = 9000; + private static final String SERVICES = "_services._dns-sd._udp.local"; + + private final AtomicReference services = new AtomicReference(new Service[0]); + private final AtomicReference browses = new AtomicReference(new Browse[0]); + private final AtomicReference thread = new AtomicReference(); + + // Everything below belongs to the engine thread. + private int fd = -1; + private String hostName; + private byte[][] addresses = new byte[0][]; + private final ArrayList live = new ArrayList(); + private final ArrayList watching = new ArrayList(); + private final byte[] in = new byte[MAX_PACKET]; + + /// Opens the multicast socket: UDP port 5353, joined to 224.0.0.251 on + /// every IPv4 interface. Answers a handle >= 0, or a negative error. + protected abstract int rawOpen(); + + /// Sends a packet to 224.0.0.251:5353 on every interface. Answers >= 0 or a + /// negative error. + protected abstract int rawSend(int handle, byte[] packet, int length); + + /// Waits up to timeoutMs for a packet. Answers its length, 0 on a timeout, + /// or a negative error. + protected abstract int rawReceive(int handle, byte[] buffer, int timeoutMs); + + /// The host name without a domain, then each IPv4 address of an interface + /// that is up, not loopback and multicast capable, one per line. + protected abstract String rawHostInfo(); + + /// Closes the socket. + protected abstract void rawClose(int handle); + + /// Milliseconds on a clock that does not jump; `System.currentTimeMillis` + /// by default. Tests replace it. + protected long now() { + return System.currentTimeMillis(); + } + + // ------------------------------------------------------------------ API + + /// Publishes a service; answers a handle for [#unpublish(Object)]. The + /// type is `_name._tcp` or `_name._udp`, with or without `.local.`. A name + /// another host already uses gets a numeric suffix, as Bonjour does. + public Object publish(String name, String type, int port, Map txt) { + Service s = new Service(name, normalizeType(type), port, copy(txt)); + Service[] cur; + Service[] next; + do { + cur = services.get(); + next = new Service[cur.length + 1]; + System.arraycopy(cur, 0, next, 0, cur.length); + next[cur.length] = s; + } while (!services.compareAndSet(cur, next)); + start(); + return s; + } + + /// Withdraws a published service; its goodbye is sent at once. + public void unpublish(Object handle) { + Service[] cur; + Service[] next; + do { + cur = services.get(); + int i = indexOf(cur, handle); + if (i < 0) { + return; + } + next = new Service[cur.length - 1]; + System.arraycopy(cur, 0, next, 0, i); + System.arraycopy(cur, i + 1, next, i, next.length - i); + } while (!services.compareAndSet(cur, next)); + } + + /// Starts browsing for a type; answers a handle for [#stopBrowse(Object)]. + public Object browse(String type, Listener listener) { + Browse b = new Browse(normalizeType(type), listener); + Browse[] cur; + Browse[] next; + do { + cur = browses.get(); + next = new Browse[cur.length + 1]; + System.arraycopy(cur, 0, next, 0, cur.length); + next[cur.length] = b; + } while (!browses.compareAndSet(cur, next)); + start(); + return b; + } + + /// Stops a browse; its listener is not called again. + public void stopBrowse(Object handle) { + Browse[] cur; + Browse[] next; + do { + cur = browses.get(); + int i = indexOf(cur, handle); + if (i < 0) { + return; + } + next = new Browse[cur.length - 1]; + System.arraycopy(cur, 0, next, 0, i); + System.arraycopy(cur, i + 1, next, i, next.length - i); + } while (!browses.compareAndSet(cur, next)); + } + + /// The name a published service ended up with, which differs from the one + /// asked for when another host had it. Null for an unknown handle. + public String publishedName(Object handle) { + return handle instanceof Service ? ((Service) handle).currentName.get() : null; + } + + // Arrays are built by hand throughout: toArray(T[]) creates its result + // reflectively, which ParparVM cannot do for an arbitrary component type. + private static int indexOf(Object[] cur, Object item) { + for (int i = 0; i < cur.length; i++) { + if (cur[i] == item) { // NOPMD CompareObjectsWithEquals - a handle is found by identity + return i; + } + } + return -1; + } + + private static Map copy(Map txt) { + HashMap out = new HashMap(); + if (txt != null) { + out.putAll(txt); + } + return out; + } + + /// `_http._tcp.` or `_http._tcp.local.` to `_http._tcp.local`. + static String normalizeType(String type) { + String t = type == null ? "" : type.trim(); + while (t.endsWith(".")) { + t = t.substring(0, t.length() - 1); + } + if (!t.regionMatches(true, Math.max(0, t.length() - 6), ".local", 0, 6)) { + t = t + ".local"; + } + return t; + } + + private void start() { + if (thread.get() != null) { + return; + } + Thread t = new Thread(new Runnable() { + @Override + public void run() { + loop(); + } + }, "mDNS"); + // Discovery never keeps a process alive by itself. + t.setDaemon(true); + if (thread.compareAndSet(null, t)) { + t.start(); + } + } + + // --------------------------------------------------------------- engine + + private void loop() { + fd = rawOpen(); + if (fd < 0) { + fail("mDNS socket could not be opened (error " + (-fd) + ")"); + return; + } + readHostInfo(); + long lastHostInfo = now(); + while (true) { + long t = now(); + if (t - lastHostInfo > 30000) { + // Addresses come and go (DHCP, a cable); re-read them now and then. + readHostInfo(); + lastHostInfo = t; + } + syncServices(t); + syncBrowses(t); + if (live.isEmpty() && watching.isEmpty() && services.get().length == 0 + && browses.get().length == 0) { + // Nothing left to do; a later publish or browse starts a new thread. + rawClose(fd); + fd = -1; + thread.set(null); + if (services.get().length == 0 && browses.get().length == 0) { + return; + } + start(); + return; + } + long next = t + 1000; + for (Service s : live) { + next = Math.min(next, s.due); + } + for (Browse b : watching) { + next = Math.min(next, b.nextQuery); + } + int wait = (int) Math.max(20, Math.min(1000, next - now())); + int n = rawReceive(fd, in, wait); + if (n < 0) { + fail("mDNS receive failed (error " + (-n) + ")"); + rawClose(fd); + fd = -1; + thread.set(null); + return; + } + if (n > 0) { + handlePacket(in, n); + } + } + } + + private void fail(String message) { + Browse[] b = browses.get(); + for (Browse each : b) { + each.listener.failed(each, message); + } + } + + private void readHostInfo() { + String info = rawHostInfo(); + ArrayList list = new ArrayList(); + String host = null; + if (info != null) { + int start = 0; + for (int i = 0; i <= info.length(); i++) { + if (i == info.length() || info.charAt(i) == '\n') { + String line = info.substring(start, i).trim(); + start = i + 1; + if (line.length() == 0) { + continue; + } + if (host == null) { + host = line; + } else { + byte[] a = parseIpv4(line); + if (a != null) { + list.add(a); + } + } + } + } + } + if (host == null || host.length() == 0) { + host = "codenameone"; + } + int dot = host.indexOf('.'); + if (dot > 0) { + host = host.substring(0, dot); + } + hostName = host + ".local"; + byte[][] out = new byte[list.size()][]; + for (int i = 0; i < out.length; i++) { + out[i] = list.get(i); + } + addresses = out; + } + + static byte[] parseIpv4(String s) { + byte[] out = new byte[4]; + int part = 0; + int value = -1; + for (int i = 0; i <= s.length(); i++) { + char c = i == s.length() ? '.' : s.charAt(i); + if (c == '.') { + if (value < 0 || value > 255 || part > 3) { + return null; + } + out[part++] = (byte) value; + value = -1; + } else if (c >= '0' && c <= '9') { + value = (value < 0 ? 0 : value * 10) + (c - '0'); + } else { + return null; + } + } + return part == 4 ? out : null; + } + + // ------------------------------------------------------------ publishing + + private void syncServices(long t) { + Service[] want = services.get(); + // Withdrawn services say goodbye. + for (Iterator it = live.iterator(); it.hasNext();) { + Service s = it.next(); + if (!contains(want, s)) { + it.remove(); + if (s.state >= Service.ANNOUNCING) { + Packet p = new Packet(true); + addServiceRecords(p, s, 0); + send(p); + } + } + } + for (int i = 0; i < want.length; i++) { + if (!live.contains(want[i])) { + want[i].due = t; + live.add(want[i]); + } + } + for (Service s : live) { + if (s.due > t) { + continue; + } + if (s.state == Service.PROBING) { + if (s.step < 3) { + // A probe asks who owns the name and proposes our records. + Packet p = new Packet(false); + p.question(s.instance(), TYPE_ANY, true); + p.section = AUTHORITY; + p.srv(s.instance(), TTL_HOST, s.port, hostName, false); + send(p); + s.step++; + s.due = t + 250; + } else { + s.state = Service.ANNOUNCING; + s.step = 0; + s.due = t; + } + } + if (s.state == Service.ANNOUNCING && s.due <= t) { + Packet p = new Packet(true); + addServiceRecords(p, s, -1); + send(p); + s.step++; + if (s.step >= 2) { + s.state = Service.LIVE; + s.due = Long.MAX_VALUE; + } else { + s.due = t + 1000; + } + } + } + } + + private static boolean contains(Object[] a, Object o) { + for (Object each : a) { + if (each == o) { // NOPMD CompareObjectsWithEquals - membership by identity + return true; + } + } + return false; + } + + /// The PTR, SRV and TXT records of a service and the host's addresses; ttl + /// -1 means each record's usual TTL, 0 a goodbye. + private void addServiceRecords(Packet p, Service s, int ttl) { + p.ptr(s.type, ttl < 0 ? TTL_OTHER : ttl, s.instance()); + p.ptr(SERVICES, ttl < 0 ? TTL_OTHER : ttl, s.type); + p.srv(s.instance(), ttl < 0 ? TTL_HOST : ttl, s.port, hostName, true); + p.txt(s.instance(), ttl < 0 ? TTL_OTHER : ttl, s.txt); + if (ttl != 0) { + addAddresses(p); + } + } + + private void addAddresses(Packet p) { + for (byte[] address : addresses) { + p.a(hostName, TTL_HOST, address); + } + } + + private void answer(String name, int type) { + Packet p = new Packet(true); + boolean any = type == TYPE_ANY; + for (Service s : live) { + if (s.state == Service.PROBING) { + continue; + } + if ((type == TYPE_PTR || any) && name.equalsIgnoreCase(s.type)) { + p.section = ANSWER; + p.ptr(s.type, TTL_OTHER, s.instance()); + // The rest of what a browser needs travels as additionals. + p.section = ADDITIONAL; + p.srv(s.instance(), TTL_HOST, s.port, hostName, true); + p.txt(s.instance(), TTL_OTHER, s.txt); + addAddresses(p); + } else if ((type == TYPE_PTR || any) && SERVICES.equalsIgnoreCase(name)) { + p.section = ANSWER; + p.ptr(SERVICES, TTL_OTHER, s.type); + } else if (name.equalsIgnoreCase(s.instance())) { + p.section = ANSWER; + if (type == TYPE_SRV || any) { + p.srv(s.instance(), TTL_HOST, s.port, hostName, true); + } + if (type == TYPE_TXT || any) { + p.txt(s.instance(), TTL_OTHER, s.txt); + } + p.section = ADDITIONAL; + addAddresses(p); + } + } + if ((type == TYPE_A || any) && name.equalsIgnoreCase(hostName) && !live.isEmpty()) { + p.section = ANSWER; + addAddresses(p); + } + if (p.answers() > 0) { + send(p); + } + } + + private void send(Packet p) { + if (fd >= 0) { + byte[] bytes = p.toBytes(); + rawSend(fd, bytes, bytes.length); + } + } + + // ------------------------------------------------------------- browsing + + private void syncBrowses(long t) { + Browse[] want = browses.get(); + for (Iterator it = watching.iterator(); it.hasNext();) { + if (!contains(want, it.next())) { + it.remove(); + } + } + for (int i = 0; i < want.length; i++) { + if (!watching.contains(want[i])) { + want[i].nextQuery = t; + watching.add(want[i]); + } + } + for (Browse b : watching) { + // Expired instances are lost; incomplete ones are asked about again. + for (Iterator it = b.instances.values().iterator(); it.hasNext();) { + Instance inst = it.next(); + if (inst.expires <= t) { + it.remove(); + if (inst.reported) { + b.listener.lost(b, inst.label, displayType(b.type)); + } + } else if (!inst.complete() && inst.nextAsk <= t) { + Packet p = new Packet(false); + if (inst.target == null) { + p.question(inst.fullName, TYPE_SRV, false); + } + if (inst.txt == null) { + p.question(inst.fullName, TYPE_TXT, false); + } + if (inst.target != null && inst.host == null) { + p.question(inst.target, TYPE_A, false); + } + send(p); + inst.nextAsk = t + 1000; + } + } + if (b.nextQuery <= t) { + Packet p = new Packet(false); + p.question(b.type, TYPE_PTR, false); + send(p); + b.interval = b.interval == 0 ? 1000 : Math.min(b.interval * 2, 60000); + b.nextQuery = t + b.interval; + } + } + } + + private static String displayType(String type) { + // _http._tcp.local to the _http._tcp. form BonjourService reports. + return type.substring(0, type.length() - 5); + } + + // -------------------------------------------------------------- packets + + private void handlePacket(byte[] data, int length) { + Reader r = new Reader(data, length); + ArrayList records = new ArrayList(); + try { + r.pos = 2; + int flags = r.u16(); + int qd = r.u16(); + int an = r.u16(); + int ns = r.u16(); + int ar = r.u16(); + boolean response = (flags & 0x8000) != 0; + for (int i = 0; i < qd; i++) { + String name = r.name(); + int type = r.u16(); + r.u16(); + if (!response) { + answer(name, type); + } + } + if (!response) { + // A probe's authority section is the other host's claim. + for (int i = 0; i < ns; i++) { + Record rec = r.record(); + if (rec != null) { + checkConflict(rec, true); + } + } + return; + } + int total = an + ns + ar; + for (int i = 0; i < total; i++) { + Record rec = r.record(); + if (rec != null) { + records.add(rec); + } + } + } catch (RuntimeException malformed) { + // A truncated or malformed packet from the network is ignored. + return; + } + // Acted on outside the try, so nothing here relies on the catch. + for (Record rec : records) { + checkConflict(rec, false); + } + absorb(records); + } + + /// Another host claiming the name one of our services is probing for, or + /// already uses, renames ours: "Name (2)", then "(3)" and so on. + private void checkConflict(Record rec, boolean probe) { + if (rec.type != TYPE_SRV && rec.type != TYPE_TXT) { + return; + } + for (Service s : live) { + if (!rec.name.equalsIgnoreCase(s.instance())) { + continue; + } + if (rec.type == TYPE_SRV && rec.port == s.port && hostName.equalsIgnoreCase(rec.target)) { + // Our own packet, looped back. + continue; + } + if (rec.type == TYPE_TXT) { + continue; + } + // Two hosts probing for one name at once: the lexicographically later + // claim keeps it (RFC 6762 8.2, compared here on port then target), + // so exactly one of them renames rather than both, forever. + boolean lose = !probe || (s.state == Service.PROBING && (rec.port > s.port + || (rec.port == s.port && asciiLower(rec.target).compareTo(asciiLower(hostName)) > 0))); + if (lose) { + s.suffix++; + s.currentName.set(s.baseName + " (" + (s.suffix + 1) + ")"); + s.state = Service.PROBING; + s.step = 0; + s.due = now() + 250; + } + } + } + + private void absorb(ArrayList records) { + long t = now(); + for (Browse b : watching) { + for (Record rec : records) { + if (rec.type == TYPE_PTR && rec.name.equalsIgnoreCase(b.type)) { + String key = asciiLower(rec.target); + Instance inst = b.instances.get(key); + if (rec.ttl == 0) { + if (inst != null) { + // A goodbye: gone in a second unless re-announced (RFC 6762 10.1). + inst.expires = Math.min(inst.expires, t + 1000); + } + continue; + } + if (inst == null) { + inst = new Instance(rec.target, label(rec.target, b.type)); + b.instances.put(key, inst); + } + inst.expires = t + rec.ttl * 1000L; + } + } + for (Instance inst : b.instances.values()) { + boolean changed = false; + for (Record rec : records) { + if (rec.type == TYPE_SRV && rec.name.equalsIgnoreCase(inst.fullName) && rec.ttl > 0) { + if (inst.port != rec.port || !rec.target.equalsIgnoreCase(inst.target)) { + inst.port = rec.port; + inst.target = rec.target; + inst.host = null; + changed = true; + } + } else if (rec.type == TYPE_TXT && rec.name.equalsIgnoreCase(inst.fullName) && rec.ttl > 0) { + if (inst.txt == null || !inst.txt.equals(rec.txt)) { + inst.txt = rec.txt; + changed = true; + } + } + } + if (inst.target != null) { + for (Record rec : records) { + if (rec.type == TYPE_A && rec.name.equalsIgnoreCase(inst.target) && rec.ttl > 0) { + if (!rec.address.equals(inst.host)) { + inst.host = rec.address; + changed = true; + } + break; + } + } + } + if (inst.complete() && (changed || !inst.reported)) { + inst.reported = true; + b.listener.resolved(b, inst.label, displayType(b.type), inst.host, inst.port, + new HashMap(inst.txt)); + } + } + } + } + + /// The instance label of `My Device._http._tcp.local`. + private static String label(String fullName, String type) { + int end = fullName.length() - type.length() - 1; + return end > 0 ? fullName.substring(0, end) : fullName; + } + + /// ASCII-only lower case: `toLowerCase` folds by locale, and a DNS name is + /// compared without regard to ASCII case only (RFC 4343). + static String asciiLower(String s) { + char[] c = s.toCharArray(); + for (int i = 0; i < c.length; i++) { + if (c[i] >= 'A' && c[i] <= 'Z') { + c[i] = (char) (c[i] + 32); + } + } + return new String(c); + } + + // ---------------------------------------------------------------- model + + static final class Service { + static final int PROBING = 0; + static final int ANNOUNCING = 1; + static final int LIVE = 2; + + final String baseName; + final AtomicReference currentName; + final String type; + final int port; + final Map txt; + int state; + int step; + int suffix; + long due; + + Service(String name, String type, int port, Map txt) { + String n = name == null || name.length() == 0 ? "Codename One" : name; + // A label is at most 63 bytes; leave room for a " (n)" suffix. + while (utf8(n).length > 56) { + n = n.substring(0, n.length() - 1); + } + this.baseName = n; + this.currentName = new AtomicReference(n); + this.type = type; + this.port = port; + this.txt = txt; + } + + String instance() { + return currentName.get() + "." + type; + } + } + + static final class Browse { + final String type; + final Listener listener; + final HashMap instances = new HashMap(); + long nextQuery; + long interval; + + Browse(String type, Listener listener) { + this.type = type; + this.listener = listener; + } + } + + static final class Instance { + final String fullName; + final String label; + String target; + int port; + String host; + Map txt; + long expires; + long nextAsk; + boolean reported; + + Instance(String fullName, String label) { + this.fullName = fullName; + this.label = label; + } + + boolean complete() { + return target != null && host != null && txt != null; + } + } + + static final class Record { + String name; + int type; + long ttl; + // Empty rather than null for a record of another type, so a match on + // the type is all a reader checks. + String target = ""; + int port; + String address = ""; + Map txt; + } + + // ------------------------------------------------------------- encoding + + static byte[] utf8(String s) { + try { + return s.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // Every runtime has UTF-8; this cannot happen. + throw new IllegalStateException("UTF-8", e); + } + } + + static String utf8(byte[] b, int off, int len) { + try { + return new String(b, off, len, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8", e); + } + } + + /// Splits a name into labels. The first label of a service instance may + /// contain dots, so instance names are split after it by the caller. + private static void writeName(Writer w, String name) { + String rest = name; + // An instance name is "