diff --git a/.agent/evidence/skia-generated-image.jsonl b/.agent/evidence/skia-generated-image.jsonl new file mode 100644 index 0000000000..55e3f73210 --- /dev/null +++ b/.agent/evidence/skia-generated-image.jsonl @@ -0,0 +1,23 @@ +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T02:44:00-03:00","revision":"894febba7dd1c188b91f2af938028b5274126a19","milestone":"0","target":"attachment","command":"curl -fL --retry 2 --connect-timeout 20 --max-time 120 https://github.com/user-attachments/files/30019877/Tcsort.zip -o /tmp/Tcsort.zip; sha256sum /tmp/Tcsort.zip; unzip -l /tmp/Tcsort.zip","status":"pass","archiveSha256":"de4df098fc00ff35b419ff616d3a0172f4eb2e17ae7c72b8808a477037c8b641","archiveEntries":[{"name":"Tcsort.apk","length":22480377},{"name":"tcsort.java","length":1770}],"artifact":"/tmp/Tcsort.zip","limitation":"The attachment and extracted files remain outside tracked source under /tmp."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T02:44:00-03:00","revision":"894febba7dd1c188b91f2af938028b5274126a19","milestone":"0","target":"source-inspection","command":"unzip -q /tmp/Tcsort.zip -d /tmp/tcsort-inspect.CD298m; nl -ba /tmp/tcsort-inspect.CD298m/tcsort.java; aapt dump badging /tmp/tcsort-inspect.CD298m/Tcsort.apk","status":"pass","entryPoint":"Tcsort.initUI -> testGraphics","imageType":"totalcross.io.device.printer.MonoImage","dimensions":"576x576","drawing":"fillRect(0, y, 576, 576) with Color.WHITE; drawRect(10, y + 10, 556, 556) with Color.BLACK; inherited Control.y is zero for this new MainWindow","output":"Settings.appPath/nome.png via Image.createPng(File)","launch":"Vm.exec(\"viewer\", path, 0, true)","package":"totalcross.appphdb","activity":"totalcross.appphdb.Loader","expectedPixels":{"background":"opaque white","border":"opaque black at inset 10","interior":"opaque white"}} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T02:44:00-03:00","revision":"894febba7dd1c188b91f2af938028b5274126a19","milestone":"0","target":"android-baseline","command":"adb install -r /tmp/tcsort-inspect.CD298m/Tcsort.apk; adb shell am start -n totalcross.appphdb/totalcross.appphdb.Loader; sleep 8; adb pull /data/user/0/totalcross.appphdb/nome.png /tmp/tcsort-baseline-nome.png; ffmpeg -v error -i /tmp/tcsort-baseline-nome.png -f rawvideo -pix_fmt rgba /tmp/tcsort-baseline-nome.rgba","status":"expected-failure","device":"emulator-5554","assertions":1,"assertion":"decoded RGBA stream has zero non-zero bytes","observed":"PNG 576x576 RGBA; nonzero_bytes=0; output is fully transparent/blank","artifacts":["/tmp/tcsort-baseline-nome.png","/tmp/tcsort-baseline-nome.rgba","/tmp/tcsort-baseline-logcat.log","/tmp/tcsort-baseline-screen.png"],"artifactHashes":{"png":"f73f4977d1a7f2640681b824dc820c657a33886ab941d6d567cd363d7c4d631","screen":"1d1c2a10355f735782a3766b824d89cc9562468f0b822301468cd01ec6439f20"},"limitation":"The baseline was reproduced on the connected emulator, not on a physical Android device."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T02:55:00-03:00","revision":"9bf9faeb31eaa72933676ab96fe6f38bdd7581d2","milestone":"1","target":"source-relocation","command":"git mv TotalCrossVM/src/nm/ui/android/skia.h TotalCrossVM/src/nm/ui/skia/skia.h; git mv TotalCrossVM/src/nm/ui/android/skia.cpp TotalCrossVM/src/nm/ui/skia/skia.cpp; update CMake and native include paths; git diff --check","status":"pass","changedPaths":["TotalCrossVM/CMakeLists.txt","TotalCrossVM/src/nm/ui/GraphicsPrimitives.h","TotalCrossVM/src/nm/ui/PalmFont_c.h","TotalCrossVM/src/nm/ui/android/gfx_Graphics_c.h","TotalCrossVM/src/nm/ui/font_Font.c","TotalCrossVM/src/nm/ui/image_Image.c","TotalCrossVM/src/nm/ui/linux/gfx_Graphics_c.h","TotalCrossVM/src/nm/ui/skia/skia.h","TotalCrossVM/src/nm/ui/skia/skia.cpp"],"assertions":2,"assertionsDetail":["CMake references nm/ui/skia/skia.cpp","tracked native includes reference skia/skia.h or the correct relative path"],"limitation":"The structural CMake/Ninja build is deferred until the remaining source split is complete; the ignored generated tcvm.xcodeproj was not changed."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:03:18-03:00","revision":"WORKTREE_AFTER_24ca5c87194d309f017de0b469d14a2923ad2165","milestone":"1","target":"source-split","command":"update CMake with skia_surface.cpp and skia_primitives.cpp; extract skia_internal.h, skia_surface.cpp, skia_primitives.cpp and GraphicsPrimitives{Skia,Text,Shapes,Screen}_c.h; preserve original include positions; git diff --check","status":"pass","changedPaths":["TotalCrossVM/src/nm/ui/skia/skia_internal.h","TotalCrossVM/src/nm/ui/skia/skia_surface.cpp","TotalCrossVM/src/nm/ui/skia/skia_primitives.cpp","TotalCrossVM/src/nm/ui/GraphicsPrimitivesSkia_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitivesText_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitivesShapes_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitivesScreen_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h"],"assertions":3,"assertionsDetail":["no skia source remains under TotalCrossVM/src/nm/ui/android","skia.cpp is 350 lines and surface/primitives units are 167/255 lines","CMake lists all three Skia translation units"],"limitation":"The fragments are included at their original positions to preserve C preprocessor order; no behavior change was intended."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:03:18-03:00","revision":"WORKTREE_AFTER_24ca5c87194d309f017de0b469d14a2923ad2165","milestone":"1","target":"native-structure-build","command":"cmake -S TotalCrossVM -B build-skia-structure -DCMAKE_BUILD_TYPE=Release -G Ninja; ninja -C build-skia-structure","status":"pass","assertions":2,"observed":"CMake configured successfully and Ninja linked build-skia-structure/libtcvm.dylib; final Ninja run completed with only duplicate-library linker warnings","logs":["/tmp/skia-m1-cmake.log","/tmp/skia-m1-ninja-rerun4.log"],"limitation":"This is a macOS arm64 structural build; it does not validate Android deployment or bitmap authority."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:03:18-03:00","revision":"WORKTREE_AFTER_24ca5c87194d309f017de0b469d14a2923ad2165","milestone":"1","target":"sdk-build","command":"cd TotalCrossSDK && ./gradlew-agent clean dist --warning-mode=none --console=plain","status":"pass","assertions":1,"observed":"BUILD SUCCESSFUL in 1m 5s; 22 actionable tasks, 20 executed, 2 up-to-date, 0 failed tasks","log":"TotalCrossSDK/agent-logs/20260723-030119-clean-agent.log","limitation":"The build emitted existing Java compilation and Javadoc warnings summarized by Gradle Agent."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:03:18-03:00","revision":"WORKTREE_AFTER_24ca5c87194d309f017de0b469d14a2923ad2165","milestone":"1","target":"registered-tests","command":"ctest --test-dir build-skia-structure --output-on-failure","status":"pass-no-tests","assertions":0,"observed":"CTest returned 0 and reported No tests were found!!!","limitation":"The repository does not register focused graphics tests in this structural CMake build."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:20:00-03:00","revision":"6c840cd41","milestone":"2","target":"surface-ownership","command":"replace bitmap vector with std::vector>; add checked skiaGetCanvas/skiaGetBitmap; commit 6c840cd41","status":"pass","assertions":4,"assertionsDetail":["screen ID is -1","image IDs are stable zero-based slots","bitmap and canvas are replaced/destroyed together","deleted and invalid image slots resolve to null"],"changedPaths":["TotalCrossVM/src/nm/ui/skia/skia.h","TotalCrossVM/src/nm/ui/skia/skia_internal.h","TotalCrossVM/src/nm/ui/skia/skia.cpp","TotalCrossVM/src/nm/ui/skia/skia_surface.cpp"]} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:20:00-03:00","revision":"2a8e8c194","milestone":"2","target":"surface-routing","command":"route clipping, pixels, image drawing and primitives through skiaSurfaceForGraphics; restore active graphics fragments; commit 2a8e8c194","status":"pass","assertions":3,"assertionsDetail":["screen and image rendering paths use selected canvas","GraphicsPrimitivesSkia_c.h obtains or creates image target surfaces","fragmented C compilation model remains valid"],"changedPaths":["TotalCrossVM/src/nm/ui/skia/skia_primitives.cpp","TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitivesSkia_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitivesText_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitivesShapes_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitivesScreen_c.h"]} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:20:00-03:00","revision":"2a8e8c194","milestone":"2","target":"native-surface-build","command":"ninja -C build-skia-structure","status":"pass","assertions":1,"observed":"C and C++ graphics sources compiled and libtcvm.dylib linked; only existing warnings and duplicate-library linker warnings remained","limitation":"macOS arm64 native build only; no later milestone deployment validation was run."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:20:00-03:00","revision":"2a8e8c194","milestone":"2","target":"surface-invariant-probe","command":"temporary HEADLESS C++ probe linked against build-skia-structure/libtcvm.dylib","status":"pass","assertions":8,"assertionsDetail":["screen resolves at -1","first and second image surfaces resolve at 0 and 1","image drawing leaves screen unchanged","clipping affects selected image canvas","deleted surface has no canvas or bitmap","drawing on deleted and invalid IDs is safe","recreating a deleted slot is rejected"],"artifact":"/tmp/skia-surface-probe","limitation":"The probe is temporary and outside the repository; the initial version had an incorrect zero-color expectation and was not used as final evidence."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:32:44-03:00","revision":"14082b5d1","milestone":"3","target":"bitmap-authority","command":"implement guarded promotion, Skia pixel/RGB/row access, explicit PixelConv color conversion, and bitmap-authoritative applyChanges; commit 14082b5d1","status":"pass","assertions":6,"assertionsDetail":["existing texture IDs are not reuploaded from Image.pixels","getPixelRow reads canvas content","getPixel and getRGB use explicit conversion","multi-frame row width and offsets are preserved","array-only transforms remain outside the affected path","native sources compile with the authority changes"],"changedPaths":["TotalCrossVM/src/nm/ui/image_Image.c","TotalCrossVM/src/nm/ui/ImagePrimitives_c.h","TotalCrossVM/src/nm/ui/skia/skia.h","TotalCrossVM/src/nm/ui/skia/skia_internal.h","TotalCrossVM/src/nm/ui/skia/skia_surface.cpp","TotalCrossVM/src/nm/ui/skia/skia_primitives.cpp"]} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:32:44-03:00","revision":"5973bd890","milestone":"3","target":"image-drawing-authority","command":"route active Skia drawSurface image-to-image operations through bitmap/canvas surfaces and remove the active Java-array CPU-copy path; commit 5973bd890","status":"pass","assertions":5,"assertionsDetail":["draw source image into another image","draw source image after mutating its original Java array without stale reupload","target retains authoritative Skia content","delete and recreate a surface without stale content","screen and image targets remain distinct"],"changedPaths":["TotalCrossVM/src/nm/ui/GraphicsPrimitivesSkia_c.h","TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h"],"artifact":"/tmp/skia-authority-probe","limitation":"The probe is temporary and outside the repository; output encoding and cross-platform deployment remain deferred to later milestones."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:32:44-03:00","revision":"WORKTREE_AFTER_5973bd890","milestone":"3","target":"native-authority-validation","command":"git diff --check; ninja -C build-skia-structure; /tmp/skia-authority-probe; /tmp/skia-surface-probe","status":"pass","assertions":2,"assertionsDetail":["native build linked build-skia-structure/libtcvm.dylib","authority and surface invariant probes passed"],"logs":["/tmp/skia-m3-ninja.log"],"artifacts":["/tmp/skia-authority-probe","/tmp/skia-surface-probe"],"limitation":"No SDK, Java SE, Android/macOS deployment, smoke test, or later-milestone validation was run."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:43:31-03:00","revision":"3c20258f9","milestone":"4","target":"issue-417-smoke-source","command":"add tests/smoke/issue-417-generated-image/Tcsort.java with the exact MonoImage/getGraphics/fill/draw/createPng flow and commit 3c20258f9","status":"pass","assertions":4,"assertionsDetail":["source preserves MonoImage 576x576","source preserves white fill and black inset border","source emits nome.png and issue-417-result.json","source does not require the original ZIP after commit"],"changedPaths":["tests/smoke/issue-417-generated-image/Tcsort.java","tests/smoke/issue-417-generated-image/README.md"]} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:43:31-03:00","revision":"WORKTREE_AFTER_3c20258f9","milestone":"4","target":"issue-417-smoke-validation","command":"javac -Xlint:none -cp TotalCrossSDK/build/libs/totalcross-sdk-7.2.2.jar:TotalCrossSDK/build/libs/tcui-7.2.2.jar -d tests/smoke/issue-417-generated-image/Tcsort.java; python3 -m py_compile tests/smoke/issue-417-generated-image/check_result.py; git diff --check","status":"pass","assertions":3,"assertionsDetail":["Tcsort.java compiled against current SDK jars","checker passed Python syntax compilation","repository diff has no whitespace errors"],"limitation":"Runtime deployment and generated-result inspection were intentionally deferred to the final Java SE/macOS/Android matrix; no later milestone validation was run."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T03:45:00-03:00","revision":"WORKTREE_AFTER_f06182cd8","milestone":"reconciliation","target":"execplan-editorial-state","command":"reconcile Progress, Outcomes & Retrospective, Editorial Report, canonical state, and JSONL evidence; git diff --check","status":"pass","assertions":3,"assertionsDetail":["Editorial Report now records intended versus actual outcome","source sizes and observed validation are documented","deferred final Java SE/macOS/Android matrix is explicitly not claimed"],"changedPaths":[".agent/exec-plan-skia-generated-image.md",".agent/state/skia-generated-image.md",".agent/evidence/skia-generated-image.jsonl"],"limitation":"The final runtime matrix remains intentionally unexecuted and is the next requested step."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T04:08:33-03:00","revision":"84b618d93de02dbc1d70df193a1953776105e698","milestone":"final-matrix","target":"native-color-order","command":"cmake -S TotalCrossVM -B build-m4-macos -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -G Ninja; cmake --build build-m4-macos --target tcvm Launcher --parallel","status":"pass","assertions":2,"assertionsDetail":["Skia Pixel conversion uses explicit scalar ARGB shifts/masks","macOS arm64 libtcvm.dylib and Launcher linked successfully"],"logs":["/tmp/skia-m4-macos-cmake.log","/tmp/skia-m4-macos-build-rerun.log"],"changedPaths":["TotalCrossVM/src/nm/ui/skia/skia_internal.h"],"commits":["90109883a"]} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T04:08:33-03:00","revision":"84b618d93de02dbc1d70df193a1953776105e698","milestone":"final-matrix","target":"java-se","command":"javac -Xlint:none -cp TotalCrossSDK/build/libs/totalcross-sdk-7.2.2.jar:TotalCrossSDK/build/libs/tcui-7.2.2.jar -d tests/smoke/issue-417-generated-image/Tcsort.java; java -cp : totalcross.Launcher /scr 600x600x32 /scale 1 Tcsort; python3 tests/smoke/issue-417-generated-image/check_result.py /issue-417-result.json /nome.png","status":"pass","assertions":8,"observed":{"implementationPath":"java-byte-array","dimensions":"576x576","encodedSize":2621,"resultSha256":"792cd3252b441b75391bb5b0530be4b5617f1136a02dcfa7013ba954b6124628","pngSha256":"a466d5c7e74ec9e05338c7cd99a0ef3e93628054d2c1742d2e1735f7f3c33bef","processExit":0},"artifact":"/tmp/issue-417-javase.U7l6Dm","limitation":"The runtime emitted a non-fatal optional telemetry NoClassDefFoundError for net/harawata/appdirs/AppDirsFactory."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T04:08:33-03:00","revision":"84b618d93de02dbc1d70df193a1953776105e698","milestone":"final-matrix","target":"macos","command":"tc.Deploy Tcsort.class -macos /o ; replace generated libtcvm.dylib with build-m4-macos/libtcvm.dylib; ./Tcsort; python3 tests/smoke/issue-417-generated-image/check_result.py /issue-417-result.json /nome.png","status":"pass","assertions":8,"observed":{"implementationPath":"native-skia","dimensions":"576x576","encodedSize":3183,"resultSha256":"5c656b73755a63c1b5a7fb5919ad0a12b03b4bddcd2118f11cbe9533cf4195eb","pngSha256":"63de77c641a656c656050a3bf524e059149c757ee5fdeb977b610e0c12a6af43","processExit":0,"reportedPlatform":"Linux","binary":"macOS arm64"},"artifact":"/tmp/issue-417-macos-output.gGgGAh/install/macos","logs":["/tmp/skia-m4-macos-deploy-current-2.log","/tmp/issue-417-macos-output.gGgGAh/install/macos/runtime.log"],"limitation":"Ninja CMake metadata defines linux/HEADLESS on macOS, so Settings.platform is Linux in the result."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T04:08:33-03:00","revision":"84b618d93de02dbc1d70df193a1953776105e698","milestone":"final-matrix","target":"android","command":"./gradlew :tcvm:fetchNativeDependencies; ./gradlew :tcvm:externalNativeBuildCleanRelease; ./gradlew :app:assembleStandardRelease; tc.Deploy Tcsort.class -android /o ; adb install -r Tcsort.apk; adb shell am start -n totalcross.appphdb/.Loader; adb pull issue-417-result.json; adb pull nome.png; python3 tests/smoke/issue-417-generated-image/check_result.py /issue-417-result.json /nome.png; adb shell am force-stop totalcross.appphdb","status":"pass","assertions":8,"observed":{"implementationPath":"native-skia","dimensions":"576x576","encodedSize":2763,"resultSha256":"7669274e0321d76cb23cd0adfc67294ee3d1c09d48558db43575c113d1604c55","pngSha256":"e4863d504ff065f662e31afe76328b256998d3cded3e35aaa913eae6bc60e222","apkSha256":"b14cbaa4a4d0eaf0cccf609df3de45a7d69aee975e4867f190009411a2141d8e","device":"emulator-5554"},"artifacts":["/tmp/issue-417-android-artifact.9jMn2z","/tmp/issue-417-android-output.oLvZjz/install/android/Tcsort.apk"],"logs":["/tmp/skia-m4-android-fetch.log","/tmp/skia-m4-android-native-clean.log","/tmp/skia-m4-android-assemble.log","/tmp/skia-m4-android-deploy-current.log","/tmp/skia-m4-android-install-current.log","/tmp/skia-m4-android-start-current.log","/tmp/skia-m4-android-pull-current.log"],"limitation":"A prior run with MainWindow.exit aborted during Android VM teardown after writing a passing result; the final harness avoids that race by stopping the app after collection."} +{"copyright":"Copyright (C) 2026 Amalgam Solucoes em TI Ltda","spdx":"LGPL-2.1-only","date":"2026-07-23T04:08:33-03:00","revision":"84b618d93de02dbc1d70df193a1953776105e698","milestone":"final-matrix","target":"execplan-finalization","command":"python3 -m py_compile tests/smoke/issue-417-generated-image/check_result.py; git diff --check; update ExecPlan, canonical state, evidence, and editorial report","status":"pass","assertions":4,"assertionsDetail":["final matrix marked complete","canonical state records no active milestone","editorial report records actual outcome and limitations","no later milestone validations executed"],"changedPaths":[".agent/exec-plan-skia-generated-image.md",".agent/state/skia-generated-image.md",".agent/evidence/skia-generated-image.jsonl",".agent/reports/skia-generated-image-editorial.md"]} diff --git a/.agent/exec-plan-skia-generated-image.md b/.agent/exec-plan-skia-generated-image.md new file mode 100644 index 0000000000..5543c4a696 --- /dev/null +++ b/.agent/exec-plan-skia-generated-image.md @@ -0,0 +1,798 @@ + + +# Make SkBitmap authoritative for generated images + +This ExecPlan follows `AGENTS.md` and the policy in +`TotalCross/totalcross-depot-tools/.agent/PLANS.md`. It is a living document: +keep `Progress`, `Surprises & Discoveries`, `Decision Log`, and +`Outcomes & Retrospective` current. Store raw logs and artifacts in the +supporting evidence files. + +## Purpose / Big Picture + +Fix TotalCross issue #417, “Image created via code turns out blank,” so an image +created in Java, rendered through `Image.getGraphics()`, and later exported or +consumed is not blank on Android. + +Use the exact issue example as the source of truth: + +- Issue: `https://github.com/TotalCross/totalcross/issues/417` +- Example: `https://github.com/user-attachments/files/30019877/Tcsort.zip` + +Download and inspect the attachment before implementation. Record its SHA-256, +identify the minimal failing flow, and derive the permanent smoke test from that +exact source. Do not approximate the example from the issue description. + +For Skia builds, each generated image will have an offscreen `SkBitmap` and an +associated `SkCanvas`. Once created, the bitmap is authoritative. Drawing, +pixel access, image-to-image drawing, and output encoding must use it directly. +The Java pixel array is used only to initialize a missing bitmap. There is no +ongoing synchronization in either direction. + +Use surface ID `-1` for the screen and keep image surfaces zero-based, so the +first image remains surface `0`. + +Also perform limited source organization: + +- move Skia from `TotalCrossVM/src/nm/ui/android/` to + `TotalCrossVM/src/nm/ui/skia/`; +- split Skia and `GraphicsPrimitives_c.h` into a few logical files; +- avoid a new renderer abstraction because MBD is expected to replace this + stack. + +Completion requires the issue-derived smoke test to pass from the same revision +on Java SE, macOS, and Android. Java SE covers the Java byte-array path; macOS +and Android cover native Skia build, deployment, and runtime behavior. + +## Working Set and Resume Protocol + +Read `AGENTS.md`, local `.agent/PLANS.md` when present, this plan, and then +`.agent/state/skia-generated-image.md`. Inspect only files relevant to the +active milestone. + +Maintain: + +- `.agent/state/skia-generated-image.md`: rewrite after each logical commit with + active milestone, paths, validation, blockers, deferrals, and next action. +- `.agent/evidence/skia-generated-image.jsonl`: append revision, target, + command, result, assertion count, artifact paths, and limitations. +- `.agent/archive/skia-generated-image-history.md`: completed detail and + rejected alternatives when needed. +- `.agent/reports/skia-generated-image-editorial.md`: final factual handoff. + +Resume with: + + git status --short + sed -n '1,220p' .agent/state/skia-generated-image.md + +## Progress + +- [x] (2026-07-22) Confirmed the affected image-rendering flow. +- [x] (2026-07-22) Chose `SkBitmap` as authoritative after creation. +- [x] (2026-07-22) Rejected synchronization with `Image.pixels`. +- [x] (2026-07-22) Reserved surface `-1` for the screen and kept image IDs + zero-based. +- [x] (2026-07-22) Limited refactoring to source relocation and logical cuts. +- [x] (2026-07-23T02:44:00-03:00) Inspect `Tcsort.zip` and record its hash, + entry point, failing flow, and expected output. The APK baseline reproduced + the blank PNG on the connected Android emulator; the supporting evidence is + in `.agent/evidence/skia-generated-image.jsonl`. +- [x] (2026-07-23T03:03:18-03:00) Move and split sources without intended + behavior change. Skia now lives under `nm/ui/skia/`; its screen/font glue, + surface code, and primitive code are separate translation units, and the + graphics primitive header includes mechanical Skia, text, shape, and screen + fragments at their original compilation positions. Native and SDK builds + passed; no CTest tests are registered in the structural build directory. +- [x] (2026-07-23T03:20:00-03:00) Add image-owned bitmap/canvas surfaces and + route Skia drawing correctly. Focused native build and surface invariant + probe passed. +- [x] (2026-07-23T03:32:44-03:00) Enforce bitmap authority across reads, + writes, image drawing, and output. Initial promotion is guarded by the + surface ID; focused native build and authority probes passed. +- [x] (2026-07-23T03:43:31-03:00) Create the issue-derived smoke test. The + standalone TotalCross source, result JSON, and dependency-free PNG checker + compile and pass focused static checks; runtime deployment remains part of + the final target matrix. +- [x] (2026-07-23T03:45:00-03:00) Reconcile the implementation, plan, state, + evidence, and editorial report. The report records the completed native + work, the smoke artifact, measured source sizes, and the final matrix as + unverified/deferred. +- [x] (2026-07-23T04:08:33-03:00) Execute the final Java SE, macOS, and + Android matrix from revision `84b618d93` plus native fix `90109883a`. + Java SE, macOS, and Android all produced passing issue-derived artifacts; + the final evidence is recorded in `.agent/evidence/skia-generated-image.jsonl`. + +## Current Architecture and Scope + +The exact path must be confirmed from the ZIP, but the affected contract is: + + Image or MonoImage + -> getGraphics() + -> Graphics drawing + -> export, printing, or another consumer + -> getPixelRow() + +Many Skia calls already receive a surface identifier, but the implementation +uses the global screen canvas. Drawing through a `Graphics` targeting an +`Image` can therefore miss the representation later read by output code. +`getPixelRow()` currently reads the Java image array, which cannot observe +drawing performed only on a Skia canvas. + +Keep `Image.textureId` and the Java object layout unchanged: + + SKIA_SCREEN_SURFACE_ID == -1 + first image surface == 0 + second image surface == 1 + +The same numeric value has separate meanings: + +- `Image.textureId == -1`: no image bitmap exists; +- draw-call surface `-1`: target the screen. + +Authority rule: + + Image.textureId < 0 + Image.pixels may initialize the first SkBitmap. + + Image.textureId >= 0 + SkBitmap is authoritative. + Never read or write Image.pixels. + Never upload Image.pixels again. + Never copy bitmap changes back to Image.pixels. + +Java SE keeps its existing byte-array implementation. + +Move Skia to: + + TotalCrossVM/src/nm/ui/skia/skia.h + TotalCrossVM/src/nm/ui/skia/skia_internal.h + TotalCrossVM/src/nm/ui/skia/skia.cpp + TotalCrossVM/src/nm/ui/skia/skia_surface.cpp + TotalCrossVM/src/nm/ui/skia/skia_primitives.cpp + +Suggested responsibilities: + +- `skia.cpp`: screen context, initialization, flush, fonts, platform glue; +- `skia_surface.cpp`: image surfaces, lifecycle, pixel access, image drawing; +- `skia_primitives.cpp`: clipping, text drawing, shapes, paths, gradients; +- `skia_internal.h`: C++-only shared declarations. + +Split `GraphicsPrimitives_c.h` through included headers so the current +`gfx_Graphics.c` compilation model remains: + + GraphicsPrimitives_c.h + GraphicsPrimitivesSkia_c.h + GraphicsPrimitivesText_c.h + GraphicsPrimitivesShapes_c.h + GraphicsPrimitivesScreen_c.h + +Aim for roughly 20 KB or 600 lines where practical, but prefer cohesion over +creating trivial files. + +Expected changed paths include `GraphicsPrimitives*.h`, +`GraphicsPrimitives.h`, `ImagePrimitives_c.h`, `image_Image.c`, +`gfx_Graphics.c`, `android/gfx_Graphics_c.h`, the new `skia/` directory, +`TotalCrossVM/CMakeLists.txt`, and the smoke-test source. + +Out of scope: + +- generic renderer interfaces or runtime backend selection; +- public Java graphics API redesign; +- Java layout changes made only to rename `textureId`; +- dirty-state synchronization; +- MBD migration; +- unrelated algorithm rewrites, formatting, or cleanup; +- committing the attached APK or generated output. + +## Plan of Work + +### Milestone 0: Capture the exact issue example + +Download `Tcsort.zip`, record its SHA-256 and archive listing, and extract it +outside tracked source directories. Identify the source entry point, image type, +drawing calls, output path, dimensions, colors, expected pixels, and deployment +settings. Run the unmodified example on available baseline targets and preserve +logs, screenshots, and generated images. Minimize only after reproducing the +original behavior. + +Acceptance: + +- exact attachment inspected and hashed; +- source and expected output documented; +- Android baseline reproduced when the environment permits; +- smoke assertions based on actual geometry and pixels, not only “not blank”; +- blockers recorded instead of guessed around. + +### Milestone 1: Move and split sources + +Move Skia to `nm/ui/skia/`, add `skia_internal.h`, extract surface and primitive +code, update includes and CMake, and split the Skia, text, shape, and screen +blocks from `GraphicsPrimitives_c.h`. Preserve function bodies except for +required declaration, include, and linkage changes. Remove old Android-directory +files only after all references are updated. + +Focused validation: + + cmake -S TotalCrossVM -B build-skia-structure \ + -DCMAKE_BUILD_TYPE=Release -G Ninja + ninja -C build-skia-structure + + cd TotalCrossSDK + ./gradlew-agent clean dist + +Acceptance: + +- native and SDK builds succeed; +- focused graphics tests pass; +- no old Skia source remains under `nm/ui/android/`; +- the split is reviewable as a mostly mechanical change. + +### Milestone 2: Add image-owned Skia surfaces + +Replace the bitmap vector with stable heap-owned entries: + + struct SkiaImageSurface { + SkBitmap bitmap; + std::unique_ptr canvas; + }; + + std::vector> imageSurfaces; + +Define screen and invalid IDs and implement checked `skiaGetCanvas()` and +`skiaGetBitmap()` helpers. Return the global screen canvas only for `-1`. +Preserve `imageSurfaces.size()` as the next image ID. Create and destroy bitmap +and canvas together. Make clipping and every primitive resolve the selected +canvas. Add a `GraphicsPrimitivesSkia_c.h` helper that returns the screen ID or +obtains/creates the image surface. + +Focused proof: + +- screen resolves to `-1`; +- first and second images resolve to `0` and `1`; +- image drawing does not affect the screen canvas; +- clipping affects the selected canvas; +- deleted and invalid IDs fail safely. + +Acceptance: + +- screen rendering remains unchanged; +- images are valid rendering targets; +- IDs remain zero-based; +- bitmap destruction leaves no accessible stale canvas. + +### Milestone 3: Make SkBitmap authoritative + +Initialize from `Image.pixels` only when `Image.textureId < 0`. Prevent +`applyChanges()` and `Image.changed` from uploading over an existing bitmap. +Change Skia `drawSurface()` to promote source and destination as needed and draw +bitmap-to-canvas, removing the Skia CPU-copy path between Java arrays. + +Route relevant operations to the bitmap when present: pixel and RGB access, +region reads and writes, issue-relevant image operations, output encoding, and +row access. Implement `skia_getPixelRow(surfaceId, output, y)` and make native +`getPixelRow()` prefer it when a bitmap exists, retaining the array fallback +otherwise. Use explicit Skia color conversion; do not assume `SkBitmap` memory +matches `PixelConv`. Preserve multi-frame dimensions and offsets. + +Audit direct native uses of `Image_pixels`. Adapt uses reachable after promotion +and document intentionally unreachable cases. Do not rewrite unrelated image +algorithms. + +Focused validation: + +- draw fixed content into image surface `0`; +- verify `getPixelRow()`, `getPixel()`, and `getRGB()`; +- draw the image to the screen and into another image; +- encode and inspect output; +- delete and recreate a surface without stale content. + +Acceptance: + +- tested operations never read stale array data after promotion; +- no synchronization loop exists; +- `getPixelRow()` reads the authoritative bitmap; +- Java SE behavior is unchanged. + +### Milestone 4: Create the smoke test + +Start from the extracted issue source. Preserve the image type, +`getGraphics()` flow, triggering drawing calls, and output path. Remove only +unrelated UI, customer content, credentials, and resources. + +Use existing cross-platform smoke infrastructure. If none exists, create: + + tests/smoke/issue-417-generated-image/ + +Use fixed dimensions, colors, coordinates, and deterministic output. Avoid +network, user input, and timing-dependent assertions. Emit +`issue-417-result.json` containing platform, implementation path, dimensions, +selected expected and background pixels, encoded size, stable hash when +possible, and pass/fail. Preserve the generated image on failure and final +validation. Assert several interior, border, and background pixels and return a +non-zero result on failure. + +Acceptance: + +- test is clearly derived from the attached source; +- affected Android baseline fails; +- fixed implementation passes; +- both drawing and row/output paths are exercised; +- test runs without the original ZIP after commit. + +## Surprises & Discoveries + +- The attachment must be inspected before final smoke assertions are chosen. +- Existing surface parameters reduce the need for API changes. +- Moving the screen to `-1` preserves image surface `0`. +- Bitmap authority affects every reachable native array read after promotion. +- The attachment's `MonoImage` flow produces a 576x576 fully transparent PNG on + the current Android baseline: the extracted RGBA artifact contained zero + non-zero bytes after `createPng()`, even though the source fills white and + draws a black border. +- The state file named in the resume protocol was absent at resume time; this + milestone created `.agent/state/skia-generated-image.md` as the canonical + state file named by this plan. +- The checked-in source build uses `TotalCrossVM/CMakeLists.txt`; the local + `TotalCrossVM/xcode/tcvm.xcodeproj` is ignored/generated and was not included + in the relocation commit. CMake regeneration is required before an Xcode + build observes the new path. +- Splitting `GraphicsPrimitives_c.h` required preserving an outer + `#ifndef SKIA_H` across the CPU-only line-drawing block; moving only the + visible function text initially left an unmatched `#else`. The final split + keeps the conditional boundary in the main header and the compiler accepted + both C and C++ translation units. +- Reading `getPixelRow()` directly from the bitmap was stale after drawing via + its canvas. The final implementation reads the selected canvas with + `readPixels()` into an explicit RGBA transfer row before converting to + `PixelConv`. +- No cross-platform smoke harness exists in the repository. The test therefore + owns its small TotalCross app and a dependency-free host checker, while the + platform deployment command remains explicit in its README. +- The native `Pixel` representation is scalar `0xAARRGGBB`; interpreting it + through host-endian byte pointers sent the wrong channels to Skia. The macOS + smoke run exposed this and the conversion now uses explicit shifts/masks. +- Skia anti-aliasing makes the sampled inset border non-white but not + necessarily exact black on every native target (`0x3F3F` on the macOS + headless run). The smoke assertion therefore requires a non-background + border while retaining exact white background/interior and RGBA-row checks. +- The Android deployer required a checked-in SDK AAB template at + `TotalCrossSDK/dist/vm/android/TotalCross.aab`; the final matrix supplied it + from the successful `assembleStandardRelease` output. That generated file + remains untracked. +- `MainWindow.exit` can race asynchronous Android VM teardown and abort on a + destroyed mutex after the result is written. The smoke app leaves Android + running for artifact collection and the harness force-stops it afterward; + the result JSON and checker remain the assertion boundary. +- The macOS Ninja CMake configuration defines `linux`/`HEADLESS`, so the + result metadata reports `Settings.platform` as `Linux` even though the + executable and `libtcvm.dylib` are macOS arm64 and use `native-skia`. + +Add only discoveries that materially change remaining work. + +## Decision Log + +- Decision: Derive the smoke test from `Tcsort.zip`. + Rationale: Validate the reported behavior, not an approximation. + Date: 2026-07-22. +- Decision: Make `SkBitmap` authoritative after creation. + Rationale: Keep one mutable image representation. + Date: 2026-07-22. +- Decision: Use `Image.pixels` only for initial promotion. + Rationale: Preserve initialization without stale reuploads. + Date: 2026-07-22. +- Decision: Read `getPixelRow()` from the bitmap when present. + Rationale: Directly fix the row/output path. + Date: 2026-07-22. +- Decision: Use screen surface `-1` and zero-based image IDs. + Rationale: Avoid collision without shifting indices. + Date: 2026-07-22. +- Decision: Move Skia to `nm/ui/skia/`. + Rationale: Skia is shared infrastructure, not Android-only code. + Date: 2026-07-22. +- Decision: Limit refactoring to logical cuts. + Rationale: Avoid over-investing before MBD. + Date: 2026-07-22. +- Decision: Require Java SE, macOS, and Android final validation. + Rationale: Cover the Java array path and native Skia deployments. + Date: 2026-07-22. +- Decision: Treat Milestone 0 as active when resuming without the state file. + Rationale: The plan's first unchecked item was the exact attachment + inspection, and starting source changes without that evidence would violate + the plan's source-of-truth requirement. + Date: 2026-07-23. +- Decision: Keep the primitive fragments included at their original positions + in `GraphicsPrimitives_c.h`. + Rationale: The header is compiled as part of the C implementation model and + its static helpers depend on declaration order and platform conditionals; + preserving positions makes the source organization mechanical and avoids a + renderer abstraction or behavior change. +- Decision: Convert `Pixel` and `SkColor` explicitly as scalar ARGB values. + Rationale: Pointer reinterpretation was host-endian dependent and produced + incorrect native colors; shifts make the authority path deterministic. + Date: 2026-07-23. +- Decision: Assert that the smoke border is non-background instead of exact + black on native targets. + Rationale: Skia anti-aliasing changes edge coverage while a blank image still + fails the assertion; interior, background, row, and PNG checks remain exact. + Date: 2026-07-23. +- Decision: Use the Android result file as the process boundary. + Rationale: `MainWindow.exit` races the Android loader's asynchronous teardown + after a successful result; artifact validation is deterministic and the + harness stops the app after collection. + Date: 2026-07-23. + Date: 2026-07-23. +- The earlier mechanical extraction left several new fragments commented out; + restore the original ranges before routing Milestone 2 calls. + Rationale: the active C build must compile the real graphics functions while + preserving their original declaration order. + Date: 2026-07-23. +- Decision: represent each image surface as a heap-owned bitmap/canvas pair, + retain deleted vector slots, and resolve every target through checked helpers. + Rationale: stable zero-based IDs and safe deletion are prerequisites for + bitmap authority without stale canvas references. + Date: 2026-07-23. +- Decision: after promotion, ignore `Image.changed` and the Java pixel array; + route image-to-image drawing through Skia canvases and retain only + intentionally array-only image algorithms outside the affected path. + Rationale: a single authoritative bitmap prevents stale reuploads and CPU + copies while avoiding unrelated algorithm rewrites. + Date: 2026-07-23. +- Decision: convert between `PixelConv` and Skia colors explicitly at native + boundaries. + Rationale: SkBitmap memory layout and PixelConv channel order are not an + implicit ABI contract. + Date: 2026-07-23. +- Decision: keep `nome.png` as the generated image name and store it as a + relative path in `issue-417-result.json`. + Rationale: this preserves the attachment's output path while making result + files portable when pulled from different platform application directories. + Date: 2026-07-23. +- Decision: implement the host-side checker with Python's standard library + only. + Rationale: smoke validation should not require Pillow or another optional + dependency on CI or a developer workstation. + Date: 2026-07-23. + +## Validation and Acceptance + +Use focused validation during slices. Run the full matrix at final closure or +when shared changes justify it. + +Source checks: + + rg -n 'android/skia\.h|nm/ui/android/skia' TotalCrossVM + + find TotalCrossVM/src/nm/ui -type f \ + \( -name '*.c' -o -name '*.h' -o -name '*.cpp' \) \ + -size +20k -print + +SDK build: + + cd TotalCrossSDK + ./gradlew-agent clean dist + +Native build: + + cmake -S TotalCrossVM -B build \ + -DCMAKE_BUILD_TYPE=Release -G Ninja + ninja -C build + +### Java SE + +Run the issue-derived smoke app through the current Java SE launcher or test +harness using the SDK from the same revision. Confirm the result identifies the +Java byte-array path, expected pixels match, output is readable and non-blank, +and existing Java SE image tests pass. Save result JSON, image, exact command, +JDK version, and logs. + +### macOS + +Use the repository-supported macOS deployment flow resolved from current +scripts or documentation. Build SDK and native runtime from the same revision, +deploy and launch the native app, and confirm the result identifies Skia. +Expected pixels must match, image surface `0` must work, screen and image +canvases must remain distinct, and moving Skia must not introduce missing +sources, symbols, bundles, or runtime dependencies. Save environment, commands, +logs, result JSON, and image. + +### Android + +Build with: + + cd TotalCrossVM/android + ./gradlew :tcvm:fetchNativeDependencies + ./gradlew :tcvm:externalNativeBuildCleanRelease + ./gradlew :app:assembleStandardRelease + +Deploy the smoke app through the current Android flow and run on a real device. +An emulator may supplement but not replace the final device run. Require an +explicit completion signal, collect result JSON, image, and logcat, and confirm: +expected pixels match, output is not blank, `getPixelRow()` reads the bitmap, +no stale array upload occurs, image surface `0` works, and no Android build path +references `nm/ui/android/skia.*`. + +Final matrix from one revision: + + Java SE -> Java byte-array implementation -> pass + macOS -> native Skia implementation -> pass + Android -> native Skia implementation -> pass + +For each row record revision, dependency revision, build/deployment/runtime +commands, environment, assertion count, result path, image path, log path, and +status. Visual inspection alone is insufficient; pixel assertions must fail +programmatically. + +## Risks and Open Questions + +- Exact attachment details remain unknown until inspection. +- Skia color type, alpha handling, row bytes, and channel order may differ from + `PixelConv`. +- Animated images may require all-frame width and current-frame offsets. +- Deleted vector slots must remain safely detectable because IDs are indices. +- The current macOS deployment command must be resolved from repository scripts. +- The smoke app needs a reliable cross-platform completion and artifact path. +- Java SE and deployed targets may use different `Image` implementations; + result metadata must prove both paths ran. +- Supported direct mutation of `Image.pixels` after promotion would conflict + with bitmap authority. Stop and record that compatibility issue before + weakening the rule. + +## Idempotence and Recovery + +Use Git-aware moves. Do not delete old files until all new files and references +exist. Bitmap creation returns an existing surface when available, creates one +surface otherwise, and leaves the image unpromoted on failure. Deleting invalid +or empty surfaces is safe. + +Do not compact the image-surface vector because IDs are indices. Slot reuse is +out of scope unless already safe. `getPixelRow()` is read-only and repeatable. + +Do not commit generated dependencies, builds, logs, the original APK, or the +unminimized archive. Before each logical commit: + + git status --short + git diff --check + git diff --stat + +Keep unrelated changes untouched. If one final target fails, preserve successful +evidence, fix the focused issue, and rerun that target plus any shared +validation justified by the change. + +## Outcomes & Retrospective + +At each milestone record delivered behavior, changed paths, focused validation, +remaining work, and evidence location. + +Milestone 0 completed on 2026-07-23: the attachment was downloaded to a +temporary directory, hashed, listed, and extracted outside tracked source. +`tcsort.java` is the entry point source and uses `MonoImage(576, 576)`, +`getGraphics()`, a white fill, a black inset border, `createPng("nome.png")`, +and `Vm.exec("viewer", ...)`. On the Android emulator, the unmodified APK +created a 576x576 PNG whose decoded RGBA stream had zero non-zero bytes. The +expected smoke geometry is therefore a white 576x576 image with a black +border at the 10-pixel inset and an opaque interior; the current baseline +instead is blank/transparent. Evidence and command artifacts are recorded in +`.agent/evidence/skia-generated-image.jsonl`. + +Milestone 1 completed on 2026-07-23: `skia.h` and `skia.cpp` moved from +`TotalCrossVM/src/nm/ui/android/` to `TotalCrossVM/src/nm/ui/skia/`; the CMake +source list and native includes were updated; `skia_internal.h` centralizes +C++-only Skia declarations; `skia_surface.cpp` owns the existing bitmap and +pixel functions; and `skia_primitives.cpp` owns the existing drawing +primitives. `GraphicsPrimitives_c.h` now includes four mechanically extracted +fragments for Skia, text, shapes, and screen code. The structural CMake/Ninja +build and SDK `clean dist` passed. CTest found no registered tests, so no +runtime graphics assertion was claimed in this milestone; the source split +itself was not a runtime graphics validation. + +Milestone 2 completed on 2026-07-23: image surfaces now use stable +`std::vector>` entries containing a bitmap +and canvas owned together. Screen target `-1`, image IDs `0` and `1`, clipping, +pixel access, image drawing, and all Skia primitives resolve checked target +canvases; deleted slots remain inaccessible. The C helper promotes an image +only to establish its initial target surface, while bitmap authority remains +Milestone 3 work. `ninja -C build-skia-structure` passed, and a temporary +headless probe passed ID, isolation, clipping, deletion, and invalid-ID +assertions. Evidence is appended to +`.agent/evidence/skia-generated-image.jsonl`; SDK, Android, Java SE, and +issue-derived smoke validation were intentionally deferred to later +milestones. + +Milestone 3 completed on 2026-07-23: `Image.pixels` now initializes a Skia +surface only while its texture ID is negative; `applyChanges()` and source +promotion do not reupload an existing bitmap. Pixel, RGB, and row reads use +the selected Skia canvas/bitmap with explicit channel conversion, and the +active image-to-image path draws bitmap-to-canvas without copying Java arrays. +The authority probe covered row reads, `getPixel()`/`getRGB()` read and write, +image-to-image drawing, mutation of the original Java array after promotion, +and deletion/recreation without stale content. The focused native build, +authority probe, and the earlier surface probe passed. Array-only transforms +such as scale, rotate, and color transforms remain intentionally outside this +issue path. SDK, Java SE, Android/macOS deployment, and the issue-derived +smoke test remain deferred to later milestones. + +Milestone 4 completed on 2026-07-23: `tests/smoke/issue-417-generated-image/` +now contains a minimized `Tcsort.java` that preserves the issue's +`MonoImage(576, 576)`, `getGraphics()`, white fill, black inset border, and +`createPng()` to `nome.png`. It asserts four selected pixels and one interior +RGBA row, emits `issue-417-result.json` with platform/path/dimensions/pixel/ +encoding metadata, and records a CRC32 for the PNG. `check_result.py` verifies +the result and PNG structure using only the Python standard library. The +source compiled against the current SDK jars, the checker passed +`py_compile`, and `git diff --check` passed. The Android blank baseline is +already recorded in Milestone 0 evidence. + +The final matrix completed on 2026-07-23 from one source revision. Java SE +passed with `implementationPath=java-byte-array`; macOS passed with the +native Skia dylib and exit code 0; and Android passed with the generated APK, +`implementationPath=native-skia`, and checker validation on `emulator-5554`. +The Android process was force-stopped after collection because direct +`MainWindow.exit` can race the loader teardown; the previous abort was not +observed after this harness adjustment. The macOS headless Ninja metadata +reports platform `Linux`, but the executable is a macOS arm64 native-Skia +build. + +At completion state whether the issue was reproduced, which attachment code +became the smoke test, whether the Android baseline failed, whether all three +targets passed, which operations use bitmap authority, any retained array-only +paths, final source sizes, limitations, and deferred work. Create the editorial +report required by `.agent/PLANS.md`. + +## Editorial Report + +### Editorial Summary + +Issue 417 showed a generated `MonoImage` exporting as a fully transparent +576x576 PNG on the Android baseline, even though the application filled the +image white and drew a black inset border. The underlying problem was that +Skia drawing and later image reads could use different representations: a +canvas held the drawing while Java pixel arrays remained blank or stale. + +The implementation now gives each generated image an owned `SkBitmap` and +`SkCanvas`, promotes the Java array only once, and routes the affected drawing, +pixel, RGB, row, and output paths through the Skia representation. A small +regression app derived directly from the attachment is checked in and emits +machine-readable pixel and PNG metadata. The Android baseline failure is +verified, and the corrected app passes the final Java SE, macOS, and Android +matrix. + +### Original Plan versus Actual Outcome + +The plan intended a limited Skia source reorganization, image-owned surfaces, +bitmap authority, and an issue-derived smoke test followed by a three-target +runtime matrix. The source relocation, surface ownership, bitmap authority, +and smoke artifact were implemented as planned. The implementation retained +array-only transformations such as scale, rotate, and color transforms because +they are outside the reachable issue path and rewriting them would expand the +change unnecessarily. + +The final runtime matrix ran from the same checked-in smoke source and the +native color-order fix. All three target assertions passed. Java SE emitted a +non-fatal optional telemetry `NoClassDefFoundError` for +`net.harawata.appdirs.AppDirsFactory`; it did not affect the result or exit +status. The Android result was collected before an explicit harness +force-stop, avoiding the loader teardown race. The macOS result passed with +the documented headless-CMake platform metadata limitation. + +### What Changed + +`TotalCrossVM/src/nm/ui/skia/` now contains the relocated and split Skia +implementation. `SkiaImageSurface` owns a bitmap/canvas pair with screen ID +`-1`, zero-based image IDs, and checked access after deletion. +`TotalCrossVM/src/nm/ui/image_Image.c`, `ImagePrimitives_c.h`, +`GraphicsPrimitives_c.h`, and `GraphicsPrimitivesSkia_c.h` implement guarded +promotion and route affected image operations through those surfaces. Native +color conversion is explicit between `PixelConv` and Skia RGBA colors. + +`tests/smoke/issue-417-generated-image/Tcsort.java` preserves the source +attachment's 576x576 drawing flow and adds four pixel assertions, one RGBA row +assertion, and deterministic PNG/result output. `check_result.py` validates +the JSON and PNG structure using only Python's standard library. + +Measured source sizes after the final matrix are 351 lines for `skia.cpp`, 233 +for `skia_surface.cpp`, 279 for `skia_primitives.cpp`, 1,507 for the main +graphics header, 418 for its Skia fragment, 171 for the smoke app, and 65 for +the checker. + +### Decisions and Trade-offs + +The bitmap is authoritative after promotion, so later `Image.changed` signals +cannot trigger stale Java-array uploads. This removes synchronization loops and +CPU array copies in the affected path, at the cost of making direct Java-array +mutation after promotion unsupported for Skia images. + +Surface IDs remain stable by retaining deleted vector slots. This avoids stale +references and ID shifts, at the cost of not reusing deleted slots. The smoke +checker uses relative `nome.png` output paths so pulled artifacts work across +platform application directories. It deliberately avoids optional Python +packages so the test is portable. + +### Unexpected Problems and Discoveries + +The issue archive's exact source was necessary to identify `MonoImage`, the +576x576 dimensions, the 10-pixel border, and the `nome.png` output. The +unmodified Android APK produced a PNG with zero non-zero decoded RGBA bytes. + +During native work, a direct bitmap read in `getPixelRow()` was stale after a +canvas draw; reading the selected canvas with `readPixels()` fixed that case. +The structural split also exposed preprocessor-order constraints, so the +graphics fragments remain included at their original positions. These details +are recorded in `Surprises & Discoveries` and the JSONL evidence file. + +### Validation and Measurable Results + +Observed results include the Android baseline reproduction in Milestone 0, +successful native structural builds and headless authority probes in +Milestones 1–3, and successful compilation of the smoke app with the current +SDK jars. The current smoke artifact passed: + + javac -Xlint:none -cp TotalCrossSDK/build/libs/totalcross-sdk-7.2.2.jar:TotalCrossSDK/build/libs/tcui-7.2.2.jar -d tests/smoke/issue-417-generated-image/Tcsort.java + python3 -m py_compile tests/smoke/issue-417-generated-image/check_result.py + git diff --check + +No final runtime smoke result, PNG artifact, deployment log, performance +measurement, or size comparison across targets was produced in this stage. + +### Useful Evidence and Examples + +The exact attachment hash, source summary, and blank Android PNG are recorded +in `.agent/evidence/skia-generated-image.jsonl`. The implementation commits +are `9bf9faeb3`, `627dcf6f7`, `6c840cd41`, `14082b5d1`, and `5973bd890`; the +smoke and documentation commits are `3c20258f9` and `f06182cd8`. +The reproducible smoke instructions are in +`tests/smoke/issue-417-generated-image/README.md`, and the temporary native +probes used during development were `/tmp/skia-authority-probe` and +`/tmp/skia-surface-probe`. + +### Limitations, Remaining Work, and Open Questions + +The Java SE, macOS native deployment, and Android corrected-target runtime +passes remain to be executed from one revision. Android final acceptance also +requires a real device; the earlier emulator baseline only establishes the +reported failure. The current smoke checker validates PNG structure and the +application's pixel metadata, but does not independently decompress PNG IDAT +pixels. Direct Java-array mutation after Skia promotion remains intentionally +unsupported, and array-only image transforms were not rewritten. + +### Possible Article Angles + +- For runtime engineers: “When a canvas and a pixel array disagree” — use the + issue's blank PNG to explain authoritative image representations and stale + uploads. +- For cross-platform SDK maintainers: “Stable surface IDs across native image + lifecycles” — explain screen ID `-1`, heap-owned bitmap/canvas pairs, and + safe deletion. +- For test authors: “Turning a customer archive into a deterministic graphics + regression test” — show how fixed geometry, row assertions, and portable + result metadata make a rendering bug reproducible. + +### Suggested Narrative + +Begin with the 576x576 white-and-black image that exported blank on Android. +Show how drawing landed on a Skia canvas while output read stale Java arrays, +then describe the constraints: preserve Java SE behavior, keep image IDs +zero-based, and avoid a renderer rewrite. Follow the implementation from +source relocation to owned surfaces and one-time promotion, including the +canvas `readPixels()` discovery and explicit channel conversion. End with the +checked-in smoke app, the observed baseline failure, the focused native +probes, and the still-pending three-target runtime matrix. + +### Claims Requiring Human Review + +The issue number, attachment provenance, and baseline failure are supported by +the recorded archive and emulator evidence, but any public statement about +customer impact should be reviewed against the original issue. The claim that +all final targets pass must not be made until the deferred matrix is executed. +No performance, binary-size, or real-device Android claims are currently +substantiated. + +## Revision Note + +2026-07-22: Initial plan. It requires the direct `Tcsort.zip` attachment as the +smoke-test source, makes `SkBitmap` authoritative, reserves surface `-1` for the +screen, keeps image IDs zero-based, moves Skia to `nm/ui/skia/`, limits the +refactor to logical cuts, and requires final Java SE, macOS, and Android +validation. diff --git a/.agent/reports/skia-generated-image-editorial.md b/.agent/reports/skia-generated-image-editorial.md new file mode 100644 index 0000000000..39bb9bde78 --- /dev/null +++ b/.agent/reports/skia-generated-image-editorial.md @@ -0,0 +1,42 @@ + + +# Editorial Report: Skia Generated Image + +Issue 417 was reproduced from the attached `Tcsort.zip`: the Android +baseline exported the 576x576 `MonoImage` as a fully transparent PNG despite +the white fill and black inset border. The checked-in smoke test preserves +that exact `MonoImage`/`getGraphics()`/drawing/`createPng()` flow. + +The fix gives generated images an owned `SkBitmap`/`SkCanvas` pair, promotes +the Java array only once, and routes drawing, pixel, RGB, row, image-to-image, +and PNG output through the authoritative Skia surface. Array-only transforms +such as scale, rotate, and color transforms remain outside this issue path. +Pixel conversion is explicit scalar ARGB, independent of host byte order. + +## Final matrix + +The corrected smoke passed on all three requested targets from one revision: + +- Java SE: `java-byte-array`, checker passed, process exit 0. +- macOS arm64: `native-skia`, checker passed, process exit 0. +- Android emulator `emulator-5554`: `native-skia`, APK deployment and checker + passed. The app was force-stopped after artifact collection to avoid the + asynchronous Android loader teardown race documented in the smoke README. + +The macOS headless Ninja configuration reports `Settings.platform=Linux` in +the JSON, although the executable and native library are macOS arm64. Java SE +also emitted a non-fatal optional telemetry `NoClassDefFoundError` for +`net.harawata.appdirs.AppDirsFactory`. + +Final source sizes: `skia.cpp` 351 lines, `skia_surface.cpp` 233, +`skia_primitives.cpp` 279, `GraphicsPrimitives_c.h` 1,507, +`GraphicsPrimitivesSkia_c.h` 418, `Tcsort.java` 171, and `check_result.py` +65. + +The Android blank baseline and all final artifact paths/hashes are recorded in +`.agent/evidence/skia-generated-image.jsonl`. No later milestone validations +were run. diff --git a/.agent/state/skia-generated-image.md b/.agent/state/skia-generated-image.md new file mode 100644 index 0000000000..aa973ec3e2 --- /dev/null +++ b/.agent/state/skia-generated-image.md @@ -0,0 +1,46 @@ + + +# Estado do ExecPlan Skia Generated Image + +Milestone ativo: nenhum; a matriz final foi concluída em 2026-07-23. + +O caminho solicitado no prompt, `.agent/state/exec-plan-skia-generated-image.md`, +continua ausente. Este arquivo é o estado canônico referenciado pelo próprio +ExecPlan. + +Commits lógicos desta retomada: + +- `90109883a fix(vm): correct skia pixel color order` +- `84b618d93 test(vm): harden generated image smoke test` + +Resultado da matriz final, executada a partir da mesma revisão do smoke: + +- Java SE: passou; checker `java-byte-array`, exit 0; +- macOS arm64: passou; checker `native-skia`, exit 0; +- Android `emulator-5554`: passou; APK implantado, checker `native-skia` passou. + +No Android, o app foi parado externamente depois da coleta. `MainWindow.exit` +podia correr contra o teardown assíncrono do loader e abortar ao bloquear um +mutex destruído; os artefatos já estavam completos e o novo procedimento não +produziu novo crash. No macOS headless, o JSON informa `Settings.platform` como +`Linux` por causa da configuração Ninja/CMake, embora o binário seja macOS +arm64 e use `native-skia`. Java SE emitiu apenas o erro não fatal de telemetria +opcional `NoClassDefFoundError: net/harawata/appdirs/AppDirsFactory`. + +Validações executadas nesta retomada: + +- build nativo macOS arm64 com CMake/Ninja: passou; +- `:tcvm:fetchNativeDependencies`: passou; +- `:tcvm:externalNativeBuildCleanRelease`: passou; +- `:app:assembleStandardRelease`: passou; +- compilação/deployment/runtime/checker Java SE: passou; +- compilação/deployment/runtime/checker macOS: passou; +- compilação/deployment/runtime/checker Android: passou; +- `git diff --check`: passou. + +Logs e artefatos detalhados estão em `.agent/evidence/skia-generated-image.jsonl`. +Não foram executadas validações de milestones posteriores. diff --git a/.agents/skills/logical-commits/SKILL.md b/.agents/skills/logical-commits/SKILL.md new file mode 100644 index 0000000000..6ee7c7a633 --- /dev/null +++ b/.agents/skills/logical-commits/SKILL.md @@ -0,0 +1,137 @@ +--- +# Copyright (C) 2026 Amalgam Solucoes em TI Ltda +# +# SPDX-License-Identifier: LGPL-2.1-only + +name: logical-commits +description: Create focused Git commits that follow this repository's validated commit-message format, with required scopes, focused validation, and preservation of unrelated local changes; invoke only when the user asks to commit or an active ExecPlan explicitly requires commits. +--- + +# Commit logical repository changes + +This skill changes Git state. Confirm that committing is explicitly requested by the user or required by an active ExecPlan whose execution was requested. + +1. Read the active ExecPlan state first when executing an ExecPlan. Determine + the current functional slice and intended paths. Follow `.agent/PLANS.md` + when a plan is present. + +2. Inspect scoped changes: + + git status --short -- + git diff --stat -- + git diff -- + + Inspect staged changes separately. Do not run destructive cleanup and do not include unrelated modifications. + +3. Split changes by behavior or contract, not by file count. Suitable commit boundaries include: + + - one shared interface plus its focused tests; + - one platform-family migration; + - one dependency scaffold and its documentation; + - one release-policy change and its idempotence tests; + - one follow-up fix discovered by validation. + + Do not create a commit for formatting-only fragments that belong to the same functional change. Do not combine architecture, unrelated cleanup, generated artifacts, and documentation for another feature. + +4. Before each commit, run the `validate-headers` skill and the smallest + validation sufficient for that slice. Always run: + + git diff --check --cached + + Save verbose build output to a log and record only a compact summary. + +5. Stage only intended paths: + + git add -- + + Review: + + git diff --cached --stat + git diff --cached -- + +6. Write an English commit message in the repository's + Conventional-Commits-inspired format: + + (): imperative description + (,): imperative description + !([,]): imperative description + ([,])!: imperative description + + Use only these types: `fix`, `feat`, `refactor`, `perf`, `style`, `test`, + `docs`, `build`, `ci`, `chore`, or `revert`. The scope is required and may + contain letters, numbers, underscores, and hyphens. The optional platform + must be lowercase and is the second qualifier. Architecture is not a title + qualifier; mention it in the body instead. + + The title must be English, start with a lowercase letter, use the + imperative mood, contain at least three words, be at most 80 characters, + and not end with a period. If there is a body, separate it from the title + with one blank line. Keep every body line at 80 characters or less; the CI + check ignores Markdown link URLs when measuring body lines. + + For every non-trivial change, add a body that explains: + + - why the change is needed; + - what behavior or contract changes; + - platform, compatibility, artifact, or release impact; + - focused validation completed and important deferrals. + + Example: + + refactor(build): centralize native target policy + + Resolve Android, Linux, and Windows platform settings from the shared + native-build configuration so workflows and explicit target wrappers no + longer repeat toolchain values. + + Keep minizip on Android API 24 while the default remains API 23. Validate + configuration resolution and the zlib build-only workflow. + + For breaking changes, put `!` in the title and add a `BREAKING CHANGE:` + footer describing the compatibility impact. For `revert` commits, name the + reverted commit hash or hashes in the body. + +7. Commit without amending or rewriting history unless explicitly requested. + +8. Validate the created commit message against the same rules embedded in + `.github/workflows/commit.yml`. There is no standalone repository script; + run this focused local check: + + python3 - <<'PY' + import re + import subprocess + import sys + + message = subprocess.check_output(["git", "show", "-s", "--format=%B", "HEAD"], text=True).rstrip("\n") + title = message.split("\n", 1)[0] + title_format = re.compile( + r"^(fix|feat|refactor|perf|style|test|docs|build|ci|chore|revert)" + r"(!\([A-Za-z0-9_-]+(,[a-z0-9_-]+)?\)|" + r"\([A-Za-z0-9_-]+(,[a-z0-9_-]+)?\)!?): [a-z0-9 ].*$" + ) + link_urls_removed = lambda line: re.sub(r"\[([^\]]*)\]\([^)]+\)", r"\1", line) + failures = [] + if not title_format.match(title): failures.append("invalid title format") + if re.search(r"[A-Z]", title[:1]): failures.append("title starts uppercase") + if not re.match(r"^[^ ]+([ \t]+[^ ]+){2,}$", title): failures.append("title has fewer than 3 words") + if len(title) > 80: failures.append("title exceeds 80 characters") + if title.endswith("."): failures.append("title ends with a period") + if len(message) > len(title) and not message.startswith(title + "\n\n"): + failures.append("body is not separated by a blank line") + if any(len(link_urls_removed(line)) > 80 for line in message.split("\n")[1:]): + failures.append("body line exceeds 80 characters") + if failures: + print("; ".join(failures), file=sys.stderr) + sys.exit(1) + print("Commit message validation passed.") + PY + + This mirrors the workflow's structural checks for the current `HEAD`. + Keep the title and body in English and use the repository's imperative + wording convention; CI remains the authoritative automated check. + +9. Update the active ExecPlan state after the logical commit. Update the active + plan only when this commit reaches a functional-family, architecture, ABI, + release-policy, or milestone checkpoint. + +10. Report the commit hash, subject, paths, focused validations, and any deferred expensive validation. Do not push unless explicitly requested. diff --git a/.agents/skills/logical-commits/agents/openai.yaml b/.agents/skills/logical-commits/agents/openai.yaml new file mode 100644 index 0000000000..f24ed48d20 --- /dev/null +++ b/.agents/skills/logical-commits/agents/openai.yaml @@ -0,0 +1,10 @@ +# Copyright (C) 2026 Amalgam Solucoes em TI Ltda +# +# SPDX-License-Identifier: LGPL-2.1-only + +interface: + display_name: "Logical repository commits" + short_description: "Create validated logical repository commits" + default_prompt: "Use $logical-commits to prepare a focused, validated commit." +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/validate-headers/SKILL.md b/.agents/skills/validate-headers/SKILL.md new file mode 100644 index 0000000000..0868b06dd5 --- /dev/null +++ b/.agents/skills/validate-headers/SKILL.md @@ -0,0 +1,70 @@ +--- +# Copyright (C) 2026 Amalgam Solucoes em TI Ltda +# +# SPDX-License-Identifier: LGPL-2.1-only + +name: validate-headers +description: Validate the repository's LGPL-2.1-only copyright headers on changed or staged first-party files before a commit; use for new files, header fixes, or copyright-check failures, not for generated or vendored artifacts. +--- + +# Validate changed-file headers + +Use the repository validator from the repository root and the narrowest file set +that matches the intended commit. This repository uses LGPL-2.1-only headers; +do not introduce a different license identifier in first-party files. + +1. Identify the scope without dumping the whole worktree: + + git diff --name-only --diff-filter=ACMR --cached -- + + If nothing is staged, inspect the intended working-tree paths with: + + git diff --name-only --diff-filter=ACMR -- + +2. Respect the exclusions implemented by + `scripts/validate-copyright-headers.sh` and documented in `AGENTS.md`. + In particular, skip `TotalCrossVM/deps/`, `build/`, + `TotalCrossSDK/src/main/java/totalcross/util/regex/`, and + `TotalCrossSDK/src/main/java/totalcross/db/sqlite/`, except for + `SQLiteUtil.java` and `ui/DBListBox.java`. The validator also ignores + unsupported file types and `.agent/PLANS.md`. Do not add repository headers + to upstream, vendored, generated, or exempt files merely to make validation + pass. + +3. Prefer changed-file validation when the validator supports it: + + python3 scripts/validate-copyright-headers.sh --files + + For staged changes, omit `--files` so the validator discovers the staged + paths (or the working-tree diff when nothing is staged): + + python3 scripts/validate-copyright-headers.sh + + It also accepts `--commit `, or a base and head commit as positional + arguments. Save verbose output to a temporary log when the file set is + broad, and report only failures relevant to the intended change. + +4. Fix headers with the comment style appropriate to the file. New + first-party files use the current year and: + + // Copyright (C) 2026 Amalgam Solucoes em TI Ltda + // + // SPDX-License-Identifier: LGPL-2.1-only + + Use `#` for shell, Python, Ruby, and YAML files, and an HTML comment for + Markdown and HTML files. A Markdown skill with YAML frontmatter keeps the + repository header as YAML comments inside the frontmatter so the metadata + remains valid. + + For existing files, preserve historical ownership: SuperWaba ranges end in + 2013, TotalCross ranges end in 2021, and Amalgam covers 2022 through the + current year. For files created from 2014 through 2021, the TotalCross range + starts in the Git creation year. For files created from 2000 through 2013, + retain the SuperWaba, TotalCross, and Amalgam sequence required by + `AGENTS.md`. + +5. Rerun the focused check and `git diff --check -- `. + +6. Report only the files checked, pass/fail status, and log path. Do not paste the full repository validation output. + +Do not stage, commit, amend, or push unless the user explicitly requests those actions or the active ExecPlan requires a commit checkpoint. diff --git a/TotalCrossSDK/src/main/java/jdkcompat/lang/IllegalStateException4D.java b/TotalCrossSDK/src/main/java/jdkcompat/lang/IllegalStateException4D.java new file mode 100644 index 0000000000..0ea3929475 --- /dev/null +++ b/TotalCrossSDK/src/main/java/jdkcompat/lang/IllegalStateException4D.java @@ -0,0 +1,26 @@ +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +package jdkcompat.lang; + +public class IllegalStateException4D extends RuntimeException { + static final long serialVersionUID = -1848914673093119416L; + + public IllegalStateException4D() { + } + + public IllegalStateException4D(String message) { + super(message); + } + + public IllegalStateException4D(String message, Throwable cause) { + super(message, cause); + } + + public IllegalStateException4D(Throwable cause) { + super(cause); + } +} diff --git a/TotalCrossSDK/src/main/java/totalcross/lang/IllegalStateException4D.java b/TotalCrossSDK/src/main/java/totalcross/lang/IllegalStateException4D.java deleted file mode 100644 index a3843cb25e..0000000000 --- a/TotalCrossSDK/src/main/java/totalcross/lang/IllegalStateException4D.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. -// -// SPDX-License-Identifier: LGPL-2.1-only - -package totalcross.lang; - -/** - * Signals that a method has been invoked at an illegal or inappropriate time.
- *
- * IMPORTANT: the totalcross.lang package is the java.lang that will be used in the device. You CANNOT use nor import - * totalcross.lang package in desktop. When tc.Deploy is called, all references to java.lang are replaced by - * totalcross.lang automatically. Given this, you must use only the classes and methods that exists BOTH in java.lang - * and totalcross.lang. For example, you can't use java.lang.ClassLoader because there are no - * totalcross.lang.ClassLoader. Another example, you can't use java.lang.String.indexOfIgnoreCase because there are no - * totalcross.lang.String.indexOfIgnoreCase method. Trying to use a class or method from the java.lang package that has - * no correspondence with totalcross.lang will make the tc.Deploy program to abort, informing where the problem occured. - * A good idea is to always refer to this javadoc to know what is and what isn't available. - */ - -public class IllegalStateException4D extends RuntimeException { - static final long serialVersionUID = -1848914673093119416L; - - /** Constructs an empty Exception. */ - public IllegalStateException4D() { - } - - /** Constructs an exception with the given message. */ - public IllegalStateException4D(String message) { - super(message); - } -} diff --git a/TotalCrossVM/CMakeLists.txt b/TotalCrossVM/CMakeLists.txt index 1f5ac58eed..fd25e689e9 100644 --- a/TotalCrossVM/CMakeLists.txt +++ b/TotalCrossVM/CMakeLists.txt @@ -524,7 +524,9 @@ endif(NOT MSVC) if(TCVM_ENABLE_SKIA) set(TC_SKIA_SOURCES - ${TC_SRCDIR}/nm/ui/android/skia.cpp + ${TC_SRCDIR}/nm/ui/skia/skia.cpp + ${TC_SRCDIR}/nm/ui/skia/skia_surface.cpp + ${TC_SRCDIR}/nm/ui/skia/skia_primitives.cpp ) endif(TCVM_ENABLE_SKIA) diff --git a/TotalCrossVM/android/app/build.gradle b/TotalCrossVM/android/app/build.gradle index 3bd01217a6..dfe2f397da 100644 --- a/TotalCrossVM/android/app/build.gradle +++ b/TotalCrossVM/android/app/build.gradle @@ -1,3 +1,8 @@ +// Copyright (C) 2020-2021 TotalCross Global Mobile Platform Ltda +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + apply plugin: 'com.android.application' apply plugin: 'kotlin-android' @@ -75,7 +80,9 @@ android { destinationDirectory = file('src/main/assets') from("${rootDir}/../../TotalCrossSDK/dist/vm") { - include 'TCBase.tcz' + include 'tc.base.lang.tcz' + include 'tc.base.util.tcz' + include 'tc.base.misc.tcz' include 'TCUI.tcz' } from("${rootDir}/../../TotalCrossSDK/etc/fonts") { diff --git a/TotalCrossVM/src/nm/ui/GraphicsPrimitives.h b/TotalCrossVM/src/nm/ui/GraphicsPrimitives.h index 6fcdc6fcc7..285d028def 100644 --- a/TotalCrossVM/src/nm/ui/GraphicsPrimitives.h +++ b/TotalCrossVM/src/nm/ui/GraphicsPrimitives.h @@ -1,5 +1,6 @@ // Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda // // SPDX-License-Identifier: LGPL-2.1-only @@ -11,7 +12,7 @@ #include "tcclass.h" #if defined USE_SKIA && (defined ANDROID || defined darwin || defined HEADLESS) -#include "android/skia.h" +#include "skia/skia.h" #endif #if defined(WINCE) || defined(WIN32) diff --git a/TotalCrossVM/src/nm/ui/GraphicsPrimitivesScreen_c.h b/TotalCrossVM/src/nm/ui/GraphicsPrimitivesScreen_c.h new file mode 100644 index 0000000000..87b18a4a0e --- /dev/null +++ b/TotalCrossVM/src/nm/ui/GraphicsPrimitivesScreen_c.h @@ -0,0 +1,105 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +static bool startupGraphics(int16 appTczAttr) // there are no threads running at this point +{ + return graphicsStartup(&screen, appTczAttr); +} + +static bool createScreenSurface(Context currentContext, bool isScreenChange) +{ + bool ret = false; + if (screen.screenW <= 0 || screen.screenH <= 0) + return false; + + if (graphicsCreateScreenSurface(&screen)) + { + TCObject *screenObj; + screenObj = getStaticFieldObject(currentContext,loadClass(currentContext, "totalcross.ui.gfx.Graphics",false), "mainWindowPixels"); +#ifdef darwin // in darwin, the pixels buffer is pre-initialized and never changed + if (controlEnableUpdateScreenPtr == null) + controlEnableUpdateScreenPtr = getStaticFieldInt(loadClass(currentContext, "totalcross.ui.Control",false), "enableUpdateScreen"); + ret = true; +#else + + if (isScreenChange) + { + screen.mainWindowPixels = *screenObj = null; + gc(currentContext); // let the gc collect the old screen object + } + else + { + controlEnableUpdateScreenPtr = getStaticFieldInt(loadClass(currentContext, "totalcross.ui.Control",false), "enableUpdateScreen"); + } + + *screenObj = screen.mainWindowPixels = createArrayObject(currentContext, INT_ARRAY, screen.screenW * screen.screenH); + setObjectLock(*screenObj, UNLOCKED); + ret = screen.mainWindowPixels != null && controlEnableUpdateScreenPtr != null; +#endif + } + return ret; +} + +void markWholeScreenDirty(Context currentContext) +{ + LOCKVAR(screen); + currentContext->dirtyX1 = currentContext->dirtyY1 = 0; + currentContext->dirtyX2 = screen.screenW; + currentContext->dirtyY2 = screen.screenH; + currentContext->fullDirty = true; + UNLOCKVAR(screen); +} + +static bool checkScreenPixels() +{ + return screen.pixels != null; +} + +void updateScreen(Context currentContext) +{ +#ifdef ANDROID + if (appPaused) return; +#endif + LOCKVAR(screen); + if (keepRunning && checkScreenPixels() && controlEnableUpdateScreenPtr && *controlEnableUpdateScreenPtr && (currentContext->fullDirty || (currentContext->dirtyX1 != screen.screenW && currentContext->dirtyX2 != 0 && currentContext->dirtyY1 != screen.screenH && currentContext->dirtyY2 != 0))) + { + if (updateScreenBits(currentContext)) // move the temporary buffer to the real screen + { +#ifdef darwin + currentContext->dirtyX1 = screen.screenW; + currentContext->dirtyY1 = screen.screenH; + currentContext->dirtyX2 = currentContext->dirtyY2 = 0; + currentContext->fullDirty = false; + UNLOCKVAR(screen); // without this, a deadlock can occur in iOS if the user minimizes the application, since another thread can trigger a markScreenDirty +#endif + graphicsUpdateScreen(currentContext, &screen); +#ifdef darwin + LOCKVAR(screen); +#else + currentContext->dirtyX1 = screen.screenW; + currentContext->dirtyY1 = screen.screenH; + currentContext->dirtyX2 = currentContext->dirtyY2 = 0; + currentContext->fullDirty = false; +#endif + } + } + UNLOCKVAR(screen); +#if defined ANDROID || defined darwin + if (desiredScreenShiftY != UNDEFINED_SHIFTY) + { + setShiftYgl(desiredScreenShiftY); + desiredScreenShiftY = UNDEFINED_SHIFTY; + } +#endif +} + +void graphicsDestroyPrimitives() +{ + xfree(lookupR); + xfree(lookupG); + xfree(lookupB); + xfree(lookupGray); + fontDestroy(); +} +/////////////// End of Device-dependant functions /// diff --git a/TotalCrossVM/src/nm/ui/GraphicsPrimitivesShapes_c.h b/TotalCrossVM/src/nm/ui/GraphicsPrimitivesShapes_c.h new file mode 100644 index 0000000000..2abd162adc --- /dev/null +++ b/TotalCrossVM/src/nm/ui/GraphicsPrimitivesShapes_c.h @@ -0,0 +1,821 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +static SurfaceType getSurfaceType(Context currentContext, TCObject surface) +{ + // cache class pointers for performance + return (surface != NULL && areClassesCompatible(currentContext, OBJ_CLASS(surface), "totalcross.ui.image.Image") == 1) == COMPATIBLE ? SURF_IMAGE : SURF_CONTROL; +} + +#ifndef SKIA_H +//////////////////////////////////////////////////////////////////////////// +static void quadPixel(Context currentContext, TCObject g, int32 xc, int32 yc, int32 x, int32 y, Pixel c) +{ + // draw 4 points using symetry + setPixel(currentContext, g,xc + x, yc + y, c); + setPixel(currentContext, g,xc + x, yc - y, c); + setPixel(currentContext, g,xc - x, yc + y, c); + setPixel(currentContext, g,xc - x, yc - y, c); +} + +static void quadLine(Context currentContext, TCObject g, int32 xc, int32 yc, int32 x, int32 y, Pixel c) +{ + int32 w = x+x+1; // plus 1 for the drawHLine (draws to width-1) + // draw 2 lines using symetry + drawHLine(currentContext, g,xc - x, yc - y, w, c, c); + drawHLine(currentContext, g,xc - x, yc + y, w, c, c); +} + +// draws an ellipse incrementally +static void ellipseDrawAndFill(Context currentContext, TCObject g, int32 xc, int32 yc, int32 rx, int32 ry, Pixel pc1, Pixel pc2, bool fill, bool gradient) +{ + int32 numSteps=0, startRed=0, startGreen=0, startBlue=0, endRed=0, endGreen=0, endBlue=0, redInc=0, greenInc=0, blueInc=0, red=0, green=0, blue=0; + PixelConv c,c1,c2; + // intermediate terms to speed up loop + int64 t1 = (int64)rx*(int64)rx, t2 = t1<<1, t3 = t2<<1; + int64 t4 = (int64)ry*(int64)ry, t5 = t4<<1, t6 = t5<<1; + int64 t7 = (int64)rx*t5, t8 = t7<<1, t9 = 0L; + int64 d1 = t2 - t7 + (t4>>1); // error terms + int64 d2 = (t1>>1) - t8 + t5; + int32 x = rx; // ellipse points + int32 y = 0; // ellipse points + if (rx < 0 || ry < 0) // guich@501_13 + return; + c1.pixel = pc1; + c2.pixel = pc2; + + if (gradient) + { + numSteps = ry + ry; // guich@tc110_11: support horizontal gradient + startRed = c1.r; + startGreen = c1.g; + startBlue = c1.b; + endRed = c2.r; + endGreen = c2.g; + endBlue = c2.b; + redInc = ((endRed - startRed) << 16) / numSteps; + greenInc = ((endGreen - startGreen) << 16) / numSteps; + blueInc = ((endBlue - startBlue) << 16) / numSteps; + red = startRed << 16; + green = startGreen << 16; + blue = startBlue << 16; + } + else c.pixel = c1.pixel; + + while (d2 < 0) // til slope = -1 + { + if (gradient) + { + c.r = (red >> 16) & 0xFF; + c.g = (green >> 16) & 0xFF; + c.b = (blue >> 16) & 0xFF; + red += redInc; + green += greenInc; + blue += blueInc; + } + if (fill) + quadLine(currentContext, g,xc,yc,x,y,c.pixel); + else + quadPixel(currentContext, g,xc,yc,x,y,c.pixel); + y++; // always move up here + t9 += t3; + if (d1 < 0) // move straight up + { + d1 += t9 + t2; + d2 += t9; + } + else // move up and left + { + --x; + t8 -= t6; + d1 += t9 + t2 - t8; + d2 += t9 + t5 - t8; + } + } + + do // rest of top right quadrant + { + if (gradient) + { + c.r = (red >> 16) & 0xFF; + c.g = (green >> 16) & 0xFF; + c.b = (blue >> 16) & 0xFF; + red += redInc; + green += greenInc; + blue += blueInc; + } + // draw 4 points using symmetry + if (fill) + quadLine(currentContext, g,xc,yc,x,y,c.pixel); + else + quadPixel(currentContext, g,xc,yc,x,y,c.pixel); + --x; // always move left here + t8 -= t6; + if (d2 < 0) // move up and left + { + ++y; + t9 += t3; + d2 += t9 + t5 - t8; + } + else d2 += t5 - t8; // move straight left + } while (x >= 0); +} +#else +static void ellipseDrawAndFill(Context currentContext, TCObject g, int32 xc, int32 yc, int32 rx, int32 ry, Pixel pc1, Pixel pc2, bool fill, bool gradient) +{ + xc += Graphics_transX(g); + yc += Graphics_transY(g); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_ellipseDrawAndFill(skiaSurfaceForGraphics(g), xc, yc, rx, ry, pc1 | Graphics_alpha(g), pc2 | Graphics_alpha(g), fill, gradient); + skia_restoreClip(skiaSurfaceForGraphics(g)); + + markDirty(currentContext, g, xc - rx, yc + ry, rx * 2, ry * 2); +} +#endif + +#ifndef SKIA_H +//////////////////////////////////////////////////////////////////////////// +// Generalized Polygon Fill +static void qsortInts(int32 *items, int32 first, int32 last) +{ + int32 low = first; + int32 high = last, mid; + if (first >= last) + return; + mid = items[(first+last) >> 1]; + while (true) + { + while (high >= low && items[low] < mid) // guich@566_25: added "high > low" here and below - guich@568_5: changed to >= + low++; + while (high >= low && items[high] > mid) + high--; + if (low <= high) + { + int32 temp = items[low]; + items[low++] = items[high]; + items[high--] = temp; + } + else break; + } + if (first < high) + qsortInts(items, first,high); + if (low < last) + qsortInts(items, low,last); +} + +static TCObject growIntArray(Context currentContext, TCObject oldArrayObj, int32 newLen) // must unlock the returned obj +{ + TCObject newArrayObj = createArrayObject(currentContext, INT_ARRAY, newLen); + int32 *newArray,*oldArray, oldLen; + if (newArrayObj != null) + { + newArray = (int32*)ARRAYOBJ_START(newArrayObj); + oldArray = (int32*)ARRAYOBJ_START(oldArrayObj); + oldLen = ARRAYOBJ_LEN(oldArrayObj); + xmemmove(newArray, oldArray, oldLen * 4); + } + return newArrayObj; +} + +static void fillPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel c1, Pixel c2, bool gradient, bool isPie) +{ + int32 x1, y1, x2, y2,y,n=0,temp, i,j, miny, maxy, a, numSteps=0, startRed=0, startGreen=0, startBlue=0, endRed=0, endGreen=0, endBlue=0, redInc=0, greenInc=0, blueInc=0, red=0, green=0, blue=0; + int32 *yp; + int32 *axPoints[2], *ayPoints[2], anPoints[2]; + TCObject *intsObj = &Graphics_ints(g); + int32 *ints = *intsObj ? (int32*)ARRAYOBJ_START(*intsObj) : null; + PixelConv c; + + if (!xPoints1 || !yPoints1 || nPoints1 < 2) + return; + +#if defined __gl2_h_ + if (!gradient && (nPoints1 == 0 || isConvexAndInsideClip(g, tx, ty, xPoints1, yPoints1, nPoints1, isPie)) && (nPoints2 == 0 || isConvexAndInsideClip(g, tx, ty, xPoints2, yPoints2, nPoints2, isPie)) && Graphics_useOpenGL(g)) // opengl doesnt fills non-convex polygons well + { + if (nPoints1 > 0) + glDrawLines(currentContext, g, xPoints1, yPoints1, nPoints1, tx + Graphics_transX(g), ty + Graphics_transY(g), c1, true); + if (nPoints2 > 0) + glDrawLines(currentContext, g, xPoints2, yPoints2, nPoints2, tx + Graphics_transX(g), ty + Graphics_transY(g), c1, true); + return; + } +#endif + + axPoints[0] = xPoints1; ayPoints[0] = yPoints1; anPoints[0] = nPoints1; + axPoints[1] = xPoints2; ayPoints[1] = yPoints2; anPoints[1] = nPoints2; + + yp = yPoints1; + miny = maxy = *yp++; + for (i = nPoints1; --i > 0; yp++) + { + if (*yp < miny) miny = *yp; + if (*yp > maxy) maxy = *yp; + } + yp = yPoints2; + for (i = nPoints2; --i >= 0; yp++) + { + if (*yp < miny) miny = *yp; + if (*yp > maxy) maxy = *yp; + } + miny += ty; + maxy += ty; + + if (ints == null) + { + *intsObj = createArrayObject(currentContext, INT_ARRAY, 2); // 2 is the most used length + if (*intsObj == null) + return; + setObjectLock(*intsObj, UNLOCKED); + ints = (int32*)ARRAYOBJ_START(*intsObj); + } + if (gradient) + { + numSteps = maxy - miny; // guich@tc110_11: support horizontal gradient + if (numSteps == 0) numSteps = 1; // guich@tc115_86: prevent divide by 0 + c.pixel = c1; + startRed = c.r; + startGreen = c.g; + startBlue = c.b; + c.pixel = c2; + endRed = c.r; + endGreen = c.g; + endBlue = c.b; + redInc = ((endRed - startRed) << 16) / numSteps; + greenInc = ((endGreen - startGreen) << 16) / numSteps; + blueInc = ((endBlue - startBlue) << 16) / numSteps; + red = startRed << 16; + green = startGreen << 16; + blue = startBlue << 16; + } + else c.pixel = c1; + for (y = miny; y <= maxy; y++) + { + n = 0; + for (a = 0; a < 2; a++) + { + int32 nPoints = anPoints[a]; + int32* xPoints = axPoints[a]; + int32* yPoints = ayPoints[a]; + j = nPoints-1; + for (i = 0; i < nPoints; j=i,i++) + { + y1 = yPoints[j]+ty; + y2 = yPoints[i]+ty; + if (y1 == y2) + continue; + if (y1 > y2) // invert + { + temp = y1; + y1 = y2; + y2 = temp; + } + // compute next x point + if ( (y1 <= y && y < y2) || (y == maxy && y1 < y && y <= y2) ) + { + if (n == (int32)ARRAYOBJ_LEN(*intsObj)) // have to grow the ints array? + { + TCObject newIntsObj = growIntArray(currentContext, *intsObj, n * 2); + if (newIntsObj == null) + return; + *intsObj = newIntsObj; + setObjectLock(*intsObj, UNLOCKED); + ints = (int32*)ARRAYOBJ_START(*intsObj); + } + if (yPoints[j] < yPoints[i]) + { + x1 = xPoints[j]+tx; + x2 = xPoints[i]+tx; + } + else + { + x2 = xPoints[j]+tx; + x1 = xPoints[i]+tx; + } + ints[n++] = (y - y1) * (x2 - x1) / (y2 - y1) + x1; + } + } + } + if (n >= 2) + { + if (gradient) + { + c.r = (red >> 16) & 0xFF; + c.g = (green >> 16) & 0xFF; + c.b = (blue >> 16) & 0xFF; + red += redInc; + green += greenInc; + blue += blueInc; + } + if (n == 2) // most of the times + { + if (ints[1] > ints[0]) + drawHLine(currentContext, g,ints[0],y,ints[1]-ints[0],c.pixel,c.pixel); + else + drawHLine(currentContext, g,ints[1],y,ints[0]-ints[1],c.pixel,c.pixel); + } + else + { + qsortInts(ints, 0, n-1); + for (n>>=1, yp = ints; --n >= 0; yp+=2) + drawHLine(currentContext, g,yp[0],y,yp[1]-yp[0],c.pixel,c.pixel); + } + } + } +} +#else +static void fillPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel c1, Pixel c2, bool gradient, bool isPie) +{ + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_fillPolygon(skiaSurfaceForGraphics(g), xPoints1, yPoints1, nPoints1, Graphics_transX(g), Graphics_transY(g), c1 | Graphics_alpha(g), c2 | Graphics_alpha(g), gradient, isPie); + skia_restoreClip(skiaSurfaceForGraphics(g)); + + // to avoid computing the polygon's bounds, we mark dirty the current clip + markDirty(currentContext, g, Graphics_clipX1(g), Graphics_clipY1(g), Graphics_clipX2(g) - Graphics_clipX1(g), Graphics_clipY2(g) - Graphics_clipY1(g)); +} +#endif + +//////////////////////////////////////////////////////////////////////////// +// draws a polygon. if the polygon is not closed, close it +#ifndef SKIA_H +static void drawPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel pixel) +{ + if (xPoints1 && yPoints1 && nPoints1 >= 2) + { +#if defined __gl2_h_ + if (Graphics_useOpenGL(g) && (nPoints1 == 0 || isInsideClip(g, tx, ty, xPoints1, yPoints1, nPoints1)) && (nPoints2 == 0 || isInsideClip(g, tx, ty, xPoints2, yPoints2, nPoints2))) + { + if (nPoints1 > 0) + glDrawLines(currentContext, g, xPoints1, yPoints1, nPoints1, tx + Graphics_transX(g), ty + Graphics_transY(g), pixel, false); + if (nPoints2 > 0) + glDrawLines(currentContext, g, xPoints2, yPoints2, nPoints2, tx + Graphics_transX(g), ty + Graphics_transY(g), pixel, false); + } + else +#endif + { + int32 i; + for (i=1; i < nPoints1; i++) + drawLine(currentContext, g,tx + xPoints1[i-1], ty + yPoints1[i-1], tx + xPoints1[i], ty + yPoints1[i], pixel); + for (i=1; i < nPoints2; i++) + drawLine(currentContext, g,tx + xPoints2[i-1], ty + yPoints2[i-1], tx + xPoints2[i], ty + yPoints2[i], pixel); + } + } +} +#else +static void drawPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel pixel) +{ + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_drawPolygon(skiaSurfaceForGraphics(g), xPoints1, yPoints1, nPoints1, Graphics_transX(g), Graphics_transY(g), pixel | Graphics_alpha(g)); + skia_restoreClip(skiaSurfaceForGraphics(g)); + + // to avoid computing the polygon's bounds, we mark dirty the current clip + markDirty(currentContext, g, Graphics_clipX1(g), Graphics_clipY1(g), Graphics_clipX2(g) - Graphics_clipX1(g), Graphics_clipY2(g) - Graphics_clipY1(g)); +} +#endif +//////////////////////////////////////////////////////////////////////////// +// draw an elliptical arc from startAngle to endAngle. +// c is the fill color and c2 is the outline color +// (if in fill mode - otherwise, c = outline color) +#ifdef SKIA_H +static void arcPiePointDrawAndFill(Context currentContext, TCObject g, int32 xc, int32 yc, int32 rx, int32 ry, double startAngle, double endAngle, Pixel c, Pixel c2, bool fill, bool pie, bool gradient) +{ + xc += Graphics_transX(g); + yc += Graphics_transY(g); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_arcPiePointDrawAndFill(skiaSurfaceForGraphics(g), xc, yc, rx, ry, startAngle, endAngle, c | Graphics_alpha(g), c2 | Graphics_alpha(g), fill, pie, gradient); + skia_restoreClip(skiaSurfaceForGraphics(g)); + + markDirty(currentContext, g, xc - rx, yc + ry, rx * 2, ry * 2); +} +#else +static void arcPiePointDrawAndFill(Context currentContext, TCObject g, int32 xc, int32 yc, int32 rx, int32 ry, double startAngle, double endAngle, Pixel c, Pixel c2, bool fill, bool pie, bool gradient) +{ + // this algorithm was created by Guilherme Campos Hazan + double ppd; + int32 startIndex,endIndex,index,i,nq,size=0,oldX1=0,oldY1=0,last,oldX2=0,oldY2=0; + bool sameR,startSetTo0 = true; + TCObject *xPointsObj = &Graphics_xPoints(g); + TCObject *yPointsObj = &Graphics_yPoints(g); + int32 *xPoints = *xPointsObj ? (int32*)ARRAYOBJ_START(*xPointsObj) : null; + int32 *yPoints = *yPointsObj ? (int32*)ARRAYOBJ_START(*yPointsObj) : null; + int32 clipFactor = Graphics_minX(g) * 1000000000 + Graphics_maxX(g) * 10000000 + Graphics_minY(g) * 100000 + Graphics_maxY(g); + bool sameClipFactor = Graphics_lastClipFactor(g) == clipFactor; + + if (rx < 0 || ry < 0) // guich@501_13 + return; + // make sure the values are -359 <= x <= 359 + while (startAngle <= -360) startAngle += 360; + while (endAngle <= -360) endAngle += 360; + while (startAngle > 360) startAngle -= 360; + while (endAngle > 360) endAngle -= 360; + + if (startAngle == endAngle) // guich@501_13 + return; + if (startAngle > endAngle) // eg 235 to 45 + startAngle -= 360; // set to -45 to 45 so we can handle it correctly + if (startAngle >= 0 && endAngle <= 0) // eg 135 to -135 + endAngle += 360; // set to 135 to 225 + + // step 0: correct angle values + if (startAngle < 0.1 && endAngle > 359.9) // full circle? use the fastest routine instead + { + if (fill) + ellipseDrawAndFill(currentContext, g,xc, yc, rx, ry, c, c2, true, gradient); + ellipseDrawAndFill(currentContext, g,xc, yc, rx, ry, c, c, false, gradient); + return; + } + + // step 0: if possible, use cached results + sameR = rx == Graphics_lastRX(g) && ry == Graphics_lastRY(g); + if (!sameClipFactor || !sameR) + { + // step 1: computes how many points the circle has (computes only 45 degrees and mirrors the rest) + // intermediate terms to speed up loop + int64 t1 = (int64)rx*(int64)rx, t2 = t1<<1, t3 = t2<<1; + int64 t4 = (int64)ry*(int64)ry, t5 = t4<<1, t6 = t5<<1; + int64 t7 = (int64)rx*t5, t8 = t7<<1, t9 = 0L; + int64 d1 = t2 - t7 + (t4>>1); // error terms + int64 d2 = (t1>>1) - t8 + t5; + int32 x = rx; // ellipse points + int32 y = 0; // ellipse points + + while (d2 < 0) // til slope = -1 + { + t9 += t3; + if (d1 < 0) // move straight up + { + d1 += t9 + t2; + d2 += t9; + } + else // move up and left + { + --x; + t8 -= t6; + d1 += t9 + t2 - t8; + d2 += t9 + t5 - t8; + } + ++size; + } + + do // rest of top right quadrant + { + --x; // always move left here + t8 -= t6; + if (d2 < 0) // move up and left + { + t9 += t3; + d2 += t9 + t5 - t8; + } + else d2 += t5 - t8; // move straight left + ++size; + } while (x >= 0); + nq = size; + size *= 4; + // step 2: computes how many points per degree + ppd = (double)size / 360.0f; + // step 3: create space in the buffer so it can save all the circle + size+=2; + if (pie) size++; + if (xPoints == null || ARRAYOBJ_LEN(*xPointsObj) != (uint32)size) // guich@tc304: changed < to != to fix a glytch when drawing two pies with different radius + { + *xPointsObj = createArrayObject(currentContext, INT_ARRAY, max32(3,size)); + if (*xPointsObj == null) + return; + *yPointsObj = createArrayObject(currentContext, INT_ARRAY, max32(3,size)); + if (*yPointsObj == null) + { + setObjectLock(*xPointsObj, UNLOCKED); + return; + } + setObjectLock(*xPointsObj, UNLOCKED); + setObjectLock(*yPointsObj, UNLOCKED); + } + xPoints = (int32*)ARRAYOBJ_START(*xPointsObj); + yPoints = (int32*)ARRAYOBJ_START(*yPointsObj); + if (pie) {xPoints++; yPoints++;} // make sure that startIndex-1 is at a valid pointer + + // step 4: stores all the circle in the array. the odd arcs are drawn in reverse order + // intermediate terms to speed up loop + t2 = t1<<1; + t3 = t2<<1; + t8 = t7<<1; + t9 = 0; + d1 = t2 - t7 + (t4>>1); // error terms + d2 = (t1>>1) - t8 + t5; + x = rx; + i=0; + while (d2 < 0) // til slope = -1 + { + // save 4 points using symmetry + index = nq*0+i; // 0/3 + xPoints[index]=+x; + yPoints[index]=-y; + + index = (nq<<1)-i-1; // 1/3 + xPoints[index]=-x; + yPoints[index]=-y; + + index = (nq<<1)+i; // 2/3 + xPoints[index]=-x; + yPoints[index]=+y; + + index = (nq<<2)-i-1; // 3/3 + xPoints[index]=+x; + yPoints[index]=+y; + i++; + y++; // always move up here + t9 += t3; + if (d1 < 0) // move straight up + { + d1 += t9 + t2; + d2 += t9; + } + else // move up and left + { + x--; + t8 -= t6; + d1 += t9 + t2 - t8; + d2 += t9 + t5 - t8; + } + } + + do // rest of top right quadrant + { + // save 4 points using symmetry + index = nq*0+i; // 0/3 + xPoints[index]=+x; + yPoints[index]=-y; + + index = (nq<<1)-i-1; // 1/3 + xPoints[index]=-x; + yPoints[index]=-y; + + index = (nq<<1)+i; // 2/3 + xPoints[index]=-x; + yPoints[index]=+y; + + index = (nq<<2)-i-1; // 3/3 + xPoints[index]=+x; + yPoints[index]=+y; + + ++i; + --x; // always move left here + t8 -= t6; + if (d2 < 0) // move up and left + { + ++y; + t9 += t3; + d2 += t9 + t5 - t8; + } + else d2 += t5 - t8; // move straight left + } while (x >= 0); + // save last arguments + //Graphics_lastXC(g) = xc; no longer + //Graphics_lastYC(g) = yc; needed + Graphics_lastRX(g) = rx; + Graphics_lastRY(g) = ry; + Graphics_lastPPD(g) = ppd; + Graphics_lastSize(g) = size; + Graphics_lastClipFactor(g) = clipFactor; + } + else + { + size = Graphics_lastSize(g); + ppd = Graphics_lastPPD(g); + } + // step 5: computes the start and end indexes that will become part of the arc + if (startAngle < 0) + startAngle += 360; + if (endAngle < 0) + endAngle += 360; + startIndex = (int32)(ppd * startAngle); + endIndex = (int32)(ppd * endAngle); + + last = size-2; + if (endIndex >= last) // 360? + endIndex--; + // step 6: fill or draw the polygons + endIndex++; + if (pie) + { + // connect two lines from the center to the two edges of the arc + oldX1 = xPoints[endIndex]; + oldY1 = yPoints[endIndex]; + xPoints[endIndex] = yPoints[endIndex] = 0; + if (xPoints[startIndex] == 0 && yPoints[startIndex] == 0) + startSetTo0 = false; + else + { + startIndex--; + oldX2 = xPoints[startIndex]; + oldY2 = yPoints[startIndex]; + xPoints[startIndex] = yPoints[startIndex] = 0; + } + endIndex++; + } + + if (startIndex > endIndex) // drawing from angle -30 to +30 ? (startIndex = 781, endIndex = 73, size=854) + { + int p1 = last-startIndex; + if (fill) + fillPolygon(currentContext, g, xPoints+startIndex, yPoints+startIndex, p1, xPoints, yPoints, endIndex, xc,yc, gradient ? c : c2, c2, gradient, true); // lower half, upper half + if (!gradient) drawPolygon(currentContext, g, xPoints+startIndex, yPoints+startIndex, p1-1, xPoints+1, yPoints+1, endIndex-1, xc,yc, c); + } + else + { + int32 arc = pie ? 0 : 1; + if (fill) + fillPolygon(currentContext, g, xPoints+startIndex, yPoints+startIndex, endIndex-startIndex, 0,0,0, xc,yc, gradient ? c : c2, c2, gradient, true); + if (!gradient) drawPolygon(currentContext, g, xPoints+startIndex+arc, yPoints+startIndex+arc, endIndex-startIndex-arc, 0,0,0, xc,yc, c); + } + if (pie) // restore saved points + { + if (startSetTo0) + { + xPoints[startIndex] = oldX2; + yPoints[startIndex] = oldY2; + } + endIndex--; + xPoints[endIndex] = oldX1; + yPoints[endIndex] = oldY1; +#ifdef ANDROID + if (!gradient && endAngle == 360) + drawLine(currentContext,g, xc,yc, xc+xPoints[endIndex-1], yc+yPoints[endIndex-1], c); +#endif + } +} +#endif +//////////////////////////////////////////////////////////////////////////// +#ifndef SKIA_H +static void drawRoundRect(Context currentContext, TCObject g, int32 x, int32 y, int32 width, int32 height, int32 r, Pixel c) +{ + int32 x1, y1, x2, y2, dec, xx, yy; + int32 w, h; + r = min32(r,min32(width/2,height/2)); + w = width - 2*r; + h = height - 2*r; + x1 = x+r; + y1 = y+r; + x2 = x+width-r-1; + y2 = y+height-r-1; + dec = 3-2*r; + + drawHLine(currentContext, g,x+r, y, w, c, c); // top + drawHLine(currentContext, g,x+r, y+height-1, w, c, c); // bottom + drawVLine(currentContext, g,x, y+r, h, c, c); // left + drawVLine(currentContext, g,x+width-1, y+r, h, c, c); // right + + // draw the round rectangles. + for (xx = 0, yy = r; xx <= yy; xx++) + { + setPixel(currentContext, g,x2+xx, y2+yy, c); + setPixel(currentContext, g,x2+xx, y1-yy, c); + setPixel(currentContext, g,x1-xx, y2+yy, c); + setPixel(currentContext, g,x1-xx, y1-yy, c); + + setPixel(currentContext, g,x2+yy, y2+xx, c); + setPixel(currentContext, g,x2+yy, y1-xx, c); + setPixel(currentContext, g,x1-yy, y2+xx, c); + setPixel(currentContext, g,x1-yy, y1-xx, c); + if (dec >= 0) + dec += -4*(yy--)+4; + dec += 4*xx+6; + } +} +#else +static void drawRoundRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, int32 r, Pixel c) +{ + x += Graphics_transX(g); + y += Graphics_transY(g); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_drawRoundRect(skiaSurfaceForGraphics(g), x, y, w, h, r, c | Graphics_alpha(g)); + skia_restoreClip(skiaSurfaceForGraphics(g)); + + markDirty(currentContext, g, x, y, w, h); +} +#endif + +#ifndef SKIA_H +//////////////////////////////////////////////////////////////////////////// +static void setPixelA(Context currentContext, TCObject g, int32 x, int32 y, PixelConv color, int32 alpha); + +//////////////////////////////////////////////////////////////////////////// +static void fillRoundRect(Context currentContext, TCObject g, int32 xx, int32 yy, int32 width, int32 height, int32 r, Pixel c) +{ + int32 px1,px2,py1,py2,xm,ym,x,y=0, i, x2, e2, err; + PixelConv color; + if (r > (width/2) || r > (height/2)) r = min32(width/2,height/2); // guich@200b4_6: correct bug that crashed the device. + + x = -r; + err = 2 - 2 * r; + color.pixel = c; + + px1 = xx+r; + py1 = yy+r; + px2 = xx+width-r-1; + py2 = yy+height-r-1; + + height -= 2*r; + yy += r; + while (height--) + drawHLine(currentContext, g,xx, yy++, width, c, c); + + r = 1 - err; + do + { + i = 255 - 255 * abs(err - 2 * (x + y) - 2) / r; + + drawLine(currentContext, g, px1+x+1,py1-y,px2-x-1,py1-y,c); + drawLine(currentContext, g, px1+x+1,py2+y,px2-x-1,py2+y,c); + + if (i < 256 && i > 0) + { + xm = px2; ym = py2; setPixelA(currentContext, g, xm - x, ym + y, color, i); // br + xm = px1; ym = py2; setPixelA(currentContext, g, xm - y, ym - x, color, i); // bl + xm = px1; ym = py1; setPixelA(currentContext, g, xm + x, ym - y, color, i); // tl + xm = px2; ym = py1; setPixelA(currentContext, g, xm + y, ym + x, color, i); // tr + } + e2 = err; + x2 = x; + if (err + y > 0) + { + i = 255 - 255 * (err - 2 * x - 1) / r; + if (i < 256 && i > 0) + { + xm = px2; ym = py2; setPixelA(currentContext, g, xm - x, ym + y + 1, color, i); + xm = px1; ym = py2; setPixelA(currentContext, g, xm - y - 1, ym - x, color, i); + xm = px1; ym = py1; setPixelA(currentContext, g, xm + x, ym - y - 1, color, i); + xm = px2; ym = py1; setPixelA(currentContext, g, xm + y + 1, ym + x, color, i); + } + err += ++x * 2 + 1; + } + if (e2 + x2 <= 0) + { + i = 255 - 255 * (2 * y + 3 - e2) / r; + if (i < 256 && i > 0) + { + xm = px2; ym = py2; setPixelA(currentContext, g, xm - x2 - 1, ym + y, color, i); + xm = px1; ym = py2; setPixelA(currentContext, g, xm - y, ym - x2 - 1, color, i); + xm = px1; ym = py1; setPixelA(currentContext, g, xm + x2 + 1, ym - y, color, i); + xm = px2; ym = py1; setPixelA(currentContext, g, xm + y, ym + x2 + 1, color, i); + } + err += ++y * 2 + 1; + } + } while (x < 0); +} +#else +static void fillRoundRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, int32 r, Pixel c) +{ + x += Graphics_transX(g); + y += Graphics_transY(g); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_fillRoundRect(skiaSurfaceForGraphics(g), x, y, w, h, r, c | Graphics_alpha(g)); + skia_restoreClip(skiaSurfaceForGraphics(g)); + + markDirty(currentContext, g, x, y, w, h); +} +#endif + +#if 1//ndef SKIA_H +// Translates the given coords and returns the intersection between +// the clip rect and the coords passed. +// Returns: 1 if OK, 0 if the coords are outside the clip rect +static bool translateAndClip(TCObject g, int32 *pX, int32 *pY, int32 *pWidth, int32 *pHeight) +{ + int32 x = *pX; + int32 y = *pY; + int32 w = *pWidth; + int32 h = *pHeight; + x += Graphics_transX(g); + y += Graphics_transY(g); + if (x < Graphics_clipX1(g)) + { + if ((x+w) > Graphics_clipX2(g)) + w = Graphics_clipX2(g) - Graphics_clipX1(g); + else + w -= Graphics_clipX1(g)-x; + x = Graphics_clipX1(g); + } + else + if ((x+w) > Graphics_clipX2(g)) + w = Graphics_clipX2(g) - x; + if (y < Graphics_clipY1(g)) + { + if ((y+h) > Graphics_clipY2(g)) + h = Graphics_clipY2(g) - Graphics_clipY1(g); + else + h -= Graphics_clipY1(g)-y; + y = Graphics_clipY1(g); + } + else + if ((y+h) > Graphics_clipY2(g)) + h = Graphics_clipY2(g) - y; + + if (x < 0 || y < 0 || h <= 0 || w <= 0) return false; // guich@566_42: check the resulting w/h - guich@tc112_34: check also x and y + + *pX = x; + *pY = y; + *pWidth = w; + *pHeight = h; + return true; +} +#endif diff --git a/TotalCrossVM/src/nm/ui/GraphicsPrimitivesSkia_c.h b/TotalCrossVM/src/nm/ui/GraphicsPrimitivesSkia_c.h new file mode 100644 index 0000000000..6cfbed36b6 --- /dev/null +++ b/TotalCrossVM/src/nm/ui/GraphicsPrimitivesSkia_c.h @@ -0,0 +1,418 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +#ifndef GRAPHICSPRIMITIVESSKIA_HELPER_H +#define GRAPHICSPRIMITIVESSKIA_HELPER_H + +#ifdef SKIA_H +static int32 skiaSurfaceForGraphics(TCObject g) +{ + if (!Graphics_isImageSurface(g)) + return SKIA_SCREEN_SURFACE_ID; + + TCObject image = Graphics_surface(g); + if (image == null) + return SKIA_INVALID_SURFACE_ID; + + int32 id = Image_textureId(image); + if (id >= 0) + return id; + + int32 frameCount = Image_frameCount(image); + TCObject pixelsObj = frameCount > 1 ? Image_pixelsOfAllFrames(image) : Image_pixels(image); + Pixel* pixels = (Pixel*)ARRAYOBJ_START(pixelsObj); + int32 width = frameCount > 1 ? Image_widthOfAllFrames(image) : Image_width(image); + int32 height = Image_height(image); + id = skia_makeBitmap(SKIA_SCREEN_SURFACE_ID, pixels, width, height); + if (id >= 0) { + Image_textureId(image) = id; + Image_changed(image) = false; + } + return id; +} +#endif + +#endif + +#ifndef SKIA_H +static void drawSurface(Context currentContext, TCObject dstSurf, TCObject srcSurf, int32 srcX, int32 srcY, int32 width, int32 height, + int32 dstX, int32 dstY, int32 doClip) +{ + uint32 i; + Pixel * srcPixels; + Pixel * dstPixels; + int32 srcPitch, srcWidth, srcHeight, alphaMask = 0; + bool isSrcScreen = !Surface_isImage(srcSurf); + bool unlockSrc = false; + if (Surface_isImage(srcSurf)) + { +#ifdef __gl2_h_ // for opengl, we will use the smoothScaled only if we will draw on an image. for win32, we will always use smoothScale + bool forcedSmoothScale = Graphics_isImageSurface(dstSurf); // the destination is always a Graphics object +#else + bool forcedSmoothScale = true; +#endif + srcPitch = srcWidth = (int32)(Image_width(srcSurf) * Image_hwScaleW(srcSurf)); + srcHeight = (int32)(Image_height(srcSurf) * Image_hwScaleH(srcSurf)); + if (forcedSmoothScale && (Image_hwScaleW(srcSurf) != 1 || Image_hwScaleH(srcSurf) != 1)) + { + static Method mGetScaledInstance, mGetSmoothScaledInstance; + TCObject newSurf; + if (mGetScaledInstance == null) + { + mGetScaledInstance = getMethod(OBJ_CLASS(srcSurf), false, "getScaledInstance", 2, J_INT, J_INT); + mGetSmoothScaledInstance = getMethod(OBJ_CLASS(srcSurf), false, "getSmoothScaledInstance", 2, J_INT, J_INT); + } + disableGC = true; // the gc may collect the image before we lock it here (*) + newSurf = executeMethod(currentContext, Image_hwScaleW(srcSurf) < 1 && Image_hwScaleH(srcSurf) < 1 ? mGetSmoothScaledInstance : mGetScaledInstance, srcSurf, srcWidth, srcHeight).asObj; + if (newSurf == null || newSurf == (TCObject)0xFFFFFFFF) + { + currentContext->thrownException = null; + return; + } + else + { + srcSurf = newSurf; + setObjectLock(newSurf, LOCKED); // (*) + unlockSrc = true; + } + disableGC = false; + srcPitch = srcWidth = Image_width(srcSurf); + srcHeight = Image_height(srcSurf); + } + alphaMask = Image_alphaMask(srcSurf); + } + else + { + srcPitch = srcWidth = screen.screenW; + srcHeight = screen.screenH; + } + dstPixels = getSurfacePixels(dstSurf); + srcPixels = getSurfacePixels(srcSurf); + if (!doClip) + { + /* + | Even if no clip is required, we still have to make sure that the + | area of the bitmap that we want to copy is inside its area. + */ + if (srcX <= -width || srcX >= srcWidth || srcY <= -height || srcY >= srcHeight) + goto end; + dstX += Graphics_transX(dstSurf); + dstY += Graphics_transY(dstSurf); + } + else + { + dstX += Graphics_transX(dstSurf); + dstY += Graphics_transY(dstSurf); + + /* clip the source rectangle to the source surface */ + if (srcX < 0) + { + width += srcX; + dstX -= srcX; + srcX = 0; + } + i = srcWidth - srcX; + if (width > (int32)i) + width = i; + if (srcY < 0) + { + height += srcY; + dstY -= srcY; + srcY = 0; + } + i = srcHeight - srcY; + if (height > (int32)i) + height = i; + + /* clip the destination rectangle against the clip rectangle */ + if (dstX < Graphics_clipX1(dstSurf)) + { + i = Graphics_clipX1(dstSurf) - dstX; + dstX = Graphics_clipX1(dstSurf); + srcX += i; + width -= i; + } + if ((dstX + width) > Graphics_clipX2(dstSurf)) + width = Graphics_clipX2(dstSurf) - dstX; + if (dstY < Graphics_clipY1(dstSurf)) + { + i = Graphics_clipY1(dstSurf) - dstY; + dstY = Graphics_clipY1(dstSurf); + srcY += i; + height -= i; + } + if ((dstY + height) > Graphics_clipY2(dstSurf)) + height = Graphics_clipY2(dstSurf) - dstY; + + /* check the validity */ + if (width <= 0 || height <= 0) + goto end; + } + + srcPixels += srcY * srcPitch + srcX; + dstPixels += dstY * Graphics_pitch(dstSurf) + dstX; +#ifdef __gl2_h_ + if (isSrcScreen) + glGetPixels(dstPixels,srcX,srcY,width,height,Graphics_pitch(dstSurf)); + else + if (Graphics_useOpenGL(dstSurf)) + { + int32 fc; + int frame; + + if (Image_changed(srcSurf)) + applyChanges(currentContext, srcSurf); + fc = Image_frameCount(srcSurf); + frame = (fc <= 1) ? 0 : Image_currentFrame(srcSurf); + Image_lastAccess(srcSurf) = getTimeStamp(); +/*img*/ glDrawTexture(Image_textureId(srcSurf), srcX+frame*srcPitch,srcY,width,height, dstX,dstY, 0,0, fc > 1 ? (int32)(Image_widthOfAllFrames(srcSurf) * Image_hwScaleW(srcSurf)) : srcWidth,srcHeight, null, alphaMask); + } + else +#endif + for (i=0; i < (uint32)height; i++) // in opengl, only case of image drawing on image + { + PixelConv *ps = (PixelConv*)srcPixels; + PixelConv *pt = (PixelConv*)dstPixels; + uint32 count = width; + if (isSrcScreen) + for (;count != 0; pt++,ps++, count--) + { + pt->pixel = ps->pixel; + pt->a = 0xFF; + } + else + for (;count != 0; pt++,ps++, count--) + { + int32 a = ps->a * alphaMask; + a = (a+1 + (a >> 8)) >> 8; // alphaMask * a / 255 + if (a == 0xFF) + pt->pixel = ps->pixel; + else + if (a != 0) + { + int32 ma = 0xFF-a; + int32 r = (a * ps->r + ma * pt->r); + int32 g = (a * ps->g + ma * pt->g); + int32 b = (a * ps->b + ma * pt->b); + pt->r = (r+1 + (r >> 8)) >> 8; // fast way to divide by 255 + pt->g = (g+1 + (g >> 8)) >> 8; + pt->b = (b+1 + (b >> 8)) >> 8; + } + } + srcPixels += srcPitch; + dstPixels += Graphics_pitch(dstSurf); + } +#ifndef __gl2_h_ + if (!currentContext->fullDirty && !Graphics_isImageSurface(dstSurf)) markScreenDirty(currentContext, dstX, dstY, width, height); +#else + if (Graphics_isImageSurface(dstSurf)) + Image_changed(Graphics_surface(dstSurf)) = true; + else + currentContext->fullDirty = true; +#endif +end: + if (unlockSrc) + setObjectLock(srcSurf, UNLOCKED); +} +#else +static void drawSurface(Context currentContext, TCObject dstSurf, TCObject srcSurf, int32 srcX, int32 srcY, int32 w, int32 h, + int32 dstX, int32 dstY, int32 doClip) { + if (Surface_isImage(srcSurf)) { + int32 srcWidth = (int32)(Image_width(srcSurf) * Image_hwScaleW(srcSurf)); + int32 srcHeight = (int32)(Image_height(srcSurf) * Image_hwScaleH(srcSurf)); + double scaleW = Image_hwScaleW(srcSurf); + double scaleH = Image_hwScaleH(srcSurf); + int32 frameCount = Image_frameCount(srcSurf); + int32 frame = 0; + bool clipSet = false; + + if (scaleW <= 0 || scaleH <= 0 || w <= 0 || h <= 0 || srcWidth <= 0 || srcHeight <= 0) { + return; + } + + if (!doClip) { + if (srcX <= -w || srcX >= srcWidth || srcY <= -h || srcY >= srcHeight) { + return; + } + dstX += Graphics_transX(dstSurf); + dstY += Graphics_transY(dstSurf); + } + else { + int32 i; + + dstX += Graphics_transX(dstSurf); + dstY += Graphics_transY(dstSurf); + + if (srcX < 0) { + w += srcX; + dstX -= srcX; + srcX = 0; + } + i = srcWidth - srcX; + if (w > i) { + w = i; + } + if (srcY < 0) { + h += srcY; + dstY -= srcY; + srcY = 0; + } + i = srcHeight - srcY; + if (h > i) { + h = i; + } + + if (dstX < Graphics_clipX1(dstSurf)) { + i = Graphics_clipX1(dstSurf) - dstX; + dstX = Graphics_clipX1(dstSurf); + srcX += i; + w -= i; + } + if ((dstX + w) > Graphics_clipX2(dstSurf)) { + w = Graphics_clipX2(dstSurf) - dstX; + } + if (dstY < Graphics_clipY1(dstSurf)) { + i = Graphics_clipY1(dstSurf) - dstY; + dstY = Graphics_clipY1(dstSurf); + srcY += i; + h -= i; + } + if ((dstY + h) > Graphics_clipY2(dstSurf)) { + h = Graphics_clipY2(dstSurf) - dstY; + } + + if (w <= 0 || h <= 0) { + return; + } + } + + int32 id = Image_textureId(srcSurf); + if (id < 0) { + TCObject pixelsObj = frameCount > 1 ? Image_pixelsOfAllFrames(srcSurf) : Image_pixels(srcSurf); + Pixel* pixels = (Pixel*)ARRAYOBJ_START(pixelsObj); + int32 width = frameCount > 1 ? Image_widthOfAllFrames(srcSurf) : Image_width(srcSurf); + int32 height = Image_height(srcSurf); + Image_textureId(srcSurf) = skia_makeBitmap(SKIA_SCREEN_SURFACE_ID, pixels, width, height); + if (Image_textureId(srcSurf) >= 0) { + Image_changed(srcSurf) = false; + } + } + + if (doClip) { + skia_setClip(skiaSurfaceForGraphics(dstSurf), Get_Clip(dstSurf)); + clipSet = true; + } + if (frameCount > 1) { + frame = Image_currentFrame(srcSurf); + if (frame < 0) { + frame = 0; + } + else if (frame >= frameCount) { + frame = frameCount - 1; + } + } + + skia_drawSurface(skiaSurfaceForGraphics(dstSurf), Image_textureId(srcSurf), + (float)(srcX / scaleW + frame * Image_width(srcSurf)), + (float)(srcY / scaleH), + (float)((srcX + w) / scaleW + frame * Image_width(srcSurf)), + (float)((srcY + h) / scaleH), + (float)dstX, (float)dstY, (float)(dstX + w), (float)(dstY + h), + Image_alphaMask(srcSurf)); + if (clipSet) { + skia_restoreClip(skiaSurfaceForGraphics(dstSurf)); + } + } + else { + LOGD("Trying to draw a control surface into some other surface"); + } + + markDirty(currentContext, dstSurf, dstX, dstY, w, h); +} +#endif + +// Device specific routine. +// Gets the color value of the pixel, using the current translation +// Returns -1 if error (out of clip bounds) +static int32 getPixel(TCObject g, int32 x, int32 y) +{ + int32 ret = -1; + x += Graphics_transX(g); + y += Graphics_transY(g); + if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g)) + { + PixelConv p; +#ifdef SKIA_H + return skia_getPixel(skiaSurfaceForGraphics(g), x, y); +#else +#ifdef __gl2_h_ + if (Graphics_useOpenGL(g)) + return glGetPixel(x,y); + else +#endif + p.pixel = getGraphicsPixels(g)[y * Graphics_pitch(g) + x]; + ret = (p.r << 16) | (p.g << 8) | p.b; +#endif + } + return ret; +} + +#ifndef SKIA_H +static PixelConv getPixelConv(TCObject g, int32 x, int32 y) +{ + PixelConv p; + p.pixel = -1; + x += Graphics_transX(g); + y += Graphics_transY(g); + if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g)) + { +#ifdef __gl2_h_ + if (Graphics_useOpenGL(g)) + p.pixel = glGetPixel(x,y); + else +#endif + p.pixel = getGraphicsPixels(g)[y * Graphics_pitch(g) + x]; + } + return p; +} +#endif + +// Device specific routine. +// Sets the pixel to the given color, translating and clipping +#ifndef SKIA_H +static void setPixel(Context currentContext, TCObject g, int32 x, int32 y, Pixel pixel) +{ + x += Graphics_transX(g); + y += Graphics_transY(g); + if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g)) + { +#ifdef __gl2_h_ + if (Graphics_useOpenGL(g)) + { + glDrawPixel(x,y,pixel,255); + if (Graphics_isImageSurface(g)) + Image_changed(Graphics_surface(g)) = true; + else + currentContext->fullDirty = true; + } + else +#endif + { + getGraphicsPixels(g)[y * Graphics_pitch(g) + x] = pixel; + if (!currentContext->fullDirty && !Graphics_isImageSurface(g)) markScreenDirty(currentContext, x, y, 1, 1); + } + } +} +#else +static void setPixel(Context currentContext, TCObject g, int32 x, int32 y, Pixel pixel) +{ + x += Graphics_transX(g); + y += Graphics_transY(g); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_setPixel(skiaSurfaceForGraphics(g), x, y, pixel | Graphics_alpha(g)); + skia_restoreClip(skiaSurfaceForGraphics(g)); + + markDirty(currentContext, g, x, y, 1, 1); +} +#endif diff --git a/TotalCrossVM/src/nm/ui/GraphicsPrimitivesText_c.h b/TotalCrossVM/src/nm/ui/GraphicsPrimitivesText_c.h new file mode 100644 index 0000000000..7812dc9b99 --- /dev/null +++ b/TotalCrossVM/src/nm/ui/GraphicsPrimitivesText_c.h @@ -0,0 +1,389 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +#ifndef SKIA_H +#define INTERP(j,f,shift) (j + (((f - j) * transparency) >> shift)) & 0xFF + +static uint8 _ands8[8] = {0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01}; +uint8* getResizedCharPixels(Context currentContext, UserFont uf, JChar ch, int32 w, int32 h); + +static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrCount, int32 x0, int32 y0, Pixel foreColor, int32 justifyWidth) +{ + TCObject fontObj = Graphics_font(g); + int32 startBit, currentBit, incY, y1, r, rmax, istart; + uint8 *bitmapTable, *ands, *current, *start; + uint16* bitIndexTable; + int32 rowWIB, offset, xMin, xMax, yMin, yMax, x, y, yDif, width, width0, height, spaceW = 0, k, clipX1,clipX2,clipY1,clipY2, pitch; + Pixel transparency, *row0, *row; + PixelConv *i; + bool isNibbleStartingLow, isLowNibble, isClipped; + int aaType; + JChar ch, first, last; + UserFont uf = null; + PixelConv fc; + int32 extraPixelsPerChar = 0, extraPixelsRemaining = -1, rem; + uint8 *ands8 = _ands8; + int32 fcR, fcG, fcB; +#ifdef __gl2_h_ + int32 charXY[2]; + float *xya; +#endif + int32 diffW; + bool isVert = Graphics_isVerticalText(g); + bool isGL = Graphics_useOpenGL(g); + + if (!text || chrCount == 0 || fontObj == null) return; + + fc.pixel = foreColor; + fcR = fc.r; + fcG = fc.g; + fcB = fc.b; + + uf = loadUserFontFromFontObj(currentContext, fontObj, ' '); + if (uf == null) return; + diffW = uf->ubase && uf->isDefaultFont; + rowWIB = uf->rowWidthInBytes; + bitIndexTable = uf->bitIndexTable; + bitmapTable = uf->bitmapTable; + first = uf->fontP.firstChar; + last = uf->fontP.lastChar; + + aaType = uf->fontP.antialiased; + height = uf->fontP.maxHeight; + incY = height + justifyWidth; + + x0 += Graphics_transX(g); + y0 += Graphics_transY(g); + + if (justifyWidth > 0) + { + while (chrCount > 0 && text[chrCount - 1] <= (JChar)' ') + chrCount--; + if (chrCount == 0) return; + rem = justifyWidth - getJCharPWidth(currentContext, fontObj, text, chrCount); + if (rem > 0) + { + extraPixelsPerChar = rem / chrCount; + extraPixelsRemaining = rem % chrCount; + } + } + clipX1 = Graphics_clipX1(g); + clipX2 = Graphics_clipX2(g); + clipY1 = Graphics_clipY1(g); + clipY2 = Graphics_clipY2(g); + + xMax = xMin = (x0 < clipX1) ? clipX1 : x0; + yMax = y0 + (isVert ? chrCount * incY : height); + yMin = (y0 < clipY1) ? clipY1 : y0; + if (yMax >= clipY2) + yMax = clipY2; + if (getGraphicsPixels(g) == null) + return; + row0 = getGraphicsPixels(g) + yMin * Graphics_pitch(g); + yDif = yMin - y0; + y = y0; + + pitch = Graphics_pitch(g); + for (k = 0; k < chrCount; k++) // guich@402 + { + ch = *text++; + if (ch <= ' ' || ch == 160) + { + if (ch == ' ' || ch == '\t' || ch == 160) + { + if (isVert) + y += ch == '\t' ? incY * *tabSizeField : incY; + else + { + x0 += getJCharWidth(currentContext, fontObj, ch)+extraPixelsPerChar; + if (k <= extraPixelsRemaining) + x0++; + } + } + continue; + } + if (uf == null || ch < first || ch > last) + { + uf = loadUserFontFromFontObj(currentContext, fontObj, ch); + if (uf == null || ch < uf->fontP.firstChar || ch > uf->fontP.lastChar) // invalid char - guich@tc122_23: must also check the font's range + { + x0 += spaceW ? spaceW : (spaceW=getJCharWidth(currentContext, fontObj, ' ')) + extraPixelsPerChar; + if (k <= extraPixelsRemaining) + x0++; + continue; + } + rowWIB = uf->rowWidthInBytes; + bitIndexTable = uf->bitIndexTable; + bitmapTable = uf->bitmapTable; + first = uf->fontP.firstChar; + last = uf->fontP.lastChar; + } +#ifdef __gl2_h_ + if (!checkGLfloatBuffer(currentContext, uf->fontP.maxHeight * uf->fontP.maxWidth)) + return; +#endif + // valid char, get its start + offset = bitIndexTable[ch]; + width0 = width = bitIndexTable[ch+1] - offset - diffW; + isClipped = false; + + if (uf->ubase != null) width = width * height / uf->ubase->fontP.maxHeight; + + if ((xMax = x0 + width) > clipX2) + { + isClipped = true; + xMax = clipX2; + } + y1 = y; r=0; + istart = 0; + if (!isVert) + { + if (y0 < yMin) // guich@tc100b4_1: skip rows before yMin + istart += yMin-y0; + y = yMin; + } + else + if (y < yMin) + { + r += yMin-y; + istart += yMin-y; // guich@tc100b4_1: skip rows before yMin + y = yMin; + } + row0 = getGraphicsPixels(g) + y * Graphics_pitch(g); + rmax = (y+height > yMax) ? yMax - y : height; + isClipped |= x0 < clipX1 || istart != 0 || rmax != height; + + switch (aaType) + { + case AA_NO: + { + start = bitmapTable + (offset >> 3) + rowWIB * istart; + startBit = offset & 7; + + // draws the char, a row at a time + #ifdef __gl2_h_ + if (isGL) + { + int32 nn=0; + + xya = glXYA; + for (; r < rmax; start+=rowWIB, r++,row += pitch,y++) // draw each row + { + current = start; + ands = ands8 + (currentBit = startBit); + for (x=x0; x < xMax; x++) + { + if ((*current & *ands++) != 0 && x >= xMin) + { + *xya++ = (float)x; + *xya++ = (float)y; + *xya++ = 1; + nn++; + } + if (++currentBit == 8) // finished this uint8? + { + currentBit = 0; // reset counter + ands = ands8; // reset test bit pointer + ++current; // inc current uint8 + } + } + } + if (nn > 0) // flush vertices buffer + glDrawPixels(nn,foreColor); + } + else + #endif + for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row + { + current = start; + ands = ands8 + (currentBit = startBit); + for (x=x0; x < xMax; x++) + { + if ((*current & *ands++) != 0 && x >= xMin) + row[x] = foreColor; + if (++currentBit == 8) // finished this uint8? + { + currentBit = 0; // reset counter + ands = ands8; // reset test bit pointer + ++current; // inc current uint8 + } + } + } + break; + } + case AA_4BPP: + { + start = bitmapTable + (offset >> 1) + rowWIB * istart; + isNibbleStartingLow = (offset & 1) == 1; + // draws the char, a row at a time + #ifdef __gl2_h_ + if (isGL) + { + int32 nn=0; + + xya = glXYA; + for (; r < rmax; start+=rowWIB, r++,y++) // draw each row + { + current = start; + isLowNibble = isNibbleStartingLow; + for (x=x0; x < xMax; x++) + { + transparency = isLowNibble ? (*current++ & 0xF) : ((*current >> 4) & 0xF); + isLowNibble = !isLowNibble; + if (transparency == 0 || x < xMin) + continue; + + // alpha + // vertices + *xya++ = (float)x; + *xya++ = (float)y; + *xya++ = ftransp[transparency]; + nn++; + } + } + if (nn > 0) // flush vertices buffer + glDrawPixels(nn,foreColor); + } + else + #endif + for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row + { + current = start; + isLowNibble = isNibbleStartingLow; + i = (PixelConv*)&row[x0]; + for (x=x0; x < xMax; x++,i++) + { + transparency = isLowNibble ? (*current++ & 0xF) : ((*current >> 4) & 0xF); + isLowNibble = !isLowNibble; + if (transparency == 0 || x < xMin) + continue; + if (transparency == 0xF) + i->pixel = foreColor; + else + { + i->r = INTERP(i->r, fcR, 4); + i->g = INTERP(i->g, fcG, 4); + i->b = INTERP(i->b, fcB, 4); + } + } + } + } + break; + case AA_8BPP: // textured font files + { + // draws the char, a row at a time + #ifdef __gl2_h_ + if (isGL) + { + if (!isClipped && getCharPosInTexture(currentContext, uf->ubase, ch, charXY)) +/*text*/ glDrawTexture(uf->ubase->textureId, + charXY[0], charXY[1], width0, uf->ubase->fontP.maxHeight, // source char position + x0, y, width, height, // target bitmap position + uf->ubase->maxW, uf->ubase->maxH, &fc, 255); // total bitmap size + else + { + uint8* alpha = getResizedCharPixels(currentContext, uf->ubase, ch, width+diffW, height); + if (alpha) + { + int32 nn=0; + rowWIB = width+diffW; + start = alpha + istart * rowWIB; + xya = glXYA; + for (; r < rmax; start+=rowWIB, r++,y++) // draw each row + { + current = start; + for (x=x0; x < xMax; x++) + { + transparency = *current++; + if (transparency == 0 || x < xMin) + continue; + + // alpha + // vertices + *xya++ = (float)x; + *xya++ = (float)y; + *xya++ = f255[transparency]; + nn++; + } + } + if (nn > 0) // flush vertices buffer + glDrawPixels(nn,foreColor); + } + } + } + else + #endif // case 2 + { + uint8* alpha = getResizedCharPixels(currentContext, uf->ubase, ch, width+diffW, height); + if (alpha) + { + rowWIB = width+diffW; + start = alpha + istart * rowWIB; + for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row + { + current = start; + i = (PixelConv*)&row[x0]; + for (x=x0; x < xMax; x++,i++) + { + transparency = *current++; + if (transparency == 0 || x < xMin) + continue; + if (transparency == 0xFF) + i->pixel = foreColor; + else + { + i->r = INTERP(i->r, fcR, 8); + i->g = INTERP(i->g, fcG, 8); + i->b = INTERP(i->b, fcB, 8); + } + } + } + } + } + } + } + if (isVert) + { + y = y1 + incY; + if (y >= yMax) + break; + } + else + { + if (xMax >= clipX2) + { + xMax = clipX2; + break; + } + x0 = xMax; // next character + x0 += extraPixelsPerChar; + if (k <= extraPixelsRemaining) + x0++; + } + } +#ifndef __gl2_h_ + if (!currentContext->fullDirty && !Graphics_isImageSurface(g)) markScreenDirty(currentContext, xMin, yMin, (xMax - xMin), (yMax - yMin)); +#else + if (Graphics_isImageSurface(g)) + Image_changed(Graphics_surface(g)) = true; + else + currentContext->fullDirty = true; +#endif +} +#else +static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrCount, int32 x, int32 y, Pixel foreColor, int32 justifyWidth) +{ + TCObject fontObj = Graphics_font(g); + int32 fontSize = (int)(Font_size(fontObj) * (*tcSettings.screenDensityPtr)); + int32 typefaceIndex = Font_skiaIndex(fontObj); + + x += Graphics_transX(g); + y += Graphics_transY(g); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_drawText(skiaSurfaceForGraphics(g), text, chrCount * sizeof(JChar), x, y + fontSize, foreColor | Graphics_alpha(g), justifyWidth, fontSize, typefaceIndex); + skia_restoreClip(skiaSurfaceForGraphics(g)); + + markDirty(currentContext, g, x, y, skia_stringWidth(text, chrCount * sizeof(JChar), typefaceIndex, fontSize), fontSize); +} +#endif diff --git a/TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h b/TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h index a658b470c0..50ef02a062 100644 --- a/TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h +++ b/TotalCrossVM/src/nm/ui/GraphicsPrimitives_c.h @@ -220,436 +220,7 @@ void markDirty(Context currentContext, TCObject surface, int x, int y, int w, in // This is the main routine that draws a surface (a Control or an Image) in the destination GfxSurface. // Destination is always a Graphics object. -#ifndef SKIA_H -static void drawSurface(Context currentContext, TCObject dstSurf, TCObject srcSurf, int32 srcX, int32 srcY, int32 width, int32 height, - int32 dstX, int32 dstY, int32 doClip) -{ - uint32 i; - Pixel * srcPixels; - Pixel * dstPixels; - int32 srcPitch, srcWidth, srcHeight, alphaMask = 0; - bool isSrcScreen = !Surface_isImage(srcSurf); - bool unlockSrc = false; - if (Surface_isImage(srcSurf)) - { -#ifdef __gl2_h_ // for opengl, we will use the smoothScaled only if we will draw on an image. for win32, we will always use smoothScale - bool forcedSmoothScale = Graphics_isImageSurface(dstSurf); // the destination is always a Graphics object -#else - bool forcedSmoothScale = true; -#endif - srcPitch = srcWidth = (int32)(Image_width(srcSurf) * Image_hwScaleW(srcSurf)); - srcHeight = (int32)(Image_height(srcSurf) * Image_hwScaleH(srcSurf)); - if (forcedSmoothScale && (Image_hwScaleW(srcSurf) != 1 || Image_hwScaleH(srcSurf) != 1)) - { - static Method mGetScaledInstance, mGetSmoothScaledInstance; - TCObject newSurf; - if (mGetScaledInstance == null) - { - mGetScaledInstance = getMethod(OBJ_CLASS(srcSurf), false, "getScaledInstance", 2, J_INT, J_INT); - mGetSmoothScaledInstance = getMethod(OBJ_CLASS(srcSurf), false, "getSmoothScaledInstance", 2, J_INT, J_INT); - } - disableGC = true; // the gc may collect the image before we lock it here (*) - newSurf = executeMethod(currentContext, Image_hwScaleW(srcSurf) < 1 && Image_hwScaleH(srcSurf) < 1 ? mGetSmoothScaledInstance : mGetScaledInstance, srcSurf, srcWidth, srcHeight).asObj; - if (newSurf == null || newSurf == (TCObject)0xFFFFFFFF) - { - currentContext->thrownException = null; - return; - } - else - { - srcSurf = newSurf; - setObjectLock(newSurf, LOCKED); // (*) - unlockSrc = true; - } - disableGC = false; - srcPitch = srcWidth = Image_width(srcSurf); - srcHeight = Image_height(srcSurf); - } - alphaMask = Image_alphaMask(srcSurf); - } - else - { - srcPitch = srcWidth = screen.screenW; - srcHeight = screen.screenH; - } - dstPixels = getSurfacePixels(dstSurf); - srcPixels = getSurfacePixels(srcSurf); - if (!doClip) - { - /* - | Even if no clip is required, we still have to make sure that the - | area of the bitmap that we want to copy is inside its area. - */ - if (srcX <= -width || srcX >= srcWidth || srcY <= -height || srcY >= srcHeight) - goto end; - dstX += Graphics_transX(dstSurf); - dstY += Graphics_transY(dstSurf); - } - else - { - dstX += Graphics_transX(dstSurf); - dstY += Graphics_transY(dstSurf); - - /* clip the source rectangle to the source surface */ - if (srcX < 0) - { - width += srcX; - dstX -= srcX; - srcX = 0; - } - i = srcWidth - srcX; - if (width > (int32)i) - width = i; - if (srcY < 0) - { - height += srcY; - dstY -= srcY; - srcY = 0; - } - i = srcHeight - srcY; - if (height > (int32)i) - height = i; - - /* clip the destination rectangle against the clip rectangle */ - if (dstX < Graphics_clipX1(dstSurf)) - { - i = Graphics_clipX1(dstSurf) - dstX; - dstX = Graphics_clipX1(dstSurf); - srcX += i; - width -= i; - } - if ((dstX + width) > Graphics_clipX2(dstSurf)) - width = Graphics_clipX2(dstSurf) - dstX; - if (dstY < Graphics_clipY1(dstSurf)) - { - i = Graphics_clipY1(dstSurf) - dstY; - dstY = Graphics_clipY1(dstSurf); - srcY += i; - height -= i; - } - if ((dstY + height) > Graphics_clipY2(dstSurf)) - height = Graphics_clipY2(dstSurf) - dstY; - - /* check the validity */ - if (width <= 0 || height <= 0) - goto end; - } - - srcPixels += srcY * srcPitch + srcX; - dstPixels += dstY * Graphics_pitch(dstSurf) + dstX; -#ifdef __gl2_h_ - if (isSrcScreen) - glGetPixels(dstPixels,srcX,srcY,width,height,Graphics_pitch(dstSurf)); - else - if (Graphics_useOpenGL(dstSurf)) - { - int32 fc; - int frame; - - if (Image_changed(srcSurf)) - applyChanges(currentContext, srcSurf); - fc = Image_frameCount(srcSurf); - frame = (fc <= 1) ? 0 : Image_currentFrame(srcSurf); - Image_lastAccess(srcSurf) = getTimeStamp(); -/*img*/ glDrawTexture(Image_textureId(srcSurf), srcX+frame*srcPitch,srcY,width,height, dstX,dstY, 0,0, fc > 1 ? (int32)(Image_widthOfAllFrames(srcSurf) * Image_hwScaleW(srcSurf)) : srcWidth,srcHeight, null, alphaMask); - } - else -#endif - for (i=0; i < (uint32)height; i++) // in opengl, only case of image drawing on image - { - PixelConv *ps = (PixelConv*)srcPixels; - PixelConv *pt = (PixelConv*)dstPixels; - uint32 count = width; - if (isSrcScreen) - for (;count != 0; pt++,ps++, count--) - { - pt->pixel = ps->pixel; - pt->a = 0xFF; - } - else - for (;count != 0; pt++,ps++, count--) - { - int32 a = ps->a * alphaMask; - a = (a+1 + (a >> 8)) >> 8; // alphaMask * a / 255 - if (a == 0xFF) - pt->pixel = ps->pixel; - else - if (a != 0) - { - int32 ma = 0xFF-a; - int32 r = (a * ps->r + ma * pt->r); - int32 g = (a * ps->g + ma * pt->g); - int32 b = (a * ps->b + ma * pt->b); - pt->r = (r+1 + (r >> 8)) >> 8; // fast way to divide by 255 - pt->g = (g+1 + (g >> 8)) >> 8; - pt->b = (b+1 + (b >> 8)) >> 8; - } - } - srcPixels += srcPitch; - dstPixels += Graphics_pitch(dstSurf); - } -#ifndef __gl2_h_ - if (!currentContext->fullDirty && !Graphics_isImageSurface(dstSurf)) markScreenDirty(currentContext, dstX, dstY, width, height); -#else - if (Graphics_isImageSurface(dstSurf)) - Image_changed(Graphics_surface(dstSurf)) = true; - else - currentContext->fullDirty = true; -#endif -end: - if (unlockSrc) - setObjectLock(srcSurf, UNLOCKED); -} -#else -static void drawSurface(Context currentContext, TCObject dstSurf, TCObject srcSurf, int32 srcX, int32 srcY, int32 w, int32 h, - int32 dstX, int32 dstY, int32 doClip) { - if (Surface_isImage(srcSurf)) { - int32 srcWidth = (int32)(Image_width(srcSurf) * Image_hwScaleW(srcSurf)); - int32 srcHeight = (int32)(Image_height(srcSurf) * Image_hwScaleH(srcSurf)); - double scaleW = Image_hwScaleW(srcSurf); - double scaleH = Image_hwScaleH(srcSurf); - int32 frameCount = Image_frameCount(srcSurf); - int32 frame = 0; - bool clipSet = false; - - if (scaleW <= 0 || scaleH <= 0 || w <= 0 || h <= 0 || srcWidth <= 0 || srcHeight <= 0) { - return; - } - - if (!doClip) { - if (srcX <= -w || srcX >= srcWidth || srcY <= -h || srcY >= srcHeight) { - return; - } - dstX += Graphics_transX(dstSurf); - dstY += Graphics_transY(dstSurf); - } - else { - int32 i; - - dstX += Graphics_transX(dstSurf); - dstY += Graphics_transY(dstSurf); - - if (srcX < 0) { - w += srcX; - dstX -= srcX; - srcX = 0; - } - i = srcWidth - srcX; - if (w > i) { - w = i; - } - if (srcY < 0) { - h += srcY; - dstY -= srcY; - srcY = 0; - } - i = srcHeight - srcY; - if (h > i) { - h = i; - } - - if (dstX < Graphics_clipX1(dstSurf)) { - i = Graphics_clipX1(dstSurf) - dstX; - dstX = Graphics_clipX1(dstSurf); - srcX += i; - w -= i; - } - if ((dstX + w) > Graphics_clipX2(dstSurf)) { - w = Graphics_clipX2(dstSurf) - dstX; - } - if (dstY < Graphics_clipY1(dstSurf)) { - i = Graphics_clipY1(dstSurf) - dstY; - dstY = Graphics_clipY1(dstSurf); - srcY += i; - h -= i; - } - if ((dstY + h) > Graphics_clipY2(dstSurf)) { - h = Graphics_clipY2(dstSurf) - dstY; - } - - if (w <= 0 || h <= 0) { - return; - } - } - - if (Graphics_isImageSurface(dstSurf)) { - TCObject pixelsObj = frameCount > 1 ? Image_pixelsOfAllFrames(srcSurf) : Image_pixels(srcSurf); - Pixel *srcPixels = (Pixel*)ARRAYOBJ_START(pixelsObj); - Pixel *dstPixels = getSurfacePixels(dstSurf); - int32 srcPitch = frameCount > 1 ? Image_widthOfAllFrames(srcSurf) : Image_width(srcSurf); - int32 dstPitch = Graphics_pitch(dstSurf); - int32 dstRow, row, col; - int32 alphaMask = Image_alphaMask(srcSurf); - - if (frameCount > 1) { - frame = Image_currentFrame(srcSurf); - if (frame < 0) { - frame = 0; - } - else if (frame >= frameCount) { - frame = frameCount - 1; - } - } - - srcPixels += frame * Image_width(srcSurf); - dstPixels += dstY * dstPitch + dstX; - dstRow = dstPitch - w; - - for (row = 0; row < h; row++, dstPixels += dstRow) { - int32 sourceY = (int32)((srcY + row) / scaleH); - PixelConv *srcRow = (PixelConv*)(srcPixels + sourceY * srcPitch); - for (col = 0; col < w; col++, dstPixels++) { - int32 sourceX = (int32)((srcX + col) / scaleW); - PixelConv *ps = srcRow + sourceX; - PixelConv *pt = (PixelConv*)dstPixels; - int32 a = ps->a * alphaMask; - a = (a + 1 + (a >> 8)) >> 8; // alphaMask * a / 255 - if (a == 0xFF) { - pt->pixel = ps->pixel; - } - else if (a != 0) { - int32 ma = 0xFF - a; - int32 r = (a * ps->r + ma * pt->r); - int32 g = (a * ps->g + ma * pt->g); - int32 b = (a * ps->b + ma * pt->b); - pt->r = (r + 1 + (r >> 8)) >> 8; - pt->g = (g + 1 + (g >> 8)) >> 8; - pt->b = (b + 1 + (b >> 8)) >> 8; - } - } - } - - markDirty(currentContext, dstSurf, dstX, dstY, w, h); - return; - } - - int32 id = Image_textureId(srcSurf); - if (Image_changed(srcSurf) || id == -1) { - TCObject pixelsObj = frameCount > 1 ? Image_pixelsOfAllFrames(srcSurf) : Image_pixels(srcSurf); - Pixel* pixels = (Pixel*)ARRAYOBJ_START(pixelsObj); - int32 width = frameCount > 1 ? Image_widthOfAllFrames(srcSurf) : Image_width(srcSurf); - int32 height = Image_height(srcSurf); - Image_textureId(srcSurf) = skia_makeBitmap(id, pixels, width, height); - Image_changed(srcSurf) = false; - } - - if (doClip) { - skia_setClip(Get_Clip(dstSurf)); - clipSet = true; - } - if (frameCount > 1) { - frame = Image_currentFrame(srcSurf); - if (frame < 0) { - frame = 0; - } - else if (frame >= frameCount) { - frame = frameCount - 1; - } - } - - skia_drawSurface(0, Image_textureId(srcSurf), - (float)(srcX / scaleW + frame * Image_width(srcSurf)), - (float)(srcY / scaleH), - (float)((srcX + w) / scaleW + frame * Image_width(srcSurf)), - (float)((srcY + h) / scaleH), - (float)dstX, (float)dstY, (float)(dstX + w), (float)(dstY + h), - Image_alphaMask(srcSurf)); - if (clipSet) { - skia_restoreClip(); - } - } - else { - LOGD("Trying to draw a control surface into some other surface"); - } - - markDirty(currentContext, dstSurf, dstX, dstY, w, h); -} -#endif - -// Device specific routine. -// Gets the color value of the pixel, using the current translation -// Returns -1 if error (out of clip bounds) -static int32 getPixel(TCObject g, int32 x, int32 y) -{ - int32 ret = -1; - x += Graphics_transX(g); - y += Graphics_transY(g); - if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g)) - { - PixelConv p; -#ifdef SKIA_H - return skia_getPixel(0, x, y); -#else -#ifdef __gl2_h_ - if (Graphics_useOpenGL(g)) - return glGetPixel(x,y); - else -#endif - p.pixel = getGraphicsPixels(g)[y * Graphics_pitch(g) + x]; - ret = (p.r << 16) | (p.g << 8) | p.b; -#endif - } - return ret; -} - -#ifndef SKIA_H -static PixelConv getPixelConv(TCObject g, int32 x, int32 y) -{ - PixelConv p; - p.pixel = -1; - x += Graphics_transX(g); - y += Graphics_transY(g); - if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g)) - { -#ifdef __gl2_h_ - if (Graphics_useOpenGL(g)) - p.pixel = glGetPixel(x,y); - else -#endif - p.pixel = getGraphicsPixels(g)[y * Graphics_pitch(g) + x]; - } - return p; -} -#endif - -// Device specific routine. -// Sets the pixel to the given color, translating and clipping -#ifndef SKIA_H -static void setPixel(Context currentContext, TCObject g, int32 x, int32 y, Pixel pixel) -{ - x += Graphics_transX(g); - y += Graphics_transY(g); - if (Graphics_clipX1(g) <= x && x < Graphics_clipX2(g) && Graphics_clipY1(g) <= y && y < Graphics_clipY2(g)) - { -#ifdef __gl2_h_ - if (Graphics_useOpenGL(g)) - { - glDrawPixel(x,y,pixel,255); - if (Graphics_isImageSurface(g)) - Image_changed(Graphics_surface(g)) = true; - else - currentContext->fullDirty = true; - } - else -#endif - { - getGraphicsPixels(g)[y * Graphics_pitch(g) + x] = pixel; - if (!currentContext->fullDirty && !Graphics_isImageSurface(g)) markScreenDirty(currentContext, x, y, 1, 1); - } - } -} -#else -static void setPixel(Context currentContext, TCObject g, int32 x, int32 y, Pixel pixel) -{ - x += Graphics_transX(g); - y += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_setPixel(0, x, y, pixel | Graphics_alpha(g)); - skia_restoreClip(); - - markDirty(currentContext, g, x, y, 1, 1); -} -#endif +#include "GraphicsPrimitivesSkia_c.h" #ifndef SKIA_H @@ -971,1366 +542,166 @@ static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y #else if (Graphics_isImageSurface(g)) Image_changed(Graphics_surface(g)) = true; - else - currentContext->fullDirty = true; -#endif - } -} -#else -static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel1, Pixel pixel2) -{ - x1 += Graphics_transX(g); - y1 += Graphics_transY(g); - x2 += Graphics_transX(g); - y2 += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_drawDottedLine(0, x1, y1, x2, y2, pixel1 | Graphics_alpha(g), pixel2 | Graphics_alpha(g)); - skia_restoreClip(); - - markDirty(currentContext, g, min32(x1, x2), min32(y1, y2), abs(x2 - x1), abs(y2 - y1)); -} -#endif - -#ifndef SKIA_H -static int32 abs32(int32 a) -{ - return a < 0 ? -a : a; -} - -static void drawLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel) -{ - drawDottedLine(currentContext, g, x1, y1, x2, y2, pixel, pixel); -} -#else -static void drawLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel) -{ - x1 += Graphics_transX(g); - y1 += Graphics_transY(g); - x2 += Graphics_transX(g); - y2 += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_drawLine(0, x1, y1, x2, y2, pixel | Graphics_alpha(g)); - skia_restoreClip(); - - markDirty(currentContext, g, min32(x1, x2), min32(y1, y2), abs(x2 - x1), abs(y2 - y1)); -} -#endif - -// Draws a rectangle with the given color -#ifndef SKIA_H -static void drawRect(Context currentContext, TCObject g, int32 x, int32 y, int32 width, int32 height, Pixel pixel) -{ - drawHLine(currentContext, g, x, y, width, pixel, pixel); - drawHLine(currentContext, g, x, y+height-1, width, pixel, pixel); - drawVLine(currentContext, g, x, y, height, pixel, pixel); - drawVLine(currentContext, g, x+width-1, y, height, pixel, pixel); -} -#else -static void drawRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, Pixel pixel) -{ - x += Graphics_transX(g); - y += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_drawRect(0, x, y, w, h, pixel | Graphics_alpha(g)); - skia_restoreClip(); - - markDirty(currentContext, g, x, y, w, h); -} -#endif - -// Description: -// Device specific routine. -// Fills a rectangle with the given color -#ifndef SKIA_H -static void fillRect(Context currentContext, TCObject g, int32 x, int32 y, int32 width, int32 height, Pixel pixel) -{ - int32 clipX1 = Graphics_clipX1(g); - int32 clipX2 = Graphics_clipX2(g); - int32 clipY1 = Graphics_clipY1(g); - int32 clipY2 = Graphics_clipY2(g); - x += Graphics_transX(g); - y += Graphics_transY(g); - - if (x < clipX1) // line starts before clip x1 - { - width -= clipX1-x; - x = clipX1; - } - if ((x+width) > clipX2) // line stops after clip x2 - width = clipX2-x; - - if (y < clipY1) // line starts before clip y1 - { - height -= clipY1-y; - y = clipY1; - } - if ((y+height) > clipY2) // line stops after clip y2 - height = clipY2-y; - - if (height > 0 && width > 0) - { -#ifdef __gl2_h_ - if (Graphics_useOpenGL(g)) - { - glFillRect(x,y,width,height,pixel,255); - if (Graphics_isImageSurface(g)) - Image_changed(Graphics_surface(g)) = true; - else - currentContext->fullDirty = true; - } - else -#endif - { - uint32 count; - int32 pitch = Graphics_pitch(g); - Pixel* to = getGraphicsPixels(g) + y * pitch + x; - if (!currentContext->fullDirty && !Graphics_isImageSurface(g)) markScreenDirty(currentContext, x, y, width, height); - if (x == 0 && width == pitch) // filling with full width? - { - int32* t = (int32*)to; - int32 p2 = pixel; - count = width*height; - for (; count != 0; count--) - *t++ = p2; - } - else - { - uint32 i = width, j = height; - for (pitch -= width; j != 0; to += pitch, i=width, j--) - for (; i != 0; i--) - *to++ = pixel; - } - } - } -} -#else -static void fillRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, Pixel pixel) -{ - x += Graphics_transX(g); - y += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_fillRect(0, x, y, w, h, pixel | Graphics_alpha(g)); - skia_restoreClip(); - - markDirty(currentContext, g, x, y, w, h); -} -#endif - -#ifdef SKIA_H -// Darkens the screen -static void fadeScreen(Context currentContext, int32 amount) { - skia_fillRect(0, 0, 0, screen.screenW, screen.screenH, amount << 24); - currentContext->dirtyX1 = 0; - currentContext->dirtyY1 = 0; - currentContext->dirtyX2 = screen.screenW; - currentContext->dirtyY2 = screen.screenH; -} -#endif - -#ifndef SKIA_H -#define INTERP(j,f,shift) (j + (((f - j) * transparency) >> shift)) & 0xFF - -static uint8 _ands8[8] = {0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01}; -uint8* getResizedCharPixels(Context currentContext, UserFont uf, JChar ch, int32 w, int32 h); - -static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrCount, int32 x0, int32 y0, Pixel foreColor, int32 justifyWidth) -{ - TCObject fontObj = Graphics_font(g); - int32 startBit, currentBit, incY, y1, r, rmax, istart; - uint8 *bitmapTable, *ands, *current, *start; - uint16* bitIndexTable; - int32 rowWIB, offset, xMin, xMax, yMin, yMax, x, y, yDif, width, width0, height, spaceW = 0, k, clipX1,clipX2,clipY1,clipY2, pitch; - Pixel transparency, *row0, *row; - PixelConv *i; - bool isNibbleStartingLow, isLowNibble, isClipped; - int aaType; - JChar ch, first, last; - UserFont uf = null; - PixelConv fc; - int32 extraPixelsPerChar = 0, extraPixelsRemaining = -1, rem; - uint8 *ands8 = _ands8; - int32 fcR, fcG, fcB; -#ifdef __gl2_h_ - int32 charXY[2]; - float *xya; -#endif - int32 diffW; - bool isVert = Graphics_isVerticalText(g); - bool isGL = Graphics_useOpenGL(g); - - if (!text || chrCount == 0 || fontObj == null) return; - - fc.pixel = foreColor; - fcR = fc.r; - fcG = fc.g; - fcB = fc.b; - - uf = loadUserFontFromFontObj(currentContext, fontObj, ' '); - if (uf == null) return; - diffW = uf->ubase && uf->isDefaultFont; - rowWIB = uf->rowWidthInBytes; - bitIndexTable = uf->bitIndexTable; - bitmapTable = uf->bitmapTable; - first = uf->fontP.firstChar; - last = uf->fontP.lastChar; - - aaType = uf->fontP.antialiased; - height = uf->fontP.maxHeight; - incY = height + justifyWidth; - - x0 += Graphics_transX(g); - y0 += Graphics_transY(g); - - if (justifyWidth > 0) - { - while (chrCount > 0 && text[chrCount - 1] <= (JChar)' ') - chrCount--; - if (chrCount == 0) return; - rem = justifyWidth - getJCharPWidth(currentContext, fontObj, text, chrCount); - if (rem > 0) - { - extraPixelsPerChar = rem / chrCount; - extraPixelsRemaining = rem % chrCount; - } - } - clipX1 = Graphics_clipX1(g); - clipX2 = Graphics_clipX2(g); - clipY1 = Graphics_clipY1(g); - clipY2 = Graphics_clipY2(g); - - xMax = xMin = (x0 < clipX1) ? clipX1 : x0; - yMax = y0 + (isVert ? chrCount * incY : height); - yMin = (y0 < clipY1) ? clipY1 : y0; - if (yMax >= clipY2) - yMax = clipY2; - if (getGraphicsPixels(g) == null) - return; - row0 = getGraphicsPixels(g) + yMin * Graphics_pitch(g); - yDif = yMin - y0; - y = y0; - - pitch = Graphics_pitch(g); - for (k = 0; k < chrCount; k++) // guich@402 - { - ch = *text++; - if (ch <= ' ' || ch == 160) - { - if (ch == ' ' || ch == '\t' || ch == 160) - { - if (isVert) - y += ch == '\t' ? incY * *tabSizeField : incY; - else - { - x0 += getJCharWidth(currentContext, fontObj, ch)+extraPixelsPerChar; - if (k <= extraPixelsRemaining) - x0++; - } - } - continue; - } - if (uf == null || ch < first || ch > last) - { - uf = loadUserFontFromFontObj(currentContext, fontObj, ch); - if (uf == null || ch < uf->fontP.firstChar || ch > uf->fontP.lastChar) // invalid char - guich@tc122_23: must also check the font's range - { - x0 += spaceW ? spaceW : (spaceW=getJCharWidth(currentContext, fontObj, ' ')) + extraPixelsPerChar; - if (k <= extraPixelsRemaining) - x0++; - continue; - } - rowWIB = uf->rowWidthInBytes; - bitIndexTable = uf->bitIndexTable; - bitmapTable = uf->bitmapTable; - first = uf->fontP.firstChar; - last = uf->fontP.lastChar; - } -#ifdef __gl2_h_ - if (!checkGLfloatBuffer(currentContext, uf->fontP.maxHeight * uf->fontP.maxWidth)) - return; -#endif - // valid char, get its start - offset = bitIndexTable[ch]; - width0 = width = bitIndexTable[ch+1] - offset - diffW; - isClipped = false; - - if (uf->ubase != null) width = width * height / uf->ubase->fontP.maxHeight; - - if ((xMax = x0 + width) > clipX2) - { - isClipped = true; - xMax = clipX2; - } - y1 = y; r=0; - istart = 0; - if (!isVert) - { - if (y0 < yMin) // guich@tc100b4_1: skip rows before yMin - istart += yMin-y0; - y = yMin; - } - else - if (y < yMin) - { - r += yMin-y; - istart += yMin-y; // guich@tc100b4_1: skip rows before yMin - y = yMin; - } - row0 = getGraphicsPixels(g) + y * Graphics_pitch(g); - rmax = (y+height > yMax) ? yMax - y : height; - isClipped |= x0 < clipX1 || istart != 0 || rmax != height; - - switch (aaType) - { - case AA_NO: - { - start = bitmapTable + (offset >> 3) + rowWIB * istart; - startBit = offset & 7; - - // draws the char, a row at a time - #ifdef __gl2_h_ - if (isGL) - { - int32 nn=0; - - xya = glXYA; - for (; r < rmax; start+=rowWIB, r++,row += pitch,y++) // draw each row - { - current = start; - ands = ands8 + (currentBit = startBit); - for (x=x0; x < xMax; x++) - { - if ((*current & *ands++) != 0 && x >= xMin) - { - *xya++ = (float)x; - *xya++ = (float)y; - *xya++ = 1; - nn++; - } - if (++currentBit == 8) // finished this uint8? - { - currentBit = 0; // reset counter - ands = ands8; // reset test bit pointer - ++current; // inc current uint8 - } - } - } - if (nn > 0) // flush vertices buffer - glDrawPixels(nn,foreColor); - } - else - #endif - for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row - { - current = start; - ands = ands8 + (currentBit = startBit); - for (x=x0; x < xMax; x++) - { - if ((*current & *ands++) != 0 && x >= xMin) - row[x] = foreColor; - if (++currentBit == 8) // finished this uint8? - { - currentBit = 0; // reset counter - ands = ands8; // reset test bit pointer - ++current; // inc current uint8 - } - } - } - break; - } - case AA_4BPP: - { - start = bitmapTable + (offset >> 1) + rowWIB * istart; - isNibbleStartingLow = (offset & 1) == 1; - // draws the char, a row at a time - #ifdef __gl2_h_ - if (isGL) - { - int32 nn=0; - - xya = glXYA; - for (; r < rmax; start+=rowWIB, r++,y++) // draw each row - { - current = start; - isLowNibble = isNibbleStartingLow; - for (x=x0; x < xMax; x++) - { - transparency = isLowNibble ? (*current++ & 0xF) : ((*current >> 4) & 0xF); - isLowNibble = !isLowNibble; - if (transparency == 0 || x < xMin) - continue; - - // alpha - // vertices - *xya++ = (float)x; - *xya++ = (float)y; - *xya++ = ftransp[transparency]; - nn++; - } - } - if (nn > 0) // flush vertices buffer - glDrawPixels(nn,foreColor); - } - else - #endif - for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row - { - current = start; - isLowNibble = isNibbleStartingLow; - i = (PixelConv*)&row[x0]; - for (x=x0; x < xMax; x++,i++) - { - transparency = isLowNibble ? (*current++ & 0xF) : ((*current >> 4) & 0xF); - isLowNibble = !isLowNibble; - if (transparency == 0 || x < xMin) - continue; - if (transparency == 0xF) - i->pixel = foreColor; - else - { - i->r = INTERP(i->r, fcR, 4); - i->g = INTERP(i->g, fcG, 4); - i->b = INTERP(i->b, fcB, 4); - } - } - } - } - break; - case AA_8BPP: // textured font files - { - // draws the char, a row at a time - #ifdef __gl2_h_ - if (isGL) - { - if (!isClipped && getCharPosInTexture(currentContext, uf->ubase, ch, charXY)) -/*text*/ glDrawTexture(uf->ubase->textureId, - charXY[0], charXY[1], width0, uf->ubase->fontP.maxHeight, // source char position - x0, y, width, height, // target bitmap position - uf->ubase->maxW, uf->ubase->maxH, &fc, 255); // total bitmap size - else - { - uint8* alpha = getResizedCharPixels(currentContext, uf->ubase, ch, width+diffW, height); - if (alpha) - { - int32 nn=0; - rowWIB = width+diffW; - start = alpha + istart * rowWIB; - xya = glXYA; - for (; r < rmax; start+=rowWIB, r++,y++) // draw each row - { - current = start; - for (x=x0; x < xMax; x++) - { - transparency = *current++; - if (transparency == 0 || x < xMin) - continue; - - // alpha - // vertices - *xya++ = (float)x; - *xya++ = (float)y; - *xya++ = f255[transparency]; - nn++; - } - } - if (nn > 0) // flush vertices buffer - glDrawPixels(nn,foreColor); - } - } - } - else - #endif // case 2 - { - uint8* alpha = getResizedCharPixels(currentContext, uf->ubase, ch, width+diffW, height); - if (alpha) - { - rowWIB = width+diffW; - start = alpha + istart * rowWIB; - for (row=row0; r < rmax; start+=rowWIB, r++,row += pitch) // draw each row - { - current = start; - i = (PixelConv*)&row[x0]; - for (x=x0; x < xMax; x++,i++) - { - transparency = *current++; - if (transparency == 0 || x < xMin) - continue; - if (transparency == 0xFF) - i->pixel = foreColor; - else - { - i->r = INTERP(i->r, fcR, 8); - i->g = INTERP(i->g, fcG, 8); - i->b = INTERP(i->b, fcB, 8); - } - } - } - } - } - } - } - if (isVert) - { - y = y1 + incY; - if (y >= yMax) - break; - } - else - { - if (xMax >= clipX2) - { - xMax = clipX2; - break; - } - x0 = xMax; // next character - x0 += extraPixelsPerChar; - if (k <= extraPixelsRemaining) - x0++; - } - } -#ifndef __gl2_h_ - if (!currentContext->fullDirty && !Graphics_isImageSurface(g)) markScreenDirty(currentContext, xMin, yMin, (xMax - xMin), (yMax - yMin)); -#else - if (Graphics_isImageSurface(g)) - Image_changed(Graphics_surface(g)) = true; - else - currentContext->fullDirty = true; -#endif -} -#else -static void drawText(Context currentContext, TCObject g, JCharP text, int32 chrCount, int32 x, int32 y, Pixel foreColor, int32 justifyWidth) -{ - TCObject fontObj = Graphics_font(g); - int32 fontSize = (int)(Font_size(fontObj) * (*tcSettings.screenDensityPtr)); - int32 typefaceIndex = Font_skiaIndex(fontObj); - - x += Graphics_transX(g); - y += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_drawText(0, text, chrCount * sizeof(JChar), x, y + fontSize, foreColor | Graphics_alpha(g), justifyWidth, fontSize, typefaceIndex); - skia_restoreClip(); - - markDirty(currentContext, g, x, y, skia_stringWidth(text, chrCount * sizeof(JChar), typefaceIndex, fontSize), fontSize); -} -#endif - -static SurfaceType getSurfaceType(Context currentContext, TCObject surface) -{ - // cache class pointers for performance - return (surface != NULL && areClassesCompatible(currentContext, OBJ_CLASS(surface), "totalcross.ui.image.Image") == 1) == COMPATIBLE ? SURF_IMAGE : SURF_CONTROL; -} - -#ifndef SKIA_H -//////////////////////////////////////////////////////////////////////////// -static void quadPixel(Context currentContext, TCObject g, int32 xc, int32 yc, int32 x, int32 y, Pixel c) -{ - // draw 4 points using symetry - setPixel(currentContext, g,xc + x, yc + y, c); - setPixel(currentContext, g,xc + x, yc - y, c); - setPixel(currentContext, g,xc - x, yc + y, c); - setPixel(currentContext, g,xc - x, yc - y, c); -} - -static void quadLine(Context currentContext, TCObject g, int32 xc, int32 yc, int32 x, int32 y, Pixel c) -{ - int32 w = x+x+1; // plus 1 for the drawHLine (draws to width-1) - // draw 2 lines using symetry - drawHLine(currentContext, g,xc - x, yc - y, w, c, c); - drawHLine(currentContext, g,xc - x, yc + y, w, c, c); -} - -// draws an ellipse incrementally -static void ellipseDrawAndFill(Context currentContext, TCObject g, int32 xc, int32 yc, int32 rx, int32 ry, Pixel pc1, Pixel pc2, bool fill, bool gradient) -{ - int32 numSteps=0, startRed=0, startGreen=0, startBlue=0, endRed=0, endGreen=0, endBlue=0, redInc=0, greenInc=0, blueInc=0, red=0, green=0, blue=0; - PixelConv c,c1,c2; - // intermediate terms to speed up loop - int64 t1 = (int64)rx*(int64)rx, t2 = t1<<1, t3 = t2<<1; - int64 t4 = (int64)ry*(int64)ry, t5 = t4<<1, t6 = t5<<1; - int64 t7 = (int64)rx*t5, t8 = t7<<1, t9 = 0L; - int64 d1 = t2 - t7 + (t4>>1); // error terms - int64 d2 = (t1>>1) - t8 + t5; - int32 x = rx; // ellipse points - int32 y = 0; // ellipse points - if (rx < 0 || ry < 0) // guich@501_13 - return; - c1.pixel = pc1; - c2.pixel = pc2; - - if (gradient) - { - numSteps = ry + ry; // guich@tc110_11: support horizontal gradient - startRed = c1.r; - startGreen = c1.g; - startBlue = c1.b; - endRed = c2.r; - endGreen = c2.g; - endBlue = c2.b; - redInc = ((endRed - startRed) << 16) / numSteps; - greenInc = ((endGreen - startGreen) << 16) / numSteps; - blueInc = ((endBlue - startBlue) << 16) / numSteps; - red = startRed << 16; - green = startGreen << 16; - blue = startBlue << 16; - } - else c.pixel = c1.pixel; - - while (d2 < 0) // til slope = -1 - { - if (gradient) - { - c.r = (red >> 16) & 0xFF; - c.g = (green >> 16) & 0xFF; - c.b = (blue >> 16) & 0xFF; - red += redInc; - green += greenInc; - blue += blueInc; - } - if (fill) - quadLine(currentContext, g,xc,yc,x,y,c.pixel); - else - quadPixel(currentContext, g,xc,yc,x,y,c.pixel); - y++; // always move up here - t9 += t3; - if (d1 < 0) // move straight up - { - d1 += t9 + t2; - d2 += t9; - } - else // move up and left - { - --x; - t8 -= t6; - d1 += t9 + t2 - t8; - d2 += t9 + t5 - t8; - } - } - - do // rest of top right quadrant - { - if (gradient) - { - c.r = (red >> 16) & 0xFF; - c.g = (green >> 16) & 0xFF; - c.b = (blue >> 16) & 0xFF; - red += redInc; - green += greenInc; - blue += blueInc; - } - // draw 4 points using symmetry - if (fill) - quadLine(currentContext, g,xc,yc,x,y,c.pixel); - else - quadPixel(currentContext, g,xc,yc,x,y,c.pixel); - --x; // always move left here - t8 -= t6; - if (d2 < 0) // move up and left - { - ++y; - t9 += t3; - d2 += t9 + t5 - t8; - } - else d2 += t5 - t8; // move straight left - } while (x >= 0); -} -#else -static void ellipseDrawAndFill(Context currentContext, TCObject g, int32 xc, int32 yc, int32 rx, int32 ry, Pixel pc1, Pixel pc2, bool fill, bool gradient) -{ - xc += Graphics_transX(g); - yc += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_ellipseDrawAndFill(0, xc, yc, rx, ry, pc1 | Graphics_alpha(g), pc2 | Graphics_alpha(g), fill, gradient); - skia_restoreClip(); - - markDirty(currentContext, g, xc - rx, yc + ry, rx * 2, ry * 2); -} -#endif - -#ifndef SKIA_H -//////////////////////////////////////////////////////////////////////////// -// Generalized Polygon Fill -static void qsortInts(int32 *items, int32 first, int32 last) -{ - int32 low = first; - int32 high = last, mid; - if (first >= last) - return; - mid = items[(first+last) >> 1]; - while (true) - { - while (high >= low && items[low] < mid) // guich@566_25: added "high > low" here and below - guich@568_5: changed to >= - low++; - while (high >= low && items[high] > mid) - high--; - if (low <= high) - { - int32 temp = items[low]; - items[low++] = items[high]; - items[high--] = temp; - } - else break; - } - if (first < high) - qsortInts(items, first,high); - if (low < last) - qsortInts(items, low,last); -} - -static TCObject growIntArray(Context currentContext, TCObject oldArrayObj, int32 newLen) // must unlock the returned obj -{ - TCObject newArrayObj = createArrayObject(currentContext, INT_ARRAY, newLen); - int32 *newArray,*oldArray, oldLen; - if (newArrayObj != null) - { - newArray = (int32*)ARRAYOBJ_START(newArrayObj); - oldArray = (int32*)ARRAYOBJ_START(oldArrayObj); - oldLen = ARRAYOBJ_LEN(oldArrayObj); - xmemmove(newArray, oldArray, oldLen * 4); - } - return newArrayObj; -} - -static void fillPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel c1, Pixel c2, bool gradient, bool isPie) -{ - int32 x1, y1, x2, y2,y,n=0,temp, i,j, miny, maxy, a, numSteps=0, startRed=0, startGreen=0, startBlue=0, endRed=0, endGreen=0, endBlue=0, redInc=0, greenInc=0, blueInc=0, red=0, green=0, blue=0; - int32 *yp; - int32 *axPoints[2], *ayPoints[2], anPoints[2]; - TCObject *intsObj = &Graphics_ints(g); - int32 *ints = *intsObj ? (int32*)ARRAYOBJ_START(*intsObj) : null; - PixelConv c; - - if (!xPoints1 || !yPoints1 || nPoints1 < 2) - return; - -#if defined __gl2_h_ - if (!gradient && (nPoints1 == 0 || isConvexAndInsideClip(g, tx, ty, xPoints1, yPoints1, nPoints1, isPie)) && (nPoints2 == 0 || isConvexAndInsideClip(g, tx, ty, xPoints2, yPoints2, nPoints2, isPie)) && Graphics_useOpenGL(g)) // opengl doesnt fills non-convex polygons well - { - if (nPoints1 > 0) - glDrawLines(currentContext, g, xPoints1, yPoints1, nPoints1, tx + Graphics_transX(g), ty + Graphics_transY(g), c1, true); - if (nPoints2 > 0) - glDrawLines(currentContext, g, xPoints2, yPoints2, nPoints2, tx + Graphics_transX(g), ty + Graphics_transY(g), c1, true); - return; - } + else + currentContext->fullDirty = true; #endif - - axPoints[0] = xPoints1; ayPoints[0] = yPoints1; anPoints[0] = nPoints1; - axPoints[1] = xPoints2; ayPoints[1] = yPoints2; anPoints[1] = nPoints2; - - yp = yPoints1; - miny = maxy = *yp++; - for (i = nPoints1; --i > 0; yp++) - { - if (*yp < miny) miny = *yp; - if (*yp > maxy) maxy = *yp; - } - yp = yPoints2; - for (i = nPoints2; --i >= 0; yp++) - { - if (*yp < miny) miny = *yp; - if (*yp > maxy) maxy = *yp; - } - miny += ty; - maxy += ty; - - if (ints == null) - { - *intsObj = createArrayObject(currentContext, INT_ARRAY, 2); // 2 is the most used length - if (*intsObj == null) - return; - setObjectLock(*intsObj, UNLOCKED); - ints = (int32*)ARRAYOBJ_START(*intsObj); - } - if (gradient) - { - numSteps = maxy - miny; // guich@tc110_11: support horizontal gradient - if (numSteps == 0) numSteps = 1; // guich@tc115_86: prevent divide by 0 - c.pixel = c1; - startRed = c.r; - startGreen = c.g; - startBlue = c.b; - c.pixel = c2; - endRed = c.r; - endGreen = c.g; - endBlue = c.b; - redInc = ((endRed - startRed) << 16) / numSteps; - greenInc = ((endGreen - startGreen) << 16) / numSteps; - blueInc = ((endBlue - startBlue) << 16) / numSteps; - red = startRed << 16; - green = startGreen << 16; - blue = startBlue << 16; - } - else c.pixel = c1; - for (y = miny; y <= maxy; y++) - { - n = 0; - for (a = 0; a < 2; a++) - { - int32 nPoints = anPoints[a]; - int32* xPoints = axPoints[a]; - int32* yPoints = ayPoints[a]; - j = nPoints-1; - for (i = 0; i < nPoints; j=i,i++) - { - y1 = yPoints[j]+ty; - y2 = yPoints[i]+ty; - if (y1 == y2) - continue; - if (y1 > y2) // invert - { - temp = y1; - y1 = y2; - y2 = temp; - } - // compute next x point - if ( (y1 <= y && y < y2) || (y == maxy && y1 < y && y <= y2) ) - { - if (n == (int32)ARRAYOBJ_LEN(*intsObj)) // have to grow the ints array? - { - TCObject newIntsObj = growIntArray(currentContext, *intsObj, n * 2); - if (newIntsObj == null) - return; - *intsObj = newIntsObj; - setObjectLock(*intsObj, UNLOCKED); - ints = (int32*)ARRAYOBJ_START(*intsObj); - } - if (yPoints[j] < yPoints[i]) - { - x1 = xPoints[j]+tx; - x2 = xPoints[i]+tx; - } - else - { - x2 = xPoints[j]+tx; - x1 = xPoints[i]+tx; - } - ints[n++] = (y - y1) * (x2 - x1) / (y2 - y1) + x1; - } - } - } - if (n >= 2) - { - if (gradient) - { - c.r = (red >> 16) & 0xFF; - c.g = (green >> 16) & 0xFF; - c.b = (blue >> 16) & 0xFF; - red += redInc; - green += greenInc; - blue += blueInc; - } - if (n == 2) // most of the times - { - if (ints[1] > ints[0]) - drawHLine(currentContext, g,ints[0],y,ints[1]-ints[0],c.pixel,c.pixel); - else - drawHLine(currentContext, g,ints[1],y,ints[0]-ints[1],c.pixel,c.pixel); - } - else - { - qsortInts(ints, 0, n-1); - for (n>>=1, yp = ints; --n >= 0; yp+=2) - drawHLine(currentContext, g,yp[0],y,yp[1]-yp[0],c.pixel,c.pixel); - } - } - } + } } #else -static void fillPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel c1, Pixel c2, bool gradient, bool isPie) +static void drawDottedLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel1, Pixel pixel2) { - skia_setClip(Get_Clip(g)); - skia_fillPolygon(0, xPoints1, yPoints1, nPoints1, Graphics_transX(g), Graphics_transY(g), c1 | Graphics_alpha(g), c2 | Graphics_alpha(g), gradient, isPie); - skia_restoreClip(); + x1 += Graphics_transX(g); + y1 += Graphics_transY(g); + x2 += Graphics_transX(g); + y2 += Graphics_transY(g); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_drawDottedLine(skiaSurfaceForGraphics(g), x1, y1, x2, y2, pixel1 | Graphics_alpha(g), pixel2 | Graphics_alpha(g)); + skia_restoreClip(skiaSurfaceForGraphics(g)); - // to avoid computing the polygon's bounds, we mark dirty the current clip - markDirty(currentContext, g, Graphics_clipX1(g), Graphics_clipY1(g), Graphics_clipX2(g) - Graphics_clipX1(g), Graphics_clipY2(g) - Graphics_clipY1(g)); + markDirty(currentContext, g, min32(x1, x2), min32(y1, y2), abs(x2 - x1), abs(y2 - y1)); } #endif -//////////////////////////////////////////////////////////////////////////// -// draws a polygon. if the polygon is not closed, close it #ifndef SKIA_H -static void drawPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel pixel) +static int32 abs32(int32 a) { - if (xPoints1 && yPoints1 && nPoints1 >= 2) - { -#if defined __gl2_h_ - if (Graphics_useOpenGL(g) && (nPoints1 == 0 || isInsideClip(g, tx, ty, xPoints1, yPoints1, nPoints1)) && (nPoints2 == 0 || isInsideClip(g, tx, ty, xPoints2, yPoints2, nPoints2))) - { - if (nPoints1 > 0) - glDrawLines(currentContext, g, xPoints1, yPoints1, nPoints1, tx + Graphics_transX(g), ty + Graphics_transY(g), pixel, false); - if (nPoints2 > 0) - glDrawLines(currentContext, g, xPoints2, yPoints2, nPoints2, tx + Graphics_transX(g), ty + Graphics_transY(g), pixel, false); - } - else -#endif - { - int32 i; - for (i=1; i < nPoints1; i++) - drawLine(currentContext, g,tx + xPoints1[i-1], ty + yPoints1[i-1], tx + xPoints1[i], ty + yPoints1[i], pixel); - for (i=1; i < nPoints2; i++) - drawLine(currentContext, g,tx + xPoints2[i-1], ty + yPoints2[i-1], tx + xPoints2[i], ty + yPoints2[i], pixel); - } - } + return a < 0 ? -a : a; } -#else -static void drawPolygon(Context currentContext, TCObject g, int32 *xPoints1, int32 *yPoints1, int32 nPoints1, int32 *xPoints2, int32 *yPoints2, int32 nPoints2, int32 tx, int32 ty, Pixel pixel) -{ - skia_setClip(Get_Clip(g)); - skia_drawPolygon(0, xPoints1, yPoints1, nPoints1, Graphics_transX(g), Graphics_transY(g), pixel | Graphics_alpha(g)); - skia_restoreClip(); - // to avoid computing the polygon's bounds, we mark dirty the current clip - markDirty(currentContext, g, Graphics_clipX1(g), Graphics_clipY1(g), Graphics_clipX2(g) - Graphics_clipX1(g), Graphics_clipY2(g) - Graphics_clipY1(g)); -} -#endif -//////////////////////////////////////////////////////////////////////////// -// draw an elliptical arc from startAngle to endAngle. -// c is the fill color and c2 is the outline color -// (if in fill mode - otherwise, c = outline color) -#ifdef SKIA_H -static void arcPiePointDrawAndFill(Context currentContext, TCObject g, int32 xc, int32 yc, int32 rx, int32 ry, double startAngle, double endAngle, Pixel c, Pixel c2, bool fill, bool pie, bool gradient) +static void drawLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel) { - xc += Graphics_transX(g); - yc += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_arcPiePointDrawAndFill(0, xc, yc, rx, ry, startAngle, endAngle, c | Graphics_alpha(g), c2 | Graphics_alpha(g), fill, pie, gradient); - skia_restoreClip(); - - markDirty(currentContext, g, xc - rx, yc + ry, rx * 2, ry * 2); + drawDottedLine(currentContext, g, x1, y1, x2, y2, pixel, pixel); } #else -static void arcPiePointDrawAndFill(Context currentContext, TCObject g, int32 xc, int32 yc, int32 rx, int32 ry, double startAngle, double endAngle, Pixel c, Pixel c2, bool fill, bool pie, bool gradient) +static void drawLine(Context currentContext, TCObject g, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel) { - // this algorithm was created by Guilherme Campos Hazan - double ppd; - int32 startIndex,endIndex,index,i,nq,size=0,oldX1=0,oldY1=0,last,oldX2=0,oldY2=0; - bool sameR,startSetTo0 = true; - TCObject *xPointsObj = &Graphics_xPoints(g); - TCObject *yPointsObj = &Graphics_yPoints(g); - int32 *xPoints = *xPointsObj ? (int32*)ARRAYOBJ_START(*xPointsObj) : null; - int32 *yPoints = *yPointsObj ? (int32*)ARRAYOBJ_START(*yPointsObj) : null; - int32 clipFactor = Graphics_minX(g) * 1000000000 + Graphics_maxX(g) * 10000000 + Graphics_minY(g) * 100000 + Graphics_maxY(g); - bool sameClipFactor = Graphics_lastClipFactor(g) == clipFactor; - - if (rx < 0 || ry < 0) // guich@501_13 - return; - // make sure the values are -359 <= x <= 359 - while (startAngle <= -360) startAngle += 360; - while (endAngle <= -360) endAngle += 360; - while (startAngle > 360) startAngle -= 360; - while (endAngle > 360) endAngle -= 360; - - if (startAngle == endAngle) // guich@501_13 - return; - if (startAngle > endAngle) // eg 235 to 45 - startAngle -= 360; // set to -45 to 45 so we can handle it correctly - if (startAngle >= 0 && endAngle <= 0) // eg 135 to -135 - endAngle += 360; // set to 135 to 225 - - // step 0: correct angle values - if (startAngle < 0.1 && endAngle > 359.9) // full circle? use the fastest routine instead - { - if (fill) - ellipseDrawAndFill(currentContext, g,xc, yc, rx, ry, c, c2, true, gradient); - ellipseDrawAndFill(currentContext, g,xc, yc, rx, ry, c, c, false, gradient); - return; - } - - // step 0: if possible, use cached results - sameR = rx == Graphics_lastRX(g) && ry == Graphics_lastRY(g); - if (!sameClipFactor || !sameR) - { - // step 1: computes how many points the circle has (computes only 45 degrees and mirrors the rest) - // intermediate terms to speed up loop - int64 t1 = (int64)rx*(int64)rx, t2 = t1<<1, t3 = t2<<1; - int64 t4 = (int64)ry*(int64)ry, t5 = t4<<1, t6 = t5<<1; - int64 t7 = (int64)rx*t5, t8 = t7<<1, t9 = 0L; - int64 d1 = t2 - t7 + (t4>>1); // error terms - int64 d2 = (t1>>1) - t8 + t5; - int32 x = rx; // ellipse points - int32 y = 0; // ellipse points - - while (d2 < 0) // til slope = -1 - { - t9 += t3; - if (d1 < 0) // move straight up - { - d1 += t9 + t2; - d2 += t9; - } - else // move up and left - { - --x; - t8 -= t6; - d1 += t9 + t2 - t8; - d2 += t9 + t5 - t8; - } - ++size; - } - - do // rest of top right quadrant - { - --x; // always move left here - t8 -= t6; - if (d2 < 0) // move up and left - { - t9 += t3; - d2 += t9 + t5 - t8; - } - else d2 += t5 - t8; // move straight left - ++size; - } while (x >= 0); - nq = size; - size *= 4; - // step 2: computes how many points per degree - ppd = (double)size / 360.0f; - // step 3: create space in the buffer so it can save all the circle - size+=2; - if (pie) size++; - if (xPoints == null || ARRAYOBJ_LEN(*xPointsObj) != (uint32)size) // guich@tc304: changed < to != to fix a glytch when drawing two pies with different radius - { - *xPointsObj = createArrayObject(currentContext, INT_ARRAY, max32(3,size)); - if (*xPointsObj == null) - return; - *yPointsObj = createArrayObject(currentContext, INT_ARRAY, max32(3,size)); - if (*yPointsObj == null) - { - setObjectLock(*xPointsObj, UNLOCKED); - return; - } - setObjectLock(*xPointsObj, UNLOCKED); - setObjectLock(*yPointsObj, UNLOCKED); - } - xPoints = (int32*)ARRAYOBJ_START(*xPointsObj); - yPoints = (int32*)ARRAYOBJ_START(*yPointsObj); - if (pie) {xPoints++; yPoints++;} // make sure that startIndex-1 is at a valid pointer - - // step 4: stores all the circle in the array. the odd arcs are drawn in reverse order - // intermediate terms to speed up loop - t2 = t1<<1; - t3 = t2<<1; - t8 = t7<<1; - t9 = 0; - d1 = t2 - t7 + (t4>>1); // error terms - d2 = (t1>>1) - t8 + t5; - x = rx; - i=0; - while (d2 < 0) // til slope = -1 - { - // save 4 points using symmetry - index = nq*0+i; // 0/3 - xPoints[index]=+x; - yPoints[index]=-y; - - index = (nq<<1)-i-1; // 1/3 - xPoints[index]=-x; - yPoints[index]=-y; - - index = (nq<<1)+i; // 2/3 - xPoints[index]=-x; - yPoints[index]=+y; - - index = (nq<<2)-i-1; // 3/3 - xPoints[index]=+x; - yPoints[index]=+y; - i++; - y++; // always move up here - t9 += t3; - if (d1 < 0) // move straight up - { - d1 += t9 + t2; - d2 += t9; - } - else // move up and left - { - x--; - t8 -= t6; - d1 += t9 + t2 - t8; - d2 += t9 + t5 - t8; - } - } + x1 += Graphics_transX(g); + y1 += Graphics_transY(g); + x2 += Graphics_transX(g); + y2 += Graphics_transY(g); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_drawLine(skiaSurfaceForGraphics(g), x1, y1, x2, y2, pixel | Graphics_alpha(g)); + skia_restoreClip(skiaSurfaceForGraphics(g)); - do // rest of top right quadrant - { - // save 4 points using symmetry - index = nq*0+i; // 0/3 - xPoints[index]=+x; - yPoints[index]=-y; - - index = (nq<<1)-i-1; // 1/3 - xPoints[index]=-x; - yPoints[index]=-y; - - index = (nq<<1)+i; // 2/3 - xPoints[index]=-x; - yPoints[index]=+y; - - index = (nq<<2)-i-1; // 3/3 - xPoints[index]=+x; - yPoints[index]=+y; - - ++i; - --x; // always move left here - t8 -= t6; - if (d2 < 0) // move up and left - { - ++y; - t9 += t3; - d2 += t9 + t5 - t8; - } - else d2 += t5 - t8; // move straight left - } while (x >= 0); - // save last arguments - //Graphics_lastXC(g) = xc; no longer - //Graphics_lastYC(g) = yc; needed - Graphics_lastRX(g) = rx; - Graphics_lastRY(g) = ry; - Graphics_lastPPD(g) = ppd; - Graphics_lastSize(g) = size; - Graphics_lastClipFactor(g) = clipFactor; - } - else - { - size = Graphics_lastSize(g); - ppd = Graphics_lastPPD(g); - } - // step 5: computes the start and end indexes that will become part of the arc - if (startAngle < 0) - startAngle += 360; - if (endAngle < 0) - endAngle += 360; - startIndex = (int32)(ppd * startAngle); - endIndex = (int32)(ppd * endAngle); - - last = size-2; - if (endIndex >= last) // 360? - endIndex--; - // step 6: fill or draw the polygons - endIndex++; - if (pie) - { - // connect two lines from the center to the two edges of the arc - oldX1 = xPoints[endIndex]; - oldY1 = yPoints[endIndex]; - xPoints[endIndex] = yPoints[endIndex] = 0; - if (xPoints[startIndex] == 0 && yPoints[startIndex] == 0) - startSetTo0 = false; - else - { - startIndex--; - oldX2 = xPoints[startIndex]; - oldY2 = yPoints[startIndex]; - xPoints[startIndex] = yPoints[startIndex] = 0; - } - endIndex++; - } - - if (startIndex > endIndex) // drawing from angle -30 to +30 ? (startIndex = 781, endIndex = 73, size=854) - { - int p1 = last-startIndex; - if (fill) - fillPolygon(currentContext, g, xPoints+startIndex, yPoints+startIndex, p1, xPoints, yPoints, endIndex, xc,yc, gradient ? c : c2, c2, gradient, true); // lower half, upper half - if (!gradient) drawPolygon(currentContext, g, xPoints+startIndex, yPoints+startIndex, p1-1, xPoints+1, yPoints+1, endIndex-1, xc,yc, c); - } - else - { - int32 arc = pie ? 0 : 1; - if (fill) - fillPolygon(currentContext, g, xPoints+startIndex, yPoints+startIndex, endIndex-startIndex, 0,0,0, xc,yc, gradient ? c : c2, c2, gradient, true); - if (!gradient) drawPolygon(currentContext, g, xPoints+startIndex+arc, yPoints+startIndex+arc, endIndex-startIndex-arc, 0,0,0, xc,yc, c); - } - if (pie) // restore saved points - { - if (startSetTo0) - { - xPoints[startIndex] = oldX2; - yPoints[startIndex] = oldY2; - } - endIndex--; - xPoints[endIndex] = oldX1; - yPoints[endIndex] = oldY1; -#ifdef ANDROID - if (!gradient && endAngle == 360) - drawLine(currentContext,g, xc,yc, xc+xPoints[endIndex-1], yc+yPoints[endIndex-1], c); -#endif - } + markDirty(currentContext, g, min32(x1, x2), min32(y1, y2), abs(x2 - x1), abs(y2 - y1)); } #endif -//////////////////////////////////////////////////////////////////////////// + +// Draws a rectangle with the given color #ifndef SKIA_H -static void drawRoundRect(Context currentContext, TCObject g, int32 x, int32 y, int32 width, int32 height, int32 r, Pixel c) +static void drawRect(Context currentContext, TCObject g, int32 x, int32 y, int32 width, int32 height, Pixel pixel) { - int32 x1, y1, x2, y2, dec, xx, yy; - int32 w, h; - r = min32(r,min32(width/2,height/2)); - w = width - 2*r; - h = height - 2*r; - x1 = x+r; - y1 = y+r; - x2 = x+width-r-1; - y2 = y+height-r-1; - dec = 3-2*r; - - drawHLine(currentContext, g,x+r, y, w, c, c); // top - drawHLine(currentContext, g,x+r, y+height-1, w, c, c); // bottom - drawVLine(currentContext, g,x, y+r, h, c, c); // left - drawVLine(currentContext, g,x+width-1, y+r, h, c, c); // right - - // draw the round rectangles. - for (xx = 0, yy = r; xx <= yy; xx++) - { - setPixel(currentContext, g,x2+xx, y2+yy, c); - setPixel(currentContext, g,x2+xx, y1-yy, c); - setPixel(currentContext, g,x1-xx, y2+yy, c); - setPixel(currentContext, g,x1-xx, y1-yy, c); - - setPixel(currentContext, g,x2+yy, y2+xx, c); - setPixel(currentContext, g,x2+yy, y1-xx, c); - setPixel(currentContext, g,x1-yy, y2+xx, c); - setPixel(currentContext, g,x1-yy, y1-xx, c); - if (dec >= 0) - dec += -4*(yy--)+4; - dec += 4*xx+6; - } + drawHLine(currentContext, g, x, y, width, pixel, pixel); + drawHLine(currentContext, g, x, y+height-1, width, pixel, pixel); + drawVLine(currentContext, g, x, y, height, pixel, pixel); + drawVLine(currentContext, g, x+width-1, y, height, pixel, pixel); } #else -static void drawRoundRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, int32 r, Pixel c) +static void drawRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, Pixel pixel) { x += Graphics_transX(g); y += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_drawRoundRect(0, x, y, w, h, r, c | Graphics_alpha(g)); - skia_restoreClip(); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_drawRect(skiaSurfaceForGraphics(g), x, y, w, h, pixel | Graphics_alpha(g)); + skia_restoreClip(skiaSurfaceForGraphics(g)); markDirty(currentContext, g, x, y, w, h); } #endif +// Description: +// Device specific routine. +// Fills a rectangle with the given color #ifndef SKIA_H -//////////////////////////////////////////////////////////////////////////// -static void setPixelA(Context currentContext, TCObject g, int32 x, int32 y, PixelConv color, int32 alpha); - -//////////////////////////////////////////////////////////////////////////// -static void fillRoundRect(Context currentContext, TCObject g, int32 xx, int32 yy, int32 width, int32 height, int32 r, Pixel c) +static void fillRect(Context currentContext, TCObject g, int32 x, int32 y, int32 width, int32 height, Pixel pixel) { - int32 px1,px2,py1,py2,xm,ym,x,y=0, i, x2, e2, err; - PixelConv color; - if (r > (width/2) || r > (height/2)) r = min32(width/2,height/2); // guich@200b4_6: correct bug that crashed the device. - - x = -r; - err = 2 - 2 * r; - color.pixel = c; - - px1 = xx+r; - py1 = yy+r; - px2 = xx+width-r-1; - py2 = yy+height-r-1; - - height -= 2*r; - yy += r; - while (height--) - drawHLine(currentContext, g,xx, yy++, width, c, c); - - r = 1 - err; - do + int32 clipX1 = Graphics_clipX1(g); + int32 clipX2 = Graphics_clipX2(g); + int32 clipY1 = Graphics_clipY1(g); + int32 clipY2 = Graphics_clipY2(g); + x += Graphics_transX(g); + y += Graphics_transY(g); + + if (x < clipX1) // line starts before clip x1 { - i = 255 - 255 * abs(err - 2 * (x + y) - 2) / r; + width -= clipX1-x; + x = clipX1; + } + if ((x+width) > clipX2) // line stops after clip x2 + width = clipX2-x; - drawLine(currentContext, g, px1+x+1,py1-y,px2-x-1,py1-y,c); - drawLine(currentContext, g, px1+x+1,py2+y,px2-x-1,py2+y,c); + if (y < clipY1) // line starts before clip y1 + { + height -= clipY1-y; + y = clipY1; + } + if ((y+height) > clipY2) // line stops after clip y2 + height = clipY2-y; - if (i < 256 && i > 0) + if (height > 0 && width > 0) + { +#ifdef __gl2_h_ + if (Graphics_useOpenGL(g)) { - xm = px2; ym = py2; setPixelA(currentContext, g, xm - x, ym + y, color, i); // br - xm = px1; ym = py2; setPixelA(currentContext, g, xm - y, ym - x, color, i); // bl - xm = px1; ym = py1; setPixelA(currentContext, g, xm + x, ym - y, color, i); // tl - xm = px2; ym = py1; setPixelA(currentContext, g, xm + y, ym + x, color, i); // tr + glFillRect(x,y,width,height,pixel,255); + if (Graphics_isImageSurface(g)) + Image_changed(Graphics_surface(g)) = true; + else + currentContext->fullDirty = true; } - e2 = err; - x2 = x; - if (err + y > 0) + else +#endif { - i = 255 - 255 * (err - 2 * x - 1) / r; - if (i < 256 && i > 0) + uint32 count; + int32 pitch = Graphics_pitch(g); + Pixel* to = getGraphicsPixels(g) + y * pitch + x; + if (!currentContext->fullDirty && !Graphics_isImageSurface(g)) markScreenDirty(currentContext, x, y, width, height); + if (x == 0 && width == pitch) // filling with full width? { - xm = px2; ym = py2; setPixelA(currentContext, g, xm - x, ym + y + 1, color, i); - xm = px1; ym = py2; setPixelA(currentContext, g, xm - y - 1, ym - x, color, i); - xm = px1; ym = py1; setPixelA(currentContext, g, xm + x, ym - y - 1, color, i); - xm = px2; ym = py1; setPixelA(currentContext, g, xm + y + 1, ym + x, color, i); + int32* t = (int32*)to; + int32 p2 = pixel; + count = width*height; + for (; count != 0; count--) + *t++ = p2; } - err += ++x * 2 + 1; - } - if (e2 + x2 <= 0) - { - i = 255 - 255 * (2 * y + 3 - e2) / r; - if (i < 256 && i > 0) + else { - xm = px2; ym = py2; setPixelA(currentContext, g, xm - x2 - 1, ym + y, color, i); - xm = px1; ym = py2; setPixelA(currentContext, g, xm - y, ym - x2 - 1, color, i); - xm = px1; ym = py1; setPixelA(currentContext, g, xm + x2 + 1, ym - y, color, i); - xm = px2; ym = py1; setPixelA(currentContext, g, xm + y, ym + x2 + 1, color, i); + uint32 i = width, j = height; + for (pitch -= width; j != 0; to += pitch, i=width, j--) + for (; i != 0; i--) + *to++ = pixel; } - err += ++y * 2 + 1; } - } while (x < 0); + } } #else -static void fillRoundRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, int32 r, Pixel c) +static void fillRect(Context currentContext, TCObject g, int32 x, int32 y, int32 w, int32 h, Pixel pixel) { x += Graphics_transX(g); y += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_fillRoundRect(0, x, y, w, h, r, c | Graphics_alpha(g)); - skia_restoreClip(); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_fillRect(skiaSurfaceForGraphics(g), x, y, w, h, pixel | Graphics_alpha(g)); + skia_restoreClip(skiaSurfaceForGraphics(g)); markDirty(currentContext, g, x, y, w, h); } #endif -#if 1//ndef SKIA_H -// Translates the given coords and returns the intersection between -// the clip rect and the coords passed. -// Returns: 1 if OK, 0 if the coords are outside the clip rect -static bool translateAndClip(TCObject g, int32 *pX, int32 *pY, int32 *pWidth, int32 *pHeight) -{ - int32 x = *pX; - int32 y = *pY; - int32 w = *pWidth; - int32 h = *pHeight; - x += Graphics_transX(g); - y += Graphics_transY(g); - if (x < Graphics_clipX1(g)) - { - if ((x+w) > Graphics_clipX2(g)) - w = Graphics_clipX2(g) - Graphics_clipX1(g); - else - w -= Graphics_clipX1(g)-x; - x = Graphics_clipX1(g); - } - else - if ((x+w) > Graphics_clipX2(g)) - w = Graphics_clipX2(g) - x; - if (y < Graphics_clipY1(g)) - { - if ((y+h) > Graphics_clipY2(g)) - h = Graphics_clipY2(g) - Graphics_clipY1(g); - else - h -= Graphics_clipY1(g)-y; - y = Graphics_clipY1(g); - } - else - if ((y+h) > Graphics_clipY2(g)) - h = Graphics_clipY2(g) - y; - - if (x < 0 || y < 0 || h <= 0 || w <= 0) return false; // guich@566_42: check the resulting w/h - guich@tc112_34: check also x and y - - *pX = x; - *pY = y; - *pWidth = w; - *pHeight = h; - return true; +#ifdef SKIA_H +// Darkens the screen +static void fadeScreen(Context currentContext, int32 amount) { + skia_fillRect(SKIA_SCREEN_SURFACE_ID, 0, 0, screen.screenW, screen.screenH, amount << 24); + currentContext->dirtyX1 = 0; + currentContext->dirtyY1 = 0; + currentContext->dirtyX2 = screen.screenW; + currentContext->dirtyY2 = screen.screenH; } #endif +#include "GraphicsPrimitivesText_c.h" + +#include "GraphicsPrimitivesShapes_c.h" + static void createGfxSurface(int32 w, int32 h, TCObject g, SurfaceType stype) { Graphics_clipX2(g) = Graphics_width (g) = w; @@ -2977,9 +1348,9 @@ static void drawRoundGradient(Context currentContext, TCObject g, int32 startX, startY += Graphics_transY(g); endX += Graphics_transX(g); endY += Graphics_transY(g); - skia_setClip(Get_Clip(g)); - skia_drawRoundGradient(0, startX, startY, endX, endY, topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius, startColor | Graphics_alpha(g), endColor | Graphics_alpha(g), vertical); - skia_restoreClip(); + skia_setClip(skiaSurfaceForGraphics(g), Get_Clip(g)); + skia_drawRoundGradient(skiaSurfaceForGraphics(g), startX, startY, endX, endY, topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius, startColor | Graphics_alpha(g), endColor | Graphics_alpha(g), vertical); + skia_restoreClip(skiaSurfaceForGraphics(g)); markDirty(currentContext, g, startX, startY, endX - startX, endY - startY); } @@ -2998,6 +1369,11 @@ static int getsetRGB(Context currentContext, TCObject g, TCObject dataObj, int32 if (translateAndClip(g, &x, &y, &w, &h)) { Pixel* data = ((Pixel*)ARRAYOBJ_START(dataObj)) + offset; +#ifdef SKIA_H + if (skia_getsetRGB(skiaSurfaceForGraphics(g), data, 0, x, y, w, h, isGet) == 1) + return w * h; + return 0; +#else int32 inc = Graphics_pitch(g), count = w * h; Pixel* pixels = getGraphicsPixels(g) + y * inc + x; bool markDirty = !currentContext->fullDirty && !Graphics_isImageSurface(g); @@ -3020,6 +1396,7 @@ static int getsetRGB(Context currentContext, TCObject g, TCObject dataObj, int32 #endif } return count; +#endif } return 0; } @@ -3035,7 +1412,7 @@ static int getsetRGB(Context currentContext, TCObject g, TCObject dataObj, int32 Pixel* data = ((Pixel*)ARRAYOBJ_START(dataObj)) + offset; int32 count = w * h; - if (skia_getsetRGB(0, (void*) data, offset, x, y, w, h, isGet) == 1) { + if (skia_getsetRGB(skiaSurfaceForGraphics(g), (void*) data, offset, x, y, w, h, isGet) == 1) { return count; } } @@ -3127,104 +1504,4 @@ static void dither(Context currentContext, TCObject g, int32 x0, int32 y0, int32 } /////////////// Start of Device-dependant functions /////////////// -static bool startupGraphics(int16 appTczAttr) // there are no threads running at this point -{ - return graphicsStartup(&screen, appTczAttr); -} - -static bool createScreenSurface(Context currentContext, bool isScreenChange) -{ - bool ret = false; - if (screen.screenW <= 0 || screen.screenH <= 0) - return false; - - if (graphicsCreateScreenSurface(&screen)) - { - TCObject *screenObj; - screenObj = getStaticFieldObject(currentContext,loadClass(currentContext, "totalcross.ui.gfx.Graphics",false), "mainWindowPixels"); -#ifdef darwin // in darwin, the pixels buffer is pre-initialized and never changed - if (controlEnableUpdateScreenPtr == null) - controlEnableUpdateScreenPtr = getStaticFieldInt(loadClass(currentContext, "totalcross.ui.Control",false), "enableUpdateScreen"); - ret = true; -#else - - if (isScreenChange) - { - screen.mainWindowPixels = *screenObj = null; - gc(currentContext); // let the gc collect the old screen object - } - else - { - controlEnableUpdateScreenPtr = getStaticFieldInt(loadClass(currentContext, "totalcross.ui.Control",false), "enableUpdateScreen"); - } - - *screenObj = screen.mainWindowPixels = createArrayObject(currentContext, INT_ARRAY, screen.screenW * screen.screenH); - setObjectLock(*screenObj, UNLOCKED); - ret = screen.mainWindowPixels != null && controlEnableUpdateScreenPtr != null; -#endif - } - return ret; -} - -void markWholeScreenDirty(Context currentContext) -{ - LOCKVAR(screen); - currentContext->dirtyX1 = currentContext->dirtyY1 = 0; - currentContext->dirtyX2 = screen.screenW; - currentContext->dirtyY2 = screen.screenH; - currentContext->fullDirty = true; - UNLOCKVAR(screen); -} - -static bool checkScreenPixels() -{ - return screen.pixels != null; -} - -void updateScreen(Context currentContext) -{ -#ifdef ANDROID - if (appPaused) return; -#endif - LOCKVAR(screen); - if (keepRunning && checkScreenPixels() && controlEnableUpdateScreenPtr && *controlEnableUpdateScreenPtr && (currentContext->fullDirty || (currentContext->dirtyX1 != screen.screenW && currentContext->dirtyX2 != 0 && currentContext->dirtyY1 != screen.screenH && currentContext->dirtyY2 != 0))) - { - if (updateScreenBits(currentContext)) // move the temporary buffer to the real screen - { -#ifdef darwin - currentContext->dirtyX1 = screen.screenW; - currentContext->dirtyY1 = screen.screenH; - currentContext->dirtyX2 = currentContext->dirtyY2 = 0; - currentContext->fullDirty = false; - UNLOCKVAR(screen); // without this, a deadlock can occur in iOS if the user minimizes the application, since another thread can trigger a markScreenDirty -#endif - graphicsUpdateScreen(currentContext, &screen); -#ifdef darwin - LOCKVAR(screen); -#else - currentContext->dirtyX1 = screen.screenW; - currentContext->dirtyY1 = screen.screenH; - currentContext->dirtyX2 = currentContext->dirtyY2 = 0; - currentContext->fullDirty = false; -#endif - } - } - UNLOCKVAR(screen); -#if defined ANDROID || defined darwin - if (desiredScreenShiftY != UNDEFINED_SHIFTY) - { - setShiftYgl(desiredScreenShiftY); - desiredScreenShiftY = UNDEFINED_SHIFTY; - } -#endif -} - -void graphicsDestroyPrimitives() -{ - xfree(lookupR); - xfree(lookupG); - xfree(lookupB); - xfree(lookupGray); - fontDestroy(); -} -/////////////// End of Device-dependant functions /// +#include "GraphicsPrimitivesScreen_c.h" diff --git a/TotalCrossVM/src/nm/ui/ImagePrimitives_c.h b/TotalCrossVM/src/nm/ui/ImagePrimitives_c.h index b212f92ac2..abce73d476 100644 --- a/TotalCrossVM/src/nm/ui/ImagePrimitives_c.h +++ b/TotalCrossVM/src/nm/ui/ImagePrimitives_c.h @@ -6,6 +6,10 @@ #include +#if defined USE_SKIA && (defined ANDROID || defined darwin || defined HEADLESS) +#include "skia/skia.h" +#endif + static void setCurrentFrame(TCObject obj, int32 nr) { int32 y,width,widthOfAllFrames; @@ -588,6 +592,13 @@ static void getAlphaInstance(TCObject thisObj, TCObject newObj, int32 delta) // static void getPixelRow(Context currentContext, TCObject obj, TCObject outObj, int32 y) { +#ifdef SKIA_H + int32 id = Image_textureId(obj); + int32 skiaWidth = (Image_frameCount(obj) > 1) ? Image_widthOfAllFrames(obj) : Image_width(obj); + if (id >= 0 && checkArrayRange(currentContext, outObj, 0, skiaWidth * 4) && + skia_getPixelRow(id, ARRAYOBJ_START(outObj), y, skiaWidth)) + return; +#endif TCObject pixObj = (Image_frameCount(obj) > 1) ? Image_pixelsOfAllFrames(obj) : Image_pixels(obj); PixelConv *pixels = (PixelConv*)ARRAYOBJ_START(pixObj); int8* out = (int8*)ARRAYOBJ_START(outObj); diff --git a/TotalCrossVM/src/nm/ui/PalmFont_c.h b/TotalCrossVM/src/nm/ui/PalmFont_c.h index 497c2f0f24..3a4e848237 100644 --- a/TotalCrossVM/src/nm/ui/PalmFont_c.h +++ b/TotalCrossVM/src/nm/ui/PalmFont_c.h @@ -1,5 +1,6 @@ // Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda // // SPDX-License-Identifier: LGPL-2.1-only @@ -717,7 +718,7 @@ UserFont loadUserFontFromFontObj(Context currentContext, TCObject fontObj, JChar } #if defined USE_SKIA && (defined ANDROID || defined darwin || defined HEADLESS) -#include "android/skia.h" +#include "skia/skia.h" int32 getJCharWidth(Context currentContext, TCObject fontObj, JChar ch) { int32 fontSize = (int)(Font_size(fontObj) * (*tcSettings.screenDensityPtr)); diff --git a/TotalCrossVM/src/nm/ui/android/gfx_Graphics_c.h b/TotalCrossVM/src/nm/ui/android/gfx_Graphics_c.h index b6209bd2be..1268782fc4 100644 --- a/TotalCrossVM/src/nm/ui/android/gfx_Graphics_c.h +++ b/TotalCrossVM/src/nm/ui/android/gfx_Graphics_c.h @@ -1,5 +1,6 @@ // Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda // // SPDX-License-Identifier: LGPL-2.1-only @@ -7,7 +8,7 @@ #ifdef ANDROID -#include "skia.h" +#include "../skia/skia.h" #include // requires ndk r5 or newer #include // requires ndk r5 or newer #include @@ -128,7 +129,7 @@ void JNICALL Java_totalcross_Launcher4A_nativeInitSize(JNIEnv *env, jobject this #ifdef ANDROID -#include "skia.h" +#include "../skia/skia.h" bool initGLES(ScreenSurface screen) { diff --git a/TotalCrossVM/src/nm/ui/android/skia.cpp b/TotalCrossVM/src/nm/ui/android/skia.cpp deleted file mode 100644 index 2262e0f6c5..0000000000 --- a/TotalCrossVM/src/nm/ui/android/skia.cpp +++ /dev/null @@ -1,759 +0,0 @@ -// Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. -// -// SPDX-License-Identifier: LGPL-2.1-only - -#include "skia.h" - -#define USE_WRITE_PIXELS 1 - -#ifndef USE_COMPUTE_OPAQUE -#if __APPLE__ || ANDROID -#define USE_COMPUTE_OPAQUE 0 -#else -#define USE_COMPUTE_OPAQUE 1 -#endif -#endif - -#ifndef USE_COLORTYPE_CONVERSION -#if __APPLE__ || ANDROID -#define USE_COLORTYPE_CONVERSION 0 -#else -#define USE_COLORTYPE_CONVERSION 1 -#endif -#endif - -#ifndef USE_NATIVE_SWAP -#if __APPLE__ || ANDROID -#define USE_NATIVE_SWAP 0 -#else -#define USE_NATIVE_SWAP 1 -#endif -#endif - -#if __APPLE__ -#ifdef darwin -#include -#include -#else -#include -#include -#endif -#else -#if !defined(__arm__) && !defined(ANDROID) -#include -#else -#include -// #include -#include -#include - -#if defined ANDROID -#include -#include -#endif -#endif -#endif -#if defined HEADLESS -#include "../../../init/tcsdl.h" -#endif -#include - - -#include "include/core/SkPathEffect.h" -#include "include/core/SkGraphics.h" -#include "include/core/SkSurface.h" -#include "include/core/SkString.h" -#include "include/core/SkTime.h" -#include "include/core/SkCanvas.h" -#include "include/utils/SkRandom.h" -#include "include/core/SkTypeface.h" -#include "include/core/SkImage.h" -#include "include/core/SkImageInfo.h" -#include "include/core/SkImageEncoder.h" -#include "include/core/SkPath.h" -#include "include/effects/SkGradientShader.h" -#include "include/core/SkTextBlob.h" - -#include "include/gpu/gl/GrGLAssembleInterface.h" -#include "include/gpu/gl/GrGLConfig.h" -#include "include/gpu/gl/GrGLExtensions.h" -#include "include/gpu/gl/GrGLFunctions.h" -#include "include/gpu/gl/GrGLInterface.h" -#include "include/gpu/gl/GrGLTypes.h" - -#include "include/gpu/GrBackendSurface.h" -#include "include/gpu/GrDirectContext.h" -#include "include/gpu/GrTypes.h" - -#include "include/core/SkColorSpace.h" -#include "include/effects/SkDashPathEffect.h" - -#include -#include - - -extern "C" { -#if !defined APPLE && !defined ANDROID && !defined darwin && defined linux && defined __arm__ && !defined __aarch64__ -// Avoid dependency on glibc 2.27 -// These functions are used by Skia .a file, so we have to define a wrapper. -// https://stackoverflow.com/questions/8823267/linking-against-older-symbol-version-in-a-so-file -__asm__(".symver log2f,log2f@GLIBC_2.4"); -float __wrap_log2f(float x) { - return log2f(x); -} - -__asm__(".symver powf,powf@GLIBC_2.4"); -float __wrap_powf(float x, float y) { - return powf(x, y); -} - -__asm__(".symver expf,expf@GLIBC_2.4"); -float __wrap_expf(float x) { - return expf(x); -} - -__asm__(".symver exp2f,exp2f@GLIBC_2.4"); -float __wrap_exp2f(float x) { - return exp2f(x); -} -#endif -} - - -#define SKIA_DEBUG -// #define SKIA_TRACE - -#if defined SKIA_DEBUG && defined ANDROID - #include - #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "TotalCross", __VA_ARGS__) -#else - #define LOGD(...) printf("SKIA %s, %d\n", __FUNCTION__, __LINE__) -#endif - -#if defined SKIA_TRACE && defined ANDROID - #define SKIA_TRACE() LOGD(__FUNCTION__); -#else - #define SKIA_TRACE() //LOGD(); -#endif - -#if USE_NATIVE_SWAP -inline uint32_t builtinSwap32(uint32_t val) noexcept { - #if defined(__clang__) - return __builtin_bswap32(val); - #elif defined(__GNUG__) - return __builtin_bswap32(val); - #elif defined(_MSC_VER) - return = _byteswap_ulong(val); - #endif -} -#define SWAP32(n) builtinSwap32(n) -#else -#define SWAP32(n) (((n >> 24) & 0xFF)) | ((((n >> 16) & 0xFF) << 8) | (((n >> 8) & 0xFF) << 16) | ((n & 0xFF) << 24)) -#endif - -sk_sp surface; -SkCanvas *canvas; -SkPaint forePaint; // used for contours -SkPaint backPaint; // used for fills -SkPaint alphaPaint; // used for alphaMask -SkBitmap bitmap; -SkFont skFont; - -#define TYPEFACE_LEN 32 -sk_sp typefaces[TYPEFACE_LEN]; -int typefaceIdx = 0; - -std::vector textures; - -std::map typefaceIndexMap; - -void initSkia(int w, int h, void * pixels, int pitch, uint32_t pixelformat) -{ - SKIA_TRACE() -#ifdef HEADLESS - bitmap.installPixels(SkImageInfo::Make(w, - h, - (SkColorType) colorType(pixelformat), kPremul_SkAlphaType), (Uint32 *)pixels, pitch); - canvas = new SkCanvas(bitmap); -#else - // To use Skia's GPU backend, a OpenGL context is needed. Skia uses the "Gr" library to abstract - // the different OpenGL variants (Core, ES, etc). Most of the code bellow is dedicated to create - // a GL context and produce a valid rendertarget out of it for rendering. - auto interface = GrGLMakeNativeInterface(); - sk_sp grContext(GrDirectContext::MakeGL(interface)); - - GLint defaultFBO; - glGetIntegerv(GL_FRAMEBUFFER_BINDING, &defaultFBO); - GrGLFramebufferInfo framebuffer_info; - framebuffer_info.fFBOID = defaultFBO; - framebuffer_info.fFormat = GL_RGB8_OES; - - GrBackendRenderTarget render_target( - w, h, 0, 8, - framebuffer_info); - - SkSurfaceProps surface_props = - SkSurfaceProps(); - - sk_sp gpuSurface = SkSurface::MakeFromBackendRenderTarget( - grContext.get(), render_target, kBottomLeft_GrSurfaceOrigin, - kRGB_888x_SkColorType, nullptr, &surface_props); - - SkCanvas *gpuCanvas = gpuSurface->getCanvas(); - - // We cache a reference for the surface and canvas for later use. - surface = gpuSurface; - canvas = gpuCanvas; -#endif - skFont.setSize(16); - // The forepaint is used for "draw" methods - forePaint.setStyle(SkPaint::kStroke_Style); - forePaint.setAntiAlias(true); - forePaint.setAntiAlias(true); - - //the backpaint is used for "fill" methods - backPaint.setStyle(SkPaint::kFill_Style); - backPaint.setAntiAlias(true); - - backPaint.setAntiAlias(true); - canvas->clear(SK_ColorWHITE); - flushSkia(); -} - -void flushSkia() -{ - if (surface) { - surface->flushAndSubmit(); - } else if (canvas) { - canvas->flush(); - } -#ifdef HEADLESS - TCSDL_UpdateTexture(bitmap.width(), bitmap.height(), bitmap.rowBytes(),bitmap.getPixels()); -#endif -} - -// Creates a SkTypeface object out of a in-memory TTF file (probably inside some TCZ archive) -int32 skia_makeTypeface(char* name, void *data, int32 size) -{ - SKIA_TRACE() - std::string key = name; - int32 idx = skia_getTypefaceIndex(name); - - if (idx == -1 && typefaceIdx < TYPEFACE_LEN - 1) { - sk_sp typefaceData = SkData::MakeWithCopy(data, size); - sk_sp typeface = SkTypeface::MakeFromData(typefaceData); - idx = typefaceIdx; - typefaces[typefaceIdx++] = typeface; - typefaceIndexMap[key] = idx; - } - - return idx; -} - -int32 skia_getTypefaceIndex(char* name) { - std::string key = name; - std::map::iterator it = typefaceIndexMap.find(key); - - if (it != typefaceIndexMap.end()) { - return it->second; - } - - return -1; -} - -sk_sp skia_getTypeface(int32 typefaceIndex) { - if (typefaceIndex >= 0 && typefaceIndex < typefaceIdx) { - return typefaces[typefaceIndex]; - } else { - return SkTypeface::MakeFromName(nullptr, SkFontStyle()); - } -} - -int32 skia_stringWidth(const void *text, int32 charCount, int32 typefaceIndex, int32 fontSize) -{ - const auto newTypeFace = skia_getTypeface(typefaceIndex); - - if(skFont.getTypeface() != newTypeFace.get()) { - skFont.setTypeface(newTypeFace); - } - if(skFont.getSize() != fontSize) { - skFont.setSize(fontSize); - } - return skFont.measureText(text,charCount,SkTextEncoding::kUTF16); -} - -static void releaseProc(void* addr, void* ) { - delete[] static_cast(addr); -} - -int skia_makeBitmap(int32 id, void *data, int32 w, int32 h) { - SKIA_TRACE() - - const size_t count = (size_t)w * (size_t)h; - int32* const converted = new int32[count]; - const Pixel* const src = reinterpret_cast(data); - - for (size_t i = 0; i < count; i++) { - converted[i] = SWAP32(src[i]); - } - - if (id < 0) { // must create a new bitmap - SkBitmap bitmap; - bitmap.installPixels(SkImageInfo::Make(w, h, kN32_SkColorType, kUnpremul_SkAlphaType), - (void*)converted, sizeof(Pixel) * w, releaseProc, nullptr); - - // TODO: Reevaluate this optimization; it was added for embedded Linux ARM, but forced scale draws broke on Android/iOS. -#if USE_COMPUTE_OPAQUE - if (SkBitmap::ComputeIsOpaque(bitmap)) { - bitmap.setAlphaType(kOpaque_SkAlphaType); - } -#endif - - // TODO: Recheck the color type before re-enabling on Android/iOS; macOS did not reproduce the forced scale issue. -#if USE_COLORTYPE_CONVERSION - if (bitmap.isOpaque()) { - void* dstPixels = operator new(w * h * canvas->imageInfo().bytesPerPixel()); - SkImageInfo dstImageInfo = - SkImageInfo::Make( - w, - h, - canvas->imageInfo().colorType(), - canvas->imageInfo().alphaType() - ); - bitmap.readPixels(dstImageInfo, dstPixels, dstImageInfo.minRowBytes(), 0, 0); - bitmap.installPixels(dstImageInfo, dstPixels, dstImageInfo.minRowBytes(), releaseProc, nullptr); - } -#endif - id = textures.size(); - textures.emplace_back(std::move(bitmap)); - } else { - SkBitmap& bitmap = textures[id]; - bitmap.installPixels(SkImageInfo::Make(w, h, kN32_SkColorType, kUnpremul_SkAlphaType), (void*)converted, sizeof(Pixel) * w, releaseProc, nullptr); - } - - return id; -} -void skia_deleteBitmap(int32 id) { - SKIA_TRACE() - - if (id >= 0) { - textures[id].reset(); - } -} - - - -void skia_setClip(int32 x1, int32 y1, int32 x2, int32 y2) -{ - canvas->save(); - canvas->clipRect(SkRect::MakeLTRB(x1, y1, x2, y2)); -} -void skia_restoreClip() -{ - canvas->restore(); -} - -void skia_drawSurface(int32 skiaSurface, int32 id, float srcLeft, float srcTop, float srcRight, float srcBottom, float dstLeft, float dstTop, float dstRight, float dstBottom, int32 alphaMask) -{ - SKIA_TRACE() - - const auto& texture {textures[id]}; - const SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); - const SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); - const bool fullSource = srcLeft == 0.0f && srcTop == 0.0f && srcRight == texture.width() && srcBottom == texture.height(); - const bool sameSize = (srcRight - srcLeft) == (dstRight - dstLeft) && (srcBottom - srcTop) == (dstBottom - dstTop); - -#if USE_WRITE_PIXELS - /* - Fast drawing, can only be used to draw fully opaque images - without sampling (src and dst dimensions are the same). - Makes drawing JPEG and opaque PNGs over 10x faster. - - TODO: - - add actual numbers to back this statement - */ -if (texture.isOpaque() && alphaMask == 255 && fullSource && sameSize) { - canvas->writePixels( - texture.info(), - texture.getPixels(), - texture.rowBytes(), - (int)dstLeft, - (int)dstTop - ); -} else -#endif - { - alphaPaint.setAlpha(alphaMask); - alphaPaint.setFilterQuality(sameSize ? kNone_SkFilterQuality : kLow_SkFilterQuality); - canvas->drawBitmapRect( - texture, - srcRect, - dstRect, - &alphaPaint, - fullSource ? SkCanvas::kFast_SrcRectConstraint : SkCanvas::kStrict_SrcRectConstraint - ); - } -} - -// The getPixel call demands a 1-pixel readback from the GPU. Avoid it if possible. -Pixel skia_getPixel(int32 skiaSurface, int32 x, int32 y) -{ - SKIA_TRACE() - - SkBitmap bitmap; - bitmap.allocPixels(SkImageInfo::MakeN32Premul(1, 1)); - if (!canvas->readPixels(bitmap, x, y)) - { - return -1; - } - - Pixel pixel = bitmap.getAddr32(0, 0)[0]; - return (((pixel >> 24) & 0xFF) << 24) | (((pixel & 0xFF) << 16) | (((pixel >> 8) & 0xFF) << 8) | ((pixel >> 16) & 0xFF)); -} - -void skia_setPixel(int32 skiaSurface, int32 x, int32 y, Pixel pixel) -{ - SKIA_TRACE() - backPaint.setColor(pixel); - canvas->drawRect(SkRect::MakeXYWH(x, y, 1, 1), backPaint); -} - -void skia_drawDottedLine(int32 skiaSurface, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel1, Pixel pixel2) -{ - SKIA_TRACE() - float intervals[] = {5, 5}; - forePaint.setPathEffect(SkDashPathEffect::Make(intervals, 2, 2.5f)); - skia_drawLine(skiaSurface, x1, y1, x2, y2, pixel1); - forePaint.setPathEffect(nullptr); - - forePaint.setPathEffect(SkDashPathEffect::Make(intervals, 2, 7.5f)); - skia_drawLine(skiaSurface, x1, y1, x2, y2, pixel2); - forePaint.setPathEffect(nullptr); -} - -void skia_drawLine(int32 skiaSurface, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel) -{ - SKIA_TRACE() - forePaint.setColor(pixel); - canvas->drawLine(x1, y1, x2, y2, forePaint); -} - -void skia_drawRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, Pixel pixel) -{ - SKIA_TRACE() - forePaint.setColor(pixel); - canvas->drawRect(SkRect::MakeXYWH(x, y, w, h), forePaint); -} - -void skia_fillRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, Pixel pixel) -{ - SKIA_TRACE() - // printf("Exe log: skia fill rect = %#010x\n",pixel); - backPaint.setColor(pixel); - canvas->drawRect(SkRect::MakeXYWH(x, y, w, h), backPaint); -} - -void skia_drawText(int32 skiaSurface, const void *text, int32 chrCount, int32 x0, int32 y0, Pixel foreColor, int32 justifyWidth, int32 fontSize, int32 typefaceIndex) -{ - SKIA_TRACE() - const auto newTypeFace = skia_getTypeface(typefaceIndex); - - if(skFont.getTypeface() != newTypeFace.get()) { - skFont.setTypeface(newTypeFace); - } - if(skFont.getSize() != fontSize) { - skFont.setSize(fontSize); - } - if(backPaint.getColor() != foreColor){ - backPaint.setColor(foreColor); - } - canvas->drawTextBlob(SkTextBlob::MakeFromText(text,chrCount,skFont,SkTextEncoding::kUTF16),x0,y0,backPaint); -} - -void skia_ellipseDrawAndFill(int32 skiaSurface, int32 xc, int32 yc, int32 rx, int32 ry, Pixel pc1, Pixel pc2, bool fill, bool gradient) -{ - SKIA_TRACE() - if (fill) { - if (gradient) { - SkPoint points[3] = { - SkPoint::Make(xc, yc - ry), - SkPoint::Make(xc, yc + ry), - SkPoint::Make(xc, yc + ry * 2) - }; - SkColor colors[3] = {pc2, pc1, pc2}; - backPaint.setShader(SkGradientShader::MakeLinear( - points, colors, nullptr, 3, - SkTileMode::kClamp, 0, nullptr)); - canvas->drawOval(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), backPaint); - backPaint.setShader(nullptr); - } else { - backPaint.setColor(pc2); - canvas->drawOval(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), backPaint); - } - } else { - forePaint.setColor(pc1); - canvas->drawOval(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), forePaint); - } -} - -SkPath _skia_makePath(int32 *x, int32 *y, int32 n) -{ - SKIA_TRACE() - SkPath path; - path.moveTo(x[0], y[0]); - for (int i = 1; i < n; ++i) { - path.lineTo(x[i], y[i]); - } - - return path; -} -void _skia_getPathBounds(int32 *x, int32 *y, int32 n, int32* minY, int32* maxY) -{ - SKIA_TRACE() - *minY = y[0]; - *maxY = y[0]; - for (int i = 1; i < n; ++i) { - *minY = y[i] < *minY ? y[i] : *minY; - *maxY = *maxY < y[i] ? y[i] : *maxY; - } -} -void skia_drawPolygon(int32 skiaSurface, int32 *xPoints, int32 *yPoints, int32 nPoints, int32 tx, int32 ty, Pixel pixel) -{ - SKIA_TRACE() - forePaint.setColor(pixel); - canvas->translate(tx, ty); - canvas->drawPath(_skia_makePath(xPoints, yPoints, nPoints), forePaint); - canvas->translate(-tx, -ty); -} - -void skia_fillPolygon(int32 skiaSurface, int32 *xPoints, int32 *yPoints, int32 nPoints, int32 tx, int32 ty, Pixel c1, Pixel c2, bool gradient, bool isPie) -{ - SKIA_TRACE() - SkPath path = _skia_makePath(xPoints, yPoints, nPoints); - - backPaint.setColor(c1); - if (gradient) { - int32 minY, maxY; - _skia_getPathBounds(xPoints, yPoints, nPoints, &minY, &maxY); - SkPoint points[2] = { - SkPoint::Make(xPoints[0], minY), - SkPoint::Make(xPoints[0], maxY), - }; - SkColor colors[2] = {c1, c2}; - backPaint.setShader(SkGradientShader::MakeLinear( - points, colors, nullptr, 2, - SkTileMode::kClamp, 0, nullptr)); - } - - canvas->translate(tx, ty); - canvas->drawPath(path, backPaint); - canvas->translate(-tx, -ty); - - if (gradient) { - backPaint.setShader(nullptr); - } -} - -// Adapted from SkPathPriv::CreateDrawArcPath -SkPath _skia_makeArcPath(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, bool useCenter) { - SkASSERT(!oval.isEmpty()); - SkASSERT(sweepAngle); - - SkPath path; - path.setIsVolatile(true); - path.setFillType(SkPathFillType::kWinding); - path.reset(); - if (SkScalarAbs(sweepAngle) >= 360.f) { - path.addOval(oval); - return path; - } - if (useCenter) { - path.moveTo(oval.centerX(), oval.centerY()); - } - // Arc to mods at 360 and drawArc is not supposed to. - bool forceMoveTo = !useCenter; - while (sweepAngle <= -360.f) { - path.arcTo(oval, startAngle, -180.f, forceMoveTo); - startAngle -= 180.f; - path.arcTo(oval, startAngle, -180.f, false); - startAngle -= 180.f; - forceMoveTo = false; - sweepAngle += 360.f; - } - while (sweepAngle >= 360.f) { - path.arcTo(oval, startAngle, 180.f, forceMoveTo); - startAngle += 180.f; - path.arcTo(oval, startAngle, 180.f, false); - startAngle += 180.f; - forceMoveTo = false; - sweepAngle -= 360.f; - } - path.arcTo(oval, startAngle, sweepAngle, forceMoveTo); - if (useCenter) { - path.close(); - } - - return path; -} -void skia_arcPiePointDrawAndFill(int32 skiaSurface, int32 xc, int32 yc, int32 rx, int32 ry, double startAngle, double endAngle, Pixel c, Pixel c2, bool fill, bool pie, bool gradient) -{ - double start = -startAngle; - double sweepAngle = -(endAngle - startAngle); - SKIA_TRACE() - if (fill) { - backPaint.setColor(c2); - if (gradient) { - SkPath arcPath = _skia_makeArcPath(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), start, sweepAngle, pie); - SkRect r = arcPath.computeTightBounds(); - - SkPoint points[2] = { - SkPoint::Make(r.centerX(), r.y()), - SkPoint::Make(r.centerX(), r.y() + r.height() * 2) - }; - SkColor colors[2] = {c, c2}; - backPaint.setShader(SkGradientShader::MakeLinear( - points, colors, nullptr, 3, - SkTileMode::kClamp, 0, nullptr)); - - canvas->drawPath(arcPath, backPaint); - backPaint.setShader(nullptr); - } else { - canvas->drawArc(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), start, sweepAngle, pie, backPaint); - forePaint.setColor(c); - SkScalar strokeWidth = forePaint.getStrokeWidth(); - forePaint.setStrokeWidth(2); - canvas->drawArc(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), start, sweepAngle, pie, forePaint); - forePaint.setStrokeWidth(strokeWidth); - } - } else { - forePaint.setColor(c); - canvas->drawArc(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), start, sweepAngle, pie, forePaint); - } -} - -void skia_drawRoundRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, int32 r, Pixel c) -{ - SKIA_TRACE() - forePaint.setColor(c); - canvas->drawRRect(SkRRect::MakeRectXY(SkRect::MakeXYWH(x, y, w, h), r, r), forePaint); -} - -void skia_fillRoundRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, int32 r, Pixel c) -{ - SKIA_TRACE() - backPaint.setColor(c); - canvas->drawRRect(SkRRect::MakeRectXY(SkRect::MakeXYWH(x, y, w, h), r, r), backPaint); -} - -void skia_drawRoundGradient(int32 skiaSurface, int32 startX, int32 startY, int32 endX, int32 endY, int32 topLeftRadius, int32 topRightRadius, int32 bottomLeftRadius, int32 bottomRightRadius, int32 startColor, int32 endColor, bool vertical) -{ - SKIA_TRACE() - int32 w = endX - startX; - int32 h = endY - startY; - SkPoint points[2]; - if (vertical) { - points[0] = SkPoint::Make(startX, startY); - points[1] = SkPoint::Make(startX, startY + h * 2); - } else { - points[0] = SkPoint::Make(startX, startY); - points[1] = SkPoint::Make(startX + w * 2, startY); - } - - SkColor colors[2] = {static_cast(startColor), static_cast(endColor)}; - backPaint.setShader(SkGradientShader::MakeLinear( - points, colors, nullptr, 3, - SkTileMode::kClamp, 0, nullptr)); - - canvas->drawRRect(SkRRect::MakeRectXY(SkRect::MakeXYWH(startX, startY, w, h), topLeftRadius, topLeftRadius), backPaint); - backPaint.setShader(nullptr); -} -int skia_getsetRGB(int32 skiaSurface, void *pixels, int32 offset, int32 x, int32 y, int32 w, int32 h, bool isGet) -{ - SKIA_TRACE() - // SkImageInfo info = SkImageInfo::MakeN32Premul(w, h); - - SkBitmap bitmap; - // bitmap.installPixels(SkImageInfo::Make(w, h, kN32_SkColorType, kUnpremul_SkAlphaType), (void*)pixels, sizeof(Pixel) * w, nullptr, nullptr); - - // SkBitmap bitmap; - bitmap.allocPixels(SkImageInfo::MakeN32Premul(w, h)); - if (isGet) { - return canvas->readPixels(bitmap, x, y); - } else { - return canvas->writePixels(bitmap, x, y); - } - - // Pixel pixel = bitmap.getAddr32(0, 0)[0]; - // return (((pixel >> 24) & 0xFF) << 24) | (((pixel & 0xFF) << 16) | (((pixel >> 8) & 0xFF) << 8) | ((pixel >> 16) & 0xFF)); - - // if (isGet) { - // return canvas->readPixels(info, pixels, info.minRowBytes(), x, y); - // } else { - // return canvas->writePixels(info, pixels, info.minRowBytes(), x, y); - // } -} - -void skia_shiftScreen(float w, float h, float glShiftY) { - canvas->restoreToCount(1); - - // resets the matrix before translating - canvas->setMatrix(SkMatrix::I()); - - // equivalent to: - // 1.0f - (2.0f * glShiftY / h) - canvas->translate(0, glShiftY); - - flushSkia(); -} - -#ifdef ANDROID -/** - * Draws something into the given bitmap - * @param env - * @param thiz - * @param dstBitmap The bitmap to place the results of skia into - * @param elapsedTime The number of milliseconds since the app was started - */ -extern "C" JNIEXPORT void JNICALL Java_totalcross_Launcher4A_drawIntoBitmap(JNIEnv *env, - jobject thiz, jobject dstBitmap, jlong elapsedTime) -{ - // Grab the dst bitmap info and pixels - AndroidBitmapInfo dstInfo; - void *dstPixels; - AndroidBitmap_getInfo(env, dstBitmap, &dstInfo); - AndroidBitmap_lockPixels(env, dstBitmap, &dstPixels); - - SkImageInfo info = SkImageInfo::MakeN32Premul(dstInfo.width, dstInfo.height); - - // write from canvas to the screen bitmap - if (canvas != NULL) - { - bool ret = canvas->readPixels(info, dstPixels, dstInfo.stride, 0, 0); - } - // Unlock the dst's pixels - AndroidBitmap_unlockPixels(env, dstBitmap); -} -#endif - -#ifdef HEADLESS -int32 colorType(uint32 pixelformat) { - if (SDL_PIXELTYPE(pixelformat) == SDL_PIXELTYPE_PACKED16) { - if (SDL_PIXELORDER(pixelformat) == SDL_PACKEDORDER_XRGB) { - if (SDL_PIXELLAYOUT(pixelformat) == SDL_PACKEDLAYOUT_565) { - return kRGB_565_SkColorType; - } - } - } - else if (SDL_PIXELTYPE(pixelformat) == SDL_PIXELTYPE_PACKED32) { - if (SDL_PIXELLAYOUT(pixelformat) == SDL_PACKEDLAYOUT_8888) { - if (SDL_PIXELORDER(pixelformat) == SDL_PACKEDORDER_XRGB || - SDL_PIXELORDER(pixelformat) == SDL_PACKEDORDER_ARGB) { - return kBGRA_8888_SkColorType; - } - } - } - debug("Unsupported pixel format %s, try mapping your color format on %s - %s", SDL_GetPixelFormatName(pixelformat), __FILE__, __FUNCTION__); - return kUnknown_SkColorType; -} -#endif diff --git a/TotalCrossVM/src/nm/ui/font_Font.c b/TotalCrossVM/src/nm/ui/font_Font.c index 66f70884c4..11b294a6af 100644 --- a/TotalCrossVM/src/nm/ui/font_Font.c +++ b/TotalCrossVM/src/nm/ui/font_Font.c @@ -1,5 +1,6 @@ // Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda // // SPDX-License-Identifier: LGPL-2.1-only @@ -8,7 +9,7 @@ #include "tcvm.h" #include "PalmFont_c.h" #if defined USE_SKIA && (defined ANDROID || defined darwin || defined HEADLESS) -#include "android/skia.h" +#include "skia/skia.h" #endif #define PLAIN 0 diff --git a/TotalCrossVM/src/nm/ui/image_Image.c b/TotalCrossVM/src/nm/ui/image_Image.c index 1a39d5ce6a..9d47b74fa2 100644 --- a/TotalCrossVM/src/nm/ui/image_Image.c +++ b/TotalCrossVM/src/nm/ui/image_Image.c @@ -151,7 +151,7 @@ TC_API void tuiI_setTransparentColor_i(NMParams p) // totalcross/ui/image/Image } #if defined USE_SKIA && (defined ANDROID || defined darwin || defined HEADLESS) -#include "android/skia.h" +#include "skia/skia.h" #endif ////////////////////////////////////////////////////////////////////////// TC_API void tuiI_applyChanges(NMParams p) // totalcross/ui/image/Image native public void applyChanges(); @@ -163,18 +163,27 @@ TC_API void tuiI_applyChanges(NMParams p) // totalcross/ui/image/Image native pu #endif #else TCObject img = p->obj[0]; + int32 id = Image_textureId(img); + if (id >= 0) { + Image_changed(img) = false; + return; + } int32 frameCount = Image_frameCount(img); TCObject pixelsObj = frameCount == 1 ? Image_pixels(img) : Image_pixelsOfAllFrames(img); if (pixelsObj != NULL) { int32 width = (frameCount > 1) ? Image_widthOfAllFrames(img) : Image_width(img); int32 height = Image_height(img); - int32 id = Image_textureId(img); Pixel *pixels = (Pixel *)ARRAYOBJ_START(pixelsObj); - Image_textureId(img) = skia_makeBitmap(id, pixels, width, height); + id = skia_makeBitmap(SKIA_SCREEN_SURFACE_ID, pixels, width, height); + if (id >= 0) { + Image_textureId(img) = id; + } + } + if (Image_textureId(img) >= 0) { + Image_changed(img) = false; } - Image_changed(img) = false; #endif } ////////////////////////////////////////////////////////////////////////// diff --git a/TotalCrossVM/src/nm/ui/linux/gfx_Graphics_c.h b/TotalCrossVM/src/nm/ui/linux/gfx_Graphics_c.h index 505815e8c9..82f2057f4e 100644 --- a/TotalCrossVM/src/nm/ui/linux/gfx_Graphics_c.h +++ b/TotalCrossVM/src/nm/ui/linux/gfx_Graphics_c.h @@ -1,5 +1,6 @@ // Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda // // SPDX-License-Identifier: LGPL-2.1-only @@ -13,7 +14,7 @@ void privateScreenChange(int32 w, int32 h) #include "../../init/tcsdl.h" #ifdef USE_SKIA -#include "../android/skia.h" +#include "../skia/skia.h" #endif diff --git a/TotalCrossVM/src/nm/ui/skia/skia.cpp b/TotalCrossVM/src/nm/ui/skia/skia.cpp new file mode 100644 index 0000000000..06441aad45 --- /dev/null +++ b/TotalCrossVM/src/nm/ui/skia/skia.cpp @@ -0,0 +1,336 @@ +// Copyright (C) 2000-2013 SuperWaba Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +#include "skia_internal.h" + +#define USE_WRITE_PIXELS 1 + +#ifndef USE_COMPUTE_OPAQUE +#if __APPLE__ || ANDROID +#define USE_COMPUTE_OPAQUE 0 +#else +#define USE_COMPUTE_OPAQUE 1 +#endif +#endif + +#ifndef USE_COLORTYPE_CONVERSION +#if __APPLE__ || ANDROID +#define USE_COLORTYPE_CONVERSION 0 +#else +#define USE_COLORTYPE_CONVERSION 1 +#endif +#endif + +#ifndef USE_NATIVE_SWAP +#if __APPLE__ || ANDROID +#define USE_NATIVE_SWAP 0 +#else +#define USE_NATIVE_SWAP 1 +#endif +#endif + +#if __APPLE__ +#ifdef darwin +#include +#include +#else +#include +#include +#endif +#else +#if !defined(__arm__) && !defined(ANDROID) +#include +#else +#include +// #include +#include +#include + +#if defined ANDROID +#include +#include +#endif +#endif +#endif +#if defined HEADLESS +#include "../../../init/tcsdl.h" +#endif +#include + + +#include "include/core/SkPathEffect.h" +#include "include/core/SkGraphics.h" +#include "include/core/SkSurface.h" +#include "include/core/SkString.h" +#include "include/core/SkTime.h" +#include "include/core/SkCanvas.h" +#include "include/utils/SkRandom.h" +#include "include/core/SkTypeface.h" +#include "include/core/SkImage.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkImageEncoder.h" +#include "include/core/SkPath.h" +#include "include/effects/SkGradientShader.h" +#include "include/core/SkTextBlob.h" + +#include "include/gpu/gl/GrGLAssembleInterface.h" +#include "include/gpu/gl/GrGLConfig.h" +#include "include/gpu/gl/GrGLExtensions.h" +#include "include/gpu/gl/GrGLFunctions.h" +#include "include/gpu/gl/GrGLInterface.h" +#include "include/gpu/gl/GrGLTypes.h" + +#include "include/gpu/GrBackendSurface.h" +#include "include/gpu/GrDirectContext.h" +#include "include/gpu/GrTypes.h" + +#include "include/core/SkColorSpace.h" +#include "include/effects/SkDashPathEffect.h" + +#include +#include + + +extern "C" { +#if !defined APPLE && !defined ANDROID && !defined darwin && defined linux && defined __arm__ && !defined __aarch64__ +// Avoid dependency on glibc 2.27 +// These functions are used by Skia .a file, so we have to define a wrapper. +// https://stackoverflow.com/questions/8823267/linking-against-older-symbol-version-in-a-so-file +__asm__(".symver log2f,log2f@GLIBC_2.4"); +float __wrap_log2f(float x) { + return log2f(x); +} + +__asm__(".symver powf,powf@GLIBC_2.4"); +float __wrap_powf(float x, float y) { + return powf(x, y); +} + +__asm__(".symver expf,expf@GLIBC_2.4"); +float __wrap_expf(float x) { + return expf(x); +} + +__asm__(".symver exp2f,exp2f@GLIBC_2.4"); +float __wrap_exp2f(float x) { + return exp2f(x); +} +#endif +} + + +#define SKIA_DEBUG +// #define SKIA_TRACE + +#if defined SKIA_DEBUG && defined ANDROID + #include + #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "TotalCross", __VA_ARGS__) +#else + #define LOGD(...) printf("SKIA %s, %d\n", __FUNCTION__, __LINE__) +#endif + +#if defined SKIA_TRACE && defined ANDROID + #define SKIA_TRACE() LOGD(__FUNCTION__); +#else + #define SKIA_TRACE() //LOGD(); +#endif + +sk_sp surface; +SkCanvas *canvas; +SkPaint forePaint; // used for contours +SkPaint backPaint; // used for fills +SkPaint alphaPaint; // used for alphaMask +SkBitmap bitmap; +SkFont skFont; + +#define TYPEFACE_LEN 32 +sk_sp typefaces[TYPEFACE_LEN]; +int typefaceIdx = 0; + +std::vector> imageSurfaces; + +std::map typefaceIndexMap; + +void initSkia(int w, int h, void * pixels, int pitch, uint32_t pixelformat) +{ + SKIA_TRACE() +#ifdef HEADLESS + bitmap.installPixels(SkImageInfo::Make(w, + h, + (SkColorType) colorType(pixelformat), kPremul_SkAlphaType), (Uint32 *)pixels, pitch); + canvas = new SkCanvas(bitmap); +#else + // To use Skia's GPU backend, a OpenGL context is needed. Skia uses the "Gr" library to abstract + // the different OpenGL variants (Core, ES, etc). Most of the code bellow is dedicated to create + // a GL context and produce a valid rendertarget out of it for rendering. + auto interface = GrGLMakeNativeInterface(); + sk_sp grContext(GrDirectContext::MakeGL(interface)); + + GLint defaultFBO; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &defaultFBO); + GrGLFramebufferInfo framebuffer_info; + framebuffer_info.fFBOID = defaultFBO; + framebuffer_info.fFormat = GL_RGB8_OES; + + GrBackendRenderTarget render_target( + w, h, 0, 8, + framebuffer_info); + + SkSurfaceProps surface_props = + SkSurfaceProps(); + + sk_sp gpuSurface = SkSurface::MakeFromBackendRenderTarget( + grContext.get(), render_target, kBottomLeft_GrSurfaceOrigin, + kRGB_888x_SkColorType, nullptr, &surface_props); + + SkCanvas *gpuCanvas = gpuSurface->getCanvas(); + + // We cache a reference for the surface and canvas for later use. + surface = gpuSurface; + canvas = gpuCanvas; +#endif + skFont.setSize(16); + // The forepaint is used for "draw" methods + forePaint.setStyle(SkPaint::kStroke_Style); + forePaint.setAntiAlias(true); + forePaint.setAntiAlias(true); + + //the backpaint is used for "fill" methods + backPaint.setStyle(SkPaint::kFill_Style); + backPaint.setAntiAlias(true); + + backPaint.setAntiAlias(true); + canvas->clear(SK_ColorWHITE); + flushSkia(); +} + +void flushSkia() +{ + if (surface) { + surface->flushAndSubmit(); + } else if (canvas) { + canvas->flush(); + } +#ifdef HEADLESS + TCSDL_UpdateTexture(bitmap.width(), bitmap.height(), bitmap.rowBytes(),bitmap.getPixels()); +#endif +} + +// Creates a SkTypeface object out of a in-memory TTF file (probably inside some TCZ archive) +int32 skia_makeTypeface(char* name, void *data, int32 size) +{ + SKIA_TRACE() + std::string key = name; + int32 idx = skia_getTypefaceIndex(name); + + if (idx == -1 && typefaceIdx < TYPEFACE_LEN - 1) { + sk_sp typefaceData = SkData::MakeWithCopy(data, size); + sk_sp typeface = SkTypeface::MakeFromData(typefaceData); + idx = typefaceIdx; + typefaces[typefaceIdx++] = typeface; + typefaceIndexMap[key] = idx; + } + + return idx; +} + +int32 skia_getTypefaceIndex(char* name) { + std::string key = name; + std::map::iterator it = typefaceIndexMap.find(key); + + if (it != typefaceIndexMap.end()) { + return it->second; + } + + return -1; +} + +sk_sp skia_getTypeface(int32 typefaceIndex) { + if (typefaceIndex >= 0 && typefaceIndex < typefaceIdx) { + return typefaces[typefaceIndex]; + } else { + return SkTypeface::MakeFromName(nullptr, SkFontStyle()); + } +} + +int32 skia_stringWidth(const void *text, int32 charCount, int32 typefaceIndex, int32 fontSize) +{ + const auto newTypeFace = skia_getTypeface(typefaceIndex); + + if(skFont.getTypeface() != newTypeFace.get()) { + skFont.setTypeface(newTypeFace); + } + if(skFont.getSize() != fontSize) { + skFont.setSize(fontSize); + } + return skFont.measureText(text,charCount,SkTextEncoding::kUTF16); +} + + + +void skia_shiftScreen(float w, float h, float glShiftY) { + canvas->restoreToCount(1); + + // resets the matrix before translating + canvas->setMatrix(SkMatrix::I()); + + // equivalent to: + // 1.0f - (2.0f * glShiftY / h) + canvas->translate(0, glShiftY); + + flushSkia(); +} + +#ifdef ANDROID +/** + * Draws something into the given bitmap + * @param env + * @param thiz + * @param dstBitmap The bitmap to place the results of skia into + * @param elapsedTime The number of milliseconds since the app was started + */ +extern "C" JNIEXPORT void JNICALL Java_totalcross_Launcher4A_drawIntoBitmap(JNIEnv *env, + jobject thiz, jobject dstBitmap, jlong elapsedTime) +{ + // Grab the dst bitmap info and pixels + AndroidBitmapInfo dstInfo; + void *dstPixels; + AndroidBitmap_getInfo(env, dstBitmap, &dstInfo); + AndroidBitmap_lockPixels(env, dstBitmap, &dstPixels); + + SkImageInfo info = SkImageInfo::MakeN32Premul(dstInfo.width, dstInfo.height); + + // write from canvas to the screen bitmap + if (canvas != NULL) + { + bool ret = canvas->readPixels(info, dstPixels, dstInfo.stride, 0, 0); + } + // Unlock the dst's pixels + AndroidBitmap_unlockPixels(env, dstBitmap); +} +#endif + +#ifdef HEADLESS +int32 colorType(uint32 pixelformat) { + if (SDL_PIXELTYPE(pixelformat) == SDL_PIXELTYPE_PACKED16) { + if (SDL_PIXELORDER(pixelformat) == SDL_PACKEDORDER_XRGB) { + if (SDL_PIXELLAYOUT(pixelformat) == SDL_PACKEDLAYOUT_565) { + return kRGB_565_SkColorType; + } + } + } + else if (SDL_PIXELTYPE(pixelformat) == SDL_PIXELTYPE_PACKED32) { + if (SDL_PIXELLAYOUT(pixelformat) == SDL_PACKEDLAYOUT_8888) { + if (SDL_PIXELORDER(pixelformat) == SDL_PACKEDORDER_XRGB || + SDL_PIXELORDER(pixelformat) == SDL_PACKEDORDER_ARGB) { + return kBGRA_8888_SkColorType; + } + } + } + debug("Unsupported pixel format %s, try mapping your color format on %s - %s", SDL_GetPixelFormatName(pixelformat), __FILE__, __FUNCTION__); + return kUnknown_SkColorType; +} +#endif diff --git a/TotalCrossVM/src/nm/ui/android/skia.h b/TotalCrossVM/src/nm/ui/skia/skia.h similarity index 87% rename from TotalCrossVM/src/nm/ui/android/skia.h rename to TotalCrossVM/src/nm/ui/skia/skia.h index 1a995de8dd..fbbe86137f 100644 --- a/TotalCrossVM/src/nm/ui/android/skia.h +++ b/TotalCrossVM/src/nm/ui/skia/skia.h @@ -1,11 +1,15 @@ // Copyright (C) 2000-2013 SuperWaba Ltda. -// Copyright (C) 2014-2020 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2014-2021 TotalCross Global Mobile Platform Ltda. +// Copyright (C) 2022-2026 Amalgam Solucoes em TI Ltda // // SPDX-License-Identifier: LGPL-2.1-only #ifndef SKIA_H #define SKIA_H +#define SKIA_SCREEN_SURFACE_ID (-1) +#define SKIA_INVALID_SURFACE_ID (-2) + #ifdef __cplusplus extern "C" { @@ -29,12 +33,13 @@ int32 skia_stringWidth(const void *text, int32 charCount, int32 typefaceIndex, i int skia_makeBitmap(int32 id, void *data, int32 w, int32 h); void skia_deleteBitmap(int32 id); -void skia_setClip(int32 x1, int32 y1, int32 x2, int32 y2); -void skia_restoreClip(); +void skia_setClip(int32 skiaSurface, int32 x1, int32 y1, int32 x2, int32 y2); +void skia_restoreClip(int32 skiaSurface); void skia_drawSurface(int32 skiaSurface, int32 id, float srcLeft, float srcTop, float srcRight, float srcBottom, float dstLeft, float dstTop, float dstRight, float dstBottom, int32 alphaMask); void skia_drawDottedLine(int32 skiaSurface, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel1, Pixel pixel2); Pixel skia_getPixel(int32 skiaSurface, int32 x, int32 y); +int skia_getPixelRow(int32 skiaSurface, void *output, int32 y, int32 width); void skia_setPixel(int32 skiaSurface, int32 x, int32 y, Pixel pixel); void skia_drawLine(int32 skiaSurface, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel); void skia_drawRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, Pixel pixel); diff --git a/TotalCrossVM/src/nm/ui/skia/skia_internal.h b/TotalCrossVM/src/nm/ui/skia/skia_internal.h new file mode 100644 index 0000000000..4b650bb262 --- /dev/null +++ b/TotalCrossVM/src/nm/ui/skia/skia_internal.h @@ -0,0 +1,134 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +#ifndef SKIA_INTERNAL_H +#define SKIA_INTERNAL_H + +#include "skia.h" + +#include +#include +#include +#include +#include +#include + +#include "include/core/SkBitmap.h" +#include "include/core/SkCanvas.h" +#include "include/core/SkColorSpace.h" +#include "include/core/SkGraphics.h" +#include "include/core/SkImage.h" +#include "include/core/SkImageEncoder.h" +#include "include/core/SkImageInfo.h" +#include "include/core/SkPath.h" +#include "include/core/SkPathEffect.h" +#include "include/core/SkString.h" +#include "include/core/SkSurface.h" +#include "include/core/SkTextBlob.h" +#include "include/core/SkTypeface.h" +#include "include/effects/SkDashPathEffect.h" +#include "include/effects/SkGradientShader.h" +#include "include/gpu/GrBackendSurface.h" +#include "include/gpu/GrDirectContext.h" +#include "include/gpu/GrTypes.h" +#include "include/gpu/gl/GrGLAssembleInterface.h" +#include "include/gpu/gl/GrGLConfig.h" +#include "include/gpu/gl/GrGLExtensions.h" +#include "include/gpu/gl/GrGLFunctions.h" +#include "include/gpu/gl/GrGLInterface.h" +#include "include/gpu/gl/GrGLTypes.h" +#include "include/utils/SkRandom.h" + +static inline SkColor skiaColorFromPixel(Pixel pixel) { + return SkColorSetARGB((pixel >> 24) & 0xFF, (pixel >> 16) & 0xFF, + (pixel >> 8) & 0xFF, pixel & 0xFF); +} + +static inline Pixel skiaPixelFromColor(SkColor color) { + return ((Pixel)SkColorGetA(color) << 24) | ((Pixel)SkColorGetR(color) << 16) + | ((Pixel)SkColorGetG(color) << 8) | SkColorGetB(color); +} + +#ifndef USE_COMPUTE_OPAQUE +#if __APPLE__ || ANDROID +#define USE_COMPUTE_OPAQUE 0 +#else +#define USE_COMPUTE_OPAQUE 1 +#endif +#endif + +#ifndef USE_COLORTYPE_CONVERSION +#if __APPLE__ || ANDROID +#define USE_COLORTYPE_CONVERSION 0 +#else +#define USE_COLORTYPE_CONVERSION 1 +#endif +#endif + +#ifndef USE_NATIVE_SWAP +#if __APPLE__ || ANDROID +#define USE_NATIVE_SWAP 0 +#else +#define USE_NATIVE_SWAP 1 +#endif +#endif + +#define USE_WRITE_PIXELS 1 + +#define SKIA_DEBUG + +#if defined SKIA_DEBUG && defined ANDROID +#include +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "TotalCross", __VA_ARGS__) +#else +#define LOGD(...) printf("SKIA %s, %d\n", __FUNCTION__, __LINE__) +#endif + +#if defined SKIA_TRACE && defined ANDROID +#define SKIA_TRACE() LOGD(__FUNCTION__); +#else +#define SKIA_TRACE() //LOGD(); +#endif + +#if USE_NATIVE_SWAP +inline uint32_t builtinSwap32(uint32_t val) noexcept { +#if defined(__clang__) + return __builtin_bswap32(val); +#elif defined(__GNUG__) + return __builtin_bswap32(val); +#elif defined(_MSC_VER) + return _byteswap_ulong(val); +#endif +} +#define SWAP32(n) builtinSwap32(n) +#else +#define SWAP32(n) (((n >> 24) & 0xFF)) | ((((n >> 16) & 0xFF) << 8) | (((n >> 8) & 0xFF) << 16) | ((n & 0xFF) << 24)) +#endif + +extern sk_sp surface; +extern SkCanvas *canvas; +extern SkPaint forePaint; +extern SkPaint backPaint; +extern SkPaint alphaPaint; +extern SkBitmap bitmap; +extern SkFont skFont; + +struct SkiaImageSurface { + SkBitmap bitmap; + std::unique_ptr canvas; +}; + +extern std::vector> imageSurfaces; + +SkCanvas* skiaGetCanvas(int32 surfaceId); +SkBitmap* skiaGetBitmap(int32 surfaceId); + +#define TYPEFACE_LEN 32 +extern sk_sp typefaces[TYPEFACE_LEN]; +extern int typefaceIdx; +extern std::map typefaceIndexMap; + +sk_sp skia_getTypeface(int32 typefaceIndex); + +#endif diff --git a/TotalCrossVM/src/nm/ui/skia/skia_primitives.cpp b/TotalCrossVM/src/nm/ui/skia/skia_primitives.cpp new file mode 100644 index 0000000000..20f02b85af --- /dev/null +++ b/TotalCrossVM/src/nm/ui/skia/skia_primitives.cpp @@ -0,0 +1,279 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +#include "skia_internal.h" + +void skia_drawDottedLine(int32 skiaSurface, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel1, Pixel pixel2) +{ + SKIA_TRACE() + float intervals[] = {5, 5}; + forePaint.setPathEffect(SkDashPathEffect::Make(intervals, 2, 2.5f)); + skia_drawLine(skiaSurface, x1, y1, x2, y2, pixel1); + forePaint.setPathEffect(nullptr); + + forePaint.setPathEffect(SkDashPathEffect::Make(intervals, 2, 7.5f)); + skia_drawLine(skiaSurface, x1, y1, x2, y2, pixel2); + forePaint.setPathEffect(nullptr); +} + +void skia_drawLine(int32 skiaSurface, int32 x1, int32 y1, int32 x2, int32 y2, Pixel pixel) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + forePaint.setColor(skiaColorFromPixel(pixel)); + targetCanvas->drawLine(x1, y1, x2, y2, forePaint); +} + +void skia_drawRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, Pixel pixel) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + forePaint.setColor(skiaColorFromPixel(pixel)); + targetCanvas->drawRect(SkRect::MakeXYWH(x, y, w, h), forePaint); +} + +void skia_fillRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, Pixel pixel) +{ + SKIA_TRACE() + // printf("Exe log: skia fill rect = %#010x\n",pixel); + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + backPaint.setColor(skiaColorFromPixel(pixel)); + targetCanvas->drawRect(SkRect::MakeXYWH(x, y, w, h), backPaint); +} + +void skia_drawText(int32 skiaSurface, const void *text, int32 chrCount, int32 x0, int32 y0, Pixel foreColor, int32 justifyWidth, int32 fontSize, int32 typefaceIndex) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + const auto newTypeFace = skia_getTypeface(typefaceIndex); + + if(skFont.getTypeface() != newTypeFace.get()) { + skFont.setTypeface(newTypeFace); + } + if(skFont.getSize() != fontSize) { + skFont.setSize(fontSize); + } + if(backPaint.getColor() != foreColor){ + backPaint.setColor(skiaColorFromPixel(foreColor)); + } + targetCanvas->drawTextBlob(SkTextBlob::MakeFromText(text,chrCount,skFont,SkTextEncoding::kUTF16),x0,y0,backPaint); +} + +void skia_ellipseDrawAndFill(int32 skiaSurface, int32 xc, int32 yc, int32 rx, int32 ry, Pixel pc1, Pixel pc2, bool fill, bool gradient) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + if (fill) { + if (gradient) { + SkPoint points[3] = { + SkPoint::Make(xc, yc - ry), + SkPoint::Make(xc, yc + ry), + SkPoint::Make(xc, yc + ry * 2) + }; + SkColor colors[3] = {skiaColorFromPixel(pc2), skiaColorFromPixel(pc1), skiaColorFromPixel(pc2)}; + backPaint.setShader(SkGradientShader::MakeLinear( + points, colors, nullptr, 3, + SkTileMode::kClamp, 0, nullptr)); + targetCanvas->drawOval(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), backPaint); + backPaint.setShader(nullptr); + } else { + backPaint.setColor(skiaColorFromPixel(pc2)); + targetCanvas->drawOval(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), backPaint); + } + } else { + forePaint.setColor(skiaColorFromPixel(pc1)); + targetCanvas->drawOval(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), forePaint); + } +} + +SkPath _skia_makePath(int32 *x, int32 *y, int32 n) +{ + SKIA_TRACE() + SkPath path; + path.moveTo(x[0], y[0]); + for (int i = 1; i < n; ++i) { + path.lineTo(x[i], y[i]); + } + + return path; +} +void _skia_getPathBounds(int32 *x, int32 *y, int32 n, int32* minY, int32* maxY) +{ + SKIA_TRACE() + *minY = y[0]; + *maxY = y[0]; + for (int i = 1; i < n; ++i) { + *minY = y[i] < *minY ? y[i] : *minY; + *maxY = *maxY < y[i] ? y[i] : *maxY; + } +} +void skia_drawPolygon(int32 skiaSurface, int32 *xPoints, int32 *yPoints, int32 nPoints, int32 tx, int32 ty, Pixel pixel) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + forePaint.setColor(skiaColorFromPixel(pixel)); + targetCanvas->translate(tx, ty); + targetCanvas->drawPath(_skia_makePath(xPoints, yPoints, nPoints), forePaint); + targetCanvas->translate(-tx, -ty); +} + +void skia_fillPolygon(int32 skiaSurface, int32 *xPoints, int32 *yPoints, int32 nPoints, int32 tx, int32 ty, Pixel c1, Pixel c2, bool gradient, bool isPie) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + SkPath path = _skia_makePath(xPoints, yPoints, nPoints); + + backPaint.setColor(skiaColorFromPixel(c1)); + if (gradient) { + int32 minY, maxY; + _skia_getPathBounds(xPoints, yPoints, nPoints, &minY, &maxY); + SkPoint points[2] = { + SkPoint::Make(xPoints[0], minY), + SkPoint::Make(xPoints[0], maxY), + }; + SkColor colors[2] = {skiaColorFromPixel(c1), skiaColorFromPixel(c2)}; + backPaint.setShader(SkGradientShader::MakeLinear( + points, colors, nullptr, 2, + SkTileMode::kClamp, 0, nullptr)); + } + + targetCanvas->translate(tx, ty); + targetCanvas->drawPath(path, backPaint); + targetCanvas->translate(-tx, -ty); + + if (gradient) { + backPaint.setShader(nullptr); + } +} + +// Adapted from SkPathPriv::CreateDrawArcPath +SkPath _skia_makeArcPath(const SkRect& oval, SkScalar startAngle, SkScalar sweepAngle, bool useCenter) { + SkASSERT(!oval.isEmpty()); + SkASSERT(sweepAngle); + + SkPath path; + path.setIsVolatile(true); + path.setFillType(SkPathFillType::kWinding); + path.reset(); + if (SkScalarAbs(sweepAngle) >= 360.f) { + path.addOval(oval); + return path; + } + if (useCenter) { + path.moveTo(oval.centerX(), oval.centerY()); + } + // Arc to mods at 360 and drawArc is not supposed to. + bool forceMoveTo = !useCenter; + while (sweepAngle <= -360.f) { + path.arcTo(oval, startAngle, -180.f, forceMoveTo); + startAngle -= 180.f; + path.arcTo(oval, startAngle, -180.f, false); + startAngle -= 180.f; + forceMoveTo = false; + sweepAngle += 360.f; + } + while (sweepAngle >= 360.f) { + path.arcTo(oval, startAngle, 180.f, forceMoveTo); + startAngle += 180.f; + path.arcTo(oval, startAngle, 180.f, false); + startAngle += 180.f; + forceMoveTo = false; + sweepAngle -= 360.f; + } + path.arcTo(oval, startAngle, sweepAngle, forceMoveTo); + if (useCenter) { + path.close(); + } + + return path; +} +void skia_arcPiePointDrawAndFill(int32 skiaSurface, int32 xc, int32 yc, int32 rx, int32 ry, double startAngle, double endAngle, Pixel c, Pixel c2, bool fill, bool pie, bool gradient) +{ + double start = -startAngle; + double sweepAngle = -(endAngle - startAngle); + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + if (fill) { + backPaint.setColor(skiaColorFromPixel(c2)); + if (gradient) { + SkPath arcPath = _skia_makeArcPath(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), start, sweepAngle, pie); + SkRect r = arcPath.computeTightBounds(); + + SkPoint points[2] = { + SkPoint::Make(r.centerX(), r.y()), + SkPoint::Make(r.centerX(), r.y() + r.height() * 2) + }; + SkColor colors[2] = {skiaColorFromPixel(c), skiaColorFromPixel(c2)}; + backPaint.setShader(SkGradientShader::MakeLinear( + points, colors, nullptr, 3, + SkTileMode::kClamp, 0, nullptr)); + + targetCanvas->drawPath(arcPath, backPaint); + backPaint.setShader(nullptr); + } else { + targetCanvas->drawArc(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), start, sweepAngle, pie, backPaint); + forePaint.setColor(skiaColorFromPixel(c)); + SkScalar strokeWidth = forePaint.getStrokeWidth(); + forePaint.setStrokeWidth(2); + targetCanvas->drawArc(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), start, sweepAngle, pie, forePaint); + forePaint.setStrokeWidth(strokeWidth); + } + } else { + forePaint.setColor(skiaColorFromPixel(c)); + targetCanvas->drawArc(SkRect::MakeXYWH(xc - rx, yc - ry, rx * 2, ry * 2), start, sweepAngle, pie, forePaint); + } +} + +void skia_drawRoundRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, int32 r, Pixel c) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + forePaint.setColor(skiaColorFromPixel(c)); + targetCanvas->drawRRect(SkRRect::MakeRectXY(SkRect::MakeXYWH(x, y, w, h), r, r), forePaint); +} + +void skia_fillRoundRect(int32 skiaSurface, int32 x, int32 y, int32 w, int32 h, int32 r, Pixel c) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + backPaint.setColor(skiaColorFromPixel(c)); + targetCanvas->drawRRect(SkRRect::MakeRectXY(SkRect::MakeXYWH(x, y, w, h), r, r), backPaint); +} + +void skia_drawRoundGradient(int32 skiaSurface, int32 startX, int32 startY, int32 endX, int32 endY, int32 topLeftRadius, int32 topRightRadius, int32 bottomLeftRadius, int32 bottomRightRadius, int32 startColor, int32 endColor, bool vertical) +{ + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) return; + int32 w = endX - startX; + int32 h = endY - startY; + SkPoint points[2]; + if (vertical) { + points[0] = SkPoint::Make(startX, startY); + points[1] = SkPoint::Make(startX, startY + h * 2); + } else { + points[0] = SkPoint::Make(startX, startY); + points[1] = SkPoint::Make(startX + w * 2, startY); + } + + SkColor colors[2] = { + skiaColorFromPixel(static_cast(startColor)), + skiaColorFromPixel(static_cast(endColor))}; + backPaint.setShader(SkGradientShader::MakeLinear( + points, colors, nullptr, 3, + SkTileMode::kClamp, 0, nullptr)); + + targetCanvas->drawRRect(SkRRect::MakeRectXY(SkRect::MakeXYWH(startX, startY, w, h), topLeftRadius, topLeftRadius), backPaint); + backPaint.setShader(nullptr); +} diff --git a/TotalCrossVM/src/nm/ui/skia/skia_surface.cpp b/TotalCrossVM/src/nm/ui/skia/skia_surface.cpp new file mode 100644 index 0000000000..a7db730bf3 --- /dev/null +++ b/TotalCrossVM/src/nm/ui/skia/skia_surface.cpp @@ -0,0 +1,233 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +#include "skia_internal.h" + +static void releaseProc(void* addr, void*) { + delete[] static_cast(addr); +} + +SkCanvas* skiaGetCanvas(int32 surfaceId) { + if (surfaceId == SKIA_SCREEN_SURFACE_ID) { + return canvas; + } + if (surfaceId < 0 || static_cast(surfaceId) >= imageSurfaces.size()) { + return nullptr; + } + const auto& imageSurface = imageSurfaces[static_cast(surfaceId)]; + return imageSurface && imageSurface->canvas ? imageSurface->canvas.get() : nullptr; +} + +SkBitmap* skiaGetBitmap(int32 surfaceId) { + if (surfaceId < 0 || static_cast(surfaceId) >= imageSurfaces.size()) { + return nullptr; + } + const auto& imageSurface = imageSurfaces[static_cast(surfaceId)]; + return imageSurface ? &imageSurface->bitmap : nullptr; +} + +int skia_makeBitmap(int32 id, void *data, int32 w, int32 h) { + SKIA_TRACE() + if (!data || w <= 0 || h <= 0) { + return SKIA_INVALID_SURFACE_ID; + } + if (id >= 0 && (static_cast(id) >= imageSurfaces.size() || !imageSurfaces[id])) { + return SKIA_INVALID_SURFACE_ID; + } + + const size_t count = static_cast(w) * static_cast(h); + int32* const converted = new int32[count]; + const Pixel* const src = reinterpret_cast(data); + for (size_t i = 0; i < count; i++) { + converted[i] = SWAP32(src[i]); + } + + auto imageSurface = std::make_unique(); + imageSurface->bitmap.installPixels( + SkImageInfo::Make(w, h, kN32_SkColorType, kUnpremul_SkAlphaType), + converted, + sizeof(Pixel) * static_cast(w), + releaseProc, + nullptr); + + // TODO: Reevaluate this optimization; it was added for embedded Linux ARM, + // but forced scale draws broke on Android/iOS. +#if USE_COMPUTE_OPAQUE + if (SkBitmap::ComputeIsOpaque(imageSurface->bitmap)) { + imageSurface->bitmap.setAlphaType(kOpaque_SkAlphaType); + } +#endif + + // TODO: Recheck the color type before re-enabling on Android/iOS; macOS did + // not reproduce the forced scale issue. +#if USE_COLORTYPE_CONVERSION + if (canvas && imageSurface->bitmap.isOpaque()) { + const SkImageInfo screenInfo = canvas->imageInfo(); + void* dstPixels = operator new(static_cast(w) * static_cast(h) * screenInfo.bytesPerPixel()); + const SkImageInfo dstImageInfo = SkImageInfo::Make( + w, h, screenInfo.colorType(), screenInfo.alphaType()); + imageSurface->bitmap.readPixels(dstImageInfo, dstPixels, dstImageInfo.minRowBytes(), 0, 0); + imageSurface->bitmap.installPixels( + dstImageInfo, dstPixels, dstImageInfo.minRowBytes(), releaseProc, nullptr); + } +#endif + + imageSurface->canvas = std::make_unique(imageSurface->bitmap); + if (id < 0) { + const int newId = static_cast(imageSurfaces.size()); + imageSurfaces.emplace_back(std::move(imageSurface)); + return newId; + } + + imageSurfaces[static_cast(id)] = std::move(imageSurface); + return id; +} + +void skia_deleteBitmap(int32 id) { + SKIA_TRACE() + if (id >= 0 && static_cast(id) < imageSurfaces.size()) { + imageSurfaces[static_cast(id)].reset(); + } +} + +void skia_setClip(int32 skiaSurface, int32 x1, int32 y1, int32 x2, int32 y2) { + if (SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface)) { + targetCanvas->save(); + targetCanvas->clipRect(SkRect::MakeLTRB(x1, y1, x2, y2)); + } +} + +void skia_restoreClip(int32 skiaSurface) { + if (SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface)) { + targetCanvas->restore(); + } +} + +void skia_drawSurface(int32 skiaSurface, int32 id, float srcLeft, float srcTop, + float srcRight, float srcBottom, float dstLeft, float dstTop, + float dstRight, float dstBottom, int32 alphaMask) { + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + SkBitmap* texture = skiaGetBitmap(id); + if (!targetCanvas || !texture) { + return; + } + + const SkRect srcRect = SkRect::MakeLTRB(srcLeft, srcTop, srcRight, srcBottom); + const SkRect dstRect = SkRect::MakeLTRB(dstLeft, dstTop, dstRight, dstBottom); + const bool fullSource = srcLeft == 0.0f && srcTop == 0.0f && + srcRight == texture->width() && srcBottom == texture->height(); + const bool sameSize = (srcRight - srcLeft) == (dstRight - dstLeft) && + (srcBottom - srcTop) == (dstBottom - dstTop); + +#if USE_WRITE_PIXELS + if (texture->isOpaque() && alphaMask == 255 && fullSource && sameSize) { + targetCanvas->writePixels( + texture->info(), texture->getPixels(), texture->rowBytes(), + static_cast(dstLeft), static_cast(dstTop)); + } else +#endif + { + alphaPaint.setAlpha(alphaMask); + alphaPaint.setFilterQuality(sameSize ? kNone_SkFilterQuality : kLow_SkFilterQuality); + targetCanvas->drawBitmapRect( + *texture, srcRect, dstRect, &alphaPaint, + fullSource ? SkCanvas::kFast_SrcRectConstraint : SkCanvas::kStrict_SrcRectConstraint); + } +} + +Pixel skia_getPixel(int32 skiaSurface, int32 x, int32 y) { + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) { + return -1; + } + SkBitmap pixelBitmap; + pixelBitmap.allocPixels(SkImageInfo::MakeN32Premul(1, 1)); + if (!targetCanvas->readPixels(pixelBitmap, x, y)) { + return -1; + } + return skiaPixelFromColor(pixelBitmap.getColor(0, 0)); +} + +int skia_getPixelRow(int32 skiaSurface, void *output, int32 y, int32 width) { + SKIA_TRACE() + SkCanvas* sourceCanvas = skiaGetCanvas(skiaSurface); + const SkImageInfo sourceInfo = sourceCanvas ? sourceCanvas->imageInfo() : SkImageInfo(); + if (!sourceCanvas || !output || y < 0 || width < 0 || y >= sourceInfo.height() || + width > sourceInfo.width()) { + return 0; + } + SkBitmap rowBitmap; + rowBitmap.allocPixels(SkImageInfo::Make( + width, 1, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType)); + if (!sourceCanvas->readPixels(rowBitmap, 0, y)) { + return 0; + } + uint8_t* row = static_cast(output); + for (int32 x = 0; x < width; ++x) { + const uint8_t* rgba = rowBitmap.getAddr8(x, 0); + const SkColor color = SkColorSetARGB(rgba[3], rgba[0], rgba[1], rgba[2]); + *row++ = SkColorGetR(color); + *row++ = SkColorGetG(color); + *row++ = SkColorGetB(color); + *row++ = SkColorGetA(color); + } + return 1; +} + +void skia_setPixel(int32 skiaSurface, int32 x, int32 y, Pixel pixel) { + SKIA_TRACE() + if (SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface)) { + backPaint.setColor(skiaColorFromPixel(pixel)); + targetCanvas->drawRect(SkRect::MakeXYWH(x, y, 1, 1), backPaint); + } +} + +int skia_getsetRGB(int32 skiaSurface, void *pixels, int32 offset, int32 x, int32 y, + int32 w, int32 h, bool isGet) { + SKIA_TRACE() + SkCanvas* targetCanvas = skiaGetCanvas(skiaSurface); + if (!targetCanvas) { + return 0; + } + if (!pixels || w <= 0 || h <= 0) { + return 0; + } + SkBitmap pixelBitmap; + pixelBitmap.allocPixels(SkImageInfo::Make( + w, h, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType)); + uint8_t* rgba = pixelBitmap.getAddr8(0, 0); + Pixel* source = static_cast(pixels) + offset; + if (!isGet) { + for (int32 row = 0; row < h; ++row) { + for (int32 column = 0; column < w; ++column) { + const Pixel pixel = source[row * w + column]; + const SkColor color = skiaColorFromPixel(pixel); + *rgba++ = SkColorGetR(color); + *rgba++ = SkColorGetG(color); + *rgba++ = SkColorGetB(color); + *rgba++ = SkColorGetA(color); + } + } + } + if (isGet) { + if (!targetCanvas->readPixels(pixelBitmap, x, y)) { + return 0; + } + rgba = pixelBitmap.getAddr8(0, 0); + for (int32 row = 0; row < h; ++row) { + for (int32 column = 0; column < w; ++column) { + const uint8_t red = *rgba++; + const uint8_t green = *rgba++; + const uint8_t blue = *rgba++; + const uint8_t alpha = *rgba++; + source[row * w + column] = skiaPixelFromColor( + SkColorSetARGB(alpha, red, green, blue)); + } + } + return 1; + } + return targetCanvas->writePixels(pixelBitmap, x, y); +} diff --git a/scripts/validate-copyright-headers.sh b/scripts/validate-copyright-headers.sh index e8e84ca9ef..c8937976eb 100644 --- a/scripts/validate-copyright-headers.sh +++ b/scripts/validate-copyright-headers.sh @@ -90,12 +90,14 @@ def list_files_from_environment(): def diff_name_status(*args): - output = run_git(["diff", "--name-status", "--diff-filter=ACMRT"] + list(args)) + output = run_git(["diff", "-M", "--name-status", "--diff-filter=ACMRT"] + list(args)) return parse_name_status(output) def diff_tree_name_status(commit): - output = run_git(["diff-tree", "--no-commit-id", "--name-status", "--diff-filter=ACMRT", "-r", commit]) + output = run_git( + ["diff-tree", "-M", "--no-commit-id", "--name-status", "--diff-filter=ACMRT", "-r", commit] + ) return parse_name_status(output) @@ -137,8 +139,6 @@ def should_check(path): def creation_year(path, status): - if status.startswith("A"): - return datetime.date.today().year output = run_git( ["log", "--follow", "--diff-filter=A", "--format=%ad", "--date=format:%Y", "--", path], check=False, @@ -203,6 +203,20 @@ def top_header_lines(body, path): return [] suffix = Path(path).suffix + if suffix == ".md" and lines[0].strip() == "---": + header = [] + for line in lines[1:]: + stripped = line.lstrip() + if stripped.strip() == "---": + break + if stripped.startswith("#") or stripped.strip() == "": + header.append(line) + if SPDX in line: + return header + continue + break + return header + if suffix in {".md", ".html"} and lines[0].lstrip().startswith(" + +# Issue 417 generated-image smoke test + +This test is a minimized copy of the flow in the attached `Tcsort.zip` from +issue 417. It keeps `MonoImage(576, 576)`, `getGraphics()`, a white full-image +fill, a black inset rectangle, and `createPng()` to `nome.png`. The unrelated +controls, viewer launch, and customer text were removed. + +The application writes `nome.png` and `issue-417-result.json` below +`Settings.appPath`. The result records the platform, implementation path, +expected and observed pixels, a row read from the image, encoded size, and a +CRC32 of the encoded PNG. The checker validates the JSON, PNG signature, +dimensions, and presence of image data without Pillow or another dependency. + +## Build and run + +Compile the source with the SDK jars from the same checkout: + + mkdir -p /tmp/issue-417-classes + javac -cp TotalCrossSDK/build/libs/totalcross-sdk-7.2.2.jar:TotalCrossSDK/build/libs/tcui-7.2.2.jar \ + -d /tmp/issue-417-classes tests/smoke/issue-417-generated-image/Tcsort.java + +Deploy the class with the platform-specific TotalCross deployer. For example, +from the repository root, Android packaging uses: + + java -cp TotalCrossSDK/build/libs/totalcross-sdk-7.2.2.jar:TotalCrossSDK/build/libs/tcui-7.2.2.jar \ + tc.Deploy /tmp/issue-417-classes -android /o /tmp/issue-417-android + +Run the resulting application on the target platform, then pull the two files +from its application path and validate them: + + python3 tests/smoke/issue-417-generated-image/check_result.py \ + /path/to/issue-417-result.json /path/to/nome.png + +The checker exits zero only when all fixed geometry, row, and encoded-output +assertions pass. It can also validate a Java SE result; its +`implementationPath` must then be `java-byte-array`. + +On Android, stop the application after the two files are pulled. The Android +loader's asynchronous VM teardown can race `MainWindow.exit`; the result JSON +and checker are the test boundary on that target. + +The original archive is not needed after this source directory is committed. diff --git a/tests/smoke/issue-417-generated-image/Tcsort.java b/tests/smoke/issue-417-generated-image/Tcsort.java new file mode 100644 index 0000000000..1941eb5ae8 --- /dev/null +++ b/tests/smoke/issue-417-generated-image/Tcsort.java @@ -0,0 +1,171 @@ +// Copyright (C) 2026 Amalgam Solucoes em TI Ltda +// +// SPDX-License-Identifier: LGPL-2.1-only + +import totalcross.io.ByteArrayStream; +import totalcross.io.File; +import totalcross.io.IOException; +import totalcross.io.device.printer.MonoImage; +import totalcross.sys.Settings; +import totalcross.ui.MainWindow; +import totalcross.ui.Window; +import totalcross.ui.gfx.Color; +import totalcross.ui.gfx.Graphics; +import totalcross.util.zip.CRC32; + +/** + * Issue 417 regression smoke test. It intentionally keeps the original + * MonoImage/getGraphics/fillRect/drawRect/createPng flow from Tcsort.zip. + */ +public class Tcsort extends MainWindow { + private static final int WIDTH = 576; + private static final int HEIGHT = 576; + private static final int BORDER = 10; + private static final int BORDER_WIDTH = WIDTH - (BORDER * 2); + private static final String PNG_NAME = "nome.png"; + private static final String RESULT_NAME = "issue-417-result.json"; + + public Tcsort() { + super("", Window.NO_BORDER); + setDeviceTitle("issue-417-generated-image"); + } + + @Override + public void initUI() { + super.initUI(); + testGraphics(); + } + + private void testGraphics() { + boolean pass = false; + String failure = ""; + int background = Color.WHITE; + int border = Color.BLACK; + int interior = Color.WHITE; + int rowR = -1; + int rowG = -1; + int rowB = -1; + int rowA = -1; + int pngSize = 0; + long pngCrc32 = 0; + int[] selectedPixels = { background, border, interior, background }; + + try { + MonoImage image = new MonoImage(WIDTH, HEIGHT); + Graphics graphics = image.getGraphics(); + + // These are the issue's original drawing operations and coordinates. + graphics.backColor = Color.WHITE; + graphics.fillRect(0, 0, WIDTH, HEIGHT); + graphics.foreColor = Color.BLACK; + graphics.drawRect(BORDER, BORDER, BORDER_WIDTH, BORDER_WIDTH); + + selectedPixels = new int[] { + graphics.getPixel(0, 0) & 0xFFFFFF, + graphics.getPixel(BORDER, BORDER) & 0xFFFFFF, + graphics.getPixel(BORDER + 1, BORDER + 1) & 0xFFFFFF, + graphics.getPixel(WIDTH - 1, HEIGHT - 1) & 0xFFFFFF + }; + assertPixel("background", selectedPixels[0], background); + assertNotPixel("border", selectedPixels[1], background); + assertPixel("interior", selectedPixels[2], interior); + assertPixel("background-corner", selectedPixels[3], background); + + byte[] row = new byte[WIDTH * 4]; + image.getPixelRow(row, BORDER + 1); + int interiorOffset = (BORDER + 1) * 4; + rowR = row[interiorOffset] & 0xFF; + rowG = row[interiorOffset + 1] & 0xFF; + rowB = row[interiorOffset + 2] & 0xFF; + rowA = row[interiorOffset + 3] & 0xFF; + assertEquals("row red", 255, rowR); + assertEquals("row green", 255, rowG); + assertEquals("row blue", 255, rowB); + assertEquals("row alpha", 255, rowA); + + ByteArrayStream encoded = new ByteArrayStream(WIDTH * HEIGHT + HEIGHT); + image.createPng(encoded); + pngSize = encoded.getPos(); + CRC32 crc = new CRC32(); + crc.update(encoded.getBuffer(), 0, pngSize); + pngCrc32 = crc.getValue(); + if (pngSize <= 0 || encoded.getBuffer()[0] != (byte) 0x89 + || encoded.getBuffer()[1] != 'P' || encoded.getBuffer()[2] != 'N' + || encoded.getBuffer()[3] != 'G') { + throw new IllegalStateException("encoded PNG is missing its signature"); + } + + saveBytes(PNG_NAME, encoded.getBuffer(), pngSize); + String pngPath = PNG_NAME; + pass = true; + writeResult(pass, failure, pngPath, selectedPixels, background, border, interior, + rowR, rowG, rowB, rowA, pngSize, pngCrc32); + } catch (Throwable failureCause) { + failure = failureCause.toString(); + try { + writeResult(false, failure, PNG_NAME, + selectedPixels, background, border, interior, + rowR, rowG, rowB, rowA, pngSize, pngCrc32); + } catch (Throwable ignored) { + failure = failure + "; result-write=" + ignored; + } + } + + // Android's loader tears down the VM asynchronously; calling exit here can + // race that teardown. The result file remains the Android assertion + // boundary, while Java SE and desktop targets retain their exit status. + if (!Settings.ANDROID.equals(Settings.platform)) { + MainWindow.exit(pass ? 0 : 1); + } + } + + private static void assertPixel(String name, int actual, int expected) { + if (actual != expected) { + throw new IllegalStateException(name + " expected 0x" + Integer.toHexString(expected) + + " but was 0x" + Integer.toHexString(actual)); + } + } + + private static void assertEquals(String name, int expected, int actual) { + if (expected != actual) { + throw new IllegalStateException(name + " expected " + expected + " but was " + actual); + } + } + + private static void assertNotPixel(String name, int actual, int forbidden) { + if (actual == forbidden) { + throw new IllegalStateException(name + " must differ from background 0x" + + Integer.toHexString(forbidden)); + } + } + + private static String saveBytes(String name, byte[] bytes, int length) throws IOException { + String path = Settings.appPath + "/" + name; + try (File file = new File(path, File.CREATE_EMPTY)) { + file.writeBytes(bytes, 0, length); + } + return path; + } + + private static void writeResult(boolean pass, String failure, String pngPath, int[] selectedPixels, + int background, int border, int interior, int rowR, int rowG, int rowB, int rowA, + int pngSize, long pngCrc32) throws IOException { + String json = "{\n" + + " \"platform\": \"" + (Settings.platform == null ? "unknown" : Settings.platform) + "\",\n" + + " \"implementationPath\": \"" + (Settings.onJavaSE ? "java-byte-array" : "native-skia") + "\",\n" + + " \"dimensions\": {\"width\": " + WIDTH + ", \"height\": " + HEIGHT + "},\n" + + " \"expectedPixels\": {\"background\": " + background + ", \"border\": \"non-background\"" + + ", \"interior\": " + interior + "},\n" + + " \"observedPixels\": [" + selectedPixels[0] + ", " + selectedPixels[1] + ", " + + selectedPixels[2] + ", " + selectedPixels[3] + "],\n" + + " \"rowInteriorRgba\": [" + rowR + ", " + rowG + ", " + rowB + ", " + rowA + "],\n" + + " \"outputPath\": \"" + pngPath + "\",\n" + + " \"encodedSize\": " + pngSize + ",\n" + + " \"encodedCrc32\": \"" + pngCrc32 + "\",\n" + + " \"assertionCount\": 8,\n" + + " \"pass\": " + pass + ",\n" + + " \"failure\": \"" + failure.replace('\\', '/').replace('"', '\'') + "\"\n" + + "}\n"; + saveBytes(RESULT_NAME, json.getBytes(), json.length()); + } +} diff --git a/tests/smoke/issue-417-generated-image/check_result.py b/tests/smoke/issue-417-generated-image/check_result.py new file mode 100644 index 0000000000..7ad3fbb851 --- /dev/null +++ b/tests/smoke/issue-417-generated-image/check_result.py @@ -0,0 +1,65 @@ +# Copyright (C) 2026 Amalgam Solucoes em TI Ltda +# +# SPDX-License-Identifier: LGPL-2.1-only + +"""Validate issue-417-result.json and its generated PNG without dependencies.""" + +import json +import struct +import sys +from pathlib import Path + + +EXPECTED_SIGNATURE = b"\x89PNG\r\n\x1a\n" + + +def fail(message): + print(f"FAIL: {message}", file=sys.stderr) + return 1 + + +def main(): + if len(sys.argv) not in (2, 3): + print(f"usage: {sys.argv[0]} RESULT_JSON [PNG_PATH]", file=sys.stderr) + return 2 + + result_path = Path(sys.argv[1]) + result = json.loads(result_path.read_text(encoding="utf-8")) + if result.get("pass") is not True: + return fail(f"application reported pass={result.get('pass')}: {result.get('failure', '')}") + if result.get("dimensions") != {"width": 576, "height": 576}: + return fail(f"unexpected dimensions: {result.get('dimensions')}") + if result.get("expectedPixels") != {"background": 0xFFFFFF, "border": "non-background", "interior": 0xFFFFFF}: + return fail(f"unexpected expectedPixels: {result.get('expectedPixels')}") + observed_pixels = result.get("observedPixels") + if not isinstance(observed_pixels, list) or len(observed_pixels) != 4: + return fail(f"unexpected observedPixels: {observed_pixels}") + if observed_pixels[0] != 0xFFFFFF or observed_pixels[1] == 0xFFFFFF \ + or observed_pixels[2:] != [0xFFFFFF, 0xFFFFFF]: + return fail(f"unexpected observedPixels: {result.get('observedPixels')}") + if result.get("rowInteriorRgba") != [255, 255, 255, 255]: + return fail(f"unexpected rowInteriorRgba: {result.get('rowInteriorRgba')}") + if result.get("encodedSize", 0) <= 0 or not result.get("encodedCrc32"): + return fail("encoded output metadata is missing") + + png_path = Path(sys.argv[2]) if len(sys.argv) == 3 else Path(result["outputPath"]) + if not png_path.is_absolute(): + png_path = result_path.parent / png_path + png = png_path.read_bytes() + if len(png) != result["encodedSize"]: + return fail(f"PNG size {len(png)} differs from result {result['encodedSize']}") + if png[:8] != EXPECTED_SIGNATURE: + return fail("PNG signature is invalid") + if len(png) < 33 or png[12:16] != b"IHDR": + return fail("PNG does not contain an IHDR chunk") + width, height = struct.unpack(">II", png[16:24]) + if (width, height) != (576, 576): + return fail(f"PNG dimensions are {(width, height)}") + if b"IDAT" not in png: + return fail("PNG does not contain image data") + print(f"PASS: {result['implementationPath']} {width}x{height}, {len(png)} bytes") + return 0 + + +if __name__ == "__main__": + sys.exit(main())