From 9a261849f7670065a907ddd5d2f9ae1fa38eeefc Mon Sep 17 00:00:00 2001 From: ronsor Date: Wed, 19 Aug 2026 17:03:04 -0700 Subject: [PATCH 01/32] Harden the parser; add an error API; add web builds and a sample viewer; bump version to 1.1.0 That was quite a lot I've been dragging my feet on. Security and memory safety: * Fix discovered memory-safety bugs: signed-overflow in the warp grid expansion, an out-of-bounds read on negative keyframe indices, NULL+0 pointer arithmetic when the blend-key table is empty, NULL derefs on empty art-mesh bindings and missing key_count reads at the blend-window boundary, and an index underflow in psm__valid_range. * Improved fuzzing verification: PSM_DEBUG_MALLOC build mode that uses `malloc` for every model field. Public API: * Add per-model csmGetLastError / csmGetErrorString; record error codes in the MOC3 header's padding, and expose csmGetMocError so applications can receive detailed failure status. * Add csmGetExtendedVersionString() with embedded git hash to query the exact library build. * Misc. correctness fixes: propagate color to opacity-0 meshes and stop the v5.3 extended-modes bit masking constant-mode bits. Style / housekeeping: * .clang-format for the library (BSD-KNF) and samples/viewer (raylib layout), a clang-format pre-commit hook, make format/format-check targets, and a CI check. * Line wrapping, standardizing on `bool` for all booleans, remove VLAs, rename many symbols for consistency, and more! Sample viewer: * Implement a sample MOC3 viewer (both v5 and v6 ABI support) using Raylib. Supports all blend modes from v6. WASM port: * Web/WASM drop-in Live2DCubismCore.js for both v5 and v6. Build: * Makefile rewrite with per-OS shared rules and a proper platform matrix. WASM support also added. * Bundle now generated from a template (src/bundle.c.in) instead of manually by a script. * CMake build system added for cross-compilation. This should be easier to build on plain Windows. CI: * Misc improvements: cross-build is skipped on v* tag pushes (the release job rebuilds the matrix itself), and the release job verifies the tag matches PSM_TRUE_VERSION in the header before building. * Add WASM viewer build job. --- .clang-format | 31 + .githooks/pre-commit | 28 + .github/workflows/ci.yml | 149 +- .gitignore | 1 + CMakeLists.txt | 297 + CMakePresets.json | 111 + Makefile | 300 +- cmake/GitHash.cmake | 27 + cmake/PurismCoreConfig.cmake.in | 11 + cmake/zig-toolchain.cmake | 48 + docs/API.md | 52 + docs/BUILDING.md | 124 +- docs/SDKINFO.txt | 18 +- include/PurismCore.h | 168 +- scripts/assemble-core-js.sh | 24 + scripts/bin2h.c | 51 + scripts/build-dist.sh | 331 +- scripts/bundle.sh | 84 +- scripts/purismcore.pc.in | 2 +- scripts/zig-build.sh | 155 - src/arena.c | 58 +- src/arena.h | 25 +- src/array.h | 41 +- src/artmesh.c | 56 +- src/blendshape.c | 169 +- src/bundle.c.in | 64 + src/core.c | 16 + src/core_js.c | 89 + src/core_js.js | 694 ++ src/core_js_tail.js | 16 + src/debug.c | 6 +- src/debug.h | 4 +- src/deformer.c | 282 +- src/error.h | 2 + src/gather.h | 77 +- src/glue.c | 15 +- src/interpolate.c | 42 +- src/math2.c | 2 +- src/math2.h | 37 +- src/moc3.c | 696 +- src/moc3.h | 207 +- src/model.c | 458 +- src/model.h | 377 +- src/offscreen.c | 42 +- src/param.c | 63 +- src/param.h | 3 +- src/part.c | 22 +- src/private.h | 46 +- src/render.c | 41 +- src/samples/.clang-format | 5 + src/samples/benchmark.c | 266 + src/samples/cascadia.fnt | 99 + src/samples/cascadia_0.png | Bin 0 -> 8501 bytes src/samples/vendor/raygui.h | 6074 +++++++++++++++++ src/samples/viewer/.clang-format | 24 + src/samples/viewer/Info.plist.in | 17 + src/samples/viewer/blend.c | 78 + src/samples/viewer/graphics.c | 412 ++ src/samples/viewer/io.c | 129 + src/samples/viewer/panel.c | 261 + src/samples/viewer/render.c | 712 ++ src/samples/viewer/shaders/blend.frag | 133 + src/samples/viewer/shaders/draw.frag | 19 + src/samples/viewer/shaders/masked.frag | 29 + src/samples/viewer/shaders/maskwrite.frag | 13 + src/samples/viewer/shaders/offscreen.frag | 18 + .../viewer/shaders/offscreen_masked.frag | 25 + src/samples/viewer/shell.html | 172 + src/samples/viewer/viewer.c | 410 ++ src/samples/viewer/viewer.h | 192 + src/tests/.clang-format | 5 + src/tests/extract.c | 2 +- src/tests/fuzzer.c | 31 +- src/tests/negctl_triidx.c | 112 + src/tests/test_arena.c | 25 +- src/tests/test_endian.c | 409 ++ src/tests/test_misc.c | 85 + src/tests/test_refdata.c | 2 +- src/tests/test_transform.c | 12 +- src/tests/unit.c | 4 + src/update.c | 47 +- src/verify.c | 801 +++ src/verify.h | 22 + 83 files changed, 14259 insertions(+), 2048 deletions(-) create mode 100644 .clang-format create mode 100755 .githooks/pre-commit create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 cmake/GitHash.cmake create mode 100644 cmake/PurismCoreConfig.cmake.in create mode 100644 cmake/zig-toolchain.cmake create mode 100755 scripts/assemble-core-js.sh create mode 100644 scripts/bin2h.c delete mode 100755 scripts/zig-build.sh create mode 100644 src/bundle.c.in create mode 100644 src/core_js.c create mode 100644 src/core_js.js create mode 100644 src/core_js_tail.js create mode 100644 src/samples/.clang-format create mode 100644 src/samples/benchmark.c create mode 100755 src/samples/cascadia.fnt create mode 100755 src/samples/cascadia_0.png create mode 100644 src/samples/vendor/raygui.h create mode 100644 src/samples/viewer/.clang-format create mode 100644 src/samples/viewer/Info.plist.in create mode 100644 src/samples/viewer/blend.c create mode 100644 src/samples/viewer/graphics.c create mode 100644 src/samples/viewer/io.c create mode 100644 src/samples/viewer/panel.c create mode 100644 src/samples/viewer/render.c create mode 100644 src/samples/viewer/shaders/blend.frag create mode 100644 src/samples/viewer/shaders/draw.frag create mode 100644 src/samples/viewer/shaders/masked.frag create mode 100644 src/samples/viewer/shaders/maskwrite.frag create mode 100644 src/samples/viewer/shaders/offscreen.frag create mode 100644 src/samples/viewer/shaders/offscreen_masked.frag create mode 100644 src/samples/viewer/shell.html create mode 100644 src/samples/viewer/viewer.c create mode 100644 src/samples/viewer/viewer.h create mode 100644 src/tests/.clang-format create mode 100644 src/tests/negctl_triidx.c create mode 100644 src/tests/test_endian.c create mode 100644 src/verify.c create mode 100644 src/verify.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..eda4924 --- /dev/null +++ b/.clang-format @@ -0,0 +1,31 @@ +# Purism Core house style: BSD KNF / OpenBSD style(9)-ish. +# - return type on its own line for function definitions +# - column-aligned declarations and #define values +# - 2-space indent, control-statement braces attached, function braces on own line +# - preprocessor directives indented after the '#' +# Function-like "statement" macros (control-flow bodies) are hand-aligned and +# fenced with `// clang-format off/on` where this config would reflow them. +Language: Cpp +BasedOnStyle: LLVM +IndentWidth: 2 +UseTab: Never +ContinuationIndentWidth: 4 +ColumnLimit: 0 +AlignAfterOpenBracket: DontAlign +BreakBeforeBraces: Linux +AlwaysBreakAfterReturnType: AllDefinitions +PointerAlignment: Right +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: false +SpaceBeforeParens: ControlStatements +Cpp11BracedListStyle: false +IndentCaseLabels: false +KeepEmptyLinesAtTheStartOfBlocks: false +SortIncludes: Never +AlignEscapedNewlines: DontAlign +AlignTrailingComments: + Kind: Leave +IndentPPDirectives: AfterHash +AlignConsecutiveDeclarations: true +AlignConsecutiveMacros: true diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..7db6851 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,28 @@ +#!/bin/sh +# Pre-commit: reject staged C sources that aren't clang-format-clean. +# +# Enable once per clone: git config core.hooksPath .githooks (or: make hooks) +# Override the binary: CLANG_FORMAT=clang-format-19 git commit ... +# +# Each file's nearest .clang-format decides the style; DisableFormat trees +# (tests, samples, vendored) are no-ops. Checks the working-tree copy of the +# staged files (the usual, fast approximation) -- if a file has both staged and +# unstaged edits, format the whole file before committing. + +CLANG_FORMAT="${CLANG_FORMAT:-clang-format}" + +files=$(git diff --cached --name-only --diff-filter=ACM -- '*.c' '*.h') +[ -z "$files" ] && exit 0 + +if ! command -v "$CLANG_FORMAT" >/dev/null 2>&1; then + echo "pre-commit: $CLANG_FORMAT not found; skipping format check" >&2 + exit 0 +fi + +if ! "$CLANG_FORMAT" --dry-run --Werror $files; then + echo >&2 + echo "pre-commit: staged C sources need formatting." >&2 + echo " fix with: make format (then re-stage)" >&2 + exit 1 +fi +exit 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3ceb0e..e80ecb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,23 @@ on: branches: [master] jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + + # Pinned: clang-format output drifts between major versions, so the gate + # must use the same version the tree was formatted with. + - name: Install clang-format + run: pip install clang-format==19.1.7 + + - name: Check formatting + run: make format-check + build-and-test: runs-on: ubuntu-latest steps: @@ -28,16 +45,22 @@ jobs: cross-build: runs-on: ubuntu-latest needs: build-and-test + # The release job rebuilds the whole matrix itself (and it is the only job + # that can lipo a universal Viewer.app / dylib), so on a tag push the 7 + # cross-build targets + their artifact uploads are pure duplication -- skip + # them and let release be the single build. (PR and branch pushes keep the + # matrix, which uploads per-target artifacts for inspection.) + if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/v') strategy: matrix: target: - - linux-x64 - - linux-arm64 - - macos-x64 - - macos-arm64 - - win-x64 - - win-x86 - - win-arm64 + - zig-cross-linux-x86_64 + - zig-cross-linux-arm64 + - zig-cross-macos-x86_64 + - zig-cross-macos-arm64 + - zig-cross-windows-x86_64 + - zig-cross-windows-x86 + - zig-cross-windows-arm64 steps: - uses: actions/checkout@v4 @@ -45,24 +68,106 @@ jobs: with: version: 0.16.0 + # raylib v6.0 release archive for this target (viewer-in-dist is opt-in + # via RAYLIB_DIR_ env vars; if a target has no archive -- e.g. + # zig-cross-windows-arm64 only ships MSVC-built raylib -- the viewer is skipped and + # the library still builds). Pinned to v6.0; bump here + in the release + # job together when upgrading. + - name: Fetch raylib v6.0 for ${{ matrix.target }} + shell: bash + run: | + case "${{ matrix.target }}" in + zig-cross-linux-x86_64) url=raylib-6.0_linux_amd64.tar.gz; var=RAYLIB_DIR_LINUX_AMD64; ext=tar.gz ;; + zig-cross-linux-arm64) url=raylib-6.0_linux_arm64.tar.gz; var=RAYLIB_DIR_LINUX_ARM64; ext=tar.gz ;; + zig-cross-macos-x86_64) url=raylib-6.0_macos.tar.gz; var=RAYLIB_DIR_MACOS; ext=tar.gz ;; + zig-cross-macos-arm64) url=raylib-6.0_macos.tar.gz; var=RAYLIB_DIR_MACOS; ext=tar.gz ;; + zig-cross-windows-x86_64) url=raylib-6.0_win64_mingw-w64.zip; var=RAYLIB_DIR_WINDOWS_AMD64; ext=zip ;; + zig-cross-windows-x86) url=raylib-6.0_win32_mingw-w64.zip; var=RAYLIB_DIR_WINDOWS_X86; ext=zip ;; + zig-cross-windows-arm64) echo "no raylib archive for windows-arm64"; exit 0 ;; + esac + mkdir -p build/raylib + base="${url%.$ext}" + if [ "$ext" = tar.gz ]; then + curl -sL "https://github.com/raysan5/raylib/releases/download/6.0/$url" \ + -o "build/raylib/$url" + tar -C build/raylib -xzf "build/raylib/$url" + else + curl -sL "https://github.com/raysan5/raylib/releases/download/6.0/$url" \ + -o "build/raylib/$url" + ( cd build/raylib && unzip -q "$url" ) + fi + # Expose the extracted tree path to subsequent steps via GITHUB_ENV. + echo "$var=$PWD/build/raylib/$base" >> "$GITHUB_ENV" + - name: Cross-build (${{ matrix.target }}) - run: ./scripts/zig-build.sh ${{ matrix.target }} --both + run: ./scripts/build-dist.sh ${{ matrix.target }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ matrix.target }} - path: build/${{ matrix.target }}/ + path: dist/sdk/ + + wasm: + runs-on: ubuntu-latest + needs: build-and-test + steps: + - uses: actions/checkout@v4 + + # Pinned to the emsdk version raylib 6.0's own web build used, so the + # archived libraylib.web.a stays ABI-compatible with this emcc. + - uses: emscripten-core/setup-emsdk@v15 + with: + version: 5.0.3 + + - name: Fetch raylib v6.0 (webassembly) + run: | + mkdir -p build/raylib + curl -sL https://github.com/raysan5/raylib/releases/download/6.0/raylib-6.0_webassembly.zip \ + -o build/raylib/raylib-6.0_webassembly.zip + ( cd build/raylib && unzip -q raylib-6.0_webassembly.zip ) + echo "RAYLIB_WEB_DIR=$PWD/build/raylib/raylib-6.0_webassembly/include" >> "$GITHUB_ENV" + echo "RAYLIB_WEB_LIB=$PWD/build/raylib/raylib-6.0_webassembly/lib/libraylib.web.a" >> "$GITHUB_ENV" + + - name: Build web viewer (Emscripten) + run: make viewer-web + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: wasm + path: | + dist/Live2DCubismCore*.js + dist/viewer.* release: if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest - needs: cross-build + # Cross-build is skipped on tag pushes, so depend on build-and-test (which + # always runs) rather than the matrix; release rebuilds the full matrix + # itself anyway. + needs: build-and-test permissions: contents: write steps: - uses: actions/checkout@v4 + - name: Check tag matches PSM_TRUE_VERSION + shell: bash + run: | + tag="${GITHUB_REF#refs/tags/}"; tag="${tag#v}" + ver_hex=$(sed -n 's/^#define PSM_TRUE_VERSION[[:space:]]*\(0x[0-9A-Fa-f]*\).*/\1/p' include/PurismCore.h) + [ -n "$ver_hex" ] || { echo "error: PSM_TRUE_VERSION not found in include/PurismCore.h" >&2; exit 1; } + v_major=$(( ($ver_hex >> 24) & 0xFF )) + v_minor=$(( ($ver_hex >> 16) & 0xFF )) + v_patch=$(( $ver_hex & 0xFFFF )) + version="$v_major.$v_minor.$v_patch" + if [ "$tag" != "$version" ]; then + echo "error: tag v$tag does not match PSM_TRUE_VERSION ($version)" >&2 + exit 1 + fi + echo "ok: tag v$tag matches PSM_TRUE_VERSION ($version)" + - uses: mlugg/setup-zig@v2 with: version: 0.16.0 @@ -73,6 +178,30 @@ jobs: -o /usr/local/bin/lipo chmod +x /usr/local/bin/lipo + # Download every raylib v6.0 archive the dist matrix needs (viewer-in-dist + # is opt-in via RAYLIB_DIR_; without these env vars, build-dist.sh + # silently skips the viewer and ships the libraries only). + - name: Fetch raylib v6.0 (all targets) + shell: bash + run: | + mkdir -p build/raylib + fetch() { # + url="$1"; top="$2"; ext="$3"; var="$4" + curl -sL "https://github.com/raysan5/raylib/releases/download/6.0/$url" \ + -o "build/raylib/$url" + if [ "$ext" = tar.gz ]; then + tar -C build/raylib -xzf "build/raylib/$url" + else + ( cd build/raylib && unzip -q "$url" ) + fi + echo "$var=$PWD/build/raylib/$top" >> "$GITHUB_ENV" + } + fetch raylib-6.0_linux_amd64.tar.gz raylib-6.0_linux_amd64 tar.gz RAYLIB_DIR_LINUX_AMD64 + fetch raylib-6.0_linux_arm64.tar.gz raylib-6.0_linux_arm64 tar.gz RAYLIB_DIR_LINUX_ARM64 + fetch raylib-6.0_macos.tar.gz raylib-6.0_macos tar.gz RAYLIB_DIR_MACOS + fetch raylib-6.0_win64_mingw-w64.zip raylib-6.0_win64_mingw-w64 zip RAYLIB_DIR_WINDOWS_AMD64 + fetch raylib-6.0_win32_mingw-w64.zip raylib-6.0_win32_mingw-w64 zip RAYLIB_DIR_WINDOWS_X86 + - name: Build distribution run: ./scripts/build-dist.sh diff --git a/.gitignore b/.gitignore index fda048c..b247bee 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ testdata/* # OS .DS_Store Thumbs.db +/shot.png diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a1544ee --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,297 @@ +# Purism Core: CMake build +# +# Copyright (c) 2026 Sakura Motion Project +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.21) + +# Decode version from PurismCore.h +file(STRINGS include/PurismCore.h _psm_ver_line REGEX "define[ \t]+PSM_TRUE_VERSION") +string(REGEX MATCH "0x[0-9A-Fa-f]+" _psm_ver "${_psm_ver_line}") +math(EXPR _psm_major "(${_psm_ver} >> 24) & 0xFF") +math(EXPR _psm_minor "(${_psm_ver} >> 16) & 0xFF") +math(EXPR _psm_patch "${_psm_ver} & 0xFFFF") + +project(PurismCore + VERSION ${_psm_major}.${_psm_minor}.${_psm_patch} + DESCRIPTION "Live2D Cubism Core compatible library" + LANGUAGES C) + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +set(PURISM_CORE_ABI "v6" CACHE STRING "ABI to build: v6 (default) or v5") +set_property(CACHE PURISM_CORE_ABI PROPERTY STRINGS v6 v5) +set(PURISM_CORE_GIT_HASH "" CACHE STRING + "Pin the baked git revision (default: derived from git at build time)") +option(PURISM_CORE_BUILD_SAMPLES "Build the moc3info/benchmark samples" OFF) +option(PURISM_CORE_BUILD_VIEWER "Build the raylib viewer sample (desktop)" OFF) +option(PURISM_CORE_BUILD_TESTS "Build the unit/conformance tests (ctest)" OFF) +option(PURISM_CORE_VERSION_RC + "Embed a Windows version resource in the DLL (needs a working RC compiler)" + ON) +set(PURISM_CORE_BIN2H "" CACHE FILEPATH + "Pre-built host bin2h tool for cross-compile viewer embeds (optional)") +set(PURISM_CORE_RAYLIB_DIR "" CACHE PATH + "Prebuilt raylib tree (include/ + lib/) for the viewer (optional)") +if(PURISM_CORE_ABI STREQUAL "v5") + set(_psm_abi_def PSM_COMPAT_VERSION=0x05010000L) + set(_psm_suffix "-v5") +else() + set(_psm_abi_def "") + set(_psm_suffix "") +endif() + +set(PURISM_CORE_SOURCES + src/core.c src/debug.c src/arena.c src/math2.c src/moc3.c src/verify.c + src/model.c src/update.c src/param.c src/part.c src/deformer.c src/artmesh.c + src/glue.c src/offscreen.c src/blendshape.c src/interpolate.c src/render.c) + +add_library(PurismCore ${PURISM_CORE_SOURCES}) +add_library(PurismCore::PurismCore ALIAS PurismCore) + +target_compile_features(PurismCore PUBLIC c_std_11) +set_target_properties(PurismCore PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + OUTPUT_NAME "PurismCore${_psm_suffix}" + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}) + +# macOS dylib install_name: @rpath/ so consumers find it via rpath +if(APPLE) + set_target_properties(PurismCore PROPERTIES + INSTALL_NAME_DIR "@rpath" + BUILD_WITH_INSTALL_NAME_DIR ON) +endif() + +# Windows dll: remove MinGW-style `lib` prefix +if(WIN32) + set_target_properties(PurismCore PROPERTIES PREFIX "") +endif() + +target_include_directories(PurismCore + PUBLIC $ + $ + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +if(_psm_abi_def) + target_compile_definitions(PurismCore PUBLIC ${_psm_abi_def}) +endif() + +if(NOT WIN32) + target_link_libraries(PurismCore PUBLIC m) +endif() + +set(_psm_githash_hdr "${CMAKE_CURRENT_BINARY_DIR}/purism_core_githash.h") +add_custom_target(purism_core_githash + BYPRODUCTS "${_psm_githash_hdr}" + COMMAND ${CMAKE_COMMAND} + -DHDR=${_psm_githash_hdr} -DOVERRIDE=${PURISM_CORE_GIT_HASH} + -DSRCDIR=${CMAKE_CURRENT_SOURCE_DIR} + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/GitHash.cmake + COMMENT "Resolving git revision for csmGetExtendedVersionString()") +add_dependencies(PurismCore purism_core_githash) +if(MSVC) + set(_psm_fi "/FI${_psm_githash_hdr}") +else() + set(_psm_fi "-include" "${_psm_githash_hdr}") +endif() +set_source_files_properties(src/core.c PROPERTIES + OBJECT_DEPENDS "${_psm_githash_hdr}" + COMPILE_OPTIONS "${_psm_fi}") + +if(WIN32 AND BUILD_SHARED_LIBS AND PURISM_CORE_VERSION_RC) + target_compile_definitions(PurismCore PRIVATE PURISM_CORE_DLL) + set(PSM_VER_MAJOR ${PROJECT_VERSION_MAJOR}) + set(PSM_VER_MINOR ${PROJECT_VERSION_MINOR}) + set(PSM_VER_PATCH ${PROJECT_VERSION_PATCH}) + set(PSM_VER_STRING "${PROJECT_VERSION}") + if(PURISM_CORE_ABI STREQUAL "v5") + set(PSM_COMPAT_STRING "5.1.0") + else() + set(PSM_COMPAT_STRING "6.0.1") + endif() + configure_file(src/version.rc.in + "${CMAKE_CURRENT_BINARY_DIR}/version.rc" @ONLY) + + set(_psm_res "${CMAKE_CURRENT_BINARY_DIR}/version.rc.res") + add_custom_command( + OUTPUT "${_psm_res}" + COMMAND "${CMAKE_RC_COMPILER}" /fo "${_psm_res}" + "${CMAKE_CURRENT_BINARY_DIR}/version.rc" + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/version.rc" + COMMENT "Compiling Windows version resource") + add_custom_target(purism_core_rc DEPENDS "${_psm_res}") + add_dependencies(PurismCore purism_core_rc) + target_link_options(PurismCore PRIVATE "${_psm_res}") +endif() + +install(TARGETS PurismCore EXPORT PurismCoreTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) +install(FILES include/PurismCore.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +# pkg-config (reuse the Makefile's template). +set(PREFIX "${CMAKE_INSTALL_PREFIX}") +set(ABI "${PURISM_CORE_ABI}") +set(ABI_SUFFIX "${_psm_suffix}") +set(VERSION "${PROJECT_VERSION}") +configure_file(scripts/purismcore.pc.in + "${CMAKE_CURRENT_BINARY_DIR}/purismcore${_psm_suffix}.pc" @ONLY) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/purismcore${_psm_suffix}.pc" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig") + +# CMake package: find_package(PurismCore) -> PurismCore::PurismCore. +set(_psm_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/PurismCore") +install(EXPORT PurismCoreTargets + NAMESPACE PurismCore:: DESTINATION "${_psm_cmakedir}") +configure_package_config_file(cmake/PurismCoreConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/PurismCoreConfig.cmake" + INSTALL_DESTINATION "${_psm_cmakedir}") +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/PurismCoreConfigVersion.cmake" + VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion) +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/PurismCoreConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/PurismCoreConfigVersion.cmake" + DESTINATION "${_psm_cmakedir}") + +if(PURISM_CORE_BUILD_SAMPLES) + foreach(s moc3info benchmark) + add_executable(${s} src/samples/${s}.c) + target_include_directories(${s} PRIVATE src src/samples) + target_link_libraries(${s} PRIVATE PurismCore::PurismCore) + target_compile_options(${s} PRIVATE + $<$>:-Wno-unused-function>) + endforeach() +endif() + +if(PURISM_CORE_BUILD_VIEWER) + if(PURISM_CORE_RAYLIB_DIR) + if(WIN32) + add_library(raylib STATIC IMPORTED) + set_target_properties(raylib PROPERTIES + IMPORTED_LOCATION + "${PURISM_CORE_RAYLIB_DIR}/lib/libraylib.a" + INTERFACE_LINK_LIBRARIES "opengl32;gdi32;winmm") + elseif(APPLE) + add_library(raylib SHARED IMPORTED) + set_target_properties(raylib PROPERTIES + IMPORTED_LOCATION + "${PURISM_CORE_RAYLIB_DIR}/lib/libraylib.dylib") + else() + add_library(raylib SHARED IMPORTED) + set_target_properties(raylib PROPERTIES + IMPORTED_LOCATION + "${PURISM_CORE_RAYLIB_DIR}/lib/libraylib.so") + endif() + target_include_directories(raylib INTERFACE + "${PURISM_CORE_RAYLIB_DIR}/include") + else() + find_package(raylib 6.0 QUIET) + if(NOT raylib_FOUND) + include(FetchContent) + FetchContent_Declare(raylib + GIT_REPOSITORY https://github.com/raysan5/raylib.git GIT_TAG 6.0) + set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(raylib) + endif() + endif() + + if(PURISM_CORE_BIN2H) + set(_bin2h_cmd "${PURISM_CORE_BIN2H}") + set(_bin2h_dep "${PURISM_CORE_BIN2H}") + else() + add_executable(bin2h scripts/bin2h.c) + set(_bin2h_cmd "bin2h") + set(_bin2h_dep "bin2h") + endif() + + set(_embed_dir "${CMAKE_CURRENT_BINARY_DIR}/embed") + file(MAKE_DIRECTORY "${_embed_dir}") + file(GLOB _embed_inputs + "${CMAKE_CURRENT_SOURCE_DIR}/src/samples/viewer/shaders/*.frag") + list(APPEND _embed_inputs + "${CMAKE_CURRENT_SOURCE_DIR}/src/samples/cascadia.fnt" + "${CMAKE_CURRENT_SOURCE_DIR}/src/samples/cascadia_0.png") + set(_viewer_embeds "") + foreach(input IN LISTS _embed_inputs) + get_filename_component(_name "${input}" NAME) + string(REPLACE "." "_" _sym "${_name}") + set(_out "${_embed_dir}/${_name}.h") + add_custom_command(OUTPUT "${_out}" + COMMAND "${_bin2h_cmd}" "${input}" "${_sym}" "${_out}" + DEPENDS "${_bin2h_dep}" "${input}" VERBATIM) + list(APPEND _viewer_embeds "${_out}") + endforeach() + + add_executable(viewer + src/samples/viewer/io.c src/samples/viewer/blend.c + src/samples/viewer/graphics.c src/samples/viewer/render.c + src/samples/viewer/panel.c src/samples/viewer/viewer.c + ${_viewer_embeds}) + target_include_directories(viewer PRIVATE + src src/samples/viewer src/samples/vendor "${CMAKE_CURRENT_BINARY_DIR}") + target_link_libraries(viewer PRIVATE PurismCore::PurismCore raylib) + + if(PURISM_CORE_RAYLIB_DIR) + if(NOT WIN32) + set_target_properties(viewer PROPERTIES SKIP_BUILD_RPATH TRUE) + if(APPLE) + target_link_options(viewer PRIVATE "SHELL:-Wl,-rpath,@loader_path") + else() + target_link_options(viewer PRIVATE "SHELL:-Wl,-rpath,\$ORIGIN") + endif() + endif() + else() + if(APPLE) + target_link_libraries(viewer PRIVATE + "-framework Cocoa" "-framework IOKit" + "-framework CoreVideo" "-framework OpenGL") + elseif(WIN32) + target_link_libraries(viewer PRIVATE opengl32 gdi32 winmm) + else() + find_package(OpenGL REQUIRED) + find_package(X11 REQUIRED) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) + target_link_libraries(viewer PRIVATE + OpenGL::GL X11::X11 Threads::Threads ${CMAKE_DL_LIBS} rt m) + endif() + endif() + + target_compile_options(viewer PRIVATE + $<$>:-Wno-unused-function>) +endif() + +if(PURISM_CORE_BUILD_TESTS) + enable_testing() + + add_executable(purism_unit src/tests/unit.c ${PURISM_CORE_SOURCES}) + target_include_directories(purism_unit PRIVATE src include src/samples) + if(_psm_abi_def) + target_compile_definitions(purism_unit PRIVATE ${_psm_abi_def}) + endif() + target_compile_options(purism_unit PRIVATE + $<$>:-Wno-unused-function>) + if(NOT WIN32) + target_link_libraries(purism_unit PRIVATE m) + endif() + add_test(NAME unit COMMAND purism_unit) + + add_executable(stageplay src/tests/stageplay.c) + target_include_directories(stageplay PRIVATE src src/samples) + target_link_libraries(stageplay PRIVATE PurismCore::PurismCore) + if(NOT WIN32) + target_link_libraries(stageplay PRIVATE m) + endif() + if(NOT CMAKE_HOST_WIN32) + add_test(NAME conformance + COMMAND ${CMAKE_COMMAND} -E env "STAGEPLAY=$" + sh "${CMAKE_CURRENT_SOURCE_DIR}/scripts/run-tests.sh" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") + endif() +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..b1297f2 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,111 @@ +{ + "version": 3, + "cmakeMinimumRequired": { "major": 3, "minor": 21, "patch": 0 }, + "configurePresets": [ + { + "name": "zig-cross-linux-x86_64", + "description": "Cross-build for x86_64 Linux (ELF, gnu ABI) via zig cc", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/_cmake/zig-cross-linux-x86_64", + "toolchainFile": "${sourceDir}/cmake/zig-toolchain.cmake", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Linux", + "CMAKE_SYSTEM_PROCESSOR": "x86_64", + "CMAKE_BUILD_TYPE": "Release" + }, + "environment": { + "ZIG_TARGET": "x86_64-linux-gnu" + } + }, + { + "name": "zig-cross-linux-arm64", + "description": "Cross-build for aarch64 Linux (ELF, gnu ABI) via zig cc", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/_cmake/zig-cross-linux-arm64", + "toolchainFile": "${sourceDir}/cmake/zig-toolchain.cmake", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Linux", + "CMAKE_SYSTEM_PROCESSOR": "arm64", + "CMAKE_BUILD_TYPE": "Release" + }, + "environment": { + "ZIG_TARGET": "aarch64-linux-gnu" + } + }, + { + "name": "zig-cross-macos-x86_64", + "description": "Cross-build for x86_64 macOS (Mach-O) via zig cc", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/_cmake/zig-cross-macos-x86_64", + "toolchainFile": "${sourceDir}/cmake/zig-toolchain.cmake", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "x86_64", + "CMAKE_BUILD_TYPE": "Release" + }, + "environment": { + "ZIG_TARGET": "x86_64-macos" + } + }, + { + "name": "zig-cross-macos-arm64", + "description": "Cross-build for arm64 macOS (Mach-O) via zig cc", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/_cmake/zig-cross-macos-arm64", + "toolchainFile": "${sourceDir}/cmake/zig-toolchain.cmake", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "arm64", + "CMAKE_BUILD_TYPE": "Release" + }, + "environment": { + "ZIG_TARGET": "aarch64-macos" + } + }, + { + "name": "zig-cross-windows-x86_64", + "description": "Cross-build for x86_64 Windows (PE, mingw GNU ABI) via zig cc", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/_cmake/zig-cross-windows-x86_64", + "toolchainFile": "${sourceDir}/cmake/zig-toolchain.cmake", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Windows", + "CMAKE_SYSTEM_PROCESSOR": "x86_64", + "CMAKE_BUILD_TYPE": "Release" + }, + "environment": { + "ZIG_TARGET": "x86_64-windows-gnu" + } + }, + { + "name": "zig-cross-windows-x86", + "description": "Cross-build for i686 Windows (PE, mingw GNU ABI) via zig cc", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/_cmake/zig-cross-windows-x86", + "toolchainFile": "${sourceDir}/cmake/zig-toolchain.cmake", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Windows", + "CMAKE_SYSTEM_PROCESSOR": "x86", + "CMAKE_BUILD_TYPE": "Release" + }, + "environment": { + "ZIG_TARGET": "x86-windows-gnu" + } + }, + { + "name": "zig-cross-windows-arm64", + "description": "Cross-build for aarch64 Windows (PE, mingw GNU ABI) via zig cc", + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/_cmake/zig-cross-windows-arm64", + "toolchainFile": "${sourceDir}/cmake/zig-toolchain.cmake", + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Windows", + "CMAKE_SYSTEM_PROCESSOR": "arm64", + "CMAKE_BUILD_TYPE": "Release" + }, + "environment": { + "ZIG_TARGET": "aarch64-windows-gnu" + } + } + ] +} diff --git a/Makefile b/Makefile index dd04500..259e7e5 100644 --- a/Makefile +++ b/Makefile @@ -1,128 +1,235 @@ -# Purism Core - Makefile +# Purism Core: Makefile # # Copyright (c) 2026 Sakura Motion Project # SPDX-License-Identifier: MIT +# +ifeq ($(filter else-if,$(.FEATURES)),) +$(error This build requires GNU Make 3.81 or newer (on *BSD: gmake; or use CMake)) +endif -CC ?= cc -AR ?= ar -CFLAGS ?= -O2 -g -CFLAGS += -Wall -Wextra -I./include -I./src +CC ?= cc +HOSTCC ?= $(CC) +AR ?= ar +WINDRES ?= windres +CFLAGS ?= -O2 -g +CFLAGS += -Wall -Wextra -I./include -I./src LDFLAGS ?= -# ABI version: v5 or v6 (default) +# ABI: v6 (default) or v5. v5 reports the 5.1 compat version and omits the +# v6-only API; the suffix keeps v5/v6 artifacts (and objects) from colliding. ABI ?= v6 ifeq ($(ABI),v5) -CFLAGS += -DPSM_COMPAT_VERSION=0x05010000L -ABI_SUFFIX = -v5 + ABI_CPPFLAGS = -DPSM_COMPAT_VERSION=0x05010000L + ABI_SUFFIX = -v5 else -ABI_SUFFIX = + ABI_CPPFLAGS = + ABI_SUFFIX = endif +CFLAGS += $(ABI_CPPFLAGS) -SRC = src/core.c src/debug.c src/arena.c src/math2.c \ - src/moc3.c src/model.c src/update.c src/param.c \ - src/part.c src/deformer.c src/artmesh.c src/glue.c \ - src/offscreen.c src/blendshape.c src/interpolate.c \ - src/render.c -OBJ = $(SRC:src/%.c=build/%.o) +GIT_HASH ?= $(shell git describe --always --dirty --tags 2>/dev/null || echo unknown) +CFLAGS += -DPSM_GIT_HASH='"$(GIT_HASH)"' -HDR = include/PurismCore.h $(wildcard src/*.h) +PSM_TRUE_VER := $(shell sed -n 's/^#define PSM_TRUE_VERSION[[:space:]]*\(0x[0-9A-Fa-f]*\).*/\1/p' include/PurismCore.h) +VERSION := $(shell printf '%d.%d.%d' $$(($(PSM_TRUE_VER) >> 24 & 255)) $$(($(PSM_TRUE_VER) >> 16 & 255)) $$(($(PSM_TRUE_VER) & 65535))) + +SRC = src/core.c src/debug.c src/arena.c src/math2.c src/moc3.c src/verify.c \ + src/model.c src/update.c src/param.c src/part.c src/deformer.c \ + src/artmesh.c src/glue.c src/offscreen.c src/blendshape.c \ + src/interpolate.c src/render.c +HDR = include/PurismCore.h $(wildcard src/*.h) COMMON_H = src/samples/common.h -LIB_A = build/libPurismCore$(ABI_SUFFIX).a +OBJDIR = build/obj$(ABI_SUFFIX) +OBJ = $(SRC:src/%.c=$(OBJDIR)/%.o) OS ?= +LIBSUF = .a +EXESUF = ifeq ($(OS),windows) -WINDRES ?= windres -CFLAGS += -DPURISM_CORE_DLL -LIB_SHARED = build/PurismCore$(ABI_SUFFIX).dll -RC_OBJ = build/version.o + DLLPRE = + DLLSUF = .dll + EXESUF = .exe + CFLAGS += -DPURISM_CORE_DLL + RC_OBJ = $(OBJDIR)/version.o else ifneq ($(filter macos darwin,$(OS)),) -LIB_SHARED = build/libPurismCore$(ABI_SUFFIX).dylib -RC_OBJ = + DLLPRE = lib + DLLSUF = .dylib else -LIB_SHARED = build/libPurismCore$(ABI_SUFFIX).so -RC_OBJ = + DLLPRE = lib + DLLSUF = .so endif +LIB_A = build/libPurismCore$(ABI_SUFFIX)$(LIBSUF) +LIB_SHARED = build/$(DLLPRE)PurismCore$(ABI_SUFFIX)$(DLLSUF) + +ifeq ($(OS),wasm) +all: dist/Live2DCubismCore$(ABI_SUFFIX).js +else all: static-lib shared-lib +endif static-lib: $(LIB_A) $(LIB_A): $(OBJ) | build $(AR) rcs $@ $^ -shared-lib: $(LIB_SHARED) -$(LIB_SHARED): $(OBJ) $(RC_OBJ) | build - $(CC) -shared -o $@ $^ $(LDFLAGS) -lm +$(OBJDIR)/%.o: src/%.c $(HDR) | $(OBJDIR) + $(CC) $(CFLAGS) -fPIC -c $< -o $@ -build/version.rc: src/version.rc.in include/PurismCore.h | build +shared-lib: $(LIB_SHARED) +build/lib%.so: $(OBJ) | build # ELF: embed the soname + $(CC) -shared -Wl,-soname,$(@F) -o $@ $(OBJ) $(LDFLAGS) -lm +build/lib%.dylib: $(OBJ) | build # Mach-O: rpath-relative install name + $(CC) -dynamiclib -install_name @rpath/$(@F) -o $@ $(OBJ) $(LDFLAGS) -lm +build/%.dll: $(OBJ) $(RC_OBJ) | build # PE: no lib prefix, + version resource + $(CC) -shared -o $@ $(OBJ) $(RC_OBJ) $(LDFLAGS) -lm + +# Windows version resource (linked only into the .dll). +$(OBJDIR)/version.rc: src/version.rc.in include/PurismCore.h | $(OBJDIR) ./scripts/gen-version-rc.sh > $@ - -build/version.o: build/version.rc | build +$(OBJDIR)/version.o: $(OBJDIR)/version.rc | $(OBJDIR) $(WINDRES) $< -o $@ -build/%.o: src/%.c $(HDR) | build - $(CC) $(CFLAGS) -fPIC -c $< -o $@ - -stageplay: build/stageplay -moc3info: build/moc3info +moc3info: build/moc3info$(EXESUF) +benchmark: build/benchmark$(EXESUF) -build/stageplay: src/tests/stageplay.c \ - src/tests/partcl.h $(COMMON_H) $(LIB_A) | build +build/moc3info$(EXESUF): src/samples/moc3info.c $(COMMON_H) $(LIB_A) | build $(CC) $(CFLAGS) $< -o $@ $(LIB_A) -lm +build/benchmark$(EXESUF): src/samples/benchmark.c $(COMMON_H) $(LIB_A) | build + $(CC) $(CFLAGS) -O2 -Wno-unused-function $< -o $@ $(LIB_A) -lm + +vpath %.frag src/samples/viewer/shaders +vpath %.fnt src/samples +vpath %.png src/samples +VIEWER_SHADERS = $(wildcard src/samples/viewer/shaders/*.frag) +VIEWER_EMBEDS = $(patsubst src/samples/viewer/shaders/%,build/embed/%.h,$(VIEWER_SHADERS)) \ + build/embed/cascadia.fnt.h build/embed/cascadia_0.png.h + +build/bin2h: scripts/bin2h.c | build + $(HOSTCC) -O2 -o $@ $< +build/embed/%.h: % build/bin2h | build/embed + @./build/bin2h $< $(subst .,_,$*) $@ + +RAYLIB_PREFIX ?= /usr/local +RAYLIB_DIR ?= +ifeq ($(RAYLIB_DIR),) + RAYLIB_CFLAGS ?= -I$(RAYLIB_PREFIX)/include + RAYLIB_LIBS ?= -L$(RAYLIB_PREFIX)/lib -lraylib -lGL -lm -lpthread -ldl -lrt -lX11 +else + RAYLIB_CFLAGS ?= -I$(RAYLIB_DIR) + RAYLIB_LIBS ?= -L$(RAYLIB_DIR) -lraylib -lGL -lm -lpthread -ldl -lrt -lX11 +endif + +VIEWER_SRC = src/samples/viewer/io.c src/samples/viewer/blend.c \ + src/samples/viewer/graphics.c src/samples/viewer/render.c \ + src/samples/viewer/panel.c src/samples/viewer/viewer.c + +viewer: build/viewer$(ABI_SUFFIX)$(EXESUF) +build/viewer$(ABI_SUFFIX)$(EXESUF): $(VIEWER_SRC) src/samples/viewer/viewer.h \ + $(VIEWER_EMBEDS) src/samples/vendor/raygui.h $(LIB_A) | build + $(CC) -O2 -g -std=gnu11 -I./include -I./src -I./src/samples/viewer -Ibuild \ + $(RAYLIB_CFLAGS) $(ABI_CPPFLAGS) -Wall -Wno-unused-function \ + $(VIEWER_SRC) -o $@ $(LIB_A) $(RAYLIB_LIBS) + +EMCC ?= emcc +RAYLIB_WEB_DIR ?= $(HOME)/raylib/src +RAYLIB_WEB_LIB ?= $(RAYLIB_WEB_DIR)/libraylib.web.a + +viewer-web: dist/viewer.html +dist/viewer.html: $(VIEWER_SRC) src/samples/viewer/viewer.h \ + src/samples/viewer/shell.html $(VIEWER_EMBEDS) \ + src/samples/vendor/raygui.h $(SRC) $(HDR) | dist + $(EMCC) -O2 -std=gnu11 $(ABI_CPPFLAGS) \ + -I./include -I./src -I./src/samples/viewer -Ibuild -I$(RAYLIB_WEB_DIR) \ + -Wall -Wno-unused-function \ + $(SRC) $(VIEWER_SRC) $(RAYLIB_WEB_LIB) -o $@ \ + -s USE_GLFW=3 -s FULL_ES3=1 -s MIN_WEBGL_VERSION=2 -s MAX_WEBGL_VERSION=2 \ + -s ALLOW_MEMORY_GROWTH=1 -s FORCE_FILESYSTEM=1 \ + -s EXPORTED_FUNCTIONS=_main,_ViewerRequestLoad \ + -s EXPORTED_RUNTIME_METHODS=ccall,FS \ + --shell-file src/samples/viewer/shell.html + +WASM_SRC = $(SRC) src/core_js.c +WASM_RT_METHODS := ccall,cwrap,addFunction,removeFunction,UTF8ToString +WASM_RT_METHODS := $(WASM_RT_METHODS),HEAP8,HEAPU8,HEAPU16,HEAP32,HEAPU32,HEAPF32 + +dist/Live2DCubismCore$(ABI_SUFFIX).js: $(WASM_SRC) src/core_js.js src/core_js_tail.js \ + scripts/assemble-core-js.sh $(HDR) | dist build/wasm + $(EMCC) $(ABI_CPPFLAGS) -O3 -DPSM_GIT_HASH='"$(GIT_HASH)"' \ + -I./include -I./src $(WASM_SRC) \ + -o build/wasm/em-module$(ABI_SUFFIX).js \ + -sSINGLE_FILE=1 -sMODULARIZE=1 -sEXPORT_NAME=_em_module \ + -sWASM_ASYNC_COMPILATION=0 -sALLOW_TABLE_GROWTH=1 -sALLOW_MEMORY_GROWTH=1 \ + -sENVIRONMENT=web,worker,node -sFILESYSTEM=0 --closure 0 \ + -sEXPORTED_RUNTIME_METHODS=$(WASM_RT_METHODS) + @./scripts/assemble-core-js.sh \ + src/core_js.js build/wasm/em-module$(ABI_SUFFIX).js src/core_js_tail.js > $@ + @echo "Wrote $@ ($(ABI))" + +wasm: + $(MAKE) OS=wasm ABI=v6 +wasm-v5: + $(MAKE) OS=wasm ABI=v5 +wasm-all: wasm wasm-v5 -build/moc3info: src/samples/moc3info.c \ - $(COMMON_H) $(LIB_A) | build +stageplay: build/stageplay +build/stageplay: src/tests/stageplay.c src/tests/partcl.h $(COMMON_H) $(LIB_A) | build $(CC) $(CFLAGS) $< -o $@ $(LIB_A) -lm +test: build/stageplay + @./scripts/run-tests.sh +quick-test: build/stageplay + @./scripts/run-tests.sh -q + unit: build/unit ./build/unit - build/unit: src/tests/unit.c $(SRC) $(HDR) $(COMMON_H) | build $(CC) $(CFLAGS) -Wno-unused-function $< -o $@ -lm -test: build/stageplay - @./scripts/run-tests.sh +endian-test: build/endian-test + ./build/endian-test testdata/moc3 $(TEST_DATA) +build/endian-test: src/tests/test_endian.c $(SRC) $(HDR) $(COMMON_H) | build + $(CC) $(CFLAGS) -Wno-unused-function $< -o $@ -lm -quick-test: build/stageplay - @./scripts/run-tests.sh -q +verify-negctl: build/negctl-triidx + ./build/negctl-triidx testdata/moc3 $(TEST_DATA) +build/negctl-triidx: src/tests/negctl_triidx.c $(SRC) $(HDR) $(COMMON_H) | build + $(CC) $(CFLAGS) -Wno-unused-function $< -o $@ -lm -FUZZ_CC ?= clang +FUZZ_CC ?= clang FUZZ_CFLAGS ?= -g -O1 -fsanitize=fuzzer,address,undefined \ - -fno-sanitize-recover=undefined - + -fno-sanitize-recover=undefined fuzzer: build/fuzzer - build/fuzzer: src/tests/fuzzer.c $(COMMON_H) $(LIB_A) | build - $(FUZZ_CC) $(FUZZ_CFLAGS) -I./include \ - $< -o $@ $(LIB_A) -lm - + $(FUZZ_CC) $(FUZZ_CFLAGS) -I./include $< -o $@ $(LIB_A) -lm fuzz: build/fuzzer | build/corpus ./build/fuzzer build/corpus/ -build/corpus: | build +# Fuzz with one malloc per model field (PSM_DEBUG_MALLOC) so ASan puts redzones +# between adjacent model arrays, catching intra-model overflows the single arena +# buffer would otherwise hide. Rebuilds from clean. +MALLOC_CFLAGS = -g -O1 -I./include -I./src -DPSM_DEBUG_MALLOC \ + -fsanitize=address,undefined,fuzzer-no-link -fno-sanitize-recover=undefined +fuzz-malloc: | build/corpus + $(MAKE) clean @mkdir -p build/corpus + $(MAKE) static-lib CC=$(FUZZ_CC) CFLAGS="$(MALLOC_CFLAGS)" + $(FUZZ_CC) $(FUZZ_CFLAGS) -DPSM_DEBUG_MALLOC -I./include \ + src/tests/fuzzer.c $(LIB_A) -lm -o build/fuzzer + ./build/fuzzer build/corpus/ -bundle: dist/PurismCoreBundle.h - -dist/PurismCoreBundle.h: $(HDR) $(SRC) | dist - ./scripts/bundle.sh > $@ - -PREFIX ?= /usr/local -LIBDIR ?= $(PREFIX)/lib -INCLUDEDIR ?= $(PREFIX)/include +PREFIX ?= /usr/local +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include PKGCONFIGDIR ?= $(LIBDIR)/pkgconfig - -PC = build/purismcore$(ABI_SUFFIX).pc +PC = build/purismcore$(ABI_SUFFIX).pc install: $(LIB_A) $(LIB_SHARED) $(PC) - install -d $(DESTDIR)$(LIBDIR) - install -d $(DESTDIR)$(INCLUDEDIR) - install -d $(DESTDIR)$(PKGCONFIGDIR) - install -m 644 $(LIB_A) $(DESTDIR)$(LIBDIR)/ - install -m 755 $(LIB_SHARED) $(DESTDIR)$(LIBDIR)/ - install -m 644 include/PurismCore.h \ - $(DESTDIR)$(INCLUDEDIR)/ - install -m 644 $(PC) $(DESTDIR)$(PKGCONFIGDIR)/ + install -d $(DESTDIR)$(LIBDIR) $(DESTDIR)$(INCLUDEDIR) $(DESTDIR)$(PKGCONFIGDIR) + install -m 644 $(LIB_A) $(DESTDIR)$(LIBDIR)/ + install -m 755 $(LIB_SHARED) $(DESTDIR)$(LIBDIR)/ + install -m 644 include/PurismCore.h $(DESTDIR)$(INCLUDEDIR)/ + install -m 644 $(PC) $(DESTDIR)$(PKGCONFIGDIR)/ uninstall: rm -f $(DESTDIR)$(LIBDIR)/$(notdir $(LIB_A)) @@ -132,23 +239,50 @@ uninstall: .PHONY: $(PC) $(PC): scripts/purismcore.pc.in | build - @sed -e 's|@PREFIX@|$(PREFIX)|' \ - -e 's|@ABI@|$(ABI)|' \ - -e 's|@ABI_SUFFIX@|$(ABI_SUFFIX)|' \ - $< > $@ + @sed -e 's|@PREFIX@|$(PREFIX)|' -e 's|@ABI@|$(ABI)|' \ + -e 's|@ABI_SUFFIX@|$(ABI_SUFFIX)|' -e 's|@VERSION@|$(VERSION)|' $< > $@ + +bundle: dist/PurismCoreBundle.h +dist/PurismCoreBundle.h: src/bundle.c.in scripts/bundle.sh $(HDR) $(SRC) | dist + ./scripts/bundle.sh > $@ + +print-%: ; @echo '$($*)' + +CLANG_FORMAT ?= clang-format -build: - @mkdir -p build +# AlignConsecutiveDeclarations isn't always idempotent, so reformat to a fixed +# point (loop until --dry-run is clean) rather than a single pass. +format: + @files=`git ls-files '*.c' '*.h'`; \ + for i in 1 2 3 4 5 6; do \ + $(CLANG_FORMAT) -i $$files; \ + if $(CLANG_FORMAT) --dry-run --Werror $$files >/dev/null 2>&1; then \ + echo "formatted ($$i pass(es))"; exit 0; fi; \ + done; echo "clang-format did not converge after 6 passes" >&2; exit 1 -dist: - @mkdir -p dist +# Verify-only: fails (non-zero) if any tracked C source isn't formatted. +format-check: + @files=`git ls-files '*.c' '*.h'`; \ + $(CLANG_FORMAT) --dry-run --Werror $$files && echo "format OK" + +# One-time: point git at the committed hooks (runs format-check pre-commit). +hooks: + git config core.hooksPath .githooks + @echo "git hooks enabled (.githooks)" + +build dist: + @mkdir -p $@ +$(OBJDIR) build/embed build/wasm build/corpus: | build + @mkdir -p $@ clean: rm -rf build/ - dist-clean: rm -rf dist/ -.PHONY: all unit stageplay moc3info fuzzer \ - test quick-test fuzz bundle \ - install uninstall clean dist-clean +.PHONY: all static-lib shared-lib moc3info benchmark viewer viewer-web \ + wasm wasm-v5 wasm-all validate-wasm \ + stageplay test quick-test unit endian-test verify-negctl \ + fuzzer fuzz fuzz-malloc \ + format format-check hooks \ + bundle install uninstall clean dist-clean diff --git a/cmake/GitHash.cmake b/cmake/GitHash.cmake new file mode 100644 index 0000000..a1104d6 --- /dev/null +++ b/cmake/GitHash.cmake @@ -0,0 +1,27 @@ +# Purism Core: extract git revision +# +# Copyright (c) 2026 Sakura Motion Project +# SPDX-License-Identifier: MIT + +if(OVERRIDE) + set(hash "${OVERRIDE}") +else() + execute_process( + COMMAND git describe --always --dirty --tags + WORKING_DIRECTORY "${SRCDIR}" + OUTPUT_VARIABLE hash OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET RESULT_VARIABLE rc) + if(NOT rc EQUAL 0 OR hash STREQUAL "") + set(hash "unknown") + endif() +endif() + +set(content "#define PSM_GIT_HASH \"${hash}\"\n") +if(EXISTS "${HDR}") + file(READ "${HDR}" old) +else() + set(old "") +endif() +if(NOT old STREQUAL content) + file(WRITE "${HDR}" "${content}") +endif() diff --git a/cmake/PurismCoreConfig.cmake.in b/cmake/PurismCoreConfig.cmake.in new file mode 100644 index 0000000..9d393d1 --- /dev/null +++ b/cmake/PurismCoreConfig.cmake.in @@ -0,0 +1,11 @@ +@PACKAGE_INIT@ + +# Purism Core CMake package config. Provides the imported target +# PurismCore::PurismCore (include dirs + linkage). Consumers: +# +# find_package(PurismCore REQUIRED) +# target_link_libraries(app PRIVATE PurismCore::PurismCore) + +include("${CMAKE_CURRENT_LIST_DIR}/PurismCoreTargets.cmake") + +check_required_components(PurismCore) diff --git a/cmake/zig-toolchain.cmake b/cmake/zig-toolchain.cmake new file mode 100644 index 0000000..6f48f10 --- /dev/null +++ b/cmake/zig-toolchain.cmake @@ -0,0 +1,48 @@ +# Purism Core: zig CMake toolchain definition +# +# Copyright (c) 2026 Sakura Motion Project +# SPDX-License-Identifier: MIT + +if(NOT DEFINED ENV{ZIG_TARGET}) + message(FATAL_ERROR + "zig-toolchain.cmake: ZIG_TARGET env var is required " + "(e.g. x86_64-linux-gnu, aarch64-macos, x86_64-windows-gnu). " + "It is set by each preset in CMakePresets.json.") +endif() + +set(_zig_target "$ENV{ZIG_TARGET}") +find_program(ZIG_EXECUTABLE zig REQUIRED) + +# Wrapper scripts in the build dir since CMake needs plain executable paths +# for CMAKE_C_COMPILER / CMAKE_AR / CMAKE_RANLIB. +set(_zig_tool_dir "${CMAKE_BINARY_DIR}/_zig-tools") +file(MAKE_DIRECTORY "${_zig_tool_dir}") + +file(WRITE "${_zig_tool_dir}/zig-cc" + "#!/bin/sh\nexec \"${ZIG_EXECUTABLE}\" cc -target ${_zig_target} \"$@\"\n") +file(WRITE "${_zig_tool_dir}/zig-cxx" + "#!/bin/sh\nexec \"${ZIG_EXECUTABLE}\" c++ -target ${_zig_target} \"$@\"\n") +file(WRITE "${_zig_tool_dir}/zig-ar" + "#!/bin/sh\nexec \"${ZIG_EXECUTABLE}\" ar \"$@\"\n") +file(WRITE "${_zig_tool_dir}/zig-ranlib" + "#!/bin/sh\nexec \"${ZIG_EXECUTABLE}\" ranlib \"$@\"\n") +execute_process(COMMAND chmod +x + "${_zig_tool_dir}/zig-cc" "${_zig_tool_dir}/zig-cxx" + "${_zig_tool_dir}/zig-ar" "${_zig_tool_dir}/zig-ranlib") + +set(CMAKE_C_COMPILER "${_zig_tool_dir}/zig-cc") +set(CMAKE_CXX_COMPILER "${_zig_tool_dir}/zig-cxx") +set(CMAKE_AR "${_zig_tool_dir}/zig-ar") +set(CMAKE_RANLIB "${_zig_tool_dir}/zig-ranlib") + +# Windows resource compiler. zig ships `zig rc` (LLVM-rc). +file(WRITE "${_zig_tool_dir}/zig-rc" + "#!/bin/sh\nexec \"${ZIG_EXECUTABLE}\" rc \"$@\"\n") +execute_process(COMMAND chmod +x "${_zig_tool_dir}/zig-rc") +set(CMAKE_RC_COMPILER "${_zig_tool_dir}/zig-rc") + +# Don't try to run cross binaries during configure. +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +# Don't accidentally grab host executables for tools. +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) diff --git a/docs/API.md b/docs/API.md index b713fa8..ecb51ee 100644 --- a/docs/API.md +++ b/docs/API.md @@ -160,6 +160,52 @@ int csmHasMocConsistency(data, size); /* returns 0 or 1 */ > Always call `csmHasMocConsistency` before `csmReviveMocInPlace` when loading > untrusted data. +## Error Handling + +```C +csmError csmGetMocError(moc); +csmError csmGetLastError(model); +const char *csmGetErrorString(error); +``` + +- `csmGetMocError` returns the outcome of the most recent `csmReviveMocInPlace` + or `csmInitializeModelInPlace` on the MOC3 file. Returns `csmError_NoError` + on success. + +> [!NOTE] +> Both calls return NULL on failure, so query with the same moc/buffer pointer +> afterwards. + +- `csmGetLastError` returns the error recorded by the model's most recent + `csmUpdateModel`. It is cleared to `csmError_NoError` at the start of each update. + +- `csmGetErrorString` maps a code to a static string (`"unknown error"` for + out-of-range codes). + +Passing NULL to either getter returns `csmError_NoError`. + +Error codes: + +```C +csmError_NoError /* 0: success */ +csmError_Failed /* 1: operation failed */ +csmError_ParameterRange /* 2: parameter outside [min, max], was clamped */ +csmError_FileUnrecognized /* 3: not a MOC3 file */ +csmError_FileCorrupt /* 4: MOC3 failed validation */ +csmError_InvalidData /* 5: e.g. model buffer too small */ +csmError_InvalidParameter /* 6 */ +``` + +Since a failed revive returns NULL, the query pattern uses the original +buffer pointer: + +```C +csmMoc *moc = csmReviveMocInPlace(buf, size); +if (!moc) + fprintf(stderr, "load failed: %s\n", + csmGetErrorString(csmGetMocError((const csmMoc *)buf))); +``` + ## Parameters ```C @@ -386,6 +432,7 @@ and pixels-per-unit from the model. ```C csmVersion csmGetVersion(); csmVersion csmGetTrueVersion(); +const char *csmGetExtendedVersionString(); /* Purism Core extension */ csmMocVersion csmGetLatestMocVersion(); ``` @@ -394,6 +441,10 @@ csmMocVersion csmGetLatestMocVersion(); - `csmGetTrueVersion` returns the actual Purism Core implementation version. +- `csmGetExtendedVersionString` returns a human-readable build identity + (e.g. "1.1.0 (a1b2c3d)" as the true version plus the git revision the + library was built from or "unknown" if built outside). + - `csmGetLatestMocVersion` returns the highest MOC3 format version the library can load (always `csmMocVersion_53`). @@ -430,6 +481,7 @@ typedef unsigned int csmVersion; typedef unsigned int csmMocVersion; typedef unsigned char csmFlags; typedef int csmParameterType; +typedef int csmError; typedef struct { float X, Y; } csmVector2; typedef struct { float X, Y, Z, W; } csmVector4; diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 087115e..7c4f1a7 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -3,10 +3,14 @@ Purism Core is written in standard C99, but it also compiles properly with a C++ compiler. -If you intend to work on the library itself or simply want maximum flexibility, -the primary way to build is using the supplied Makefile. This requires a Unix- -like environment with GCC or Clang, GNU make, and standard POSIX utilities; -MSYS, Cygwin, or busybox-w32 *may* work on Windows. +There are two main ways to build Purism Core: + +- **Make** for development, tests, the WASM libraries, and the single-file + bundle. Requires a Unix-like environment with GCC or Clang, GNU make, and + standard POSIX utilities; MSYS, Cygwin, or busybox-w32 *may* work on + Windows, but we haven't tested. +- **CMake** — IDE integration, `find_package` consumers, install/packaging, + and all cross-compilation (via `zig cc` and the bundled configure presets). If you do not intend to modify the library itself, the easiest method is to use the single-file bundle, `PurismCoreBundle.h`. It is included with the standard @@ -30,11 +34,14 @@ Except for the unit test binary, Purism Core should build without any warnings | `make unit` | Build and run unit tests | | `make stageplay` | Build the Tcl-based test runner | | `make moc3info` | Build the MOC3 info tool | +| `make viewer` | Build the raylib model viewer | +| `make viewer-web` | Build the web viewer (Emscripten; dist/viewer.*) | | `make test` | Run integration tests (requires MOC3 test data) | | `make quick-test` | Run tests with fewer scenarios | | `make fuzzer` | Build the libFuzzer harness | | `make fuzz` | Run the fuzzer | | `make bundle` | Generate single-file header (dist/PurismCoreBundle.h) | +| `make wasm-all` | Build both WASM drop-ins (see "Web (WASM)" below) | | `make install` | Install library, header, and pkg-config file | | `make uninstall` | Remove installed files | | `make clean` | Remove build directory | @@ -45,11 +52,13 @@ Except for the unit test binary, Purism Core should build without any warnings | Variable | Default | Description | |----------|---------|-------------| | `CC` | `cc` | C compiler | +| `HOSTCC` | `$(CC)` | Host C compiler for build-time codegen tools (set when `CC` is a cross compiler) | | `AR` | `ar` | Archiver | | `CFLAGS` | `-O2 -g` | Compiler flags (appended to) | | `LDFLAGS` | (empty) | Linker flags | | `ABI` | `v6` | Target ABI: `v5` or `v6` | -| `OS` | (empty) | Set to `windows` for DLL build or `macos`/`darwin` for dylib build | +| `OS` | (empty) | `windows` for DLL, `macos`/`darwin` for dylib, `wasm` for the web drop-in | +| `RAYLIB_DIR` | (empty) | Link the viewer against an extracted raylib tree instead of the system install | | `PREFIX` | `/usr/local` | Install prefix | | `DESTDIR` | (empty) | Staging directory for packaging | @@ -71,26 +80,115 @@ make OS=windows # builds PurismCore.dll (v6) make OS=windows ABI=v5 # builds PurismCore-v5.dll ``` -On Windows, `__declspec(dllexport)` visbility and `__stdcall` calling +On Windows, `__declspec(dllexport)` visibility and `__stdcall` calling convention are used for compatibility reasons. If `windres` is available and `src/version.rc.in` exists, a version resource is compiled and linked into the DLL. +## Using CMake + +```sh +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +cmake --install build # optional; respects CMAKE_INSTALL_PREFIX +``` + +### Options + +| Option | Default | Description | +|--------|---------|-------------| +| `PURISM_CORE_ABI` | `v6` | Target ABI: `v5` or `v6` | +| `BUILD_SHARED_LIBS` | `OFF` | Build the shared library instead of the static one | +| `PURISM_CORE_BUILD_SAMPLES` | `OFF` | Build the moc3info/benchmark tools | +| `PURISM_CORE_BUILD_VIEWER` | `OFF` | Build the raylib viewer (see below) | +| `PURISM_CORE_BUILD_TESTS` | `OFF` | Build the unit/conformance tests | +| `PURISM_CORE_VERSION_RC` | `ON` | Embed a version resource in Windows DLLs | + +With `PURISM_CORE_BUILD_TESTS=ON`, run the suite via ctest (`unit` and +`conformance`; the latter needs MOC3 test data, like `make test`): + +```sh +ctest --test-dir build +``` + +### Consumers + +Installation provides the header, a pkg-config file, and a CMake package +config, so a downstream project can simply: + +```cmake +find_package(PurismCore REQUIRED) +target_link_libraries(my_app PRIVATE PurismCore::PurismCore) +``` + ## Cross-Compiling -We rely on `zig` for easy cross-compiling to our supported targets. +Cross-compilation is handled by CMake configure presets using `zig cc` via +`cmake/zig-toolchain.cmake`: + +```sh +cmake --preset zig-cross-linux-x86_64 +cmake --build build/_cmake/zig-cross-linux-x86_64 +``` -Read `scripts/zig-build.sh` for more information. +| Preset | Target | +|--------|--------| +| `zig-cross-linux-x86_64` | Linux x86_64 (ELF, gnu) | +| `zig-cross-linux-arm64` | Linux arm64 (ELF, gnu) | +| `zig-cross-macos-x86_64` | macOS x86_64 (Mach-O) | +| `zig-cross-macos-arm64` | macOS arm64 (Mach-O) | +| `zig-cross-windows-x86_64` | Windows x86_64 (PE, mingw) | +| `zig-cross-windows-x86` | Windows x86 (PE, mingw) | +| `zig-cross-windows-arm64` | Windows arm64 (PE, mingw) | -On non-macOS platforms, you need to install `https://github.com/konoui/lipo` to -cross-build the macOS dylib! +Add `-DPURISM_CORE_ABI=v5` for a v5 build, and `-DBUILD_SHARED_LIBS=ON` for a +shared library (the default is static). ### Distribution -The full SDK distribution is built using `scripts/build-dist.sh`. -Static and shared libraries for supported platforms, headers, the single-file -bundle, and documentation are all output to `dist/sdk/`. +The full SDK distribution is built using `scripts/build-dist.sh`, which drives +the presets above. Static and shared libraries for supported platforms, +headers, the single-file bundle, the model viewer, and documentation are all +output to `dist/sdk/` (see `docs/SDKINFO.txt` for the layout). + +```sh +./scripts/build-dist.sh # full matrix +./scripts/build-dist.sh linux # one OS family (linux/macos/windows) +./scripts/build-dist.sh zig-cross-linux-x86_64 # a single target +``` + +Windows ships both v6 and v5 ABIs while Linux/macOS ship v6 only. + +To build the viewers, set the appropriate environment variables to unpacked +[raylib release archives](https://github.com/raysan5/raylib/releases/tag/6.0): +`RAYLIB_DIR_LINUX_AMD64`, `RAYLIB_DIR_LINUX_ARM64`, `RAYLIB_DIR_MACOS`, +`RAYLIB_DIR_WINDOWS_AMD64`, and/or `RAYLIB_DIR_WINDOWS_X86`. + +On non-macOS platforms, you need to install `https://github.com/konoui/lipo` +to produce the universal macOS binaries! + +## Viewer + +The raylib model viewer (`src/samples/viewer/`) builds three ways: + +- **Native, Make:** `make viewer` against the system raylib, or + `make viewer RAYLIB_DIR=/path/to/raylib` for an extracted/source tree. +- **Native, CMake:** `-DPURISM_CORE_BUILD_VIEWER=ON` (uses + `find_package(raylib)`, falling back to fetching the raylib 6.0 source). +- **Cross / dist:** via `scripts/build-dist.sh` with the `RAYLIB_DIR_*` + variables described above; binaries land in `dist/sdk/bin/`. +- **Web:** `make viewer-web` (Emscripten; needs a WebGL2/GLES3 raylib build). + The shell page accepts `?model=`. + +## Web (Emscripten/WASM) + +The Emscripten build is the most compatible way of using Purism Core in +existing web-based projects: + +```sh +make wasm-all # dist/Live2DCubismCore.js (v6) + dist/Live2DCubismCore-v5.js +``` ## Single-file bundle diff --git a/docs/SDKINFO.txt b/docs/SDKINFO.txt index 146e8f0..fc44e64 100644 --- a/docs/SDKINFO.txt +++ b/docs/SDKINFO.txt @@ -1,6 +1,7 @@ From scripts/build-dist.sh: # Output layout: +# # dist/sdk/ # include/PurismCore.h # include/Live2DCubismCore.h @@ -12,15 +13,22 @@ From scripts/build-dist.sh: # lib/windows/x86_64/PurismCore.a # lib/windows/x86/PurismCore.a # lib/windows/arm64/PurismCore.a -# dll/linux/x86_64/libPurismCore.so -# dll/macos/x86_64/libPurismCore.dylib -# dll/macos/arm64/libPurismCore.dylib -# dll/macos/universal/libPurismCore.dylib +# dll/linux/x86_64/libPurismCore.so (.so.1, .so.1.1.0 symlinks) +# dll/macos/x86_64/libPurismCore.1.dylib (+ .dylib symlink) +# dll/macos/arm64/libPurismCore.1.dylib +# dll/macos/universal/libPurismCore.1.dylib # dll/windows/x86_64/PurismCore.dll # dll/windows/x86/PurismCore.dll # dll/windows/arm64/PurismCore.dll +# bin/linux/x86_64/viewer + libraylib.so* (rpath $ORIGIN) +# bin/linux/arm64/viewer + libraylib.so* +# bin/macos/x86_64/viewer + libraylib.dylib (rpath @loader_path) +# bin/macos/arm64/viewer + libraylib.dylib +# bin/macos/universal/Viewer.app/ (lipo'd; Contents/{Info.plist,MacOS/}) +# bin/windows/x86_64/viewer.exe (static raylib -- self-contained) +# bin/windows/x86/viewer.exe # bundle/PurismCoreBundle.h # -# v5 compat builds go under lib-v5/ and dll-v5/ (Windows only). +# v5 compat builds go under lib-v5/ and dll-v5/ (Windows only; no viewer). Documentation is included as well (*.md and *.txt files). diff --git a/include/PurismCore.h b/include/PurismCore.h index d37a97f..721d629 100644 --- a/include/PurismCore.h +++ b/include/PurismCore.h @@ -15,24 +15,30 @@ extern "C" { /* PSM_COMPAT_VERSION is the version reported by csmGetVersion for compatibility and determines what public API functions are available. */ #ifndef PSM_COMPAT_VERSION -#define PSM_COMPAT_VERSION 0x06000001L +# define PSM_COMPAT_VERSION 0x06000001L #endif #if PSM_COMPAT_VERSION != 0x06000001L && PSM_COMPAT_VERSION != 0x05010000L -# error Unsupported Cubism compatibility level +# error Unsupported Cubism compatibility level #endif /* PSM_TRUE_VERSION is the actual Purism Core implementation version. */ -#define PSM_TRUE_VERSION 0x01000001L +#define PSM_TRUE_VERSION 0x01010000L /* CSM_CORE_WIN32_DLL is an alias for PURISM_CORE_DLL. */ #ifdef CSM_CORE_WIN32_DLL -# define PURISM_CORE_DLL +# define PURISM_CORE_DLL #endif /* PSMDEF specifies the linkage and attributes of public API functions. */ #ifndef PSMDEF -# if defined(PURISM_CORE_STATIC) +# if defined(__EMSCRIPTEN__) +# include +/* Mark every public function EMSCRIPTEN_KEEPALIVE so the wasm build exports the + whole API automatically -- the JS/WASM export surface then tracks exactly + what is compiled (so the v5/v6 difference needs no maintained list). */ +# define PSMDEF EMSCRIPTEN_KEEPALIVE +# elif defined(PURISM_CORE_STATIC) # define PSMDEF static # elif defined(_WIN32) && defined(PURISM_CORE_DLL) # define PSMDEF __declspec(dllexport) __stdcall @@ -43,31 +49,31 @@ extern "C" { /* PSM_HAS_STDINT determines whether C99 is available. */ #ifdef PSM_HAS_STDINT -# if PSM_HAS_STDINT -# if defined(PSM_STDINT_HEADER) -# include PSM_STDINT_HEADER -# else -# include -# endif -# endif +# if PSM_HAS_STDINT +# if defined(PSM_STDINT_HEADER) +# include PSM_STDINT_HEADER +# else +# include +# endif +# endif #elif defined(__has_include) -# if __has_include() -# include -# define PSM_HAS_STDINT 1 -# endif +# if __has_include() +# include +# define PSM_HAS_STDINT 1 +# endif #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L -# include -# define PSM_HAS_STDINT 1 +# include +# define PSM_HAS_STDINT 1 #elif defined(_MSC_VER) && _MSC_VER >= 1600 -# include -# define PSM_HAS_STDINT 1 +# include +# define PSM_HAS_STDINT 1 #elif defined(__cplusplus) && __cplusplus >= 201103L -# include -# define PSM_HAS_STDINT 1 +# include +# define PSM_HAS_STDINT 1 #endif #ifndef PSM_HAS_STDINT -# define PSM_HAS_STDINT 0 +# define PSM_HAS_STDINT 0 #endif #if defined(PSM_HAS_STDINT) && PSM_HAS_STDINT @@ -86,24 +92,24 @@ typedef signed int psm__i32; typedef unsigned int psm__u32; #endif -typedef float psm__f32; +typedef float psm__f32; typedef psm__u32 psm_size; #ifndef psm__static_assert -# if defined(PSM_HAS_STATIC_ASSERT) && PSM_HAS_STATIC_ASSERT -# define psm__static_assert(cond, msg) _Static_assert(cond, msg) -# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L -# define psm__static_assert(cond, msg) _Static_assert(cond, msg) -# elif defined(__cplusplus) && __cplusplus >= 201103L -# define psm__static_assert(cond, msg) static_assert(cond, msg) -# else -# ifndef PSM__JOIN -# define PSM__JOIN_(a, b) a##b -# define PSM__JOIN(a, b) PSM__JOIN_(a, b) -# endif -# define psm__static_assert(cond, msg) \ +# if defined(PSM_HAS_STATIC_ASSERT) && PSM_HAS_STATIC_ASSERT +# define psm__static_assert(cond, msg) _Static_assert(cond, msg) +# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define psm__static_assert(cond, msg) _Static_assert(cond, msg) +# elif defined(__cplusplus) && __cplusplus >= 201103L +# define psm__static_assert(cond, msg) static_assert(cond, msg) +# else +# ifndef PSM__JOIN +# define PSM__JOIN_(a, b) a##b +# define PSM__JOIN(a, b) PSM__JOIN_(a, b) +# endif +# define psm__static_assert(cond, msg) \ typedef char PSM__JOIN(psm__static_assertion_, __LINE__)[(cond) ? 1 : -1] -# endif +# endif #endif /* csmMoc is an opaque handle to a revived MOC3 file. */ @@ -205,6 +211,18 @@ enum { csmMocVersion_53 = 6 /* 5.3.00+ */ }; +/* Error codes returned by csmGetLastError. */ +typedef psm__i32 csmError; +enum { + csmError_NoError = 0, + csmError_Failed = 1, + csmError_ParameterRange = 2, + csmError_FileUnrecognized = 3, + csmError_FileCorrupt = 4, + csmError_InvalidData = 5, + csmError_InvalidParameter = 6 +}; + /* csmParameterType distinguishes normal parameters from blend shape parameters. */ typedef psm__i32 csmParameterType; @@ -231,8 +249,13 @@ typedef void (*csmLogFunction)(const char *message); /* * Version and logging */ -PSMDEF csmVersion csmGetVersion(void); -PSMDEF csmVersion csmGetTrueVersion(void); +PSMDEF csmVersion csmGetVersion(void); +PSMDEF csmVersion csmGetTrueVersion(void); +/* Human-readable build identity, e.g. "1.0.1 (a1b2c3d)": the true version plus + the git revision the library was built from ("unknown" when built outside a + git checkout). For diagnostics / bug reports; the returned string is static + and must not be freed. */ +PSMDEF const char *csmGetExtendedVersionString(void); PSMDEF csmMocVersion csmGetLatestMocVersion(void); PSMDEF csmMocVersion csmGetMocVersion(const void *, unsigned int); PSMDEF int csmHasMocConsistency(void *, unsigned int); @@ -256,10 +279,27 @@ PSMDEF void csmSetLogLevel(int); PSMDEF csmMoc *csmReviveMocInPlace(void *, unsigned int); PSMDEF unsigned int csmGetSizeofModel(const csmMoc *); PSMDEF csmModel *csmInitializeModelInPlace(const csmMoc *, - void *, unsigned int); + void *, unsigned int); PSMDEF void csmUpdateModel(csmModel *); PSMDEF void csmReadCanvasInfo(const csmModel *, - csmVector2 *, csmVector2 *, float *); + csmVector2 *, csmVector2 *, float *); + +/* + * Error reporting (Purism Core extension; not present in Cubism Core). + * csmGetMocError returns the outcome of the most recent csmReviveMocInPlace + * or csmInitializeModelInPlace on this moc: csmError_NoError on success, or + * e.g. csmError_FileUnrecognized / csmError_FileCorrupt after a failed revive, + * or csmError_InvalidData when init was given too small a model buffer. Both + * calls return NULL on failure, so query with the same moc/buffer afterwards. + * A freshly initialized model inherits this code until its first update. + * csmGetLastError returns the error recorded by the model's most recent + * csmUpdateModel (cleared to csmError_NoError at the start of each update), + * e.g. csmError_ParameterRange when an input parameter was outside [min,max] + * and got clamped. csmGetErrorString maps a code to a static string. + */ +PSMDEF csmError csmGetMocError(const csmMoc *); +PSMDEF csmError csmGetLastError(const csmModel *); +PSMDEF const char *csmGetErrorString(csmError); /* * Parameters @@ -267,17 +307,17 @@ PSMDEF void csmReadCanvasInfo(const csmModel *, * csmGetParameterValues returns a writable array. Modify then call * csmUpdateModel. Values are clamped to [min, max] unless repeat is set. */ -PSMDEF int csmGetParameterCount(const csmModel *); -PSMDEF const char **csmGetParameterIds(const csmModel *); -PSMDEF const csmParameterType *csmGetParameterTypes(const csmModel *); -PSMDEF const float *csmGetParameterMinimumValues(const csmModel *); -PSMDEF const float *csmGetParameterMaximumValues(const csmModel *); -PSMDEF const float *csmGetParameterDefaultValues(const csmModel *); -PSMDEF float *csmGetParameterValues(csmModel *); -PSMDEF const int *csmGetParameterKeyCounts(const csmModel *); -PSMDEF const float **csmGetParameterKeyValues(const csmModel *); +PSMDEF int csmGetParameterCount(const csmModel *); +PSMDEF const char **csmGetParameterIds(const csmModel *); +PSMDEF const csmParameterType *csmGetParameterTypes(const csmModel *); +PSMDEF const float *csmGetParameterMinimumValues(const csmModel *); +PSMDEF const float *csmGetParameterMaximumValues(const csmModel *); +PSMDEF const float *csmGetParameterDefaultValues(const csmModel *); +PSMDEF float *csmGetParameterValues(csmModel *); +PSMDEF const int *csmGetParameterKeyCounts(const csmModel *); +PSMDEF const float **csmGetParameterKeyValues(const csmModel *); #if PSM_COMPAT_VERSION >= 0x06000000L -PSMDEF const int *csmGetParameterRepeats(const csmModel *); +PSMDEF const int *csmGetParameterRepeats(const csmModel *); #endif /* @@ -292,7 +332,7 @@ PSMDEF const char **csmGetPartIds(const csmModel *); PSMDEF float *csmGetPartOpacities(csmModel *); PSMDEF const int *csmGetPartParentPartIndices(const csmModel *); #if PSM_COMPAT_VERSION >= 0x06000000L -PSMDEF const int *csmGetPartOffscreenIndices(const csmModel *); +PSMDEF const int *csmGetPartOffscreenIndices(const csmModel *); #endif /* @@ -327,10 +367,10 @@ PSMDEF const csmVector4 *csmGetDrawableScreenColors(const csmModel *); PSMDEF const int *csmGetDrawableParentPartIndices(const csmModel *); PSMDEF void csmResetDrawableDynamicFlags(csmModel *); #if PSM_COMPAT_VERSION >= 0x06000000L -PSMDEF const int *csmGetDrawableBlendModes(const csmModel *); -PSMDEF const int *csmGetRenderOrders(const csmModel *); +PSMDEF const int *csmGetDrawableBlendModes(const csmModel *); +PSMDEF const int *csmGetRenderOrders(const csmModel *); #else -PSMDEF const int *csmGetDrawableRenderOrders(const csmModel *); +PSMDEF const int *csmGetDrawableRenderOrders(const csmModel *); #endif #if PSM_COMPAT_VERSION >= 0x06000000L @@ -340,15 +380,15 @@ PSMDEF const int *csmGetDrawableRenderOrders(const csmModel *); * Render-to-texture surfaces for advanced effects. * Owner index maps each offscreen to its parent part. */ -PSMDEF int csmGetOffscreenCount(const csmModel *); -PSMDEF const int *csmGetOffscreenBlendModes(const csmModel *); -PSMDEF const float *csmGetOffscreenOpacities(const csmModel *); -PSMDEF const int *csmGetOffscreenOwnerIndices(const csmModel *); -PSMDEF const csmVector4 *csmGetOffscreenMultiplyColors(const csmModel *); -PSMDEF const csmVector4 *csmGetOffscreenScreenColors(const csmModel *); -PSMDEF const int *csmGetOffscreenMaskCounts(const csmModel *); -PSMDEF const int **csmGetOffscreenMasks(const csmModel *); -PSMDEF const csmFlags *csmGetOffscreenConstantFlags(const csmModel *); +PSMDEF int csmGetOffscreenCount(const csmModel *); +PSMDEF const int *csmGetOffscreenBlendModes(const csmModel *); +PSMDEF const float *csmGetOffscreenOpacities(const csmModel *); +PSMDEF const int *csmGetOffscreenOwnerIndices(const csmModel *); +PSMDEF const csmVector4 *csmGetOffscreenMultiplyColors(const csmModel *); +PSMDEF const csmVector4 *csmGetOffscreenScreenColors(const csmModel *); +PSMDEF const int *csmGetOffscreenMaskCounts(const csmModel *); +PSMDEF const int **csmGetOffscreenMasks(const csmModel *); +PSMDEF const csmFlags *csmGetOffscreenConstantFlags(const csmModel *); #endif #ifdef __cplusplus diff --git a/scripts/assemble-core-js.sh b/scripts/assemble-core-js.sh new file mode 100755 index 0000000..c3aee2a --- /dev/null +++ b/scripts/assemble-core-js.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Purism Core: assemble compatible Emscripten JS module +# +# Usage: assemble-core-js.sh +# +# Copyright (c) 2026 Sakura Motion Project +# SPDX-License-Identifier: MIT +set -eu + +wrapper="$1" +module="$2" +tail="$3" + +cat "$wrapper" +echo '' +echo '/* Embedded Emscripten module (emcc-generated, MODULARIZE) */' +awk ' + { + i = index($0, "if(typeof exports===") + if (i) { s = substr($0, 1, i - 1); sub(/[ \t;]+$/, "", s); printf "%s;\n", s; exit } + print + }' "$module" +echo '' +cat "$tail" diff --git a/scripts/bin2h.c b/scripts/bin2h.c new file mode 100644 index 0000000..7d911c3 --- /dev/null +++ b/scripts/bin2h.c @@ -0,0 +1,51 @@ +/* + * Purism Core: binary to C header build tool + * + * Usage: bin2h [output - default: stdout] + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include + +int +main(int argc, char **argv) +{ + if (argc < 3 || argc > 4) { + fprintf(stderr, "usage: %s [output]\n", argv[0]); + return 2; + } + const char *inpath = argv[1], *sym = argv[2]; + + FILE *in = fopen(inpath, "rb"); + if (!in) { + fprintf(stderr, "bin2h: cannot open %s\n", inpath); + return 1; + } + FILE *out = stdout; + if (argc == 4) { + out = fopen(argv[3], "wb"); + if (!out) { + fprintf(stderr, "bin2h: cannot write %s\n", argv[3]); + fclose(in); + return 1; + } + } + + fprintf(out, "static const char %s[] = {\n", sym); + unsigned long size = 0; + int c; + while ((c = fgetc(in)) != EOF) { + fprintf(out, "0x%02x,", (unsigned char)c); + if (++size % 16 == 0) + fputc('\n', out); + } + fprintf(out, "0x00\n};\n"); + fprintf(out, "#define %s_size %lu\n", sym, size); + + if (out != stdout) + fclose(out); + fclose(in); + return 0; +} diff --git a/scripts/build-dist.sh b/scripts/build-dist.sh index fe2a296..64f0125 100755 --- a/scripts/build-dist.sh +++ b/scripts/build-dist.sh @@ -1,10 +1,11 @@ #!/bin/sh -# Cross-compile Purism Core SDK distribution. +# Cross-compile Purism Core SDK distribution via CMake presets + zig cc. # # Copyright (c) 2026 Sakura Motion Project # SPDX-License-Identifier: MIT # Output layout: +# # dist/sdk/ # include/PurismCore.h # include/Live2DCubismCore.h @@ -16,103 +17,293 @@ # lib/windows/x86_64/PurismCore.a # lib/windows/x86/PurismCore.a # lib/windows/arm64/PurismCore.a -# dll/linux/x86_64/libPurismCore.so -# dll/macos/x86_64/libPurismCore.dylib -# dll/macos/arm64/libPurismCore.dylib -# dll/macos/universal/libPurismCore.dylib +# dll/linux/x86_64/libPurismCore.so (.so.1, .so.1.1.0 symlinks) +# dll/macos/x86_64/libPurismCore.1.dylib (+ .dylib symlink) +# dll/macos/arm64/libPurismCore.1.dylib +# dll/macos/universal/libPurismCore.1.dylib # dll/windows/x86_64/PurismCore.dll # dll/windows/x86/PurismCore.dll # dll/windows/arm64/PurismCore.dll +# bin/linux/x86_64/viewer + libraylib.so* (rpath $ORIGIN) +# bin/linux/arm64/viewer + libraylib.so* +# bin/macos/x86_64/viewer + libraylib.dylib (rpath @loader_path) +# bin/macos/arm64/viewer + libraylib.dylib +# bin/macos/universal/Viewer.app/ (lipo'd; Contents/{Info.plist,MacOS/}) +# bin/windows/x86_64/viewer.exe (static raylib -- self-contained) +# bin/windows/x86/viewer.exe # bundle/PurismCoreBundle.h # -# v5 compat builds go under lib-v5/ and dll-v5/ (Windows only). +# v5 compat builds go under lib-v5/ and dll-v5/ (Windows only; no viewer). # # Usage: -# ./scripts/build-dist.sh # full matrix -# ./scripts/build-dist.sh linux # linux only -# ./scripts/build-dist.sh windows # windows only +# ./scripts/build-dist.sh # full matrix +# ./scripts/build-dist.sh linux # linux x86_64 + arm64 +# ./scripts/build-dist.sh macos # macos x86_64 + arm64 + universal +# ./scripts/build-dist.sh windows # windows x86_64 + x86 + arm64, v5+v6 +# ./scripts/build-dist.sh zig-cross-linux-x86_64 # single target (one preset) +# +# Viewer (bin/): opt-in via RAYLIB_DIR_ env vars pointing at an +# extracted raylib v6.0 release archive (include/ + lib/). Mapping: +# RAYLIB_DIR_LINUX_AMD64 -> zig-cross-linux-x86_64 +# RAYLIB_DIR_LINUX_ARM64 -> zig-cross-linux-arm64 +# RAYLIB_DIR_MACOS -> zig-cross-macos-x86_64 AND zig-cross-macos-arm64 (universal raylib) +# RAYLIB_DIR_WINDOWS_AMD64 -> zig-cross-windows-x86_64 +# RAYLIB_DIR_WINDOWS_X86 -> zig-cross-windows-x86 +# (zig-cross-windows-arm64 viewer: no raylib v6.0 mingw archive ships for it -- skip.) set -e cd "$(dirname "$0")/.." DIST="${DISTDIR:-dist/sdk}" +TMP="build/_cmake-dist" + +# Per-target metadata +# Format: ` ` +TARGETS=$(cat < +dlldir_for() { echo "dll/$1/$2"; } # (no viewer here, see bindir_for) +bindir_for() { echo "bin/$1/$2"; } # -build() { - target="$1" outlib="$2" outdll="$3" compat="$4" - opts="--out build/_dist_tmp" +# build one (target, ABI, flavor) combination +# Args: +# Echoes the resulting build dir to stdout (last line). +build_one() { + preset="$1" abi="$2" shared="$3" + dir="$TMP/$preset-$abi-$(if [ "$shared" = 1 ]; then echo shared; else echo static; fi)" + rm -rf "$dir" + cmake --preset "$preset" -DPURISM_CORE_ABI="$abi" \ + -DBUILD_SHARED_LIBS=$([ "$shared" = 1 ] && echo ON || echo OFF) \ + -B "$dir" >/dev/null 2>&1 + cmake --build "$dir" -j"$(nproc)" >/dev/null 2>&1 + echo "$dir" +} + +# build the library artifacts for one target +# Args: +build_libs() { + preset="$1"; os="$2"; arch="$3"; _re="$4"; abi="$5" + # CMake's OUTPUT_NAME is PurismCore${_psm_suffix}, where _psm_suffix is + # "-v5" for v5 and "" for v6 -- so the produced filenames carry -v5 for v5 + # builds. The dist layout convention puts -v5 in the *directory* (lib-v5/ + # dll-v5/), never the filename, so strip the suffix when copying. + suf="" + if [ "$abi" = v5 ]; then suf="-v5"; fi - [ -n "$compat" ] && opts="$opts $compat" + echo " [$preset] static ($abi)" + dir="$(build_one "$preset" "$abi" 0)" + outlibdir="$DIST/$(libdir_for "$os" "$arch")" + if [ "$abi" = v5 ]; then outlibdir="$DIST/lib-v5/$os/$arch"; fi + mkdir -p "$outlibdir" + # Linux/macOS produce libPurismCore.a; Windows produces PurismCore.a + # (PREFIX ""). The bare-name destination matches the original Cubism Core + # convention (no -v5 in the filename). When suf is empty (v6), the strip + # step is a literal copy. + strip_suf() { # + src="$1"; dest="$2" + if [ -n "$suf" ]; then + cp -a "$src" "$dest/$(basename "$src" | sed "s/$suf//")" + else + cp -a "$src" "$dest/" + fi + } + for src in "$dir"/libPurismCore$suf.a "$dir"/PurismCore$suf.a; do + [ -f "$src" ] && strip_suf "$src" "$outlibdir" + done - if [ -n "$outdll" ]; then - opts="$opts --both" + echo " [$preset] shared ($abi)" + dir="$(build_one "$preset" "$abi" 1)" + outdlldir="$DIST/$(dlldir_for "$os" "$arch")" + if [ "$abi" = v5 ]; then outdlldir="$DIST/dll-v5/$os/$arch"; fi + mkdir -p "$outdlldir" + # Copy the produced shared lib *and any soname symlinks*, stripping the + # -v5 suffix where present (only Windows v5 builds carry it in the filename): + # Linux: libPurismCore.so + .so.1 + .so.1.x.y (no suf for v6-only) + # macOS: libPurismCore.dylib + .1.dylib + .1.x.y.dylib + # Windows: PurismCore.dll + PurismCore-v5.dll + if [ "$os" = windows ]; then + src="$dir/PurismCore$suf.dll" + if [ -f "$src" ]; then + if [ -n "$suf" ]; then + cp -a "$src" "$outdlldir/$(basename "$src" | sed "s/$suf//")" + else + cp -a "$src" "$outdlldir/" + fi + fi else - opts="$opts --static" + # Linux/macOS only ship v6 (suf is "" here), so the suffix-strip is a no-op. + cp -a "$dir"/libPurismCore$suf.so* "$outdlldir/" 2>/dev/null || true + cp -a "$dir"/libPurismCore$suf.*dylib "$outdlldir/" 2>/dev/null || true + fi +} + +# build the viewer for one target +# Args: +build_viewer() { + preset="$1"; os="$2"; arch="$3"; raylib_env="$4" + + # windows-arm64: no mingw raylib archive -> intentional skip. + if [ -z "$raylib_env" ]; then + echo " [$preset] viewer skipped (no raylib for this target)" + return 0 + fi + eval "raylib_dir=\${$raylib_env:-}" + if [ -z "$raylib_dir" ]; then + echo " [$preset] viewer skipped ($raylib_env not set)" + return 0 + fi + if [ ! -d "$raylib_dir/include" ] || [ ! -d "$raylib_dir/lib" ]; then + # The var was set, so the viewer was requested: a bad dir is a packaging + # error (would ship a release without viewers), not an opt-out. Fail. + echo "error: [$preset] $raylib_env is set but $raylib_dir" >&2 + echo " lacks include/ or lib/ -- unset $raylib_env to skip the viewer" >&2 + exit 1 fi - ./scripts/zig-build.sh "$target" $opts + # Host-build bin2h so the cross-configure can use it (CMake's + # add_executable(bin2h) would otherwise target the cross target). + make -s build/bin2h >/dev/null 2>&1 + bin2h_path="$PWD/build/bin2h" - mkdir -p "$DIST/$outlib" - cp build/_dist_tmp/libPurismCore.a "$DIST/$outlib/" 2>/dev/null || true + dir="$TMP/$preset-viewer" + rm -rf "$dir" + echo " [$preset] viewer (raylib $raylib_dir)" + cmake --preset "$preset" -DPURISM_CORE_ABI=v6 -DBUILD_SHARED_LIBS=OFF \ + -DPURISM_CORE_BUILD_VIEWER=ON \ + -DPURISM_CORE_BIN2H="$bin2h_path" \ + -DPURISM_CORE_RAYLIB_DIR="$raylib_dir" \ + -B "$dir" >/dev/null 2>&1 + cmake --build "$dir" --target viewer -j"$(nproc)" >/dev/null 2>&1 - if [ -n "$outdll" ]; then - mkdir -p "$DIST/$outdll" - cp build/_dist_tmp/PurismCore.dll "$DIST/$outdll/" 2>/dev/null || true - cp build/_dist_tmp/libPurismCore.so "$DIST/$outdll/" 2>/dev/null || true - cp build/_dist_tmp/libPurismCore.dylib "$DIST/$outdll/" 2>/dev/null || true + outbindir="$DIST/$(bindir_for "$os" "$arch")" + mkdir -p "$outbindir" + if [ "$os" = windows ]; then + cp -a "$dir"/viewer.exe "$outbindir/" + else + cp -a "$dir"/viewer "$outbindir/" + # Stage raylib next to the binary so the rpath ($ORIGIN / @loader_path) + # finds it. Absolute-path copies keep the soname symlinks intact. + if [ "$os" = macos ]; then + cp -a "$raylib_dir"/lib/libraylib*.dylib "$outbindir/" 2>/dev/null || true + else + cp -a "$raylib_dir"/lib/libraylib.so* "$outbindir/" 2>/dev/null || true + fi fi +} - rm -rf build/_dist_tmp +# Info.plist for the universal macOS .app +write_info_plist() { + out="$1" + ver_hex=$(sed -n 's/^#define PSM_TRUE_VERSION[[:space:]]*\(0x[0-9A-Fa-f]*\).*/\1/p' include/PurismCore.h) + v_major=$(( ($ver_hex >> 24) & 0xFF )) + v_minor=$(( ($ver_hex >> 16) & 0xFF )) + v_patch=$(( $ver_hex & 0xFFFF )) + v_string="$v_major.$v_minor.$v_patch" + sed "s/@PSM_VER_STRING@/$v_string/g" src/samples/viewer/Info.plist.in > "$out" } -# ── main ── +# Build the universal Viewer.app from per-arch macOS viewers + their raylib. +build_macos_universal_app() { + x86_dir="$DIST/bin/macos/x86_64" + arm_dir="$DIST/bin/macos/arm64" + if [ ! -f "$x86_dir/viewer" ] || [ ! -f "$arm_dir/viewer" ]; then + echo " [macos universal] viewer skipped (per-arch viewers missing)" + return 0 + fi + echo " [macos universal] Viewer.app" + app="$DIST/bin/macos/universal/Viewer.app" + rm -rf "$app" + mkdir -p "$app/Contents/MacOS" + # lipo may be absent on non-macOS dev hosts; tolerate so the rest of dist + # still completes (the .app skeleton + Info.plist land without the binary). + lipo -create "$x86_dir/viewer" "$arm_dir/viewer" \ + -output "$app/Contents/MacOS/viewer" 2>/dev/null || \ + echo " [macos universal] lipo unavailable -- .app lacks the universal binary" + cp -a "$x86_dir"/libraylib*.dylib "$app/Contents/MacOS/" 2>/dev/null || true + write_info_plist "$app/Contents/Info.plist" +} -rm -rf "$DIST" +rm -rf "$DIST" "$TMP" mkdir -p "$DIST" FILTER="${1:-all}" -# Linux -if [ "$FILTER" = all ] || [ "$FILTER" = linux ]; then - echo "=== Linux ===" - build linux-x64 lib/linux/x86_64 dll/linux/x86_64 --v6 - build linux-arm64 lib/linux/arm64 "" --v6 -fi - -# macOS -if [ "$FILTER" = all ] || [ "$FILTER" = macos ]; then - echo "=== macOS ===" - build macos-x64 lib/macos/x86_64 dll/macos/x86_64 --v6 - build macos-arm64 lib/macos/arm64 dll/macos/arm64 --v6 - - # Universal binary (fat dylib) - mkdir -p "$DIST/dll/macos/universal" - lipo -create \ - "$DIST/dll/macos/x86_64/libPurismCore.dylib" \ - "$DIST/dll/macos/arm64/libPurismCore.dylib" \ - -output "$DIST/dll/macos/universal/libPurismCore.dylib" \ - 2>/dev/null || true - - # Universal static library - mkdir -p "$DIST/lib/macos/universal" - lipo -create \ - "$DIST/lib/macos/x86_64/libPurismCore.a" \ - "$DIST/lib/macos/arm64/libPurismCore.a" \ - -output "$DIST/lib/macos/universal/libPurismCore.a" \ - 2>/dev/null || true -fi - -# Windows (v6 + v5) -if [ "$FILTER" = all ] || [ "$FILTER" = windows ]; then - echo "=== Windows (v6) ===" - build win-x64 lib/windows/x86_64 dll/windows/x86_64 --v6 - build win-x86 lib/windows/x86 dll/windows/x86 --v6 - build win-arm64 lib/windows/arm64 dll/windows/arm64 --v6 - - echo "=== Windows (v5) ===" - build win-x64 lib-v5/windows/x86_64 dll-v5/windows/x86_64 --v5 - build win-x86 lib-v5/windows/x86 dll-v5/windows/x86 --v5 - build win-arm64 lib-v5/windows/arm64 dll-v5/windows/arm64 --v5 -fi +# Build the library artifacts (static + shared) for every matching target. +# Windows gets both v6 and v5 ABIs; Linux/macOS ship v6 only. +build_libs_for() { + preset="$1"; os="$2"; arch="$3"; re="$4" + if [ "$os" = windows ]; then + build_libs "$preset" "$os" "$arch" "$re" v6 + build_libs "$preset" "$os" "$arch" "$re" v5 + else + build_libs "$preset" "$os" "$arch" "$re" v6 + fi +} +each_target build_libs_for "$FILTER" + +# Viewer (opt-in via RAYLIB_DIR_ env vars). +each_target build_viewer "$FILTER" + +# macOS universal fat binaries (lib + dll + Viewer.app) +case "$FILTER" in + all|macos|zig-cross-macos-x86_64|zig-cross-macos-arm64) + if [ -f "$DIST/lib/macos/x86_64/libPurismCore.a" ] && \ + [ -f "$DIST/lib/macos/arm64/libPurismCore.a" ]; then + echo " [macos universal] lib + dll" + mkdir -p "$DIST/lib/macos/universal" "$DIST/dll/macos/universal" + lipo -create \ + "$DIST/lib/macos/x86_64/libPurismCore.a" \ + "$DIST/lib/macos/arm64/libPurismCore.a" \ + -output "$DIST/lib/macos/universal/libPurismCore.a" 2>/dev/null || true + # The versioned dylib name tracks PSM_TRUE_VERSION; resolve it by glob + # (the libPurismCore{.dylib,.N.dylib} symlinks don't match *.*.*). + dylib_ver=$(basename "$DIST"/dll/macos/x86_64/libPurismCore.*.*.*.dylib) + if [ -f "$DIST/dll/macos/x86_64/$dylib_ver" ]; then + lipo -create \ + "$DIST/dll/macos/x86_64/$dylib_ver" \ + "$DIST/dll/macos/arm64/$dylib_ver" \ + -output "$DIST/dll/macos/universal/$dylib_ver" \ + 2>/dev/null || true + # Recreate the versioned + bare dylib symlinks for the universal + # slice (only when lipo actually produced the fat dylib). + if [ -f "$DIST/dll/macos/universal/$dylib_ver" ]; then + vnums=${dylib_ver#libPurismCore.} # 1.x.y.dylib + vnums=${vnums%.dylib} # 1.x.y + ( cd "$DIST/dll/macos/universal" && \ + ln -sf "$dylib_ver" "libPurismCore.${vnums%%.*}.dylib" && \ + ln -sf "libPurismCore.${vnums%%.*}.dylib" libPurismCore.dylib ) + fi + fi + build_macos_universal_app + fi + ;; +esac # Headers echo "=== Headers ===" @@ -130,6 +321,8 @@ for f in LICENSE README.md docs/*.md docs/*.txt; do [ -f "$f" ] && cp "$f" "$DIST/" done +rm -rf "$TMP" + echo "" echo "=== Done ===" find "$DIST" -type f -not -path '*/obj/*' | sort | sed 's|^| |' diff --git a/scripts/bundle.sh b/scripts/bundle.sh index ef4dadd..ef4df56 100755 --- a/scripts/bundle.sh +++ b/scripts/bundle.sh @@ -1,64 +1,34 @@ #!/bin/sh -# Generate single-file bundle from source files. +# Assemble a single-file amalgamation from a template (default src/bundle.c.in). +# +# Each `#include "PATH"` line in the template is inlined: PATH is resolved +# relative to the template's directory, and the file's contents are emitted with +# its own quoted `#include "..."` lines dropped (their targets are bundled +# elsewhere, in order, by the template). System `#include <...>` lines and +# everything else (guards, code, comments) are kept verbatim. Non-include lines +# of the template pass through unchanged, so the template doubles as the bundle's +# skeleton. +# +# Usage: bundle.sh [template] > dist/PurismCoreBundle.h # # Copyright (c) 2026 Sakura Motion Project # SPDX-License-Identifier: MIT - -# Usage: ./scripts/bundle.sh > dist/PurismCoreBundle.h - -set -e +set -eu cd "$(dirname "$0")/.." -cat << 'HEADER' -/* - * PurismCoreBundle.h - Single-file Purism Core library - * - * Usage: - * #include "PurismCoreBundle.h" - * - * In exactly ONE .c file, define the implementation: - * #define PURISM_CORE_IMPLEMENTATION - * #include "PurismCoreBundle.h" - * - * Copyright (c) 2026 Sakura Motion Project - * SPDX-License-Identifier: MIT - */ -#ifndef PURISM_CORE_BUNDLE_H -#define PURISM_CORE_BUNDLE_H -HEADER - -sed -e '/^#ifndef PURISM_CORE_H$/d' \ - -e '/^#define PURISM_CORE_H$/d' \ - include/PurismCore.h | sed '$ { /^#endif/d }' - -echo "" -echo "#ifdef PURISM_CORE_IMPLEMENTATION" -echo "" - -# Internal headers + source files, stripping include guards -# and internal #include "..." directives -for f in \ - src/private.h src/error.h src/debug.h src/arena.h \ - src/array.h src/math2.h src/moc3.h src/model.h \ - src/gather.h src/interpolate.h src/artmesh.h \ - src/blendshape.h src/deformer.h src/glue.h \ - src/offscreen.h src/param.h src/part.h \ - src/render.h src/update.h \ - src/core.c src/debug.c src/arena.c \ - src/math2.c src/moc3.c src/model.c src/update.c \ - src/param.c src/part.c src/deformer.c src/artmesh.c \ - src/glue.c src/offscreen.c src/blendshape.c \ - src/interpolate.c src/render.c -do - echo "/* file: $(basename $f) */" - sed -e '/^#ifndef PSM__.*_H$/d' \ - -e '/^#define PSM__.*_H$/d' \ - -e '/^#endif.*PSM__.*_H/d' \ - -e '/^#include *".*"/d' \ - "$f" - echo "" -done +tpl="${1:-src/bundle.c.in}" +tpldir=$(dirname "$tpl") -echo "#endif /* PURISM_CORE_IMPLEMENTATION */" -echo "" -echo "#endif /* PURISM_CORE_BUNDLE_H */" +while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + '#include "'*) + path=${line#*\"} # drop up to the opening quote + path=${path%%\"*} # drop from the closing quote + echo "/* ===== $path ===== */" + grep -v '^[[:space:]]*#[[:space:]]*include[[:space:]]*"' "$tpldir/$path" + ;; + *) + printf '%s\n' "$line" + ;; + esac +done < "$tpl" diff --git a/scripts/purismcore.pc.in b/scripts/purismcore.pc.in index 4fd6f57..8f7d370 100644 --- a/scripts/purismcore.pc.in +++ b/scripts/purismcore.pc.in @@ -4,7 +4,7 @@ includedir=${prefix}/include Name: PurismCore@ABI_SUFFIX@ Description: Live2D Cubism Core compatible library (@ABI@ ABI) -Version: 1.0.1 +Version: @VERSION@ Cflags: -I${includedir} Libs: -L${libdir} -lPurismCore@ABI_SUFFIX@ Libs.private: -lm diff --git a/scripts/zig-build.sh b/scripts/zig-build.sh deleted file mode 100755 index 69855ea..0000000 --- a/scripts/zig-build.sh +++ /dev/null @@ -1,155 +0,0 @@ -#!/bin/sh -# Cross-build Purism Core with zig cc. -# -# Copyright (c) 2026 Sakura Motion Project -# SPDX-License-Identifier: MIT - -# Usage: -# ./scripts/zig-build.sh [options] -# -# Targets: -# linux-x64 linux-arm64 -# macos-x64 macos-arm64 -# win-x64 win-x86 win-arm64 -# native (host platform, same as plain make) -# -# Options: -# --v5 Build with v5 compat (default for Windows) -# --v6 Build with v6 compat (default for non-Windows) -# --dll Build shared library / DLL -# --static Build static library (default) -# --both Build both static and shared -# --out DIR Output directory (default: build/) -# -# Examples: -# ./scripts/zig-build.sh win-x64 --both -# ./scripts/zig-build.sh linux-arm64 --dll -# ./scripts/zig-build.sh macos-arm64 - -set -e - -cd "$(dirname "$0")/.." - -SOURCES=" - src/core.c src/debug.c src/arena.c - src/math2.c src/moc3.c src/model.c src/update.c - src/param.c src/part.c src/deformer.c src/artmesh.c - src/glue.c src/offscreen.c src/blendshape.c - src/interpolate.c src/render.c -" - -usage() { - sed -n '2,/^$/{ s/^# //; s/^#//; p }' "$0" - exit 1 -} - -[ $# -lt 1 ] && usage - -TARGET="$1"; shift -ZIG_TARGET="" -IS_WINDOWS=0 -IS_MACOS=0 -COMPAT="" -BUILD_STATIC=0 -BUILD_SHARED=0 -OUTDIR="" - -case "$TARGET" in - linux-x64) ZIG_TARGET="x86_64-linux-gnu" ;; - linux-arm64) ZIG_TARGET="aarch64-linux-gnu" ;; - macos-x64) ZIG_TARGET="x86_64-macos" ;; - macos-arm64) ZIG_TARGET="aarch64-macos"; IS_MACOS=1 ;; - win-x64) ZIG_TARGET="x86_64-windows-gnu"; IS_WINDOWS=1 ;; - win-x86) ZIG_TARGET="x86-windows-gnu"; IS_WINDOWS=1 ;; - win-arm64) ZIG_TARGET="aarch64-windows-gnu"; IS_WINDOWS=1 ;; - native) ZIG_TARGET="" ;; - *) echo "Unknown target: $TARGET"; usage ;; -esac - -while [ $# -gt 0 ]; do - case "$1" in - --v5) COMPAT="-DPSM_COMPAT_VERSION=0x05010000L" ;; - --v6) COMPAT="-DPSM_COMPAT_VERSION=0x06000001L" ;; - --dll) BUILD_SHARED=1 ;; - --static) BUILD_STATIC=1 ;; - --both) BUILD_STATIC=1; BUILD_SHARED=1 ;; - --out) OUTDIR="$2"; shift ;; - *) echo "Unknown option: $1"; usage ;; - esac - shift -done - -# Defaults -if [ $BUILD_STATIC -eq 0 ] && [ $BUILD_SHARED -eq 0 ]; then - BUILD_STATIC=1 -fi -if [ -z "$COMPAT" ]; then - if [ $IS_WINDOWS -eq 1 ]; then - COMPAT="-DPSM_COMPAT_VERSION=0x05010000L" - else - COMPAT="-DPSM_COMPAT_VERSION=0x06000001L" - fi -fi -if [ -z "$OUTDIR" ]; then - OUTDIR="build/$TARGET" -fi - -CC="${ZIG:-zig} cc" -AR="${ZIG:-zig} ar" -CFLAGS="-O2 -I./include -I./src -fPIC $COMPAT" -if [ -n "$ZIG_TARGET" ]; then - CFLAGS="-target $ZIG_TARGET $CFLAGS" -fi - -OBJDIR="$OUTDIR/obj" -mkdir -p "$OBJDIR" "$OUTDIR" - -echo "target: $TARGET ($ZIG_TARGET)" -echo "compat: $COMPAT" -echo "output: $OUTDIR" -echo "" - -# Compile objects -for src in $SOURCES; do - obj="$OBJDIR/$(basename "$src" .c).o" - $CC $CFLAGS -c "$src" -o "$obj" -done - -# Static library -if [ $BUILD_STATIC -eq 1 ]; then - $AR rcs "$OUTDIR/libPurismCore.a" "$OBJDIR"/*.o - echo "static: $OUTDIR/libPurismCore.a" -fi - -# Shared library -if [ $BUILD_SHARED -eq 1 ]; then - if [ $IS_WINDOWS -eq 1 ]; then - # Windows DLL with version resource - RC_OBJ="" - if [ -f src/version.rc.in ]; then - COMPAT_HEX=$(echo "$COMPAT" | grep -o '0x[0-9a-fA-F]*' || echo "") - ./scripts/gen-version-rc.sh $COMPAT_HEX > "$OBJDIR/version.rc" - RC="${ZIG:-zig} rc" - RC_OBJ="$OBJDIR/version.res" - $RC /fo "$RC_OBJ" "$OBJDIR/version.rc" 2>/dev/null || RC_OBJ="" - fi - $CC -target "$ZIG_TARGET" -shared -O2 \ - $COMPAT -DPURISM_CORE_DLL -I./include -I./src \ - -o "$OUTDIR/PurismCore.dll" \ - $SOURCES $RC_OBJ -lm - echo "dll: $OUTDIR/PurismCore.dll" - elif [ $IS_MACOS -eq 1 ]; then - $CC -target "$ZIG_TARGET" -shared -dynamiclib -O2 \ - $COMPAT -I./include -I./src \ - -o "$OUTDIR/libPurismCore.dylib" \ - $SOURCES -lm - echo "dylib: $OUTDIR/libPurismCore.dylib" - else - $CC $CFLAGS -shared \ - -o "$OUTDIR/libPurismCore.so" \ - $SOURCES -lm - echo "so: $OUTDIR/libPurismCore.so" - fi -fi - -echo "done." diff --git a/src/arena.c b/src/arena.c index 904743a..12d9661 100644 --- a/src/arena.c +++ b/src/arena.c @@ -7,39 +7,77 @@ #include "arena.h" +#ifdef PSM_DEBUG_MALLOC +# include + +static void **g_dbg_ptrs; +static psm_size g_dbg_count, g_dbg_cap; + +static void * +psm__dbg_alloc(psm__u32 n) +{ + void *p = calloc(1, n ? n : 1); + if (g_dbg_count == g_dbg_cap) { + psm_size nc = g_dbg_cap ? g_dbg_cap * 2 : 256; + void **np = realloc(g_dbg_ptrs, nc * sizeof(void *)); + if (!np) + return p; + g_dbg_ptrs = np; + g_dbg_cap = nc; + } + g_dbg_ptrs[g_dbg_count++] = p; + return p; +} + +void +psm__dbg_free_all(void) +{ + for (psm_size i = 0; i < g_dbg_count; i++) + free(g_dbg_ptrs[i]); + g_dbg_count = 0; +} +#endif /* PSM_DEBUG_MALLOC */ + PSM__DEF void * psm__arena_alloc(struct psm__arena *a, psm__u32 n) { - psm__u32 off = (a->offset + 15) & ~15u; - if (off < a->offset) { - a->overflow = 1; + psm__u32 off = (a->off + 15) & ~15u; + if (off < a->off) { + a->overflow = true; return NULL; } psm__u32 end = off + n; if (end < off) { - a->overflow = 1; + a->overflow = true; return NULL; } if (a->base == NULL) { - a->offset = end; + a->off = end; return NULL; } - if (end > a->capacity) { - a->overflow = 1; +#ifdef PSM_DEBUG_MALLOC + /* When fuzzing with ASAN, we use real malloc() in order to better catch OOB + memory access */ + a->off = end; + return psm__dbg_alloc(n); +#else + if (end > a->cap) { + a->overflow = true; return NULL; } void *p = a->base + off; - a->offset = end; + a->off = end; return p; +#endif } PSM__DEF psm__u32 psm__arena_total(const struct psm__arena *a) { - return (a->offset + 15) & ~15u; + return (a->off + 15) & ~15u; } -PSM__DEF psm__i32 +PSM__DEF bool psm__arena_ok(const struct psm__arena *a) { return !a->overflow; diff --git a/src/arena.h b/src/arena.h index 67d25f9..a94008a 100644 --- a/src/arena.h +++ b/src/arena.h @@ -12,32 +12,35 @@ struct psm__arena { psm__u8 *base; - psm__u32 offset; - psm__u32 capacity; - psm__i32 overflow; + psm__u32 off; + psm__u32 cap; + bool overflow; }; #define PSM__ARENA_INIT(buf, cap) \ - ((struct psm__arena){(psm__u8 *)(buf), 0, (cap), 0}) + ((struct psm__arena){ (psm__u8 *)(buf), 0, (cap), 0 }) static inline psm__u32 psm__arena_safe_mul(struct psm__arena *a, psm__u32 x, psm__u32 y) { psm__u32 r = x * y; if (x != 0 && r / x != y) { - a->overflow = 1; + a->overflow = true; return 0; } return r; } #define PSM__ARENA_NEW(a, T, n) \ - ((T *)psm__arena_alloc((a), psm__arena_safe_mul((a), sizeof(T), (n)))) + ((T *)psm__arena_alloc((a), psm__arena_safe_mul((a), sizeof(T), (n)))) #define PSM__ARENA_NEW_SIZE(a, T, sz) \ - ((T *)psm__arena_alloc((a), (sz))) - -PSM__DEF void *psm__arena_alloc(struct psm__arena *, psm__u32); -PSM__DEF psm__u32 psm__arena_total(const struct psm__arena *); -PSM__DEF psm__i32 psm__arena_ok(const struct psm__arena *); + ((T *)psm__arena_alloc((a), (sz))) + +PSM__DEF void *psm__arena_alloc(struct psm__arena *, psm__u32); +PSM__DEF psm__u32 psm__arena_total(const struct psm__arena *); +PSM__DEF bool psm__arena_ok(const struct psm__arena *); +#ifdef PSM_DEBUG_MALLOC +PSM__DEF void psm__dbg_free_all(void); +#endif #endif /* PSM__ARENA_H */ diff --git a/src/array.h b/src/array.h index 9f39e0f..ff61cf0 100644 --- a/src/array.h +++ b/src/array.h @@ -11,10 +11,12 @@ #include "private.h" #include "error.h" -static inline int psm__array_check_index(psm_size i, psm_size n) +static inline int +psm__array_check_index(psm_size i, psm_size n) { #ifdef PSM_FAST_AND_DANGEROUS - (void)i; (void)n; + (void)i; + (void)n; return PSM__OK; #else return (i < n) ? PSM__OK : PSM__ERR_PARAMETER_RANGE_ERROR; @@ -26,7 +28,7 @@ static inline int psm__array_check_index(psm_size i, psm_size n) * When PSM_FAST_AND_DANGEROUS is defined, always returns 1. */ #define psm__check_idx(idx, max) \ - (psm__array_check_index((psm_size)(idx), (max)) == PSM__OK) + (psm__array_check_index((psm_size)(idx), (max)) == PSM__OK) /* * psm__check_offset_range checks if [offset, offset+count) is within [0, max). @@ -34,11 +36,11 @@ static inline int psm__array_check_index(psm_size i, psm_size n) * When PSM_FAST_AND_DANGEROUS is defined, always returns 1. */ #ifdef PSM_FAST_AND_DANGEROUS -#define psm__check_offset_range(offset, count, max) (1) +# define psm__check_offset_range(offset, count, max) (1) #else -#define psm__check_offset_range(offset, count, max) \ +# define psm__check_offset_range(offset, count, max) \ ((offset) >= 0 && (count) >= 0 && \ - (psm__u32)(max) - (psm__u32)(offset) >= (psm__u32)(count)) + (psm__u32)(max) - (psm__u32)(offset) >= (psm__u32)(count)) #endif /* @@ -54,15 +56,15 @@ static inline int psm__array_check_index(psm_size i, psm_size n) */ #ifdef PSM_FAST_AND_DANGEROUS -#define psm__valid_idx(idx, max) 1 -#define psm__valid_opt_idx(idx, max) ((idx) >= 0 ? 1 : 0) -#define psm__valid_range(begin, count, max) 1 -#define psm__valid_opt_range(begin, count, max) \ +# define psm__valid_idx(idx, max) 1 +# define psm__valid_opt_idx(idx, max) ((idx) >= 0 ? 1 : 0) +# define psm__valid_range(begin, count, max) 1 +# define psm__valid_opt_range(begin, count, max) \ ((begin) >= 0 ? 1 : 0) #else -static inline int +static inline bool psm__valid_idx(psm__i32 idx, psm__i32 max) { return (psm__u32)idx < (psm__u32)max; @@ -75,12 +77,15 @@ psm__valid_opt_idx(psm__i32 idx, psm__i32 max) return (psm__u32)idx < (psm__u32)max ? 1 : -1; } -static inline int +static inline bool psm__valid_range(psm__i32 begin, psm__i32 count, psm__i32 max) { - return begin >= 0 && count >= 0 - && (psm__u32)max - (psm__u32)begin - >= (psm__u32)count; + /* + * begin <= max is required: without it, (u32)max - (u32)begin underflows + * to a huge value when begin > max and the range check wrongly passes. + */ + return begin >= 0 && count >= 0 && begin <= max && + (psm__u32)max - (psm__u32)begin >= (psm__u32)count; } static inline int @@ -96,7 +101,8 @@ psm__valid_opt_range(psm__i32 begin, psm__i32 count, psm__i32 max) * psm__clamp_idx clamps idx to [0, max-1] for defensive access. * Returns 0 if max <= 0. */ -static inline psm__i32 psm__clamp_idx(psm__i32 idx, psm__i32 max) +static inline psm__i32 +psm__clamp_idx(psm__i32 idx, psm__i32 max) { if (max <= 0) return 0; return psm__clamp_i32(idx, 0, max - 1); @@ -108,7 +114,8 @@ static inline psm__i32 psm__clamp_idx(psm__i32 idx, psm__i32 max) * - integer overflow in the subtraction * Returns 0 for invalid input, otherwise the positive order level. */ -static inline psm__i32 psm__safe_order_level(psm__i32 max_do, psm__i32 min_do) +static inline psm__i32 +psm__safe_order_level(psm__i32 max_do, psm__i32 min_do) { if (max_do < min_do) return 0; diff --git a/src/artmesh.c b/src/artmesh.c index 036dde1..90f7502 100644 --- a/src/artmesh.c +++ b/src/artmesh.c @@ -23,13 +23,15 @@ psm__enable_art_meshes(struct psm__model *m) return; struct psm__art_mesh *meshes = m->art_meshes.meshes; + bool *def_en = m->deformers.enable; bool *part_en = m->parts.enable; bool *enable = m->art_meshes.enable; for (psm__i32 i = 0; i < count; i++) { struct psm__art_mesh *am = &meshes[i]; - bool en = am->local_enable; + + bool en = am->local_enable; psm__i32 pp = am->parent_part_idx; psm__i32 pd = am->parent_deformer_idx; @@ -54,15 +56,14 @@ psm__gather_art_meshes(struct psm__model *m) return; psm__i32 *kb = ms->art_mesh_src.keyform_off; - psm__i32 max_keyforms = ms->count_info->art_mesh_keyforms; + psm__i32 max_keyforms = ms->count_info->art_mesh_keyforms; + struct psm__art_mesh_keydata *kd = &m->art_meshes.keydata; - struct psm__binding *bindings[count]; - for (psm__i32 i = 0; i < count; i++) - bindings[i] = meshes[i].binding; + struct psm__binding *const *bindings = m->art_meshes.bindings; struct psm__gather_channel ch[] = { - { ms->art_mesh_key_src.opacity, kd->opacity }, + { ms->art_mesh_key_src.opacity, kd->opacity }, { ms->art_mesh_key_src.draw_order, kd->draw_order }, }; psm__gather_scalars(count, bindings, kb, max_keyforms, &kd->interp, ch, 2); @@ -92,7 +93,8 @@ psm__apply_parts_to_meshes(struct psm__model *m) return; struct psm__art_mesh *meshes = m->art_meshes.meshes; - bool *en = m->art_meshes.enable; + + bool *en = m->art_meshes.enable; psm__f32 *part_opa = m->parts.opacity; psm__i32 *part_off = m->parts.offscreen_src_idx; psm__f32 *am_opa = m->art_meshes.opacity; @@ -116,7 +118,10 @@ psm__apply_parts_to_meshes(struct psm__model *m) for (psm__i32 i = 0; i < count; i++) { psm__i32 ci = i * 4; - if (!en[i] || am_opa[i] == 0.0f) + /* Color (multiply/screen) is a separate channel from render visibility; + * Cubism propagates the parent deformer's color to the child mesh even at + * opacity 0, so this must NOT skip on am_opa[i] == 0. */ + if (!en[i]) continue; psm__i32 pd = meshes[i].parent_deformer_idx; if (pd == -1) @@ -134,9 +139,9 @@ psm__apply_parts_to_meshes(struct psm__model *m) am_mul[ci + 2] = psm__clamp_f32_01(b); am_mul[ci + 3] = 1.0f; - r = fmaf(-am_scr[ci + 0], ps[0], am_scr[ci + 0] + ps[0]); - g = fmaf(-am_scr[ci + 1], ps[1], am_scr[ci + 1] + ps[1]); - b = fmaf(-am_scr[ci + 2], ps[2], am_scr[ci + 2] + ps[2]); + r = am_scr[ci + 0] + ps[0] - am_scr[ci + 0] * ps[0]; + g = am_scr[ci + 1] + ps[1] - am_scr[ci + 1] * ps[1]; + b = am_scr[ci + 2] + ps[2] - am_scr[ci + 2] * ps[2]; am_scr[ci + 0] = psm__clamp_f32_01(r); am_scr[ci + 1] = psm__clamp_f32_01(g); @@ -145,7 +150,6 @@ psm__apply_parts_to_meshes(struct psm__model *m) } } - PSMDEF int csmGetDrawableCount(const csmModel *model) { @@ -157,8 +161,7 @@ PSMDEF const char ** csmGetDrawableIds(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return ms->art_mesh_src.id_runtime; + return m->source->sections->art_mesh_src.id_runtime; } PSMDEF const csmFlags * @@ -188,8 +191,7 @@ PSMDEF const int * csmGetDrawableTextureIndices(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return ms->art_mesh_src.texture_no; + return m->source->sections->art_mesh_src.texture_no; } PSMDEF const int * @@ -210,24 +212,21 @@ PSMDEF const int * csmGetDrawableMaskCounts(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return ms->art_mesh_src.mask_len; + return m->source->sections->art_mesh_src.mask_len; } PSMDEF const int ** csmGetDrawableMasks(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return ms->art_mesh_src.drawable_mask_runtime; + return m->source->sections->art_mesh_src.drawable_mask_runtime; } PSMDEF const int * csmGetDrawableVertexCounts(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return ms->art_mesh_src.vertex_count; + return m->source->sections->art_mesh_src.vertex_count; } PSMDEF const csmVector2 ** @@ -241,24 +240,22 @@ PSMDEF const csmVector2 ** csmGetDrawableVertexUvs(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return (const csmVector2 **)ms->art_mesh_src.uv_runtime; + return (const csmVector2 **)m->source->sections->art_mesh_src.uv_runtime; } PSMDEF const int * csmGetDrawableIndexCounts(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return ms->art_mesh_src.idx_len; + return m->source->sections->art_mesh_src.idx_len; } PSMDEF const unsigned short ** csmGetDrawableIndices(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return (const unsigned short **)ms->art_mesh_src.pos_idx_runtime; + return (const unsigned short **) + m->source->sections->art_mesh_src.pos_idx_runtime; } PSMDEF const csmVector4 * @@ -279,6 +276,5 @@ PSMDEF const int * csmGetDrawableParentPartIndices(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - struct psm__sections *ms = m->source->sections; - return ms->art_mesh_src.parent_part_idx; + return m->source->sections->art_mesh_src.parent_part_idx; } diff --git a/src/blendshape.c b/src/blendshape.c index 48ccb4e..dca6024 100644 --- a/src/blendshape.c +++ b/src/blendshape.c @@ -15,13 +15,17 @@ #include "model.h" static inline psm__f32 -psm__blend_shape_interp_f32(const struct psm__blend_binding *binding, - const psm__f32 *keyform_src) +psm__blend_interp_f32(const struct psm__blend_binding *binding, + const psm__f32 *keyform_src, psm__i32 max_keyforms) { psm__i32 blend_count = binding->blend_count; psm__i32 off = binding->key_src_off; psm__f32 value; + /* keyform_idx[k]+off in [key_src_off, key_src_off+key_count) < max_keyforms, + * proved at load (F4: psm__verify_bs_keyform_window). */ + (void)max_keyforms; + switch (blend_count) { case 0: return 0.0f; @@ -33,8 +37,8 @@ psm__blend_shape_interp_f32(const struct psm__blend_binding *binding, case 2: { psm__i32 idx0 = binding->keyform_idx[0] + off; psm__i32 idx1 = binding->keyform_idx[1] + off; - value = keyform_src[idx0] * binding->weights[0] - + keyform_src[idx1] * binding->weights[1]; + value = keyform_src[idx0] * binding->weights[0] + + keyform_src[idx1] * binding->weights[1]; break; } default: @@ -47,7 +51,8 @@ psm__blend_shape_interp_f32(const struct psm__blend_binding *binding, static void blend_scalar_f32(psm__i32 count, const struct psm__blend_shape *shapes, - psm__f32 *values, const psm__f32 *keyform_src, psm__f32 lo, psm__f32 hi) + psm__f32 *values, const psm__f32 *keyform_src, psm__i32 max_keyforms, + psm__f32 lo, psm__f32 hi) { for (psm__i32 i = 0; i < count; i++) { psm__i32 ti = shapes[i].target_idx; @@ -57,7 +62,8 @@ blend_scalar_f32(psm__i32 count, const struct psm__blend_shape *shapes, struct psm__blend_binding *binds = shapes[i].bindings; if (bc > 0 && binds) { for (psm__i32 j = 0; j < bc; j++) - value += psm__blend_shape_interp_f32(&binds[j], keyform_src); + value += psm__blend_interp_f32(&binds[j], keyform_src, + max_keyforms); } values[ti] = psm__clamp_f32(value, lo, hi); @@ -66,7 +72,7 @@ blend_scalar_f32(psm__i32 count, const struct psm__blend_shape *shapes, static void blend_scalar_i32(psm__i32 count, const struct psm__blend_shape *shapes, - psm__i32 *values, const psm__f32 *keyform_src) + psm__i32 *values, const psm__f32 *keyform_src, psm__i32 max_keyforms) { for (psm__i32 i = 0; i < count; i++) { psm__i32 ti = shapes[i].target_idx; @@ -76,7 +82,8 @@ blend_scalar_i32(psm__i32 count, const struct psm__blend_shape *shapes, struct psm__blend_binding *binds = shapes[i].bindings; if (bc > 0 && binds) { for (psm__i32 j = 0; j < bc; j++) - value += psm__blend_shape_interp_f32(&binds[j], keyform_src); + value += psm__blend_interp_f32(&binds[j], keyform_src, + max_keyforms); } psm__f32 rounded = value + 0.001f; @@ -88,7 +95,8 @@ blend_scalar_i32(psm__i32 count, const struct psm__blend_shape *shapes, static void psm__blend_positions(const struct psm__model *m, psm__i32 count, const struct psm__blend_shape *shapes, const psm__i32 *keyform_pos_off, - psm__f32 **out_positions, const psm__i32 *vertex_counts) + psm__f32 **out_positions, const psm__i32 *vertex_counts, + psm__i32 max_keyforms) { if (count <= 0) return; @@ -96,9 +104,12 @@ psm__blend_positions(const struct psm__model *m, psm__i32 count, return; struct psm__sections *ms = m->source->sections; - psm__f32 *pos_xy = ms->key_pos_src.xy; + psm__f32 *pos_xy = ms->key_pos_src.xy; if (!pos_xy) return; + /* ki < max_keyforms and keyform_pos_off[ki] in [0, keyform_pos-2*vc], + * proved at load (F4 window + G2: psm__verify_bs_pos_window). */ + (void)max_keyforms; for (psm__i32 i = 0; i < count; i++) { psm__i32 ti = shapes[i].target_idx; @@ -110,8 +121,9 @@ psm__blend_positions(const struct psm__model *m, psm__i32 count, if (vc <= 0) continue; - psm__i32 pc = vc * 2; + psm__i32 pc = vc * 2; psm__f32 *out = out_positions[ti]; + struct psm__blend_binding *binds = shapes[i].bindings; if (!out || !binds) continue; @@ -126,23 +138,23 @@ psm__blend_positions(const struct psm__model *m, psm__i32 count, switch (blend_count) { case 1: { - psm__i32 ki = binds[j].keyform_idx[0] + off; - psm__i32 po = keyform_pos_off[ki]; + psm__i32 ki = binds[j].keyform_idx[0] + off; + psm__i32 po = keyform_pos_off[ki]; psm__f32 *p0 = &pos_xy[po]; - psm__f32 w0 = binds[j].weights[0]; + psm__f32 w0 = binds[j].weights[0]; for (psm__i32 k = 0; k < pc; k++) out[k] += p0[k] * w0 * cw; break; } case 2: { - psm__i32 ki0 = binds[j].keyform_idx[0] + off; - psm__i32 ki1 = binds[j].keyform_idx[1] + off; - psm__i32 po0 = keyform_pos_off[ki0]; - psm__i32 po1 = keyform_pos_off[ki1]; + psm__i32 ki0 = binds[j].keyform_idx[0] + off; + psm__i32 ki1 = binds[j].keyform_idx[1] + off; + psm__i32 po0 = keyform_pos_off[ki0]; + psm__i32 po1 = keyform_pos_off[ki1]; psm__f32 *p0 = &pos_xy[po0]; psm__f32 *p1 = &pos_xy[po1]; - psm__f32 w0 = binds[j].weights[0]; - psm__f32 w1 = binds[j].weights[1]; + psm__f32 w0 = binds[j].weights[0]; + psm__f32 w1 = binds[j].weights[1]; for (psm__i32 k = 0; k < pc; k++) out[k] += (w0 * p0[k] + p1[k] * w1) * cw; break; @@ -157,14 +169,18 @@ psm__blend_positions(const struct psm__model *m, psm__i32 count, static void psm__blend_colors(psm__i32 count, const struct psm__blend_shape *shapes, - const psm__i32 *keyform_color_off, + const psm__i32 *keyform_color_off, psm__i32 max_keyforms, const psm__f32 *src_r, const psm__f32 *src_g, const psm__f32 *src_b, - psm__f32 *out) + psm__i32 max_colors, psm__f32 *out) { if (count <= 0) return; if (!shapes || !keyform_color_off || !src_r || !src_g || !src_b || !out) return; + /* ki < max_keyforms and keyform_color_off[ki] < max_colors proved at load + * (F4 window + G3: psm__verify_bs_color_window); negative = "no color". */ + (void)max_keyforms; + (void)max_colors; for (psm__i32 i = 0; i < count; i++) { psm__i32 ti = shapes[i].target_idx; @@ -186,6 +202,8 @@ psm__blend_colors(psm__i32 count, const struct psm__blend_shape *shapes, case 1: { psm__i32 ki = binds[j].keyform_idx[0] + off; psm__i32 ci = keyform_color_off[ki]; + if (ci < 0) /* keyform has no color override */ + continue; psm__f32 w0 = binds[j].weights[0]; r = src_r[ci] * w0; g = src_g[ci] * w0; @@ -197,6 +215,8 @@ psm__blend_colors(psm__i32 count, const struct psm__blend_shape *shapes, psm__i32 ki1 = binds[j].keyform_idx[1] + off; psm__i32 ci0 = keyform_color_off[ki0]; psm__i32 ci1 = keyform_color_off[ki1]; + if (ci0 < 0 || ci1 < 0) /* keyform has no color override */ + continue; psm__f32 w0 = binds[j].weights[0]; psm__f32 w1 = binds[j].weights[1]; r = w0 * src_r[ci0] + src_r[ci1] * w1; @@ -221,7 +241,6 @@ psm__blend_colors(psm__i32 count, const struct psm__blend_shape *shapes, } } - PSM__DEF void psm__blend_parts(struct psm__model *m) { @@ -237,13 +256,14 @@ psm__blend_parts(struct psm__model *m) return; struct psm__sections *ms = m->source->sections; - psm__i32 *calc_do = m->parts.draw_order; - psm__f32 *do_src = ms->part_key_src.draw_order; + psm__i32 *calc_do = m->parts.draw_order; + psm__f32 *do_src = ms->part_key_src.draw_order; if (!calc_do || !do_src) return; - blend_scalar_i32(count, shapes, calc_do, do_src); + blend_scalar_i32(count, shapes, calc_do, do_src, + ms->count_info->part_keyforms); } PSM__DEF void @@ -252,15 +272,18 @@ psm__blend_warps(struct psm__model *m) if (m->source->header->version < csmMocVersion_42) return; - struct psm__sections *ms = m->source->sections; - psm__i32 count = m->bs_warps.count; + struct psm__sections *ms = m->source->sections; struct psm__blend_shape *shapes = m->bs_warps.items; + psm__i32 count = m->bs_warps.count; + if (count <= 0 || !shapes) return; + psm__i32 kf = ms->count_info->warp_keyforms; + psm__blend_positions(m, count, shapes, ms->warp_key_src.key_pos_off, - m->deformers.warps.pos, ms->warp_src.vertex_count); + m->deformers.warps.pos, ms->warp_src.vertex_count, kf); if (m->source->header->version < csmMocVersion_50) return; @@ -271,15 +294,17 @@ psm__blend_warps(struct psm__model *m) if (!op_src || !calc_op) return; - blend_scalar_f32(count, shapes, calc_op, op_src, 0.0f, 1.0f); + blend_scalar_f32(count, shapes, calc_op, op_src, kf, 0.0f, 1.0f); - psm__blend_colors(count, shapes, ms->warp_key_src.key_mul_color_off, + psm__blend_colors(count, shapes, ms->warp_key_src.key_mul_color_off, kf, ms->keyform_mul_color_src.r, ms->keyform_mul_color_src.g, - ms->keyform_mul_color_src.b, m->deformers.warps.mul_color); + ms->keyform_mul_color_src.b, ms->count_info->keyform_mul_colors, + m->deformers.warps.mul_color); - psm__blend_colors(count, shapes, ms->warp_key_src.key_scr_color_off, + psm__blend_colors(count, shapes, ms->warp_key_src.key_scr_color_off, kf, ms->keyform_scr_color_src.r, ms->keyform_scr_color_src.g, - ms->keyform_scr_color_src.b, m->deformers.warps.scr_color); + ms->keyform_scr_color_src.b, ms->count_info->keyform_scr_colors, + m->deformers.warps.scr_color); } PSM__DEF void @@ -288,45 +313,50 @@ psm__blend_rotations(struct psm__model *m) if (m->source->header->version < csmMocVersion_50) return; - struct psm__sections *ms = m->source->sections; - psm__i32 count = m->bs_rotations.count; + struct psm__sections *ms = m->source->sections; struct psm__blend_shape *shapes = m->bs_rotations.items; + psm__i32 count = m->bs_rotations.count; + if (count <= 0 || !shapes) return; + psm__i32 kf = ms->count_info->rotation_keyforms; + psm__f32 *ox_src = ms->rotation_key_src.origin_x; psm__f32 *calc_ox = m->deformers.rotations.origin_x; if (ox_src && calc_ox) - blend_scalar_f32(count, shapes, calc_ox, ox_src, -INFINITY, INFINITY); + blend_scalar_f32(count, shapes, calc_ox, ox_src, kf, -INFINITY, INFINITY); psm__f32 *oy_src = ms->rotation_key_src.origin_y; psm__f32 *calc_oy = m->deformers.rotations.origin_y; if (oy_src && calc_oy) - blend_scalar_f32(count, shapes, calc_oy, oy_src, -INFINITY, INFINITY); + blend_scalar_f32(count, shapes, calc_oy, oy_src, kf, -INFINITY, INFINITY); psm__f32 *op_src = ms->rotation_key_src.opacity; psm__f32 *calc_op = m->deformers.rotations.opacity; if (op_src && calc_op) - blend_scalar_f32(count, shapes, calc_op, op_src, 0.0f, 1.0f); + blend_scalar_f32(count, shapes, calc_op, op_src, kf, 0.0f, 1.0f); - psm__blend_colors(count, shapes, ms->rotation_key_src.key_mul_color_off, + psm__blend_colors(count, shapes, ms->rotation_key_src.key_mul_color_off, kf, ms->keyform_mul_color_src.r, ms->keyform_mul_color_src.g, - ms->keyform_mul_color_src.b, m->deformers.rotations.mul_color); + ms->keyform_mul_color_src.b, ms->count_info->keyform_mul_colors, + m->deformers.rotations.mul_color); - psm__blend_colors(count, shapes, ms->rotation_key_src.key_scr_color_off, + psm__blend_colors(count, shapes, ms->rotation_key_src.key_scr_color_off, kf, ms->keyform_scr_color_src.r, ms->keyform_scr_color_src.g, - ms->keyform_scr_color_src.b, m->deformers.rotations.scr_color); + ms->keyform_scr_color_src.b, ms->count_info->keyform_scr_colors, + m->deformers.rotations.scr_color); psm__f32 *ang_src = ms->rotation_key_src.angle; psm__f32 *calc_ang = m->deformers.rotations.angle; if (ang_src && calc_ang) - blend_scalar_f32(count, shapes, calc_ang, ang_src, -3600.0f, 3600.0f); + blend_scalar_f32(count, shapes, calc_ang, ang_src, kf, -3600.0f, 3600.0f); psm__f32 *sc_src = ms->rotation_key_src.scale; psm__f32 *calc_sc = m->deformers.rotations.scale; if (sc_src && calc_sc) - blend_scalar_f32(count, shapes, calc_sc, sc_src, 0.0001f, 100.0f); + blend_scalar_f32(count, shapes, calc_sc, sc_src, kf, 0.0001f, 100.0f); } PSM__DEF void @@ -335,14 +365,17 @@ psm__blend_art_meshes(struct psm__model *m) if (m->source->header->version < csmMocVersion_42) return; - struct psm__sections *ms = m->source->sections; - psm__i32 count = m->bs_art_meshes.count; + struct psm__sections *ms = m->source->sections; struct psm__blend_shape *shapes = m->bs_art_meshes.items; + + psm__i32 count = m->bs_art_meshes.count; if (count <= 0 || !shapes) return; + psm__i32 kf = ms->count_info->art_mesh_keyforms; + psm__blend_positions(m, count, shapes, ms->art_mesh_key_src.key_pos_off, - m->art_meshes.pos, ms->art_mesh_src.vertex_count); + m->art_meshes.pos, ms->art_mesh_src.vertex_count, kf); if (m->source->header->version < csmMocVersion_50) return; @@ -350,27 +383,29 @@ psm__blend_art_meshes(struct psm__model *m) psm__f32 *do_src = ms->art_mesh_key_src.draw_order; psm__i32 *calc_do = m->art_meshes.draw_order; if (do_src && calc_do) - blend_scalar_i32(count, shapes, calc_do, do_src); + blend_scalar_i32(count, shapes, calc_do, do_src, kf); psm__f32 *op_src = ms->art_mesh_key_src.opacity; psm__f32 *calc_op = m->art_meshes.opacity; if (op_src && calc_op) - blend_scalar_f32(count, shapes, calc_op, op_src, 0.0f, 1.0f); + blend_scalar_f32(count, shapes, calc_op, op_src, kf, 0.0f, 1.0f); if (ms->art_mesh_key_src.key_mul_color_off && ms->keyform_mul_color_src.r && ms->keyform_mul_color_src.g && ms->keyform_mul_color_src.b && m->art_meshes.mul_color) { - psm__blend_colors(count, shapes, ms->art_mesh_key_src.key_mul_color_off, + psm__blend_colors(count, shapes, ms->art_mesh_key_src.key_mul_color_off, kf, ms->keyform_mul_color_src.r, ms->keyform_mul_color_src.g, - ms->keyform_mul_color_src.b, m->art_meshes.mul_color); + ms->keyform_mul_color_src.b, ms->count_info->keyform_mul_colors, + m->art_meshes.mul_color); } if (ms->art_mesh_key_src.key_scr_color_off && ms->keyform_scr_color_src.r && ms->keyform_scr_color_src.g && ms->keyform_scr_color_src.b && m->art_meshes.scr_color) { - psm__blend_colors(count, shapes, ms->art_mesh_key_src.key_scr_color_off, + psm__blend_colors(count, shapes, ms->art_mesh_key_src.key_scr_color_off, kf, ms->keyform_scr_color_src.r, ms->keyform_scr_color_src.g, - ms->keyform_scr_color_src.b, m->art_meshes.scr_color); + ms->keyform_scr_color_src.b, ms->count_info->keyform_scr_colors, + m->art_meshes.scr_color); } } @@ -389,13 +424,14 @@ psm__blend_glues(struct psm__model *m) return; struct psm__sections *ms = m->source->sections; - psm__f32 *calc_int = m->glues.intensity; - psm__f32 *int_src = ms->glue_key_src.intensity; + psm__f32 *calc_int = m->glues.intensity; + psm__f32 *int_src = ms->glue_key_src.intensity; if (!calc_int || !int_src) return; - blend_scalar_f32(count, shapes, calc_int, int_src, 0.0f, 1.0f); + blend_scalar_f32(count, shapes, calc_int, int_src, + ms->count_info->glue_keyforms, 0.0f, 1.0f); } PSM__DEF void @@ -404,23 +440,28 @@ psm__blend_offscreens(struct psm__model *m) if (m->source->header->version < csmMocVersion_53) return; - struct psm__sections *ms = m->source->sections; - psm__i32 count = m->bs_offscreens.count; + struct psm__sections *ms = m->source->sections; struct psm__blend_shape *shapes = m->bs_offscreens.items; + psm__i32 count = m->bs_offscreens.count; + if (count <= 0 || !shapes) return; + psm__i32 kf = ms->count_info->offscreen_keyforms; + psm__f32 *op_src = ms->offscreen_key_src.opacity; psm__f32 *calc_op = m->offscreens.opacity; if (op_src && calc_op) - blend_scalar_f32(count, shapes, calc_op, op_src, 0.0f, 1.0f); + blend_scalar_f32(count, shapes, calc_op, op_src, kf, 0.0f, 1.0f); - psm__blend_colors(count, shapes, ms->offscreen_key_src.key_mul_color_off, + psm__blend_colors(count, shapes, ms->offscreen_key_src.key_mul_color_off, kf, ms->keyform_mul_color_src.r, ms->keyform_mul_color_src.g, - ms->keyform_mul_color_src.b, m->offscreens.mul_color); + ms->keyform_mul_color_src.b, ms->count_info->keyform_mul_colors, + m->offscreens.mul_color); - psm__blend_colors(count, shapes, ms->offscreen_key_src.key_scr_color_off, + psm__blend_colors(count, shapes, ms->offscreen_key_src.key_scr_color_off, kf, ms->keyform_scr_color_src.r, ms->keyform_scr_color_src.g, - ms->keyform_scr_color_src.b, m->offscreens.scr_color); + ms->keyform_scr_color_src.b, ms->count_info->keyform_scr_colors, + m->offscreens.scr_color); } diff --git a/src/bundle.c.in b/src/bundle.c.in new file mode 100644 index 0000000..ffe6133 --- /dev/null +++ b/src/bundle.c.in @@ -0,0 +1,64 @@ +/* + * PurismCoreBundle.h - single-file (amalgamated) Purism Core. + * + * Usage: + * #include "PurismCoreBundle.h" + * In exactly ONE translation unit, define the implementation first: + * #define PURISM_CORE_IMPLEMENTATION + * #include "PurismCoreBundle.h" + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#ifndef PURISM_CORE_BUNDLE_H +#define PURISM_CORE_BUNDLE_H + +/* Public API */ +#include "../include/PurismCore.h" + +#ifdef PURISM_CORE_IMPLEMENTATION + +/* Internal headers */ +#include "private.h" +#include "error.h" +#include "debug.h" +#include "arena.h" +#include "array.h" +#include "math2.h" +#include "moc3.h" +#include "model.h" +#include "verify.h" +#include "gather.h" +#include "interpolate.h" +#include "artmesh.h" +#include "blendshape.h" +#include "deformer.h" +#include "glue.h" +#include "offscreen.h" +#include "param.h" +#include "part.h" +#include "render.h" +#include "update.h" + +/* Implementation */ +#include "core.c" +#include "debug.c" +#include "arena.c" +#include "math2.c" +#include "moc3.c" +#include "verify.c" +#include "model.c" +#include "update.c" +#include "param.c" +#include "part.c" +#include "deformer.c" +#include "artmesh.c" +#include "glue.c" +#include "offscreen.c" +#include "blendshape.c" +#include "interpolate.c" +#include "render.c" + +#endif /* PURISM_CORE_IMPLEMENTATION */ +#endif /* PURISM_CORE_BUNDLE_H */ diff --git a/src/core.c b/src/core.c index 66d678c..d09d598 100644 --- a/src/core.c +++ b/src/core.c @@ -5,8 +5,14 @@ * SPDX-License-Identifier: MIT */ +#include + #include "private.h" +#ifndef PSM_GIT_HASH +# define PSM_GIT_HASH "unknown" +#endif + PSMDEF csmVersion csmGetVersion(void) { @@ -19,6 +25,16 @@ csmGetTrueVersion(void) return PSM_TRUE_VERSION; } +PSMDEF const char * +csmGetExtendedVersionString(void) +{ + static char buf[96]; + if (buf[0] == '\0') + snprintf(buf, sizeof buf, PSM__VERFMT " (%s)", + PSM__VERARG(PSM_TRUE_VERSION), PSM_GIT_HASH); + return buf; +} + PSMDEF csmMocVersion csmGetLatestMocVersion(void) { diff --git a/src/core_js.c b/src/core_js.c new file mode 100644 index 0000000..9a1530d --- /dev/null +++ b/src/core_js.c @@ -0,0 +1,89 @@ +/* + * Purism Core: Emscripten/WASM platform glue + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#ifdef __EMSCRIPTEN__ + +# include +# include +# include + +# include "PurismCore.h" + +static void * +psm_js_aligned_alloc(unsigned int alignment, unsigned int size) +{ + if (alignment < sizeof(void *)) + alignment = sizeof(void *); + + size_t total = (size_t)size + (size_t)alignment + sizeof(void *); + void *raw = malloc(total); + if (!raw) + return NULL; + + uintptr_t base = (uintptr_t)raw + sizeof(void *); + uintptr_t aligned = (base + (alignment - 1)) & ~(uintptr_t)(alignment - 1); + + ((void **)aligned)[-1] = raw; + + return (void *)aligned; +} + +static void +psm_js_aligned_free(void *ptr) +{ + if (!ptr) + return; + free(((void **)ptr)[-1]); +} + +EMSCRIPTEN_KEEPALIVE +void * +csmMallocMoc(unsigned int mocSize) +{ + return psm_js_aligned_alloc(csmAlignofMoc, mocSize); +} + +EMSCRIPTEN_KEEPALIVE +void * +csmMallocModelAndInitialize(csmMoc *moc) +{ + unsigned int size = csmGetSizeofModel(moc); + if (!size) + return NULL; + void *memory = psm_js_aligned_alloc(csmAlignofModel, size); + if (!memory) + return NULL; + csmModel *model = csmInitializeModelInPlace(moc, memory, size); + if (!model) { + psm_js_aligned_free(memory); + return NULL; + } + return model; +} + +EMSCRIPTEN_KEEPALIVE +void * +csmMalloc(unsigned int size) +{ + return psm_js_aligned_alloc(csmAlignofModel, size); +} + +EMSCRIPTEN_KEEPALIVE +void +csmFree(void *memory) +{ + psm_js_aligned_free(memory); +} + +EMSCRIPTEN_KEEPALIVE +void +csmInitializeAmountOfMemory(unsigned int size) +{ + (void)size; +} + +#endif /* __EMSCRIPTEN__ */ diff --git a/src/core_js.js b/src/core_js.js new file mode 100644 index 0000000..1cd48d9 --- /dev/null +++ b/src/core_js.js @@ -0,0 +1,694 @@ +/* + * Purism Core: Emscripten/WASM JS API wrapper + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ +var PurismCore; +(function (PurismCore) { + "use strict"; + + let _em = null; + + let _v6 = false; + const _has = (name) => + _em && (typeof _em["_" + name] === "function" || + (_em.asm && typeof _em.asm[name] === "function")); + + const _csm = { + getVersion() { + return _em.ccall("csmGetVersion", "number", [], []); + }, + getLatestMocVersion() { + return _em.ccall("csmGetLatestMocVersion", "number", [], []); + }, + getMocVersion(moc, mocSize) { + return _em.ccall("csmGetMocVersion", "number", + ["number", "number"], [moc, mocSize]); + }, + getTrueVersion() { + return _em.ccall("csmGetTrueVersion", "number", [], []); + }, + getExtendedVersionString() { + return _em.ccall("csmGetExtendedVersionString", "string", [], []); + }, + getLogFunction() { + return _em.ccall("csmGetLogFunction", "number", [], []); + }, + setLogFunction(handler) { + _em.ccall("csmSetLogFunction", null, ["number"], [handler]); + }, + getSizeofModel(moc) { + return _em.ccall("csmGetSizeofModel", "number", ["number"], [moc]); + }, + reviveMocInPlace(memory, mocSize) { + return _em.ccall("csmReviveMocInPlace", "number", + ["number", "number"], [memory, mocSize]); + }, + initializeModelInPlace(moc, memory, modelSize) { + return _em.ccall("csmInitializeModelInPlace", "number", + ["number", "number", "number"], [moc, memory, modelSize]); + }, + hasMocConsistency(memory, mocSize) { + return _em.ccall("csmHasMocConsistency", "number", + ["number", "number"], [memory, mocSize]); + }, + updateModel(model) { + _em.ccall("csmUpdateModel", null, ["number"], [model]); + }, + readCanvasInfo(model, s, o, p) { + _em.ccall("csmReadCanvasInfo", null, + ["number", "number", "number", "number"], [model, s, o, p]); + }, + getMocError(moc) { + return _em.ccall("csmGetMocError", "number", ["number"], [moc]); + }, + getLastError(model) { + return _em.ccall("csmGetLastError", "number", ["number"], [model]); + }, + getErrorString(error) { + return _em.ccall("csmGetErrorString", "string", ["number"], [error]); + }, + + getRenderOrders(model) { + const sym = _v6 ? "csmGetRenderOrders" : "csmGetDrawableRenderOrders"; + return _em.ccall(sym, "number", ["number"], [model]); + }, + + /* Parameters. */ + getParameterCount(model) { + return _em.ccall("csmGetParameterCount", "number", ["number"], [model]); + }, + getParameterIds(model) { + return _em.ccall("csmGetParameterIds", "number", ["number"], [model]); + }, + getParameterTypes(model) { + return _em.ccall("csmGetParameterTypes", "number", ["number"], [model]); + }, + getParameterMinimumValues(model) { + return _em.ccall("csmGetParameterMinimumValues", "number", ["number"], [model]); + }, + getParameterMaximumValues(model) { + return _em.ccall("csmGetParameterMaximumValues", "number", ["number"], [model]); + }, + getParameterDefaultValues(model) { + return _em.ccall("csmGetParameterDefaultValues", "number", ["number"], [model]); + }, + getParameterValues(model) { + return _em.ccall("csmGetParameterValues", "number", ["number"], [model]); + }, + getParameterRepeats(model) { + /* v6-only. */ + return _em.ccall("csmGetParameterRepeats", "number", ["number"], [model]); + }, + getParameterKeyCounts(model) { + return _em.ccall("csmGetParameterKeyCounts", "number", ["number"], [model]); + }, + getParameterKeyValues(model) { + return _em.ccall("csmGetParameterKeyValues", "number", ["number"], [model]); + }, + + /* Parts. */ + getPartCount(model) { + return _em.ccall("csmGetPartCount", "number", ["number"], [model]); + }, + getPartIds(model) { + return _em.ccall("csmGetPartIds", "number", ["number"], [model]); + }, + getPartOpacities(model) { + return _em.ccall("csmGetPartOpacities", "number", ["number"], [model]); + }, + getPartParentPartIndices(model) { + return _em.ccall("csmGetPartParentPartIndices", "number", ["number"], [model]); + }, + getPartOffscreenIndices(model) { + /* v6-only. */ + return _em.ccall("csmGetPartOffscreenIndices", "number", ["number"], [model]); + }, + + /* Drawables. */ + getDrawableCount(model) { + return _em.ccall("csmGetDrawableCount", "number", ["number"], [model]); + }, + getDrawableIds(model) { + return _em.ccall("csmGetDrawableIds", "number", ["number"], [model]); + }, + getDrawableConstantFlags(model) { + return _em.ccall("csmGetDrawableConstantFlags", "number", ["number"], [model]); + }, + getDrawableDynamicFlags(model) { + return _em.ccall("csmGetDrawableDynamicFlags", "number", ["number"], [model]); + }, + getDrawableTextureIndices(model) { + return _em.ccall("csmGetDrawableTextureIndices", "number", ["number"], [model]); + }, + getDrawableDrawOrders(model) { + return _em.ccall("csmGetDrawableDrawOrders", "number", ["number"], [model]); + }, + getDrawableOpacities(model) { + return _em.ccall("csmGetDrawableOpacities", "number", ["number"], [model]); + }, + getDrawableMaskCounts(model) { + return _em.ccall("csmGetDrawableMaskCounts", "number", ["number"], [model]); + }, + getDrawableMasks(model) { + return _em.ccall("csmGetDrawableMasks", "number", ["number"], [model]); + }, + getDrawableVertexCounts(model) { + return _em.ccall("csmGetDrawableVertexCounts", "number", ["number"], [model]); + }, + getDrawableVertexPositions(model) { + return _em.ccall("csmGetDrawableVertexPositions", "number", ["number"], [model]); + }, + getDrawableVertexUvs(model) { + return _em.ccall("csmGetDrawableVertexUvs", "number", ["number"], [model]); + }, + getDrawableIndexCounts(model) { + return _em.ccall("csmGetDrawableIndexCounts", "number", ["number"], [model]); + }, + getDrawableIndices(model) { + return _em.ccall("csmGetDrawableIndices", "number", ["number"], [model]); + }, + getDrawableMultiplyColors(model) { + return _em.ccall("csmGetDrawableMultiplyColors", "number", ["number"], [model]); + }, + getDrawableScreenColors(model) { + return _em.ccall("csmGetDrawableScreenColors", "number", ["number"], [model]); + }, + getDrawableParentPartIndices(model) { + return _em.ccall("csmGetDrawableParentPartIndices", "number", ["number"], [model]); + }, + getDrawableBlendModes(model) { + /* v6-only. */ + return _em.ccall("csmGetDrawableBlendModes", "number", ["number"], [model]); + }, + resetDrawableDynamicFlags(model) { + _em.ccall("csmResetDrawableDynamicFlags", null, ["number"], [model]); + }, + + /* Offscreens (v6-only): the symbols exist only on v6 builds, and these + shims are only called when _v6, so they invoke the symbol directly. */ + getOffscreenCount(model) { + return _em.ccall("csmGetOffscreenCount", "number", ["number"], [model]); + }, + getOffscreenBlendModes(model) { + return _em.ccall("csmGetOffscreenBlendModes", "number", ["number"], [model]); + }, + getOffscreenOpacities(model) { + return _em.ccall("csmGetOffscreenOpacities", "number", ["number"], [model]); + }, + getOffscreenOwnerIndices(model) { + return _em.ccall("csmGetOffscreenOwnerIndices", "number", ["number"], [model]); + }, + getOffscreenMultiplyColors(model) { + return _em.ccall("csmGetOffscreenMultiplyColors", "number", ["number"], [model]); + }, + getOffscreenScreenColors(model) { + return _em.ccall("csmGetOffscreenScreenColors", "number", ["number"], [model]); + }, + getOffscreenMaskCounts(model) { + return _em.ccall("csmGetOffscreenMaskCounts", "number", ["number"], [model]); + }, + getOffscreenMasks(model) { + return _em.ccall("csmGetOffscreenMasks", "number", ["number"], [model]); + }, + getOffscreenConstantFlags(model) { + return _em.ccall("csmGetOffscreenConstantFlags", "number", ["number"], [model]); + }, + + mallocMoc(mocSize) { + return _em.ccall("csmMallocMoc", "number", ["number"], [mocSize]); + }, + mallocModelAndInitialize(moc) { + return _em.ccall("csmMallocModelAndInitialize", "number", + ["number"], [moc]); + }, + malloc(size) { + return _em.ccall("csmMalloc", "number", ["number"], [size]); + }, + free(memory) { + _em.ccall("csmFree", null, ["number"], [memory]); + }, + initializeAmountOfMemory(size) { + _em.ccall("csmInitializeAmountOfMemory", null, ["number"], [size]); + }, + }; + + /* Alignment constants. */ + PurismCore.AlignofMoc = 64; + PurismCore.AlignofModel = 16; + + /* .moc3 file versions. */ + PurismCore.MocVersion_Unknown = 0; + PurismCore.MocVersion_30 = 1; + PurismCore.MocVersion_33 = 2; + PurismCore.MocVersion_40 = 3; + PurismCore.MocVersion_42 = 4; + PurismCore.MocVersion_50 = 5; + PurismCore.MocVersion_53 = 6; + + /* Parameter types. */ + PurismCore.ParameterType_Normal = 0; + PurismCore.ParameterType_BlendShape = 1; + + /* Error codes (Purism extension; from Moc#getError / Model#getLastError). */ + PurismCore.Error_NoError = 0; + PurismCore.Error_Failed = 1; + PurismCore.Error_ParameterRange = 2; + PurismCore.Error_FileUnrecognized = 3; + PurismCore.Error_FileCorrupt = 4; + PurismCore.Error_InvalidData = 5; + PurismCore.Error_InvalidParameter = 6; + + /* Maps a csmError code to a static string (Purism extension). */ + PurismCore.csmGetErrorString = function (error) { + return _csm.getErrorString(error); + }; + + /* Color blend types. */ + PurismCore.ColorBlendType_Normal = 0; + PurismCore.ColorBlendType_Add = 3; + PurismCore.ColorBlendType_AddGlow = 4; + PurismCore.ColorBlendType_Darken = 5; + PurismCore.ColorBlendType_Multiply = 6; + PurismCore.ColorBlendType_ColorBurn = 7; + PurismCore.ColorBlendType_LinearBurn = 8; + PurismCore.ColorBlendType_Lighten = 9; + PurismCore.ColorBlendType_Screen = 10; + PurismCore.ColorBlendType_ColorDodge = 11; + PurismCore.ColorBlendType_Overlay = 12; + PurismCore.ColorBlendType_SoftLight = 13; + PurismCore.ColorBlendType_HardLight = 14; + PurismCore.ColorBlendType_LinearLight = 15; + PurismCore.ColorBlendType_Hue = 16; + PurismCore.ColorBlendType_Color = 17; + PurismCore.ColorBlendType_AddCompatible = 1; + PurismCore.ColorBlendType_MultiplyCompatible = 2; + + /* Alpha blend types. */ + PurismCore.AlphaBlendType_Over = 0; + PurismCore.AlphaBlendType_Atop = 1; + PurismCore.AlphaBlendType_Out = 2; + PurismCore.AlphaBlendType_ConjointOver = 3; + PurismCore.AlphaBlendType_DisjointOver = 4; + + /* Version. */ + class Version { + static csmGetVersion() { + return _csm.getVersion(); + } + static csmGetTrueVersion() { + return _csm.getTrueVersion(); + } + static csmGetExtendedVersionString() { + return _csm.getExtendedVersionString(); + } + static csmGetLatestMocVersion() { + return _csm.getLatestMocVersion(); + } + static csmGetMocVersion(data, mocBytes) { + if (data instanceof Moc) { + return _csm.getMocVersion(data._ptr, mocBytes.byteLength); + } + const memory = _csm.mallocMoc(data.byteLength); + if (!memory) { + return 0; + } + const dst = new Uint8Array(_em.HEAPU8.buffer, memory, data.byteLength); + dst.set(new Uint8Array(data)); + const v = _csm.getMocVersion(memory, data.byteLength); + _csm.free(memory); + return v; + } + } + PurismCore.Version = Version; + + /* Logging. */ + class Logging { + static csmSetLogFunction(handler) { + Logging.logFunction = handler; + const pointer = _em.addFunction(Logging.wrapLogFunction, "vi"); + _csm.setLogFunction(pointer); + } + static csmGetLogFunction() { + return Logging.logFunction; + } + static wrapLogFunction(messagePtr) { + const messageStr = _em.UTF8ToString(messagePtr); + Logging.logFunction(messageStr); + } + } + PurismCore.Logging = Logging; + + /* Moc. */ + class Moc { + constructor(mocBytes) { + const memory = _csm.mallocMoc(mocBytes.byteLength); + if (!memory) { + return; + } + const dst = new Uint8Array(_em.HEAPU8.buffer, memory, mocBytes.byteLength); + dst.set(new Uint8Array(mocBytes)); + this._ptr = _csm.reviveMocInPlace(memory, mocBytes.byteLength); + if (!this._ptr) { + _csm.free(memory); + } + } + hasMocConsistency(mocBytes) { + const memory = _csm.mallocMoc(mocBytes.byteLength); + if (!memory) { + return; + } + const dst = new Uint8Array(_em.HEAPU8.buffer, memory, mocBytes.byteLength); + dst.set(new Uint8Array(mocBytes)); + const ok = _csm.hasMocConsistency(memory, mocBytes.byteLength); + _csm.free(memory); + return ok; + } + static fromArrayBuffer(buffer) { + if (!buffer) { + return null; + } + const moc = new Moc(buffer); + return (moc._ptr) ? moc : null; + } + /* Purism extension: the outcome of this moc's revive/init (a csmError code; + * map with PurismCore.csmGetErrorString). */ + getError() { + return _csm.getMocError(this._ptr); + } + _release() { + _csm.free(this._ptr); + this._ptr = 0; + } + } + PurismCore.Moc = Moc; + + /* Model. */ + class Model { + constructor(moc) { + this._ptr = _csm.mallocModelAndInitialize(moc._ptr); + if (!this._ptr) { + return; + } + /* Construct all heap views AFTER the final allocation: with + ALLOW_MEMORY_GROWTH the heap ArrayBuffer can be detached and + replaced on any _malloc, invalidating earlier views. No further + WASM allocation happens after this point in the constructor. */ + this.parameters = new Parameters(this._ptr); + this.parts = new Parts(this._ptr); + this.drawables = new Drawables(this._ptr); + if (_v6) { + this.offscreens = new Offscreens(this._ptr); + } + this.canvasinfo = new CanvasInfo(this._ptr); + const length = _csm.getDrawableCount(this._ptr) + + (_v6 ? _csm.getOffscreenCount(this._ptr) : 0); + this.renderOrders = new Int32Array(_em.HEAP32.buffer, + _csm.getRenderOrders(this._ptr), length); + } + static fromMoc(moc) { + const model = new Model(moc); + return (model._ptr) ? model : null; + } + getRenderOrders() { + return this.renderOrders; + } + update() { + _csm.updateModel(this._ptr); + } + /* Purism extension: the error recorded by this model's most recent update + * (a csmError code; map with PurismCore.csmGetErrorString). */ + getLastError() { + return _csm.getLastError(this._ptr); + } + release() { + _csm.free(this._ptr); + this._ptr = 0; + } + } + PurismCore.Model = Model; + + /* CanvasInfo. */ + class CanvasInfo { + constructor(modelPtr) { + if (!modelPtr) { + return; + } + const sizePtr = _csm.malloc(2 * 4); + const originPtr = _csm.malloc(2 * 4); + const ppuPtr = _csm.malloc(1 * 4); + _csm.readCanvasInfo(modelPtr, sizePtr, originPtr, ppuPtr); + /* Re-read the buffer: an intervening malloc may have grown and + detached it. */ + const f32 = new Float32Array(_em.HEAPF32.buffer); + this.CanvasWidth = f32[sizePtr >> 2]; + this.CanvasHeight = f32[(sizePtr >> 2) + 1]; + this.CanvasOriginX = f32[originPtr >> 2]; + this.CanvasOriginY = f32[(originPtr >> 2) + 1]; + this.PixelsPerUnit = f32[ppuPtr >> 2]; + _csm.free(sizePtr); + _csm.free(originPtr); + _csm.free(ppuPtr); + } + } + PurismCore.CanvasInfo = CanvasInfo; + + /* Parameters. */ + class Parameters { + constructor(modelPtr) { + const length = _csm.getParameterCount(modelPtr); + this.count = length; + this.ids = new Array(length); + const idsPtr = _csm.getParameterIds(modelPtr); + const _ids = new Uint32Array(_em.HEAPU32.buffer, idsPtr, length); + for (let i = 0; i < _ids.length; i++) { + this.ids[i] = _em.UTF8ToString(_ids[i]); + } + this.types = new Int32Array(_em.HEAP32.buffer, + _csm.getParameterTypes(modelPtr), length); + this.minimumValues = new Float32Array(_em.HEAPF32.buffer, + _csm.getParameterMinimumValues(modelPtr), length); + this.maximumValues = new Float32Array(_em.HEAPF32.buffer, + _csm.getParameterMaximumValues(modelPtr), length); + this.defaultValues = new Float32Array(_em.HEAPF32.buffer, + _csm.getParameterDefaultValues(modelPtr), length); + this.values = new Float32Array(_em.HEAPF32.buffer, + _csm.getParameterValues(modelPtr), length); + if (_v6) { /* repeats are a v6-only field */ + this.repeats = new Int32Array(_em.HEAP32.buffer, + _csm.getParameterRepeats(modelPtr), length); + } + this.keyCounts = new Int32Array(_em.HEAP32.buffer, + _csm.getParameterKeyCounts(modelPtr), length); + const counts = new Int32Array(_em.HEAP32.buffer, + _csm.getParameterKeyCounts(modelPtr), length); + this.keyValues = new Array(length); + const _kv = new Uint32Array(_em.HEAPU32.buffer, + _csm.getParameterKeyValues(modelPtr), length); + for (let j = 0; j < _kv.length; j++) { + this.keyValues[j] = new Float32Array(_em.HEAPF32.buffer, + _kv[j], counts[j]); + } + } + } + PurismCore.Parameters = Parameters; + + /* Parts. */ + class Parts { + constructor(modelPtr) { + const length = _csm.getPartCount(modelPtr); + this.count = length; + this.ids = new Array(length); + const _ids = new Uint32Array(_em.HEAPU32.buffer, + _csm.getPartIds(modelPtr), length); + for (let i = 0; i < _ids.length; i++) { + this.ids[i] = _em.UTF8ToString(_ids[i]); + } + this.opacities = new Float32Array(_em.HEAPF32.buffer, + _csm.getPartOpacities(modelPtr), length); + this.parentIndices = new Int32Array(_em.HEAP32.buffer, + _csm.getPartParentPartIndices(modelPtr), length); + if (_v6) { /* offscreenIndices are a v6-only field */ + this.offscreenIndices = new Int32Array(_em.HEAP32.buffer, + _csm.getPartOffscreenIndices(modelPtr), length); + } + } + } + PurismCore.Parts = Parts; + + /* Drawables. */ + class Drawables { + constructor(modelPtr) { + this._modelPtr = modelPtr; + const length = _csm.getDrawableCount(modelPtr); + this.count = length; + this.ids = new Array(length); + const _ids = new Uint32Array(_em.HEAPU32.buffer, + _csm.getDrawableIds(modelPtr), length); + for (let i = 0; i < _ids.length; i++) { + this.ids[i] = _em.UTF8ToString(_ids[i]); + } + this.constantFlags = new Uint8Array(_em.HEAPU8.buffer, + _csm.getDrawableConstantFlags(modelPtr), length); + this.dynamicFlags = new Uint8Array(_em.HEAPU8.buffer, + _csm.getDrawableDynamicFlags(modelPtr), length); + this.textureIndices = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableTextureIndices(modelPtr), length); + this.drawOrders = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableDrawOrders(modelPtr), length); + this.opacities = new Float32Array(_em.HEAPF32.buffer, + _csm.getDrawableOpacities(modelPtr), length); + this.maskCounts = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableMaskCounts(modelPtr), length); + this.vertexCounts = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableVertexCounts(modelPtr), length); + this.indexCounts = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableIndexCounts(modelPtr), length); + this.multiplyColors = new Float32Array(_em.HEAPF32.buffer, + _csm.getDrawableMultiplyColors(modelPtr), length * 4); + this.screenColors = new Float32Array(_em.HEAPF32.buffer, + _csm.getDrawableScreenColors(modelPtr), length * 4); + this.parentPartIndices = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableParentPartIndices(modelPtr), length); + /* blendModes is a v6-only field: one packed int per drawable, color + blend in the low byte and alpha blend in the next (color | alpha<<8) -- + the layout the C ABI returns and that consumers read as + blendModes[i] & 0xff / (blendModes[i] >> 8) & 0xff. A direct heap view. */ + if (_v6) { + this.blendModes = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableBlendModes(modelPtr), length); + } + + const maskCounts = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableMaskCounts(modelPtr), length); + this.masks = new Array(length); + const _masks = new Uint32Array(_em.HEAPU32.buffer, + _csm.getDrawableMasks(modelPtr), length); + for (let m = 0; m < _masks.length; m++) { + this.masks[m] = new Int32Array(_em.HEAP32.buffer, + _masks[m], maskCounts[m]); + } + + const vCounts = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableVertexCounts(modelPtr), length); + this.vertexPositions = new Array(length); + const _pos = new Uint32Array(_em.HEAPU32.buffer, + _csm.getDrawableVertexPositions(modelPtr), length); + for (let p = 0; p < _pos.length; p++) { + this.vertexPositions[p] = new Float32Array(_em.HEAPF32.buffer, + _pos[p], vCounts[p] * 2); + } + this.vertexUvs = new Array(length); + const _uvs = new Uint32Array(_em.HEAPU32.buffer, + _csm.getDrawableVertexUvs(modelPtr), length); + for (let u = 0; u < _uvs.length; u++) { + this.vertexUvs[u] = new Float32Array(_em.HEAPF32.buffer, + _uvs[u], vCounts[u] * 2); + } + + const iCounts = new Int32Array(_em.HEAP32.buffer, + _csm.getDrawableIndexCounts(modelPtr), length); + this.indices = new Array(length); + const _idx = new Uint32Array(_em.HEAPU32.buffer, + _csm.getDrawableIndices(modelPtr), length); + for (let x = 0; x < _idx.length; x++) { + this.indices[x] = new Uint16Array(_em.HEAPU16.buffer, + _idx[x], iCounts[x]); + } + } + resetDynamicFlags() { + _csm.resetDrawableDynamicFlags(this._modelPtr); + } + } + PurismCore.Drawables = Drawables; + + /* Offscreens (v6-only; constructed only on v6). A v6 model with no + * offscreens still reaches the empty branch. */ + class Offscreens { + constructor(modelPtr) { + const length = _csm.getOffscreenCount(modelPtr); + this.count = length; + if (!length) { + this.blendModes = new Int32Array(0); + this.opacities = new Float32Array(0); + this.ownerIndices = new Int32Array(0); + this.multiplyColors = new Float32Array(0); + this.screenColors = new Float32Array(0); + this.maskCounts = new Int32Array(0); + this.constantFlags = new Uint8Array(0); + this.masks = []; + return; + } + /* One packed int per offscreen (color | alpha<<8), like drawables. */ + this.blendModes = new Int32Array(_em.HEAP32.buffer, + _csm.getOffscreenBlendModes(modelPtr), length); + this.opacities = new Float32Array(_em.HEAPF32.buffer, + _csm.getOffscreenOpacities(modelPtr), length); + this.ownerIndices = new Int32Array(_em.HEAP32.buffer, + _csm.getOffscreenOwnerIndices(modelPtr), length); + this.multiplyColors = new Float32Array(_em.HEAPF32.buffer, + _csm.getOffscreenMultiplyColors(modelPtr), length * 4); + this.screenColors = new Float32Array(_em.HEAPF32.buffer, + _csm.getOffscreenScreenColors(modelPtr), length * 4); + this.maskCounts = new Int32Array(_em.HEAP32.buffer, + _csm.getOffscreenMaskCounts(modelPtr), length); + this.constantFlags = new Uint8Array(_em.HEAPU8.buffer, + _csm.getOffscreenConstantFlags(modelPtr), length); + const counts = new Int32Array(_em.HEAP32.buffer, + _csm.getOffscreenMaskCounts(modelPtr), length); + this.masks = new Array(length); + const _masks = new Uint32Array(_em.HEAPU32.buffer, + _csm.getOffscreenMasks(modelPtr), length); + for (let i = 0; i < _masks.length; i++) { + this.masks[i] = new Int32Array(_em.HEAP32.buffer, + _masks[i], counts[i]); + } + } + } + + /* Utility flag helpers. */ + class Utils { + static hasBlendAdditiveBit(b) { return (b & 1) == 1; } + static hasBlendMultiplicativeBit(b) { return (b & 2) == 2; } + static hasIsDoubleSidedBit(b) { return (b & 4) == 4; } + static hasIsInvertedMaskBit(b) { return (b & 8) == 8; } + static hasIsVisibleBit(b) { return (b & 1) == 1; } + static hasVisibilityDidChangeBit(b) { return (b & 2) == 2; } + static hasOpacityDidChangeBit(b) { return (b & 4) == 4; } + static hasDrawOrderDidChangeBit(b) { return (b & 8) == 8; } + static hasRenderOrderDidChangeBit(b) { return (b & 16) == 16; } + static hasVertexPositionsDidChangeBit(b) { return (b & 32) == 32; } + static hasBlendColorDidChangeBit(b) { return (b & 64) == 64; } + } + PurismCore.Utils = Utils; + + /* Memory. */ + class Memory { + static initializeAmountOfMemory(size) { + if (size > 16777216) { + _csm.initializeAmountOfMemory(size); + } + } + } + PurismCore.Memory = Memory; + + /* Expose the module setter for the bootstrap tail. Detect the build's ABI + once here (v5 lacks the v6-only symbols) and expose the v6-only Offscreens + class only when present. */ + PurismCore._setModule = function (m) { + _em = m; + _v6 = _has("csmGetRenderOrders"); + if (_v6) { + PurismCore.Offscreens = Offscreens; + } else { + Model.prototype.getDrawableRenderOrders = Model.prototype.getRenderOrders; + delete Model.prototype.getRenderOrders; + for (const k of Object.keys(PurismCore)) { + if (k.startsWith("ColorBlendType_") || k.startsWith("AlphaBlendType_")) { + delete PurismCore[k]; + } + } + } + }; +})(PurismCore || (PurismCore = {})); diff --git a/src/core_js_tail.js b/src/core_js_tail.js new file mode 100644 index 0000000..5b2bad6 --- /dev/null +++ b/src/core_js_tail.js @@ -0,0 +1,16 @@ +/* + * Purism Core: Live2DCubismCore.js bootstrap tail (clean-room) + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ +_em_module = (function (factory) { + return function () { + var m = {}; + factory(m); + if (!m.asm) m.asm = m; + return m; + }; +})(_em_module); +var _em = _em_module(); +PurismCore._setModule(_em); diff --git a/src/debug.c b/src/debug.c index 355d28b..eb84b0a 100644 --- a/src/debug.c +++ b/src/debug.c @@ -6,8 +6,8 @@ */ #ifndef PSM_NO_STDIO -#include -#include +# include +# include #endif #include "private.h" @@ -47,7 +47,7 @@ psm__debug_print(int level, const char *fmt, ...) return; #ifndef PSM_NO_STDIO - char buf[256]; + char buf[256]; va_list args; va_start(args, fmt); vsnprintf(buf, sizeof(buf), fmt, args); diff --git a/src/debug.h b/src/debug.h index 8a9b84f..3e43edb 100644 --- a/src/debug.h +++ b/src/debug.h @@ -46,7 +46,7 @@ enum { psm__debug_print(PSM__LOG_DEBUG, PSM__LOG_PREFIX_DEBUG fmt "\n", __VA_ARGS__) PSM__DEF psm__log_level psm__get_log_level(void); -PSM__DEF void psm__set_log_level(psm__log_level level); -PSM__DEF void psm__debug_print(int level, const char *format, ...); +PSM__DEF void psm__set_log_level(psm__log_level level); +PSM__DEF void psm__debug_print(int level, const char *format, ...); #endif /* PSM__DEBUG_H */ diff --git a/src/deformer.c b/src/deformer.c index 8548860..b48b836 100644 --- a/src/deformer.c +++ b/src/deformer.c @@ -18,12 +18,12 @@ struct psm__warp_basis { struct psm__vec2 center; - struct psm__vec2 dpdu; struct psm__vec2 dpdv; + struct psm__vec2 dpdu; }; struct psm__warp_cell { - psm__f32 fu, fv; + psm__f32 fu, fv; struct psm__vec2 p00, p10, p01, p11; }; @@ -40,8 +40,8 @@ psm__warp_extrap_basis(const psm__f32 *pos, psm__i32 row, psm__i32 col, struct psm__vec2 d10_01 = psm__v2_sub(c10, c01); struct psm__warp_basis b; - b.dpdu = psm__v2_scale(psm__v2_sub(d11_00, d10_01), 0.5f); - b.dpdv = psm__v2_scale(psm__v2_add(d10_01, d11_00), 0.5f); + b.dpdv = psm__v2_scale(psm__v2_sub(d11_00, d10_01), 0.5f); + b.dpdu = psm__v2_scale(psm__v2_add(d10_01, d11_00), 0.5f); struct psm__vec2 sum = psm__v2_add( psm__v2_add(c00, c10), psm__v2_add(c01, c11)); @@ -56,97 +56,103 @@ psm__warp_extrap_cell(psm__f32 u, psm__f32 v, psm__f32 gu, psm__f32 gv, psm__i32 row, psm__i32 col, psm__i32 stride, const psm__f32 *pos, const struct psm__warp_basis *basis) { - psm__f32 fr = (psm__f32)row, fc = (psm__f32)col; + psm__f32 fr = (psm__f32)row, fc = (psm__f32)col; struct psm__vec2 cen = basis->center; - struct psm__vec2 du = basis->dpdu; struct psm__vec2 dv = basis->dpdv; + struct psm__vec2 du = basis->dpdu; struct psm__warp_cell cell; + /* + * fu/fv and the interior-strip indices depend only on each axis's class + * (below grid / within a boundary strip / above), so resolve them per axis + * here; the per-octant switch below builds only the cell corners. + * uc/un (cu/(cv) normalized) are used by the within-strip octants. + */ + psm__i32 cu = 0, cv = 0; + psm__f32 uc = 0.0f, un = 0.0f, vc = 0.0f, vn = 0.0f; + + if (u <= 0.0f) + cell.fu = (u + 2.0f) * 0.5f; + else if (u >= 1.0f) + cell.fu = (u - 1.0f) * 0.5f; + else { + cu = (psm__i32)gu; + if (cu == col) cu = col - 1; + cell.fu = gu - (psm__f32)cu; + uc = (psm__f32)cu / fc; + un = (psm__f32)(cu + 1) / fc; + } + + if (v <= 0.0f) + cell.fv = (v + 2.0f) * 0.5f; + else if (v >= 1.0f) + cell.fv = (v - 1.0f) * 0.5f; + else { + cv = (psm__i32)gv; + if (cv == row) cv = row - 1; + cell.fv = gv - (psm__f32)cv; + vc = (psm__f32)cv / fr; + vn = (psm__f32)(cv + 1) / fr; + } + if (u <= 0.0f) { - if (v <= 0.0f) { - cell.fu = (u + 2.0f) * 0.5f; - cell.fv = (v + 2.0f) * 0.5f; - cell.p00 = psm__v2_sub(cen, psm__v2_add( - psm__v2_scale(du, 2.0f), psm__v2_scale(dv, 2.0f))); - cell.p10 = psm__v2_sub(cen, psm__v2_scale(du, 2.0f)); - cell.p01 = psm__v2_sub(cen, psm__v2_scale(dv, 2.0f)); + if (v <= 0.0f) { /* below-left corner */ + cell.p00 = psm__v2_sub(cen, + psm__v2_add(psm__v2_scale(dv, 2.0f), psm__v2_scale(du, 2.0f))); + cell.p10 = psm__v2_sub(cen, psm__v2_scale(dv, 2.0f)); + cell.p01 = psm__v2_sub(cen, psm__v2_scale(du, 2.0f)); cell.p11 = psm__v2(pos[0], pos[1]); - } else if (v < 1.0f) { - psm__i32 cv = (psm__i32)gv; - if (cv == row) cv = row - 1; - cell.fv = gv - (psm__f32)cv; - cell.fu = (u + 2.0f) * 0.5f; - psm__f32 vc = (psm__f32)cv / fr; - psm__f32 vn = (psm__f32)(cv + 1) / fr; - cell.p00 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(dv, 2.0f)), - psm__v2_scale(du, vc)); + } else if (v < 1.0f) { /* left edge */ + cell.p00 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(du, 2.0f)), + psm__v2_scale(dv, vc)); cell.p10 = psm__v2_load(pos, cv * stride); - cell.p01 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(dv, 2.0f)), - psm__v2_scale(du, vn)); + cell.p01 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(du, 2.0f)), + psm__v2_scale(dv, vn)); cell.p11 = psm__v2_load(pos, (cv + 1) * stride); - } else { - cell.fu = (u + 2.0f) * 0.5f; - cell.fv = (v - 1.0f) * 0.5f; - cell.p00 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(dv, 2.0f)), du); + } else { /* above-left corner */ + cell.p00 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(du, 2.0f)), dv); cell.p10 = psm__v2_load(pos, row * stride); - cell.p01 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(dv, 2.0f)), - psm__v2_scale(du, 3.0f)); - cell.p11 = psm__v2_add(cen, psm__v2_scale(du, 3.0f)); + cell.p01 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(du, 2.0f)), + psm__v2_scale(dv, 3.0f)); + cell.p11 = psm__v2_add(cen, psm__v2_scale(dv, 3.0f)); } } else if (u < 1.0f) { - psm__i32 cu = (psm__i32)gu; - if (cu == col) cu = col - 1; - cell.fu = gu - (psm__f32)cu; - psm__f32 uc = (psm__f32)cu / fc; - psm__f32 un = (psm__f32)(cu + 1) / fc; - if (v <= 0.0f) { - cell.fv = (v + 2.0f) * 0.5f; - cell.p00 = psm__v2_add(psm__v2_scale(dv, uc), - psm__v2_sub(cen, psm__v2_scale(du, 2.0f))); - cell.p10 = psm__v2_add(psm__v2_scale(dv, un), - psm__v2_sub(cen, psm__v2_scale(du, 2.0f))); + if (v <= 0.0f) { /* top edge */ + cell.p00 = psm__v2_add(psm__v2_scale(du, uc), + psm__v2_sub(cen, psm__v2_scale(dv, 2.0f))); + cell.p10 = psm__v2_add(psm__v2_scale(du, un), + psm__v2_sub(cen, psm__v2_scale(dv, 2.0f))); cell.p01 = psm__v2_load(pos, cu); cell.p11 = psm__v2_load(pos, cu + 1); - } else { - cell.fv = (v - 1.0f) * 0.5f; + } else { /* bottom edge */ cell.p00 = psm__v2_load(pos, row * stride + cu); cell.p10 = psm__v2_load(pos, row * stride + cu + 1); - cell.p01 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(dv, uc)), - psm__v2_scale(du, 3.0f)); - cell.p11 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(dv, un)), - psm__v2_scale(du, 3.0f)); + cell.p01 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(du, uc)), + psm__v2_scale(dv, 3.0f)); + cell.p11 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(du, un)), + psm__v2_scale(dv, 3.0f)); } } else { - if (v <= 0.0f) { - cell.fu = (u - 1.0f) * 0.5f; - cell.fv = (v + 2.0f) * 0.5f; - cell.p00 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(du, 2.0f)), dv); - cell.p10 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(du, 2.0f)), - psm__v2_scale(dv, 3.0f)); + if (v <= 0.0f) { /* below-right corner */ + cell.p00 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(dv, 2.0f)), du); + cell.p10 = psm__v2_add(psm__v2_sub(cen, psm__v2_scale(dv, 2.0f)), + psm__v2_scale(du, 3.0f)); cell.p01 = psm__v2_load(pos, col); - cell.p11 = psm__v2_add(cen, psm__v2_scale(dv, 3.0f)); - } else if (v < 1.0f) { - psm__i32 cv = (psm__i32)gv; - if (cv == row) cv = row - 1; - cell.fv = gv - (psm__f32)cv; - cell.fu = (u - 1.0f) * 0.5f; - psm__f32 vc = (psm__f32)cv / fr; - psm__f32 vn = (psm__f32)(cv + 1) / fr; + cell.p11 = psm__v2_add(cen, psm__v2_scale(du, 3.0f)); + } else if (v < 1.0f) { /* right edge */ cell.p00 = psm__v2_load(pos, col + cv * stride); - cell.p10 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(dv, 3.0f)), - psm__v2_scale(du, vc)); + cell.p10 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(du, 3.0f)), + psm__v2_scale(dv, vc)); cell.p01 = psm__v2_load(pos, col + (cv + 1) * stride); - cell.p11 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(dv, 3.0f)), - psm__v2_scale(du, vn)); - } else { - cell.fu = (u - 1.0f) * 0.5f; - cell.fv = (v - 1.0f) * 0.5f; + cell.p11 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(du, 3.0f)), + psm__v2_scale(dv, vn)); + } else { /* above-right corner */ cell.p00 = psm__v2_load(pos, row * stride + col); - cell.p10 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(dv, 3.0f)), du); - cell.p01 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(du, 3.0f)), dv); + cell.p10 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(du, 3.0f)), dv); + cell.p01 = psm__v2_add(psm__v2_add(cen, psm__v2_scale(dv, 3.0f)), du); cell.p11 = psm__v2_add(cen, - psm__v2_add(psm__v2_scale(dv, 3.0f), psm__v2_scale(du, 3.0f))); + psm__v2_add(psm__v2_scale(du, 3.0f), psm__v2_scale(dv, 3.0f))); } } @@ -154,7 +160,7 @@ psm__warp_extrap_cell(psm__f32 u, psm__f32 v, psm__f32 gu, psm__f32 gv, } static inline struct psm__vec2 -psm__triangle_interpolate(const struct psm__warp_cell *cell) +psm__interp_triangle(const struct psm__warp_cell *cell) { psm__f32 fu = cell->fu, fv = cell->fv; if (fu + fv <= 1.0f) { @@ -173,20 +179,23 @@ psm__warp_transform(struct psm__model *m, psm__i32 di, const psm__f32 *inputs, psm__f32 *outputs, psm__i32 count) { struct psm__deformer_node *dn = m->deformers.nodes; - psm__i32 si = dn[di].local_idx; + + psm__i32 si = dn[di].local_idx; struct psm__warp *wc = &m->deformers.warps.items[si]; - psm__f32 *pos = m->deformers.warps.pos[si]; + psm__f32 *pos = m->deformers.warps.pos[si]; psm__i32 row = wc->row, col = wc->col; - psm__i32 is_quad = wc->quad_transform, stride = col + 1; + bool is_quad = wc->quad_transform; + psm__i32 stride = col + 1; psm__f32 fr = (psm__f32)row, fc = (psm__f32)col; - psm__i32 extrap_setup = 0; + bool extrap_setup = false; + struct psm__warp_basis basis; for (psm__i32 i = 0; i < count; i++) { struct psm__vec2 uv = psm__v2_load(inputs, i); - psm__f32 gu = uv.x * fc, gv = uv.y * fr; + psm__f32 gu = uv.x * fc, gv = uv.y * fr; if (uv.x >= 0.0f && uv.x < 1.0f && uv.y >= 0.0f && uv.y < 1.0f) { /* Interior: interpolate within grid cell */ @@ -194,7 +203,7 @@ psm__warp_transform(struct psm__model *m, psm__i32 di, psm__f32 fu = gu - (psm__f32)cu; psm__f32 fv = gv - (psm__f32)cv; - psm__i32 bi = cv * stride + cu; + psm__i32 bi = cv * stride + cu; struct psm__vec2 p00 = psm__v2_load(pos, bi); struct psm__vec2 p10 = psm__v2_load(pos, bi + 1); struct psm__vec2 p01 = psm__v2_load(pos, bi + stride); @@ -204,29 +213,29 @@ psm__warp_transform(struct psm__model *m, psm__i32 di, if (is_quad) { result = psm__v2_bilinear(p00, p10, p01, p11, fu, fv); } else { - struct psm__warp_cell cell = {fu, fv, p00, p10, p01, p11}; - result = psm__triangle_interpolate(&cell); + struct psm__warp_cell cell = { fu, fv, p00, p10, p01, p11 }; + result = psm__interp_triangle(&cell); } psm__v2_store(outputs, i, result); } else { /* Extrapolation: compute basis if needed */ if (!extrap_setup) { basis = psm__warp_extrap_basis(pos, row, col, stride); - extrap_setup = 1; + extrap_setup = true; } if (uv.x > -2.0f && uv.x < 3.0f && uv.y > -2.0f && uv.y < 3.0f) { /* Near-exterior: virtual cell + triangle */ struct psm__warp_cell cell = psm__warp_extrap_cell(uv.x, uv.y, gu, gv, row, col, stride, pos, &basis); - struct psm__vec2 r = psm__triangle_interpolate(&cell); + struct psm__vec2 r = psm__interp_triangle(&cell); psm__v2_store(outputs, i, r); } else { /* Far-exterior: simple affine */ - psm__f32 rx = basis.dpdv.x * uv.x - + basis.center.x + basis.dpdu.x * uv.y; - psm__f32 ry = basis.dpdv.y * uv.x - + basis.center.y + basis.dpdu.y * uv.y; + psm__f32 rx = basis.dpdu.x * uv.x + basis.center.x + + basis.dpdv.x * uv.y; + psm__f32 ry = basis.dpdu.y * uv.x + basis.center.y + + basis.dpdv.y * uv.y; outputs[i * 2] = rx; outputs[i * 2 + 1] = ry; } @@ -235,19 +244,20 @@ psm__warp_transform(struct psm__model *m, psm__i32 di, } static void -psm__rot_transform(struct psm__model *m, psm__i32 di, +psm__rotation_transform(struct psm__model *m, psm__i32 di, const psm__f32 *inputs, psm__f32 *outputs, psm__i32 count) { psm__i32 si = m->deformers.nodes[di].local_idx; + struct psm__rotation *rc = &m->deformers.rotations.items[si]; - psm__f32 base_angle = rc->base_angle; - psm__f32 angle = m->deformers.rotations.angle[si]; - psm__f32 scale = m->deformers.rotations.scale[si]; + psm__f32 base_angle = rc->base_angle; + psm__f32 angle = m->deformers.rotations.angle[si]; + psm__f32 scale = m->deformers.rotations.scale[si]; struct psm__vec2 origin = psm__v2(m->deformers.rotations.origin_x[si], m->deformers.rotations.origin_y[si]); - psm__i32 rx = m->deformers.rotations.reflect_x[si]; - psm__i32 ry = m->deformers.rotations.reflect_y[si]; + psm__i32 rx = m->deformers.rotations.reflect_x[si]; + psm__i32 ry = m->deformers.rotations.reflect_y[si]; psm__f32 angle_rad = (base_angle + angle) * PSM__PI / 180.0f; psm__f32 sin_a = sinf(angle_rad); @@ -263,8 +273,8 @@ psm__rot_transform(struct psm__model *m, psm__i32 di, for (psm__i32 i = 0; i < count; i++) { struct psm__vec2 p = psm__v2_load(inputs, i); - struct psm__vec2 r = psm__v2(fmaf(m01, p.y, fmaf(m00, p.x, origin.x)), - fmaf(m11, p.y, fmaf(m10, p.x, origin.y))); + struct psm__vec2 r = psm__v2(origin.x + m00 * p.x + m01 * p.y, + origin.y + m10 * p.x + m11 * p.y); psm__v2_store(outputs, i, r); } } @@ -273,14 +283,14 @@ static inline struct psm__vec2 psm__deformer_transform_point(struct psm__model *m, psm__i32 deformer_idx, struct psm__vec2 p) { - psm__f32 in[2] = {p.x, p.y}; + psm__f32 in[2] = { p.x, p.y }; psm__f32 out[2]; psm__i32 pt = m->deformers.nodes[deformer_idx].type; if (pt == PSM__DEFORMER_TYPE_WARP) psm__warp_transform(m, deformer_idx, in, out, 1); else - psm__rot_transform(m, deformer_idx, in, out, 1); - return (struct psm__vec2){out[0], out[1]}; + psm__rotation_transform(m, deformer_idx, in, out, 1); + return (struct psm__vec2){ out[0], out[1] }; } static void @@ -301,7 +311,7 @@ psm__propagate_deformer_colors(psm__f32 *mo, psm__f32 *so, so[s4 + 2] = ss[p4 + 2]; so[s4 + 3] = 1.0f; } else { - psm__i32 pi4 = parent_idx * 4; + psm__i32 pi4 = parent_idx * 4; const psm__f32 *pm = &mo[pi4]; const psm__f32 *ps = &so[pi4]; @@ -310,9 +320,9 @@ psm__propagate_deformer_colors(psm__f32 *mo, psm__f32 *so, mo[s4 + 2] = sm[p4 + 2] * pm[2]; mo[s4 + 3] = 1.0f; - so[s4 + 0] = fmaf(-ss[p4 + 0], ps[0], ss[p4 + 0] + ps[0]); - so[s4 + 1] = fmaf(-ss[p4 + 1], ps[1], ss[p4 + 1] + ps[1]); - so[s4 + 2] = fmaf(-ss[p4 + 2], ps[2], ss[p4 + 2] + ps[2]); + so[s4 + 0] = ss[p4 + 0] + ps[0] - ss[p4 + 0] * ps[0]; + so[s4 + 1] = ss[p4 + 1] + ps[1] - ss[p4 + 1] * ps[1]; + so[s4 + 2] = ss[p4 + 2] + ps[2] - ss[p4 + 2] * ps[2]; so[s4 + 3] = 1.0f; } } @@ -322,25 +332,26 @@ psm__apply_warp(struct psm__model *m, psm__i32 di) { struct psm__deformer_node *dn = m->deformers.nodes; struct psm__deformer_node *self = &dn[di]; + psm__f32 *d_opa = m->deformers.opacity; psm__f32 *d_scl = m->deformers.scale; - psm__i32 pi = self->parent_deformer_idx; - psm__i32 si = self->local_idx; + psm__i32 pi = self->parent_deformer_idx; + psm__i32 si = self->local_idx; if (pi == -1) { d_opa[di] = m->deformers.warps.opacity[si]; d_scl[di] = 1.0f; } else { psm__f32 **pos = m->deformers.warps.pos; - psm__i32 vc = m->deformers.warps.items[si].vertex_count; - psm__i32 pt = dn[pi].type; + psm__i32 vc = m->deformers.warps.items[si].vertex_count; + psm__i32 pt = dn[pi].type; switch (pt) { case PSM__DEFORMER_TYPE_WARP: psm__warp_transform(m, pi, pos[si], pos[si], vc); break; case PSM__DEFORMER_TYPE_ROTATION: - psm__rot_transform(m, pi, pos[si], pos[si], vc); + psm__rotation_transform(m, pi, pos[si], pos[si], vc); break; } @@ -362,10 +373,11 @@ psm__apply_rotation(struct psm__model *m, psm__i32 di) { struct psm__deformer_node *dn = m->deformers.nodes; struct psm__deformer_node *self = &dn[di]; + psm__f32 *d_opa = m->deformers.opacity; psm__f32 *d_scl = m->deformers.scale; - psm__i32 pi = self->parent_deformer_idx; - psm__i32 si = self->local_idx; + psm__i32 pi = self->parent_deformer_idx; + psm__i32 si = self->local_idx; psm__f32 *r_opa = m->deformers.rotations.opacity; psm__f32 *r_scl = m->deformers.rotations.scale; @@ -378,9 +390,10 @@ psm__apply_rotation(struct psm__model *m, psm__i32 di) d_scl[di] = r_scl[si]; } else { struct psm__vec2 origin = psm__v2(r_ox[si], r_oy[si]); - struct psm__vec2 direction = {0.0f, 0.0f}; - psm__i32 pt = dn[pi].type; - psm__f32 dir_delta = (pt == PSM__DEFORMER_TYPE_ROTATION) ? -10.0f : -0.1f; + struct psm__vec2 direction = { 0.0f, 0.0f }; + psm__i32 pt = dn[pi].type; + psm__f32 dir_delta = + (pt == PSM__DEFORMER_TYPE_ROTATION) ? -10.0f : -0.1f; struct psm__vec2 t_origin = psm__deformer_transform_point(m, pi, origin); @@ -412,10 +425,10 @@ psm__apply_rotation(struct psm__model *m, psm__i32 di) PSM__WARN("rotation direction did not converge"); } - psm__f32 base_dir[2] = {0.0f, dir_delta}; - psm__f32 dir_arr[2] = {direction.x, direction.y}; + psm__f32 base_dir[2] = { 0.0f, dir_delta }; + psm__f32 dir_arr[2] = { direction.x, direction.y }; psm__f32 angle_adj = - (psm__get_angle_not_abs(base_dir, dir_arr) * -180.0f) / PSM__PI; + (psm__signed_angle(base_dir, dir_arr) * -180.0f) / PSM__PI; origin = psm__deformer_transform_point(m, pi, origin); @@ -439,7 +452,6 @@ psm__apply_rotation(struct psm__model *m, psm__i32 di) m->deformers.rotations.scr_color, di, si, pi); } - PSM__DEF void psm__enable_deformers(struct psm__model *m) { @@ -448,6 +460,7 @@ psm__enable_deformers(struct psm__model *m) return; struct psm__deformer_node *nodes = m->deformers.nodes; + bool *en = m->deformers.enable; bool *pen = m->parts.enable; bool *wen = m->deformers.warps.enable; @@ -455,7 +468,8 @@ psm__enable_deformers(struct psm__model *m) for (psm__i32 i = 0; i < count; i++) { struct psm__deformer_node *node = &nodes[i]; - bool e = node->local_enable; + + bool e = node->local_enable; psm__i32 ppi = node->parent_part_idx; psm__i32 pdi = node->parent_deformer_idx; @@ -490,17 +504,16 @@ psm__gather_warps(struct psm__model *m) return; struct psm__sections *ms = m->source->sections; - struct psm__warp *items = m->deformers.warps.items; + struct psm__warp *items = m->deformers.warps.items; if (!items) return; struct psm__warp_keydata *wk = &m->deformers.warps.keydata; + psm__i32 *kb = ms->warp_src.keyform_off; - psm__i32 max_keyforms = ms->count_info->warp_keyforms; + psm__i32 max_keyforms = ms->count_info->warp_keyforms; - struct psm__binding *bindings[count]; - for (psm__i32 i = 0; i < count; i++) - bindings[i] = items[i].binding; + struct psm__binding *const *bindings = m->deformers.warps.bindings; struct psm__gather_channel ch[] = { { ms->warp_key_src.opacity, wk->opacity }, @@ -537,19 +550,18 @@ psm__gather_rotations(struct psm__model *m) return; struct psm__rotation_keydata *rk = &m->deformers.rotations.keydata; + psm__i32 *kb = ms->rotation_src.keyform_off; - psm__i32 max_keyforms = ms->count_info->rotation_keyforms; + psm__i32 max_keyforms = ms->count_info->rotation_keyforms; - struct psm__binding *bindings[count]; - for (psm__i32 i = 0; i < count; i++) - bindings[i] = items[i].binding; + struct psm__binding *const *bindings = m->deformers.rotations.bindings; struct psm__gather_channel ch[] = { - { ms->rotation_key_src.opacity, rk->opacity }, - { ms->rotation_key_src.angle, rk->angle }, + { ms->rotation_key_src.opacity, rk->opacity }, + { ms->rotation_key_src.angle, rk->angle }, { ms->rotation_key_src.origin_x, rk->origin_x }, { ms->rotation_key_src.origin_y, rk->origin_y }, - { ms->rotation_key_src.scale, rk->scale }, + { ms->rotation_key_src.scale, rk->scale }, }; psm__gather_scalars(count, bindings, kb, max_keyforms, &rk->interp, ch, 5); @@ -578,6 +590,7 @@ psm__apply_transforms(struct psm__model *m) return; bool *en = m->deformers.enable; + struct psm__deformer_node *nodes = m->deformers.nodes; /* @@ -606,12 +619,13 @@ psm__apply_transforms_to_meshes(struct psm__model *m) if (count <= 0) return; - struct psm__art_mesh *am = m->art_meshes.meshes; - psm__f32 *d_opa = m->deformers.opacity; + struct psm__art_mesh *am = m->art_meshes.meshes; struct psm__deformer_node *dn = m->deformers.nodes; + + psm__f32 *d_opa = m->deformers.opacity; psm__f32 **cp = m->art_meshes.pos; - psm__f32 *am_opa = m->art_meshes.opacity; - bool *en = m->art_meshes.enable; + psm__f32 *am_opa = m->art_meshes.opacity; + bool *en = m->art_meshes.enable; for (psm__i32 i = 0; i < count; i++) { if (!en[i]) continue; @@ -626,6 +640,6 @@ psm__apply_transforms_to_meshes(struct psm__model *m) if (dt == PSM__DEFORMER_TYPE_WARP) psm__warp_transform(m, pdi, cp[i], cp[i], vc); else - psm__rot_transform(m, pdi, cp[i], cp[i], vc); + psm__rotation_transform(m, pdi, cp[i], cp[i], vc); } } diff --git a/src/error.h b/src/error.h index d183c43..5f52999 100644 --- a/src/error.h +++ b/src/error.h @@ -10,10 +10,12 @@ #include "private.h" +// clang-format off #define PSM__FAILM(cond, v, msg) \ if (cond) { PSM__LOG(msg); return v; } #define PSM__FAIL(cond, v, fmt, ...) \ if (cond) { PSM__LOGF(fmt, __VA_ARGS__); return v; } +// clang-format on enum { PSM__OK, diff --git a/src/gather.h b/src/gather.h index 91f368a..13ff7be 100644 --- a/src/gather.h +++ b/src/gather.h @@ -10,12 +10,13 @@ #include #include "private.h" +#include "debug.h" #include "moc3.h" #include "model.h" struct psm__gather_channel { const psm__f32 *src; - psm__f32 *dst; + psm__f32 *dst; }; /* @@ -27,15 +28,18 @@ struct psm__gather_channel { */ static inline void psm__gather_scalars( - psm__i32 count, - struct psm__binding *const *bindings, - const psm__i32 *keyform_offset, - psm__i32 max_keyforms, - struct psm__interp *interp, + psm__i32 count, + struct psm__binding *const *bindings, + const psm__i32 *keyform_offset, + psm__i32 max_keyforms, + struct psm__interp *interp, const struct psm__gather_channel *channels, - psm__i32 n_channels) + psm__i32 n_channels) { psm__i32 offset = 0; + /* keyform_idx[j]+keyform_offset[i] < keyform_off+product <= max_keyforms, + * proved at load (G1: product(key_counts) <= key_len). */ + (void)max_keyforms; for (psm__i32 i = 0; i < count; i++) { struct psm__binding *b = bindings[i]; if (!b) @@ -48,8 +52,6 @@ psm__gather_scalars( if (b->idx_dirty && cc > 0) { for (psm__i32 j = 0; j < cc; j++) { psm__i32 kfi = b->keyform_idx[j] + keyform_offset[i]; - if ((psm__u32)kfi >= (psm__u32)max_keyforms) - continue; for (psm__i32 c = 0; c < n_channels; c++) channels[c].dst[offset + j] = channels[c].src[kfi]; } @@ -70,16 +72,20 @@ psm__gather_scalars( */ static inline void psm__gather_positions( - psm__i32 count, + psm__i32 count, struct psm__binding *const *bindings, - const psm__i32 *keyform_offset, - psm__i32 max_keyforms, - const psm__f32 *pos_xy, - const psm__i32 *pos_begin, - psm__i32 max_pos, - psm__f32 **pos_dst) + const psm__i32 *keyform_offset, + psm__i32 max_keyforms, + const psm__f32 *pos_xy, + const psm__i32 *pos_begin, + psm__i32 max_pos, + psm__f32 **pos_dst) { psm__i32 offset = 0; + /* kfi < max_keyforms (G1) and pos_begin[kfi] in [0,max_pos) (F2), both + * proved at load. */ + (void)max_keyforms; + (void)max_pos; for (psm__i32 i = 0; i < count; i++) { struct psm__binding *b = bindings[i]; if (!b) @@ -87,11 +93,7 @@ psm__gather_positions( if (b->idx_dirty && b->blend_count > 0) { for (psm__i32 j = 0; j < b->blend_count; j++) { psm__i32 kfi = b->keyform_idx[j] + keyform_offset[i]; - if ((psm__u32)kfi >= (psm__u32)max_keyforms) - continue; psm__i32 pi = pos_begin[kfi]; - if ((psm__u32)pi >= (psm__u32)max_pos) - continue; pos_dst[offset + j] = (psm__f32 *)&pos_xy[pi]; } } @@ -105,16 +107,19 @@ psm__gather_positions( */ static inline void psm__gather_colors( - psm__i32 count, - struct psm__binding *const *bindings, - const psm__i32 *key_color_offset, - psm__i32 max_kf_colors, + psm__i32 count, + struct psm__binding *const *bindings, + const psm__i32 *key_color_offset, + psm__i32 max_kf_colors, const struct psm__key_color_src *mul_src, const struct psm__key_color_src *scr_src, - struct psm__color3 *mul_dst, - struct psm__color3 *scr_dst) + struct psm__color3 *mul_dst, + struct psm__color3 *scr_dst) { psm__i32 offset = 0; + /* kfi = keyform_idx[j]+key_color_offset[i] < key_color_off+product + * <= max_kf_colors, proved at load (G1). */ + (void)max_kf_colors; for (psm__i32 i = 0; i < count; i++) { struct psm__binding *b = bindings[i]; if (!b) continue; @@ -123,8 +128,6 @@ psm__gather_colors( if (b->idx_dirty && cc > 0) { for (psm__i32 j = 0; j < cc; j++) { psm__i32 kfi = b->keyform_idx[j] + key_color_offset[i]; - if ((psm__u32)kfi >= (psm__u32)max_kf_colors) - continue; psm__i32 oj = offset + j; mul_dst->r[oj] = mul_src->r[kfi]; mul_dst->g[oj] = mul_src->g[kfi]; @@ -144,22 +147,22 @@ psm__gather_colors( */ static inline void psm__gather_reflect( - psm__i32 count, + psm__i32 count, struct psm__binding *const *bindings, - const psm__i32 *keyform_offset, - psm__i32 max_keyforms, - const psm__i32 *rfx_src, - const psm__i32 *rfy_src, - psm__i32 *rfx_dst, - psm__i32 *rfy_dst) + const psm__i32 *keyform_offset, + psm__i32 max_keyforms, + const psm__i32 *rfx_src, + const psm__i32 *rfy_src, + psm__i32 *rfx_dst, + psm__i32 *rfy_dst) { + /* kfi < max_keyforms proved at load (G1). */ + (void)max_keyforms; for (psm__i32 i = 0; i < count; i++) { struct psm__binding *b = bindings[i]; if (!b || !b->idx_dirty || b->blend_count <= 0) continue; psm__i32 kfi = b->keyform_idx[0] + keyform_offset[i]; - if ((psm__u32)kfi >= (psm__u32)max_keyforms) - continue; rfx_dst[i] = rfx_src[kfi]; rfy_dst[i] = rfy_src[kfi]; } diff --git a/src/glue.c b/src/glue.c index 2f32119..782b30e 100644 --- a/src/glue.c +++ b/src/glue.c @@ -26,14 +26,13 @@ psm__gather_glues(struct psm__model *m) if (!items) return; struct psm__sections *ms = m->source->sections; + psm__i32 *keyform_base_idx = ms->glue_src.keyform_off; psm__f32 *intensity_src = ms->glue_key_src.intensity; if (!keyform_base_idx || !intensity_src) return; - struct psm__binding *bindings[count]; - for (psm__i32 i = 0; i < count; i++) - bindings[i] = items[i].binding; + struct psm__binding *const *bindings = m->glues.bindings; struct psm__gather_channel ch[] = { { intensity_src, m->glues.keydata.intensity }, @@ -50,21 +49,23 @@ psm__apply_glues(struct psm__model *m) return; struct psm__glue *items = m->glues.items; + psm__f32 **pos = m->art_meshes.pos; - psm__f32 *calc_int = m->glues.intensity; + psm__f32 *calc_int = m->glues.intensity; if (!items || !pos || !calc_int) return; for (psm__i32 gi = 0; gi < count; gi++) { struct psm__glue *glue = &items[gi]; + psm__i32 ic = glue->glue_info_count; if (ic <= 0) continue; psm__i32 m0 = glue->mesh_idx0, m1 = glue->mesh_idx1; - psm__f32 intensity = calc_int[gi]; + psm__f32 intensity = calc_int[gi]; psm__f32 *p0 = pos[m0], *p1 = pos[m1]; if (!p0 || !p1) continue; @@ -74,10 +75,10 @@ psm__apply_glues(struct psm__model *m) if (!wt || !pi) continue; - for (psm__i32 i = 0; i < ic; i += 2) { + for (psm__i32 i = 0; i + 1 < ic; i += 2) { psm__i32 i0 = pi[i], i1 = pi[i + 1]; - psm__f32 w0 = wt[i], w1 = wt[i + 1]; + struct psm__vec2 a = psm__v2_load(p0, i0); struct psm__vec2 b = psm__v2_load(p1, i1); struct psm__vec2 d = psm__v2_sub(b, a); diff --git a/src/interpolate.c b/src/interpolate.c index b7ed79a..d951702 100644 --- a/src/interpolate.c +++ b/src/interpolate.c @@ -30,7 +30,7 @@ psm__interp_f32(struct psm__interp *interp, const psm__f32 *targets, psm__i32 *comb = interp->blend_count; psm__f32 *wt = interp->weights; psm__f32 *tmp = interp->tmp; - psm__i32 tmp_len = interp->tmp_len; + psm__i32 tmp_len = interp->tmp_len; if (!max_comb || !comb || !wt) return; @@ -49,7 +49,7 @@ psm__interp_f32(struct psm__interp *interp, const psm__f32 *targets, for (psm__i32 i = 0; i < obj_count; i++) { psm__i32 mc = max_comb[i]; if (enable == NULL || enable[i]) { - psm__i32 n = psm__clamp_i32(comb[i], 0, mc); + psm__i32 n = comb[i]; psm__f32 sum = 0.0f; if (tmp) { for (psm__i32 j = 0; j < n; j++) @@ -79,7 +79,7 @@ psm__interp_i32(struct psm__interp *interp, const psm__f32 *targets, psm__i32 *comb = interp->blend_count; psm__f32 *wt = interp->weights; psm__f32 *tmp = interp->tmp; - psm__i32 tmp_len = interp->tmp_len; + psm__i32 tmp_len = interp->tmp_len; if (!max_comb || !comb || !wt) return; @@ -93,7 +93,7 @@ psm__interp_i32(struct psm__interp *interp, const psm__f32 *targets, for (psm__i32 i = 0; i < obj_count; i++) { psm__i32 mc = max_comb[i]; if (enable == NULL || enable[i]) { - psm__i32 n = psm__clamp_i32(comb[i], 0, mc); + psm__i32 n = comb[i]; psm__f32 sum = 0.0f; if (tmp) { for (psm__i32 j = 0; j < n; j++) @@ -116,7 +116,7 @@ psm__interp_f32_array(struct psm__interp *interp, if (!interp || !targets || !out || !counts) return; - psm__i32 obj_count = interp->object_count; + psm__i32 obj_count = interp->object_count; psm__i32 *max_comb = interp->max_blend; psm__i32 *comb = interp->blend_count; psm__f32 *wt = interp->weights; @@ -133,12 +133,12 @@ psm__interp_f32_array(struct psm__interp *interp, off += mc; continue; } - psm__i32 n = psm__clamp_i32(comb[i], 0, mc); + psm__i32 n = comb[i]; psm__f32 *dst = out[i]; if (dst) { memset(dst, 0, total * sizeof(psm__f32)); for (psm__i32 j = 0; j < n; j++) { - psm__f32 w = wt[off + j]; + psm__f32 w = wt[off + j]; psm__f32 *src = targets[off + j]; if (src) { for (psm__i32 k = 0; k < total; k++) @@ -153,12 +153,12 @@ psm__interp_f32_array(struct psm__interp *interp, static void psm__interp_colors( - struct psm__interp *interp, + struct psm__interp *interp, const struct psm__color3 *kd_mul, const struct psm__color3 *kd_scr, - psm__f32 *mul_out, - psm__f32 *scr_out, - const bool *enable) + psm__f32 *mul_out, + psm__f32 *scr_out, + const bool *enable) { psm__interp_f32(interp, kd_mul->r, mul_out + 0, 4, enable); psm__interp_f32(interp, kd_mul->g, mul_out + 1, 4, enable); @@ -179,11 +179,12 @@ psm__interp_parts(struct psm__model *m) PSM__DEF void psm__interp_warps(struct psm__model *m) { - struct psm__warps *w = &m->deformers.warps; - struct psm__interp *ip = &w->keydata.interp; + struct psm__warps *w = &m->deformers.warps; + struct psm__interp *ip = &w->keydata.interp; struct psm__sections *ms = m->source->sections; + psm__i32 *vc = ms->warp_src.vertex_count; - bool *en = w->enable; + bool *en = w->enable; psm__interp_f32(ip, w->keydata.opacity, w->opacity, 1, en); @@ -201,7 +202,8 @@ PSM__DEF void psm__interp_rotations(struct psm__model *m) { struct psm__rotations *r = &m->deformers.rotations; - struct psm__interp *ip = &r->keydata.interp; + struct psm__interp *ip = &r->keydata.interp; + bool *en = r->enable; psm__interp_f32(ip, r->keydata.opacity, r->opacity, 1, en); @@ -221,10 +223,11 @@ PSM__DEF void psm__interp_art_meshes(struct psm__model *m) { struct psm__art_meshes *am = &m->art_meshes; - struct psm__interp *ip = &am->keydata.interp; - struct psm__sections *ms = m->source->sections; + struct psm__interp *ip = &am->keydata.interp; + struct psm__sections *ms = m->source->sections; + psm__i32 *vc = ms->art_mesh_src.vertex_count; - bool *en = am->enable; + bool *en = am->enable; psm__interp_f32(ip, am->keydata.opacity, am->opacity, 1, en); psm__interp_i32(ip, am->keydata.draw_order, am->draw_order, en); @@ -253,7 +256,8 @@ PSM__DEF void psm__interp_offscreens(struct psm__model *m) { struct psm__offscreens *os = &m->offscreens; - struct psm__interp *ip = &os->keydata.interp; + struct psm__interp *ip = &os->keydata.interp; + bool *en = os->enable; if (m->source->header->version < csmMocVersion_53) diff --git a/src/math2.c b/src/math2.c index 9a7dd3b..4c36913 100644 --- a/src/math2.c +++ b/src/math2.c @@ -10,7 +10,7 @@ #include "math2.h" PSM__DEF psm__f32 -psm__get_angle_not_abs(const psm__f32 *v1, const psm__f32 *v2) +psm__signed_angle(const psm__f32 *v1, const psm__f32 *v2) { psm__f32 angle1 = atan2f(v1[1], v1[0]); psm__f32 angle2 = atan2f(v2[1], v2[0]); diff --git a/src/math2.h b/src/math2.h index ba8742e..b74e2bc 100644 --- a/src/math2.h +++ b/src/math2.h @@ -12,8 +12,8 @@ #include #include "private.h" -#define PSM__PI 3.14159265358979323846f -#define PSM__TWO_PI 6.28318530717958647692f +#define PSM__PI 3.14159265358979323846f +#define PSM__TWO_PI 6.28318530717958647692f struct psm__vec2 { psm__f32 x, y; @@ -22,13 +22,13 @@ struct psm__vec2 { static inline struct psm__vec2 psm__v2(psm__f32 x, psm__f32 y) { - return (struct psm__vec2){x, y}; + return (struct psm__vec2){ x, y }; } static inline struct psm__vec2 psm__v2_load(const psm__f32 *arr, psm__i32 idx) { - return (struct psm__vec2){arr[idx * 2], arr[idx * 2 + 1]}; + return (struct psm__vec2){ arr[idx * 2], arr[idx * 2 + 1] }; } static inline void @@ -41,39 +41,39 @@ psm__v2_store(psm__f32 *arr, psm__i32 idx, struct psm__vec2 v) static inline struct psm__vec2 psm__v2_add(struct psm__vec2 a, struct psm__vec2 b) { - return (struct psm__vec2){a.x + b.x, a.y + b.y}; + return (struct psm__vec2){ a.x + b.x, a.y + b.y }; } static inline struct psm__vec2 psm__v2_sub(struct psm__vec2 a, struct psm__vec2 b) { - return (struct psm__vec2){a.x - b.x, a.y - b.y}; + return (struct psm__vec2){ a.x - b.x, a.y - b.y }; } static inline struct psm__vec2 psm__v2_scale(struct psm__vec2 v, psm__f32 s) { - return (struct psm__vec2){v.x * s, v.y * s}; + return (struct psm__vec2){ v.x * s, v.y * s }; } static inline struct psm__vec2 psm__v2_neg(struct psm__vec2 v) { - return (struct psm__vec2){-v.x, -v.y}; + return (struct psm__vec2){ -v.x, -v.y }; } static inline struct psm__vec2 psm__v2_lerp(struct psm__vec2 a, struct psm__vec2 b, psm__f32 t) { - return (struct psm__vec2){fmaf(t, b.x - a.x, a.x), fmaf(t, b.y - a.y, a.y)}; + return (struct psm__vec2){ t * (b.x - a.x) + a.x, t * (b.y - a.y) + a.y }; } static inline struct psm__vec2 psm__v2_bary3(struct psm__vec2 a, struct psm__vec2 b, struct psm__vec2 c, psm__f32 wa, psm__f32 wb, psm__f32 wc) { - return (struct psm__vec2){fmaf(wc, c.x, fmaf(wb, b.x, wa * a.x)), - fmaf(wc, c.y, fmaf(wb, b.y, wa * a.y))}; + return (struct psm__vec2){ wc * c.x + (wb * b.x + wa * a.x), + wc * c.y + (wb * b.y + wa * a.y) }; } static inline struct psm__vec2 @@ -81,12 +81,12 @@ psm__v2_bilinear(struct psm__vec2 p00, struct psm__vec2 p10, struct psm__vec2 p01, struct psm__vec2 p11, psm__f32 u, psm__f32 v) { psm__f32 inv_u = 1.0f - u; - psm__f32 x0 = fmaf(u, p10.x, inv_u * p00.x), - y0 = fmaf(u, p10.y, inv_u * p00.y), - x1 = fmaf(u, p11.x, inv_u * p01.x), - y1 = fmaf(u, p11.y, inv_u * p01.y); + psm__f32 x0 = u * p10.x + inv_u * p00.x, + y0 = u * p10.y + inv_u * p00.y, + x1 = u * p11.x + inv_u * p01.x, + y1 = u * p11.y + inv_u * p01.y; psm__f32 inv_v = 1.0f - v; - return (struct psm__vec2){fmaf(v, x1, inv_v * x0), fmaf(v, y1, inv_v * y0)}; + return (struct psm__vec2){ v * x1 + inv_v * x0, v * y1 + inv_v * y0 }; } static inline psm__f32 @@ -110,9 +110,10 @@ static inline psm__i32 psm__f32_to_i32(psm__f32 v) { return (v == v) - ? (psm__i32)psm__clamp_f32(v, -2147483648.0f, 2147483520.0f) : 0; + ? (psm__i32)psm__clamp_f32(v, -2147483648.0f, 2147483520.0f) + : 0; } -PSM__DEF psm__f32 psm__get_angle_not_abs(const psm__f32 *, const psm__f32 *); +PSM__DEF psm__f32 psm__signed_angle(const psm__f32 *, const psm__f32 *); #endif /* PSM__MATH2_H */ diff --git a/src/moc3.c b/src/moc3.c index e573848..d9d93e3 100644 --- a/src/moc3.c +++ b/src/moc3.c @@ -15,11 +15,12 @@ #include "arena.h" #include "moc3.h" #include "model.h" +#include "verify.h" -#define PSM__MOC3_MAGIC "MOC3" +#define PSM__MOC3_MAGIC "MOC3" #define PSM__MOC3_MAGIC_SIZE 4 -static int +static bool psm__is_le(void) { unsigned int x = 1; @@ -46,8 +47,8 @@ psm__bswap_32(psm__u32 x) #elif defined(_MSC_VER) return _byteswap_ulong(x); #else - return ((x >> 24) & 0x000000FF) | ((x >> 8) & 0x0000FF00) | - ((x << 8) & 0x00FF0000) | ((x << 24) & 0xFF000000); + return ((x >> 24) & 0x000000FF) | ((x >> 8) & 0x0000FF00) | + ((x << 8) & 0x00FF0000) | ((x << 24) & 0xFF000000); #endif } @@ -67,36 +68,6 @@ psm__bswap_many_32(void *data, psm_size count) p[i] = psm__bswap_32(p[i]); } -/* Check section bounds and alignment. All sections must be 8-byte aligned. */ -#define psm__bounds_check_static(TYPE, COUNT, offsets, i, n) \ - if ((offsets[i] & 7) != 0) { \ - PSM__LOGF("section %d misaligned: offset=%u", i, (unsigned)offsets[i]); \ - return PSM__ERR_FILE_CORRUPT; \ - } \ - if ((COUNT) > (psm_size)-1 / sizeof(TYPE)) { \ - PSM__LOGF("section %d count overflow: count=%u", i, (unsigned)(COUNT)); \ - return PSM__ERR_FILE_CORRUPT; \ - } \ - psm_size _sz = sizeof(TYPE) * (COUNT); \ - if (offsets[i] > n || n - offsets[i] < _sz) { \ - PSM__LOGF("section %d out of bounds: offset=%u size=%u n=%u", i, (unsigned)offsets[i], (unsigned)_sz, (unsigned)n); \ - return PSM__ERR_FILE_CORRUPT; \ - } -#define psm__bounds_check_dynamic(TYPE, COUNT_MEMBER, offsets, i, n, cnt) \ - if ((offsets[i] & 7) != 0) { \ - PSM__LOGF("section %d misaligned: offset=%u", i, (unsigned)offsets[i]); \ - return PSM__ERR_FILE_CORRUPT; \ - } \ - if (cnt->COUNT_MEMBER < 0 || (psm_size)cnt->COUNT_MEMBER > (psm_size)-1 / sizeof(TYPE)) { \ - PSM__LOGF("section %d count invalid: count=%d", i, (int)cnt->COUNT_MEMBER); \ - return PSM__ERR_FILE_CORRUPT; \ - } \ - psm_size _sz = sizeof(TYPE) * (psm_size)cnt->COUNT_MEMBER; \ - if (offsets[i] > n || n - offsets[i] < _sz) { \ - PSM__LOGF("section %d out of bounds: offset=%u size=%u n=%u", i, (unsigned)offsets[i], (unsigned)_sz, (unsigned)n); \ - return PSM__ERR_FILE_CORRUPT; \ - } - static int psm__get_moc_version(csmMocVersion *v, const psm__u8 *p, psm_size n) { @@ -110,438 +81,6 @@ psm__get_moc_version(csmMocVersion *v, const psm__u8 *p, psm_size n) return PSM__OK; } -static int -psm__verify_count_info(const struct psm__count_info *cnt) -{ - PSM__FAILM(cnt->parts < 0 || cnt->deformers < 0 || - cnt->warps < 0 || cnt->rotations < 0 || - cnt->art_meshes < 0 || cnt->parameters < 0 || - cnt->bindings < 0 || cnt->key_tables < 0 || - cnt->keys < 0 || cnt->uvs < 0 || cnt->idx < 0 || cnt->masks < 0 || - cnt->glues < 0 || cnt->keyform_pos < 0 || cnt->part_keyforms < 0 || - cnt->warp_keyforms < 0 || cnt->rotation_keyforms < 0 || - cnt->art_mesh_keyforms < 0 || cnt->glue_keyforms < 0, - PSM__ERR_FILE_CORRUPT, "invalid count values"); - - PSM__FAILM(((psm__u32)cnt->warps + (psm__u32)cnt->rotations) != - (psm__u32)cnt->deformers, - PSM__ERR_FILE_CORRUPT, "deformer count mismatch"); - return PSM__OK; -} - -static int -psm__verify_indices(psm__u8 ver, const struct psm__sections *src) -{ - const struct psm__count_info *cnt = src->count_info; - -#define psm__check_nonnull(TYPE, MEMBER, COUNT_MEMBER) \ - if (cnt->COUNT_MEMBER > 0 && !src->MEMBER) { \ - PSM__LOGF("missing: %s (count=%d)", \ - #MEMBER, cnt->COUNT_MEMBER); \ - return PSM__ERR_FILE_CORRUPT; \ - } - - PSM__SECTIONS_V30(psm__nop_predicate, psm__check_nonnull) - if (ver < csmMocVersion_33) goto done_nonnull; - PSM__SECTIONS_V33(psm__nop_predicate, psm__check_nonnull) - if (ver < csmMocVersion_42) goto done_nonnull; - PSM__SECTIONS_V42(psm__nop_predicate, psm__check_nonnull) - if (ver < csmMocVersion_50) goto done_nonnull; - PSM__SECTIONS_V50(psm__nop_predicate, psm__check_nonnull) - if (ver < csmMocVersion_53) goto done_nonnull; - PSM__SECTIONS_V53(psm__nop_predicate, psm__check_nonnull) -done_nonnull: -#undef psm__check_nonnull - -#define psm__model_check_index(arr, i, max) \ - PSM__FAIL((arr)[i] < 0 || (arr)[i] >= (max), \ - PSM__ERR_FILE_CORRUPT, \ - "invalid index: %s[%d]=%d (max=%d)", \ - #arr, i, (arr)[i], (max)) - -#define psm__model_check_index_or_neg1(arr, i, max) \ - PSM__FAIL((arr)[i] < -1 || (arr)[i] >= (max), \ - PSM__ERR_FILE_CORRUPT, \ - "invalid index: %s[%d]=%d (max=%d)", \ - #arr, i, (arr)[i], (max)) - -#define psm__model_check_range(begin_arr, count_arr, i, max) \ - PSM__FAIL((count_arr)[i] < 0 || \ - ((count_arr)[i] > 0 && ((begin_arr)[i] < 0 || \ - (psm__u32)(begin_arr)[i] + \ - (psm__u32)(count_arr)[i] > (psm__u32)(max))), \ - PSM__ERR_FILE_CORRUPT, \ - "invalid range: %s[%d] begin=%d count=%d (max=%d)", \ - #begin_arr, i, (begin_arr)[i], (count_arr)[i], (max)) - - /* - * Validate key_count matches max_blend from binding. - * key_count is what the source declares; max_blend is - * 2^(param_binding_count) which is the actual range - * accessed at runtime via blend_count indexing. - */ -#define psm__check_key_combo(obj_src, keyform_total, obj_len) \ - for (psm__i32 _i = 0; _i < (obj_len); _i++) { \ - psm__i32 _bi = (obj_src).binding_idx[_i]; \ - if (_bi < 0 || _bi >= cnt->bindings) continue; \ - psm__i32 _pc = psm__clamp_i32(\ - src->binding_src.key_table_idx_len[_bi], 0, PSM__MAX_KEY_TABLES); \ - psm__i32 _mc = 1 << _pc; \ - psm__i32 _kb = (obj_src).keyform_off[_i]; \ - PSM__FAIL(!psm__valid_range(_kb, _mc, (keyform_total)), \ - PSM__ERR_FILE_CORRUPT, \ - "%s[%d] keyform_off=%d max_blend=%d total=%d", \ - #obj_src, _i, _kb, _mc, (keyform_total)); \ - } - - /* Part sources */ - for (psm__i32 i = 0; i < cnt->parts; i++) { - psm__model_check_index(src->part_src.binding_idx, i, cnt->bindings); - psm__model_check_range(src->part_src.keyform_off, - src->part_src.key_len, i, cnt->part_keyforms); - psm__model_check_index_or_neg1( - src->part_src.parent_part_idx, i, cnt->parts); - } - psm__check_key_combo(src->part_src, cnt->part_keyforms, cnt->parts) - - /* Deformer sources */ - for (psm__i32 i = 0; i < cnt->deformers; i++) { - psm__model_check_index(src->deformer_src.binding_idx, i, cnt->bindings); - psm__model_check_index_or_neg1( - src->deformer_src.parent_part_idx, i, cnt->parts); - psm__model_check_index_or_neg1(src->deformer_src.parent_deformer_idx, - i, cnt->deformers); - - psm__i32 dtype = src->deformer_src.type[i]; - psm__i32 sidx = src->deformer_src.local_idx[i]; - switch (dtype) { - case PSM__DEFORMER_TYPE_WARP: - if (sidx < 0 || sidx >= cnt->warps) { - PSM__LOGF("deformer[%d].specific=%d (warp max=%d)", - i, sidx, cnt->warps); - return PSM__ERR_FILE_CORRUPT; - } - break; - case PSM__DEFORMER_TYPE_ROTATION: - if (sidx < 0 || sidx >= cnt->rotations) { - PSM__LOGF("deformer[%d].specific=%d (rot max=%d)", - i, sidx, cnt->rotations); - return PSM__ERR_FILE_CORRUPT; - } - break; - default: - PSM__LOGF("deformer[%d].type=%d invalid", i, dtype); - return PSM__ERR_FILE_CORRUPT; - } - } - - /* Warp deformer sources */ - for (psm__i32 i = 0; i < cnt->warps; i++) { - psm__model_check_index(src->warp_src.binding_idx, - i, cnt->bindings); - psm__model_check_range(src->warp_src.keyform_off, - src->warp_src.key_len, i, cnt->warp_keyforms); - psm__i32 row = src->warp_src.row[i]; - psm__i32 col = src->warp_src.col[i]; - psm__i32 vc = src->warp_src.vertex_count[i]; - PSM__FAIL(row <= 0 || col <= 0, PSM__ERR_FILE_CORRUPT, - "warp[%d] grid row=%d col=%d", i, row, col); - psm__u32 expect = (psm__u32)(row + 1) * (psm__u32)(col + 1); - PSM__FAIL((psm__u32)vc != expect, PSM__ERR_FILE_CORRUPT, - "warp[%d] vert_count=%d expected=%u", i, vc, (unsigned)expect); - } - - psm__check_key_combo(src->warp_src, cnt->warp_keyforms, cnt->warps) - - /* Warp deformer keyform positions */ - for (psm__i32 i = 0; i < cnt->warps; i++) { - psm__i32 off = src->warp_src.keyform_off[i]; - psm__i32 count = src->warp_src.key_len[i]; - psm__i32 vc = src->warp_src.vertex_count[i]; - for (psm__i32 j = 0; j < count; j++) { - psm__i32 po = src->warp_key_src.key_pos_off[off + j]; - PSM__FAIL(po < 0 || (psm__u32)po + (psm__u32)vc > - (psm__u32)cnt->keyform_pos, PSM__ERR_FILE_CORRUPT, - "warp[%d] kf[%d] pos_off=%d vc=%d max=%d", - i, j, po, vc, cnt->keyform_pos); - } - } - - /* Rotation deformer sources */ - for (psm__i32 i = 0; i < cnt->rotations; i++) { - psm__model_check_index(src->rotation_src.binding_idx, i, cnt->bindings); - psm__model_check_range(src->rotation_src.keyform_off, - src->rotation_src.key_len, i, cnt->rotation_keyforms); - } - - psm__check_key_combo(src->rotation_src, cnt->rotation_keyforms, cnt->rotations) - - /* Art mesh sources */ - for (psm__i32 i = 0; i < cnt->art_meshes; i++) { - psm__model_check_index(src->art_mesh_src.binding_idx, i, cnt->bindings); - psm__model_check_range(src->art_mesh_src.keyform_off, - src->art_mesh_src.key_len, i, cnt->art_mesh_keyforms); - psm__model_check_index_or_neg1( - src->art_mesh_src.parent_part_idx, i, cnt->parts); - psm__model_check_index_or_neg1(src->art_mesh_src.parent_deformer_idx, - i, cnt->deformers); - PSM__FAIL(src->art_mesh_src.vertex_count[i] < 0 || - src->art_mesh_src.uv_off[i] < 0 || - (psm__u32)src->art_mesh_src.uv_off[i] + - 2u * (psm__u32)src->art_mesh_src.vertex_count[i] - > (psm__u32)cnt->uvs, PSM__ERR_FILE_CORRUPT, - "art_mesh[%d]: UV [%d, +%d*2) oob (max %d)", - i, src->art_mesh_src.uv_off[i], - src->art_mesh_src.vertex_count[i], cnt->uvs); - psm__model_check_range(src->art_mesh_src.idx_off, - src->art_mesh_src.idx_len, i, cnt->idx); - psm__model_check_range(src->art_mesh_src.mask_off, - src->art_mesh_src.mask_len, i, cnt->masks); - } - - psm__check_key_combo(src->art_mesh_src, cnt->art_mesh_keyforms, cnt->art_meshes) - - /* Art mesh keyform position indices */ - for (psm__i32 i = 0; i < cnt->art_mesh_keyforms; i++) { - psm__model_check_index(src->art_mesh_key_src.key_pos_off, - i, cnt->keyform_pos); - } - - /* Parameter sources */ - for (psm__i32 i = 0; i < cnt->parameters; i++) { - psm__model_check_range(src->param_src.key_table_off, src->param_src.key_table_len, - i, cnt->key_tables); - } - - /* Keyform binding sources */ - for (psm__i32 i = 0; i < cnt->bindings; i++) { - psm__model_check_range(src->binding_src.key_table_idx_off, - src->binding_src.key_table_idx_len, i, cnt->key_table_idx); - psm__i32 pc = src->binding_src.key_table_idx_len[i]; - PSM__FAIL(pc < 0 || pc > PSM__MAX_KEY_TABLES, PSM__ERR_FILE_CORRUPT, - "binding[%d] param_count=%d oob", i, pc); - } - - /* Parameter binding index sources */ - for (psm__i32 i = 0; i < cnt->key_table_idx; i++) { - psm__model_check_index(src->key_table_idx_src.idx, - i, cnt->key_tables); - } - - /* Parameter binding sources */ - for (psm__i32 i = 0; i < cnt->key_tables; i++) { - psm__model_check_range(src->key_table_src.keys_off, - src->key_table_src.keys_len, i, cnt->keys); - } - - /* Drawable mask sources */ - for (psm__i32 i = 0; i < cnt->masks; i++) { - psm__model_check_index_or_neg1( - src->mask_src.art_mesh_idx, i, cnt->art_meshes); - } - - psm__check_key_combo(src->glue_src, cnt->glue_keyforms, cnt->glues) - - /* Glue sources */ - for (psm__i32 i = 0; i < cnt->glues; i++) { - psm__model_check_index(src->glue_src.binding_idx, i, cnt->bindings); - psm__model_check_range(src->glue_src.keyform_off, - src->glue_src.key_len, i, cnt->glue_keyforms); - psm__model_check_index(src->glue_src.art_mesh_idx_a, i, cnt->art_meshes); - psm__model_check_index(src->glue_src.art_mesh_idx_b, i, cnt->art_meshes); - psm__model_check_range(src->glue_src.info_off, - src->glue_src.info_len, i, cnt->glue_info); - } - - /* Glue position indices */ - if (src->glue_src.info_off && src->glue_src.info_len && - src->glue_src.art_mesh_idx_a && src->glue_src.art_mesh_idx_b && - src->glue_info_src.pos_idx && src->art_mesh_src.vertex_count) { - for (psm__i32 i = 0; i < cnt->glues; i++) { - psm__i32 m0 = src->glue_src.art_mesh_idx_a[i]; - psm__i32 m1 = src->glue_src.art_mesh_idx_b[i]; - if (m0 < 0 || m0 >= cnt->art_meshes || m1 < 0 || m1 >= cnt->art_meshes) - continue; - psm__i32 vc0 = src->art_mesh_src.vertex_count[m0]; - psm__i32 vc1 = src->art_mesh_src.vertex_count[m1]; - psm__i32 ib = src->glue_src.info_off[i]; - psm__i32 ic = src->glue_src.info_len[i]; - if (ib < 0 || ic <= 0 || ib + ic > cnt->glue_info) - continue; - for (psm__i32 j = 0; j < ic; j += 2) { - psm__u16 p0 = src->glue_info_src.pos_idx[ib + j]; - PSM__FAIL(p0 >= (psm__u16)vc0, PSM__ERR_FILE_CORRUPT, - "glue[%d] pos_idx[%d]=%u OOB (vc=%d)", i, j, p0, vc0); - if (j + 1 < ic) { - psm__u16 p1 = src->glue_info_src.pos_idx[ib + j + 1]; - PSM__FAIL(p1 >= (psm__u16)vc1, PSM__ERR_FILE_CORRUPT, - "glue[%d] pos_idx[%d]=%u OOB (vc=%d)", i, j + 1, p1, vc1); - } - } - } - } - - /* Draw order group sources */ - for (psm__i32 i = 0; i < cnt->draw_groups; i++) { - psm__model_check_range(src->draw_group_src.obj_off, - src->draw_group_src.obj_len, i, cnt->draw_items); - } - - /* Draw order group object sources */ - if (src->draw_group_obj_src.type && src->draw_group_obj_src.idx) { - for (psm__i32 i = 0; i < cnt->draw_items; i++) { - psm__model_check_index_or_neg1(src->draw_group_obj_src.self_group_idx, - i, cnt->draw_groups); - psm__i32 t = src->draw_group_obj_src.type[i]; - psm__i32 oi = src->draw_group_obj_src.idx[i]; - PSM__FAIL(t != 0 && t != 1, PSM__ERR_FILE_CORRUPT, - "draw_item[%d]: bad type %d", i, t); - psm__i32 max = t ? cnt->parts : cnt->art_meshes; - PSM__FAIL(oi < 0 || oi >= max, PSM__ERR_FILE_CORRUPT, - "draw_item[%d]: index %d OOB (type=%d max=%d)", i, oi, t, max); - } - } - - if (ver < csmMocVersion_42) - goto done_ver; - - /* Warp deformer color indices */ - for (psm__i32 i = 0; i < cnt->warps; i++) { - psm__model_check_range(src->warp_src.key_color_off, - src->warp_src.key_len, i, cnt->keyform_mul_colors); - } - - /* Rotation deformer color indices */ - for (psm__i32 i = 0; i < cnt->rotations; i++) { - psm__model_check_range(src->rotation_src.key_color_off, - src->rotation_src.key_len, i, cnt->keyform_mul_colors); - } - - /* Art mesh color indices */ - for (psm__i32 i = 0; i < cnt->art_meshes; i++) { - psm__model_check_range(src->art_mesh_src.key_color_off, - src->art_mesh_src.key_len, i, cnt->keyform_mul_colors); - } - - /* Parameter extension sources */ - for (psm__i32 i = 0; i < cnt->parameters; i++) { - psm__model_check_range(src->param_keys_src.keys_off, - src->param_keys_src.keys_len, i, cnt->keys); - } - - /* Blend shape parameter binding sources */ - for (psm__i32 i = 0; - i < cnt->blend_key_tables; i++) { - psm__model_check_range(src->blend_key_table_src.keys_off, - src->blend_key_table_src.keys_len, i, cnt->keys); - } - - /* Parameter blend shape binding indices */ - for (psm__i32 i = 0; i < cnt->parameters; i++) { - psm__model_check_range(src->param_src.blend_key_table_off, - src->param_src.blend_key_table_len, i, cnt->blend_key_tables); - } - - /* Blend shape keyform binding sources */ - for (psm__i32 i = 0; i < cnt->blend_bindings; i++) { - psm__model_check_index(src->blend_binding_src.key_table_idx, - i, cnt->blend_key_tables); - psm__model_check_range(src->blend_binding_src.bs_constraint_idx_off, - src->blend_binding_src.bs_constraint_idx_len, - i, cnt->bs_constraint_idx); - } - - /* Blend shape warp deformer sources */ - for (psm__i32 i = 0; - i < cnt->bs_warps; i++) { - psm__model_check_index(src->bs_warp_src.target_idx, i, cnt->warps); - psm__model_check_range(src->bs_warp_src.bs_binding_off, - src->bs_warp_src.bs_binding_len, i, cnt->blend_bindings); - } - - /* Blend shape art mesh sources */ - for (psm__i32 i = 0; - i < cnt->bs_art_meshes; i++) { - psm__model_check_index(src->bs_art_mesh_src.target_idx, - i, cnt->art_meshes); - psm__model_check_range(src->bs_art_mesh_src.bs_binding_off, - src->bs_art_mesh_src.bs_binding_len, i, cnt->blend_bindings); - } - - /* Blend shape constraint index sources */ - for (psm__i32 i = 0; - i < cnt->bs_constraint_idx; i++) { - psm__model_check_index(src->blend_constraint_idx_src.constraint_idx, - i, cnt->bs_constraints); - } - - /* Blend shape constraint sources */ - for (psm__i32 i = 0; - i < cnt->bs_constraints; i++) { - psm__model_check_index(src->blend_constraint_src.parameter_idx, - i, cnt->parameters); - psm__model_check_range(src->blend_constraint_src.value_off, - src->blend_constraint_src.value_len, i, cnt->bs_constraint_vals); - } - - if (ver < csmMocVersion_50) - goto done_ver; - - /* Blend shape part sources */ - for (psm__i32 i = 0; i < cnt->bs_parts; i++) { - psm__model_check_index(src->bs_part_src.target_idx, i, cnt->parts); - psm__model_check_range(src->bs_part_src.bs_binding_off, - src->bs_part_src.bs_binding_len, i, cnt->blend_bindings); - } - - /* Blend shape rotation deformer sources */ - for (psm__i32 i = 0; - i < cnt->bs_rotations; i++) { - psm__model_check_index(src->bs_rotation_src.target_idx, i, cnt->rotations); - psm__model_check_range(src->bs_rotation_src.bs_binding_off, - src->bs_rotation_src.bs_binding_len, i, cnt->blend_bindings); - } - - /* Blend shape glue sources */ - for (psm__i32 i = 0; i < cnt->bs_glues; i++) { - psm__model_check_index(src->bs_glue_src.target_idx, i, cnt->glues); - psm__model_check_range(src->bs_glue_src.bs_binding_off, - src->bs_glue_src.bs_binding_len, i, cnt->blend_bindings); - } - - if (ver < csmMocVersion_53) - goto done_ver; - - /* Part offscreen rendering index */ - for (psm__i32 i = 0; i < cnt->parts; i++) { - psm__model_check_index_or_neg1(src->part_src.offscreen_idx, - i, cnt->offscreens); - } - - /* Offscreen rendering sources */ - for (psm__i32 i = 0; i < cnt->offscreens; i++) { - psm__model_check_index(src->offscreen_src.owner_idx, i, cnt->parts); - psm__model_check_range(src->offscreen_src.mask_off, - src->offscreen_src.mask_len, i, cnt->masks); - } - - /* Blend shape offscreen rendering sources */ - for (psm__i32 i = 0; - i < cnt->bs_offscreens; i++) { - psm__model_check_index(src->bs_offscreen_src.target_idx, - i, cnt->offscreens); - psm__model_check_range(src->bs_offscreen_src.bs_binding_off, - src->bs_offscreen_src.bs_binding_len, i, cnt->blend_bindings); - } - -done_ver: -#undef psm__model_check_index -#undef psm__model_check_index_or_neg1 -#undef psm__model_check_range - - return PSM__OK; -} - static void psm__bswap_model_data(psm__u8 ver, struct psm__sections *src) { @@ -555,10 +94,10 @@ psm__bswap_model_data(psm__u8 ver, struct psm__sections *src) psm__bswap_many_32(&src->canvas_info->height, 1); #define psm__bswap_predicate(TYPE, MEMBER, COUNT_MEMBER) \ - if (sizeof(TYPE) == 4) \ - psm__bswap_many_32(src->MEMBER, cnt->COUNT_MEMBER); \ - else if (sizeof(TYPE) == 2) \ - psm__bswap_many_16(src->MEMBER, cnt->COUNT_MEMBER); + if (sizeof(TYPE) == 4) \ + psm__bswap_many_32(src->MEMBER, cnt->COUNT_MEMBER); \ + else if (sizeof(TYPE) == 2) \ + psm__bswap_many_16(src->MEMBER, cnt->COUNT_MEMBER); PSM__SECTIONS_V30(psm__nop_predicate, psm__bswap_predicate) if (ver < csmMocVersion_33) goto done; @@ -577,85 +116,49 @@ done:; static int psm__init_moc3_sections(struct psm__moc3_data *moc3_data, - psm__u8 *p, psm_size n, psm_size off, psm__i32 needs_bswap) + psm__u8 *p, psm_size n, psm_size off, bool needs_bswap) { struct psm__sections *ms = moc3_data->sections; + psm__u32 *offsets = moc3_data->offsets; - psm__u8 ver = moc3_data->header->version; - psm_size sec_count, prev_end = off; + psm__u8 ver = moc3_data->header->version; + psm_size sec_count; if (ver >= csmMocVersion_53) - sec_count = sizeof(((struct psm__moc3_data_v53 *)0)->offsets) / sizeof(psm__u32); + sec_count = + sizeof(((struct psm__moc3_data_v53 *)0)->offsets) / sizeof(psm__u32); else - sec_count = sizeof(((struct psm__moc3_data_v52 *)0)->offsets) / sizeof(psm__u32); + sec_count = + sizeof(((struct psm__moc3_data_v52 *)0)->offsets) / sizeof(psm__u32); if (needs_bswap) psm__bswap_many_32(offsets, sec_count); + psm_size ci_ints = PSM__COUNT_INFO_INTS(ver); + PSM__FAILM((offsets[0] & 3) != 0, PSM__ERR_FILE_CORRUPT, "count_info misaligned"); - PSM__FAILM(offsets[0] > n || n - offsets[0] < sizeof(struct psm__count_info), + PSM__FAILM(offsets[0] > n || n - offsets[0] < ci_ints * sizeof(psm__i32), PSM__ERR_FILE_CORRUPT, "count_info offset oob"); PSM__FAILM(offsets[0] < off, PSM__ERR_FILE_CORRUPT, "count_info before header end"); if (needs_bswap) - psm__bswap_many_32(p + offsets[0], - sizeof(struct psm__count_info) / sizeof(psm__i32)); + psm__bswap_many_32(p + offsets[0], ci_ints); #ifndef PSM_FAST_AND_DANGEROUS { - int err = psm__verify_count_info( + int err = psm__verify_count_info(ver, (const struct psm__count_info *)(p + offsets[0])); if (err != PSM__OK) return err; } -#endif - - int i = 0; -#ifdef PSM_FAST_AND_DANGEROUS - (void)n; (void)prev_end; -#define psm__predicate_static(TYPE, MEMBER, COUNT) \ - ms->MEMBER = (TYPE *)(p + offsets[i++]); -#define psm__predicate_dynamic(TYPE, MEMBER, COUNT_MEMBER) \ - ms->MEMBER = (TYPE *)(p + offsets[i++]); + if (psm__verify_sections(ms, p, offsets, n, off, ver, true) != PSM__OK) + return PSM__ERR_FILE_CORRUPT; #else -#define psm__predicate_static(TYPE, MEMBER, COUNT) { \ - psm__bounds_check_static(TYPE, COUNT, offsets, i, n) \ - psm_size _static_end = offsets[i] + sizeof(TYPE) * (COUNT); \ - if (_static_end > prev_end) prev_end = _static_end; \ - ms->MEMBER = (TYPE *)(p + offsets[i]); \ - i++; \ - } -#define psm__predicate_dynamic(TYPE, MEMBER, COUNT_MEMBER) { \ - psm__bounds_check_dynamic(TYPE, COUNT_MEMBER, offsets, i, n, ms->count_info) \ - if (offsets[i] < prev_end) { \ - PSM__LOGF("section[%d] not monotonic: off=%u prev=%u sz=%u", \ - i, (unsigned)offsets[i], (unsigned)prev_end, \ - (unsigned)_sz); \ - return PSM__ERR_FILE_CORRUPT; \ - } \ - prev_end = offsets[i] + _sz; \ - ms->MEMBER = _sz ? (TYPE *)(p + offsets[i]) : NULL; \ - i++; \ - } + psm__verify_sections(ms, p, offsets, n, off, ver, false); #endif - PSM__SECTIONS_V30(psm__predicate_static, psm__predicate_dynamic) - if (ver < csmMocVersion_33) goto done; - PSM__SECTIONS_V33(psm__predicate_static, psm__predicate_dynamic) - if (ver < csmMocVersion_42) goto done; - PSM__SECTIONS_V42(psm__predicate_static, psm__predicate_dynamic) - if (ver < csmMocVersion_50) goto done; - PSM__SECTIONS_V50(psm__predicate_static, psm__predicate_dynamic) - if (ver < csmMocVersion_53) goto done; - - PSM__SECTIONS_V53(psm__predicate_static, psm__predicate_dynamic) - -done: -#undef psm__predicate_static -#undef psm__predicate_dynamic - if (needs_bswap) psm__bswap_model_data(ver, ms); @@ -666,10 +169,13 @@ static int psm__has_moc_consistency(const psm__u8 *p, psm_size n) { psm__u8 ver, endian_flag; - psm__i32 needs_bswap; - psm_size header_size, sec_count; + bool needs_bswap; + + psm_size header_size, sec_count, off; psm__u32 *offsets; + struct psm__count_info *cnt = NULL; + int result = PSM__OK; PSM__FAILM(n < sizeof(struct psm__moc3_header), @@ -688,10 +194,14 @@ psm__has_moc_consistency(const psm__u8 *p, psm_size n) if (ver >= csmMocVersion_53) { header_size = sizeof(struct psm__moc3_data_v53); - sec_count = sizeof(((struct psm__moc3_data_v53 *)0)->offsets) / sizeof(psm__u32); + sec_count = + sizeof(((struct psm__moc3_data_v53 *)0)->offsets) / sizeof(psm__u32); + off = offsetof(struct psm__moc3_data_v53, sections); } else { header_size = sizeof(struct psm__moc3_data_v52); - sec_count = sizeof(((struct psm__moc3_data_v52 *)0)->offsets) / sizeof(psm__u32); + sec_count = + sizeof(((struct psm__moc3_data_v52 *)0)->offsets) / sizeof(psm__u32); + off = offsetof(struct psm__moc3_data_v52, sections); } PSM__FAILM(n < header_size, @@ -717,56 +227,45 @@ psm__has_moc_consistency(const psm__u8 *p, psm_size n) result = PSM__ERR_FILE_CORRUPT; goto restore; } - if (offsets[0] > n || n - offsets[0] < sizeof(struct psm__count_info)) { + if (offsets[0] > n || + n - offsets[0] < PSM__COUNT_INFO_INTS(ver) * sizeof(psm__i32)) { PSM__LOG("count_info out of bounds"); result = PSM__ERR_FILE_CORRUPT; goto restore; } + if (offsets[0] < off) { + PSM__LOG("count_info before header end"); + result = PSM__ERR_FILE_CORRUPT; + goto restore; + } cnt = (struct psm__count_info *)(p + offsets[0]); if (needs_bswap) - psm__bswap_many_32(cnt, sizeof(struct psm__count_info) / sizeof(psm__i32)); + psm__bswap_many_32(cnt, PSM__COUNT_INFO_INTS(ver)); - result = psm__verify_count_info(cnt); + result = psm__verify_count_info(ver, cnt); if (result != PSM__OK) goto restore; - /* Validate all sections using bounds-checking predicates */ { - int i = 0; - -#define psm__predicate_static(TYPE, MEMBER, COUNT) { \ - psm__bounds_check_static(TYPE, COUNT, offsets, i, n) \ - i++; \ - } -#define psm__predicate_dynamic(TYPE, MEMBER, COUNT_MEMBER) { \ - psm__bounds_check_dynamic(TYPE, COUNT_MEMBER, offsets, i, n, cnt) \ - i++; \ - } - - PSM__SECTIONS_V30(psm__predicate_static, psm__predicate_dynamic) - if (ver < csmMocVersion_33) goto restore; - - PSM__SECTIONS_V33(psm__predicate_static, psm__predicate_dynamic) - if (ver < csmMocVersion_42) goto restore; - - PSM__SECTIONS_V42(psm__predicate_static, psm__predicate_dynamic) - if (ver < csmMocVersion_50) goto restore; - - PSM__SECTIONS_V50(psm__predicate_static, psm__predicate_dynamic) - if (ver < csmMocVersion_53) goto restore; - - PSM__SECTIONS_V53(psm__predicate_static, psm__predicate_dynamic) + struct psm__sections tmp; + memset(&tmp, 0, sizeof tmp); + result = + psm__verify_sections(&tmp, (psm__u8 *)p, offsets, n, off, ver, true); + if (result != PSM__OK) + goto restore; -#undef psm__predicate_static -#undef psm__predicate_dynamic + if (needs_bswap) + psm__bswap_model_data(ver, &tmp); + result = psm__verify_idx(ver, &tmp); + if (needs_bswap) + psm__bswap_model_data(ver, &tmp); } restore: if (needs_bswap) { if (cnt) - psm__bswap_many_32(cnt, - sizeof(struct psm__count_info) / sizeof(psm__i32)); + psm__bswap_many_32(cnt, PSM__COUNT_INFO_INTS(ver)); psm__bswap_many_32(offsets, sec_count); } @@ -777,10 +276,11 @@ static int psm__revive_moc_in_place(struct psm__moc3_data **moc, psm__u8 *p, psm_size n) { struct psm__moc3_data *moc3_data; + psm_size off = 0; PSM__FAILM(memcmp(p, PSM__MOC3_MAGIC, PSM__MOC3_MAGIC_SIZE) != 0, - PSM__ERR_FILE_CORRUPT, "unknown magic"); + PSM__ERR_FILE_UNRECOGNIZED, "unknown magic"); psm__u8 ver = *(p + PSM__MOC3_MAGIC_SIZE); PSM__FAILM(ver > csmMocVersion_53, @@ -795,42 +295,41 @@ psm__revive_moc_in_place(struct psm__moc3_data **moc, psm__u8 *p, psm_size n) PSM__FAILM(n < off + sizeof(struct psm__moc3_data), PSM__ERR_FILE_CORRUPT, "buffer too small"); - struct psm__moc3_data_v53 *lay = (struct psm__moc3_data_v53 *)p; - moc3_data = &lay->sections.source; + struct psm__moc3_data_v53 *layout = (struct psm__moc3_data_v53 *)p; + moc3_data = &layout->sections.source; - moc3_data->header = &lay->header; - moc3_data->offsets = lay->offsets; - moc3_data->sections = &lay->sections; - lay->header.data = moc3_data; - } else - { + moc3_data->header = &layout->header; + moc3_data->offsets = layout->offsets; + moc3_data->sections = &layout->sections; + layout->header.data = moc3_data; + } else { off = offsetof(struct psm__moc3_data_v52, sections); PSM__FAILM(n < off + sizeof(struct psm__moc3_data), PSM__ERR_FILE_CORRUPT, "buffer too small"); - struct psm__moc3_data_v52 *lay = (struct psm__moc3_data_v52 *)p; - moc3_data = &lay->sections.source; + struct psm__moc3_data_v52 *layout = (struct psm__moc3_data_v52 *)p; + moc3_data = &layout->sections.source; - moc3_data->header = &lay->header; - moc3_data->offsets = lay->offsets; - moc3_data->sections = &lay->sections; - lay->header.data = moc3_data; + moc3_data->header = &layout->header; + moc3_data->offsets = layout->offsets; + moc3_data->sections = &layout->sections; + layout->header.data = moc3_data; } - psm__i32 is_le = psm__is_le(); - psm__i32 needs_bswap = is_le != (moc3_data->header->endian_flag == 0); + bool is_le = psm__is_le(); + bool needs_bswap = is_le != (moc3_data->header->endian_flag == 0); if (needs_bswap) - moc3_data->header->endian_flag = (is_le == 0); + moc3_data->header->endian_flag = !is_le; PSM__FAILM(psm__init_moc3_sections(moc3_data, p, n, off, - needs_bswap) != PSM__OK, + needs_bswap) != PSM__OK, PSM__ERR_FILE_CORRUPT, "model data init failed"); - struct psm__sections *src = moc3_data->sections; + struct psm__sections *src = moc3_data->sections; struct psm__count_info *cnt = src->count_info; #ifndef PSM_FAST_AND_DANGEROUS - PSM__FAILM(psm__verify_indices(ver, src) != PSM__OK, + PSM__FAILM(psm__verify_idx(ver, src) != PSM__OK, PSM__ERR_FILE_CORRUPT, "source index validation failed"); #endif @@ -848,7 +347,7 @@ psm__revive_moc_in_place(struct psm__moc3_data **moc, psm__u8 *p, psm_size n) if (m_cnt <= 0 || !psm__check_offset_range(mb[i], m_cnt, cnt->masks)) continue; psm__i32 *masks = &mi[mb[i]]; - psm__i32 valid = m_cnt; + psm__i32 valid = m_cnt; if (m_cnt > 1) { psm__i32 w = 0; @@ -936,7 +435,7 @@ psm__revive_moc_in_place(struct psm__moc3_data **moc, psm__u8 *p, psm_size n) } if (src->canvas_info && (src->canvas_info->flag & - PSM__CANVAS_FLAG_Y_REVERSED) == 0) { + PSM__CANVAS_FLAG_Y_REVERSED) == 0) { psm__u16 *pos_idx = src->idx_src.idx; psm__i32 *idx_off = src->art_mesh_src.idx_off; psm__i32 *idx_cnt = src->art_mesh_src.idx_len; @@ -966,8 +465,8 @@ psm__revive_moc_in_place(struct psm__moc3_data **moc, psm__u8 *p, psm_size n) for (psm__i32 i = 0; i < count; i++) { psm__i32 vc = vert_cnt[i]; psm__i32 ub = uv_off[i]; - if (vc <= 0 || ub < 0 || (psm__u32)ub + 2u * (psm__u32)vc > - (psm__u32)cnt->uvs) + if (vc <= 0 || ub < 0 || + (psm__u32)ub + 2u * (psm__u32)vc > (psm__u32)cnt->uvs) continue; psm__f32 *uv = &uv_xy[ub]; for (psm__i32 j = 0; j < vc; j++) @@ -981,7 +480,6 @@ psm__revive_moc_in_place(struct psm__moc3_data **moc, psm__u8 *p, psm_size n) return PSM__OK; } - PSMDEF csmMocVersion csmGetMocVersion(const void *address, unsigned int size) { @@ -1000,16 +498,36 @@ csmHasMocConsistency(void *address, unsigned int size) PSMDEF csmMoc * csmReviveMocInPlace(void *address, unsigned int size) { - static psm__i32 first_call = 1; + static bool first_call = true; if (first_call) { - psm__debug_print(PSM__LOG_OFF, "Sakura2D Purism Core version " PSM__VERFMT - " (compat " PSM__VERFMT ")\n", PSM__VERARG(PSM_TRUE_VERSION), - PSM__VERARG(PSM_COMPAT_VERSION)); - first_call = 0; + psm__debug_print(PSM__LOG_OFF, + "Sakura2D Purism Core version " PSM__VERFMT " (compat " PSM__VERFMT + ")\n", + PSM__VERARG(PSM_TRUE_VERSION), PSM__VERARG(PSM_COMPAT_VERSION)); + first_call = false; } struct psm__moc3_data *moc; + int err = psm__revive_moc_in_place(&moc, (psm__u8 *)address, size); + + /* + * Record the outcome in the header's scratch space so a caller can ask + * csmGetMocError(address) why a load failed, even though we return NULL. + * Only safe once the buffer is known to hold a full header. + */ + if (size >= sizeof(struct psm__moc3_header)) + ((struct psm__moc3_header *)address)->last_error = err; + PSM__FAILM(err != PSM__OK, NULL, "could not revive MOC3"); return (csmMoc *)address; } + +/* Purism Core extension: see PurismCore.h. */ +PSMDEF csmError +csmGetMocError(const csmMoc *moc) +{ + if (!moc) + return csmError_NoError; + return (csmError)((const struct psm__moc3_header *)moc)->last_error; +} diff --git a/src/moc3.h b/src/moc3.h index 3d59d53..a9f63e8 100644 --- a/src/moc3.h +++ b/src/moc3.h @@ -17,12 +17,16 @@ struct psm__count_info; struct psm__canvas_info; struct psm__moc3_header { - char magic[4]; + char magic[4]; psm__u8 version; psm__u8 endian_flag; - psm__u8 _reserved1[2]; + psm__u8 padding1_[2]; + struct psm__moc3_data *data; - psm__u8 _reserved2[56 - sizeof(void *)]; + + psm__i32 last_error; + + psm__u8 padding2_[52 - sizeof(void *)]; }; psm__static_assert(sizeof(struct psm__moc3_header) == 64, @@ -30,8 +34,8 @@ psm__static_assert(sizeof(struct psm__moc3_header) == 64, struct psm__moc3_data { struct psm__moc3_header *header; - psm__u32 *offsets; - struct psm__sections *sections; + psm__u32 *offsets; + struct psm__sections *sections; }; struct psm__id { @@ -77,40 +81,45 @@ struct psm__count_info { psm__i32 offscreens; psm__i32 offscreen_keyforms; psm__i32 bs_offscreens; - psm__i32 _reserved; + psm__i32 padding_[26]; }; +psm__static_assert(sizeof(struct psm__count_info) == 64 * sizeof(psm__i32), + "count_info must be 256 bytes"); + +#define PSM__COUNT_INFO_INTS(ver) ((ver) >= csmMocVersion_50 ? 64 : 32) + struct psm__canvas_info { psm__f32 pix_per_unit; psm__f32 origin_x; psm__f32 origin_y; psm__f32 width; psm__f32 height; - psm__u8 flag; + psm__u8 flag; }; struct psm__part_src { - const char **id_runtime; + const char **id_runtime; struct psm__id *id; - psm__i32 *binding_idx; - psm__i32 *keyform_off; - psm__i32 *key_len; - psm__i32 *visible; - psm__i32 *enable; - psm__i32 *parent_part_idx; - psm__i32 *offscreen_idx; + psm__i32 *binding_idx; + psm__i32 *keyform_off; + psm__i32 *key_len; + psm__i32 *visible; + psm__i32 *enable; + psm__i32 *parent_part_idx; + psm__i32 *offscreen_idx; }; struct psm__deformer_src { - const char **id_runtime; + const char **id_runtime; struct psm__id *id; - psm__i32 *binding_idx; - psm__i32 *visible; - psm__i32 *enable; - psm__i32 *parent_part_idx; - psm__i32 *parent_deformer_idx; - psm__i32 *type; - psm__i32 *local_idx; + psm__i32 *binding_idx; + psm__i32 *visible; + psm__i32 *enable; + psm__i32 *parent_part_idx; + psm__i32 *parent_deformer_idx; + psm__i32 *type; + psm__i32 *local_idx; }; struct psm__warp_src { @@ -133,55 +142,55 @@ struct psm__rotation_src { }; struct psm__art_mesh_src { - const char **id_runtime; + const char **id_runtime; const psm__f32 **uv_runtime; const psm__u16 **pos_idx_runtime; const psm__i32 **drawable_mask_runtime; - void *id; - psm__i32 *binding_idx; - psm__i32 *keyform_off; - psm__i32 *key_len; - psm__i32 *key_color_off; - psm__i32 *visible; - psm__i32 *enable; - psm__i32 *parent_part_idx; - psm__i32 *parent_deformer_idx; - psm__i32 *texture_no; - psm__u8 *drawable_flag; - psm__i32 *blend_mode; - psm__i32 *vertex_count; - psm__i32 *uv_off; - psm__i32 *idx_off; - psm__i32 *idx_len; - psm__i32 *mask_off; - psm__i32 *mask_len; + void *id; + psm__i32 *binding_idx; + psm__i32 *keyform_off; + psm__i32 *key_len; + psm__i32 *key_color_off; + psm__i32 *visible; + psm__i32 *enable; + psm__i32 *parent_part_idx; + psm__i32 *parent_deformer_idx; + psm__i32 *texture_no; + psm__u8 *drawable_flag; + psm__i32 *blend_mode; + psm__i32 *vertex_count; + psm__i32 *uv_off; + psm__i32 *idx_off; + psm__i32 *idx_len; + psm__i32 *mask_off; + psm__i32 *mask_len; }; struct psm__param_src { - const char **id_runtime; + const char **id_runtime; struct psm__id *id; - psm__f32 *maximum_value; - psm__f32 *minimum_value; - psm__f32 *default_value; - psm__i32 *repeat; - psm__i32 *decimal_places; - psm__i32 *type; - psm__i32 *key_table_off; - psm__i32 *key_table_len; - psm__i32 *blend_key_table_off; - psm__i32 *blend_key_table_len; + psm__f32 *maximum_value; + psm__f32 *minimum_value; + psm__f32 *default_value; + psm__i32 *repeat; + psm__i32 *decimal_places; + psm__i32 *type; + psm__i32 *key_table_off; + psm__i32 *key_table_len; + psm__i32 *blend_key_table_off; + psm__i32 *blend_key_table_len; }; struct psm__glue_src { - const char **id_runtime; + const char **id_runtime; struct psm__id *id; - psm__i32 *binding_idx; - psm__i32 *keyform_off; - psm__i32 *key_len; - psm__i32 *art_mesh_idx_a; - psm__i32 *art_mesh_idx_b; - psm__i32 *info_off; - psm__i32 *info_len; + psm__i32 *binding_idx; + psm__i32 *keyform_off; + psm__i32 *key_len; + psm__i32 *art_mesh_idx_a; + psm__i32 *art_mesh_idx_b; + psm__i32 *info_off; + psm__i32 *info_len; }; struct psm__part_key_src { @@ -275,8 +284,8 @@ struct psm__glue_info_src { struct psm__param_keys_src { const psm__f32 **key_runtime; - psm__i32 *keys_off; - psm__i32 *keys_len; + psm__i32 *keys_off; + psm__i32 *keys_len; }; struct psm__blend_key_table_src { @@ -316,11 +325,11 @@ struct psm__blend_constraint_val_src { struct psm__offscreen_src { const psm__i32 **drawable_mask_runtime; - psm__i32 *owner_idx; - psm__u8 *drawable_flag; - psm__i32 *blend_mode; - psm__i32 *mask_off; - psm__i32 *mask_len; + psm__i32 *owner_idx; + psm__u8 *drawable_flag; + psm__i32 *blend_mode; + psm__i32 *mask_off; + psm__i32 *mask_len; }; struct psm__key_color_src { @@ -336,72 +345,72 @@ struct psm__offscreen_key_src { }; struct psm__sections { - struct psm__moc3_data source; - struct psm__count_info *count_info; + struct psm__moc3_data source; + struct psm__count_info *count_info; struct psm__canvas_info *canvas_info; - struct psm__part_src part_src; + struct psm__part_src part_src; struct psm__deformer_src deformer_src; - struct psm__warp_src warp_src; + struct psm__warp_src warp_src; struct psm__rotation_src rotation_src; struct psm__art_mesh_src art_mesh_src; - struct psm__param_src param_src; + struct psm__param_src param_src; struct psm__param_keys_src param_keys_src; - struct psm__part_key_src part_key_src; - struct psm__warp_key_src warp_key_src; + struct psm__part_key_src part_key_src; + struct psm__warp_key_src warp_key_src; struct psm__rotation_key_src rotation_key_src; struct psm__art_mesh_key_src art_mesh_key_src; - struct psm__key_pos_src key_pos_src; - struct psm__key_table_src key_table_src; + struct psm__key_pos_src key_pos_src; + struct psm__key_table_src key_table_src; struct psm__key_table_idx_src key_table_idx_src; - struct psm__binding_src binding_src; - - struct psm__blend_key_table_src blend_key_table_src; - struct psm__blend_binding_src blend_binding_src; - struct psm__blend_src bs_part_src; - struct psm__blend_src bs_warp_src; - struct psm__blend_src bs_rotation_src; - struct psm__blend_src bs_art_mesh_src; - struct psm__blend_src bs_glue_src; + struct psm__binding_src binding_src; + + struct psm__blend_key_table_src blend_key_table_src; + struct psm__blend_binding_src blend_binding_src; + struct psm__blend_src bs_part_src; + struct psm__blend_src bs_warp_src; + struct psm__blend_src bs_rotation_src; + struct psm__blend_src bs_art_mesh_src; + struct psm__blend_src bs_glue_src; struct psm__blend_constraint_idx_src blend_constraint_idx_src; - struct psm__blend_constraint_src blend_constraint_src; + struct psm__blend_constraint_src blend_constraint_src; struct psm__blend_constraint_val_src blend_constraint_val_src; - struct psm__keys_src keys_src; - struct psm__uv_src uv_src; + struct psm__keys_src keys_src; + struct psm__uv_src uv_src; struct psm__pos_idx_src idx_src; - struct psm__mask_src mask_src; + struct psm__mask_src mask_src; - struct psm__draw_group_src draw_group_src; + struct psm__draw_group_src draw_group_src; struct psm__draw_group_obj_src draw_group_obj_src; - struct psm__glue_src glue_src; + struct psm__glue_src glue_src; struct psm__glue_info_src glue_info_src; - struct psm__glue_key_src glue_key_src; + struct psm__glue_key_src glue_key_src; struct psm__key_color_src keyform_mul_color_src; struct psm__key_color_src keyform_scr_color_src; - struct psm__offscreen_src offscreen_src; + struct psm__offscreen_src offscreen_src; struct psm__offscreen_key_src offscreen_key_src; - struct psm__blend_src bs_offscreen_src; + struct psm__blend_src bs_offscreen_src; }; /* MOC3 v1-v5: 160 section offsets */ struct psm__moc3_data_v52 { struct psm__moc3_header header; - psm__u32 offsets[160]; - struct psm__sections sections; + psm__u32 offsets[160]; + struct psm__sections sections; }; /* MOC3 v6+: 480 section offsets */ struct psm__moc3_data_v53 { struct psm__moc3_header header; - psm__u32 offsets[480]; - struct psm__sections sections; + psm__u32 offsets[480]; + struct psm__sections sections; }; /* diff --git a/src/model.c b/src/model.c index ca1204d..c903423 100644 --- a/src/model.c +++ b/src/model.c @@ -49,11 +49,12 @@ psm__remap_blend_mode(psm__i32 mode) } /* - * Safe lookup of param_count via binding index; - * returns 0 for out-of-bounds. + * Number of key tables (parameter axes) bound to object idx_arr[i], clamped to + * PSM__MAX_KEY_TABLES. Self-bounds-checking: returns 0 for an out-of-range + * binding index, so it is safe to call from the size pass before validation. */ static inline psm__i32 -psm__safe_param_count(const struct psm__sections *src, +psm__binding_param_count(const struct psm__sections *src, const struct psm__count_info *cnt, const psm__i32 *idx_arr, psm__i32 i) { psm__i32 bi = idx_arr[i]; @@ -63,16 +64,74 @@ psm__safe_param_count(const struct psm__sections *src, 0, PSM__MAX_KEY_TABLES); } +/* + * Overflow-checked accumulation for workspace size totals. + * On overflow, marks the arena bad (so psm__arena_ok fails and the + * model is rejected at load) and returns the accumulator unchanged. + */ +static inline psm__u32 +psm__acc_add(struct psm__arena *a, psm__u32 acc, psm__u32 add) +{ + psm__u32 r = acc + add; + if (r < acc) { + a->overflow = 1; + return acc; + } + return r; +} + +/* + * 16-byte-aligned byte size of a vc-vertex position buffer (2 floats + * per vertex), with overflow detection before the u32 truncation that + * psm__align_to_16 would otherwise hide. Caller guarantees vc >= 0. + */ +static inline psm__u32 +psm__pos_bytes(struct psm__arena *a, psm__i32 vc) +{ + psm__u32 v = (psm__u32)vc; + if (v > (0xFFFFFFFFu - 15u) / (2u * (psm__u32)sizeof(psm__f32))) { + a->overflow = 1; + return 0; + } + return psm__align_to_16(2u * (psm__u32)sizeof(psm__f32) * v); +} + +#define PSM__MAX_COMB(pc) ((psm__u32)1 << (pc)) + +/* + * Per-object-type scratch sizing for the arena dry-run. Both use the + * overflow-checked accumulator, so a bad count saturates the arena rather than + * wrapping; a zero count or NULL source array contributes nothing. + */ +static psm__u32 +psm__sum_max_comb(struct psm__arena *a, const struct psm__sections *src, + struct psm__count_info *cnt, const psm__i32 *binding_idx, psm__i32 count) +{ + psm__u32 total = 0; + if (count > 0 && binding_idx) + for (psm__i32 i = 0; i < count; i++) + total = psm__acc_add(a, total, + PSM__MAX_COMB(psm__binding_param_count(src, cnt, binding_idx, i))); + return total; +} + +static psm__u32 +psm__sum_pos_bytes(struct psm__arena *a, const psm__i32 *vertex_count, + psm__i32 count) +{ + psm__u32 total = 0; + if (count > 0 && vertex_count) + for (psm__i32 i = 0; i < count; i++) + if (vertex_count[i] > 0) + total = psm__acc_add(a, total, psm__pos_bytes(a, vertex_count[i])); + return total; +} + static struct psm__model * psm__alloc_model(struct psm__arena *arena, psm__u8 ver, const struct psm__sections *src, struct psm__count_info *cnt) { - - /* - * Field allocation macros - allocate and assign only - * in real mode. In dry-run mode, psm__arena_alloc - * returns NULL so assignment is skipped. - */ +// clang-format off #define psm__alloc_field(field, T, n) do { \ void *_p = psm__arena_alloc(arena, \ psm__arena_safe_mul(arena, sizeof(T), (n))); \ @@ -83,70 +142,35 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, if (_p) (field) = (T *)_p; \ } while (0) -#define MAX_COMB(pc) ((psm__u32)1 << (pc)) +// clang-format on + psm__u32 part_tmp_total = psm__sum_max_comb(arena, src, cnt, + src->part_src.binding_idx, cnt->parts); - psm__u32 part_tmp_total = 0; - if (cnt->parts > 0 && src->part_src.binding_idx) { - for (psm__i32 i = 0; i < cnt->parts; i++) { - psm__i32 pc = psm__safe_param_count(src, cnt, - src->part_src.binding_idx, i); - part_tmp_total += MAX_COMB(pc); - } - } + psm__u32 warp_tmp_total = psm__sum_max_comb(arena, src, cnt, + src->warp_src.binding_idx, cnt->warps); + psm__u32 warp_pos_total = psm__sum_pos_bytes(arena, + src->warp_src.vertex_count, cnt->warps); - psm__u32 warp_pos_total = 0, warp_tmp_total = 0; - if (cnt->warps > 0 && src->warp_src.binding_idx && - src->warp_src.vertex_count) { - for (psm__i32 i = 0; i < cnt->warps; i++) { - psm__i32 vc = src->warp_src.vertex_count[i]; - if (vc > 0) - warp_pos_total += psm__align_to_16(2 * sizeof(psm__f32) * vc); - psm__i32 pc = psm__safe_param_count(src, cnt, - src->warp_src.binding_idx, i); - warp_tmp_total += MAX_COMB(pc); - } - } + psm__u32 rot_tmp_total = psm__sum_max_comb(arena, src, cnt, + src->rotation_src.binding_idx, cnt->rotations); - psm__u32 rot_tmp_total = 0; - if (cnt->rotations > 0 && src->rotation_src.binding_idx) { - for (psm__i32 i = 0; i < cnt->rotations; i++) { - psm__i32 pc = psm__safe_param_count(src, cnt, - src->rotation_src.binding_idx, i); - rot_tmp_total += MAX_COMB(pc); - } - } - - psm__u32 am_pos_total = 0, am_tmp_total = 0; - if (cnt->art_meshes > 0 && src->art_mesh_src.binding_idx && - src->art_mesh_src.vertex_count) { - for (psm__i32 i = 0; i < cnt->art_meshes; i++) { - psm__i32 vc = src->art_mesh_src.vertex_count[i]; - if (vc > 0) - am_pos_total += psm__align_to_16(2 * sizeof(psm__f32) * vc); - psm__i32 pc = psm__safe_param_count(src, cnt, - src->art_mesh_src.binding_idx, i); - am_tmp_total += MAX_COMB(pc); - } - } + psm__u32 am_tmp_total = psm__sum_max_comb(arena, src, cnt, + src->art_mesh_src.binding_idx, cnt->art_meshes); + psm__u32 am_pos_total = psm__sum_pos_bytes(arena, + src->art_mesh_src.vertex_count, cnt->art_meshes); psm__u32 kb_ptr_total = 0, kb_idx_total = 0; if (cnt->bindings > 0 && src->binding_src.key_table_idx_len) { for (psm__i32 i = 0; i < cnt->bindings; i++) { psm__i32 pc = psm__clamp_i32(src->binding_src.key_table_idx_len[i], 0, PSM__MAX_KEY_TABLES); - kb_ptr_total += pc; - kb_idx_total += MAX_COMB(pc); + kb_ptr_total = psm__acc_add(arena, kb_ptr_total, (psm__u32)pc); + kb_idx_total = psm__acc_add(arena, kb_idx_total, PSM__MAX_COMB(pc)); } } - psm__u32 glue_tmp_total = 0; - if (cnt->glues > 0 && src->glue_src.binding_idx) { - for (psm__i32 i = 0; i < cnt->glues; i++) { - psm__i32 pc = psm__safe_param_count(src, cnt, - src->glue_src.binding_idx, i); - glue_tmp_total += MAX_COMB(pc); - } - } + psm__u32 glue_tmp_total = psm__sum_max_comb(arena, src, cnt, + src->glue_src.binding_idx, cnt->glues); psm__i32 do_max_count = 0, do_max_level = 0; if (cnt->draw_groups > 0 && src->draw_group_src.obj_len && @@ -167,7 +191,8 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, if (ver >= csmMocVersion_42 && src->blend_binding_src.bs_constraint_idx_len) { for (psm__i32 i = 0; i < cnt->blend_bindings; i++) { - bs_constr_ptrs_total += src->blend_binding_src.bs_constraint_idx_len[i]; + bs_constr_ptrs_total = psm__acc_add(arena, bs_constr_ptrs_total, + (psm__u32)src->blend_binding_src.bs_constraint_idx_len[i]); } } @@ -177,9 +202,9 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, for (psm__i32 i = 0; i < cnt->offscreens; i++) { psm__i32 oi = src->offscreen_src.owner_idx[i]; if (!psm__valid_idx(oi, cnt->parts)) continue; - psm__i32 pc = psm__safe_param_count(src, cnt, + psm__i32 pc = psm__binding_param_count(src, cnt, src->part_src.binding_idx, oi); - os_tmp_total += MAX_COMB(pc); + os_tmp_total = psm__acc_add(arena, os_tmp_total, PSM__MAX_COMB(pc)); } } @@ -195,6 +220,7 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, /* Parts */ psm__alloc_field(m->parts.items, struct psm__part, cnt->parts); + psm__alloc_field(m->parts.bindings, struct psm__binding *, cnt->parts); psm__alloc_field(m->parts.opacity, psm__f32, cnt->parts); psm__alloc_field(m->parts.draw_order, psm__i32, cnt->parts); psm__alloc_field(m->parts.input_opacity, psm__f32, cnt->parts); @@ -219,9 +245,10 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, psm__alloc_field(m->deformers.scr_color, psm__f32, 4 * cnt->deformers); /* Warp deformers */ - struct psm__warps *w = &m->deformers.warps; + struct psm__warps *w = &m->deformers.warps; struct psm__warp_keydata *wk = &w->keydata; psm__alloc_field(w->items, struct psm__warp, cnt->warps); + psm__alloc_field(w->bindings, struct psm__binding *, cnt->warps); psm__alloc_field(w->enable, bool, cnt->warps); psm__alloc_field(w->opacity, psm__f32, cnt->warps); psm__alloc_field(w->pos, psm__f32 *, cnt->warps); @@ -243,9 +270,10 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, psm__alloc_field(wk->scr_color.b, psm__f32, warp_tmp_total); /* Rotation deformers */ - struct psm__rotations *r = &m->deformers.rotations; + struct psm__rotations *r = &m->deformers.rotations; struct psm__rotation_keydata *rk = &r->keydata; psm__alloc_field(r->items, struct psm__rotation, cnt->rotations); + psm__alloc_field(r->bindings, struct psm__binding *, cnt->rotations); psm__alloc_field(r->enable, bool, cnt->rotations); psm__alloc_field(r->opacity, psm__f32, cnt->rotations); psm__alloc_field(r->scale, psm__f32, cnt->rotations); @@ -273,9 +301,10 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, psm__alloc_field(rk->scr_color.b, psm__f32, rot_tmp_total); /* Art meshes */ - struct psm__art_meshes *am = &m->art_meshes; + struct psm__art_meshes *am = &m->art_meshes; struct psm__art_mesh_keydata *ak = &am->keydata; psm__alloc_field(am->meshes, struct psm__art_mesh, cnt->art_meshes); + psm__alloc_field(am->bindings, struct psm__binding *, cnt->art_meshes); psm__alloc_field(am->enable, bool, cnt->art_meshes); psm__alloc_field(am->const_flags, psm__u8, cnt->art_meshes); psm__alloc_field(am->change_flags, psm__u8, cnt->art_meshes); @@ -369,6 +398,7 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, /* Glues */ struct psm__glue_keydata *gk = &m->glues.keydata; psm__alloc_field(m->glues.items, struct psm__glue, cnt->glues); + psm__alloc_field(m->glues.bindings, struct psm__binding *, cnt->glues); psm__alloc_field(gk->interp.max_blend, psm__i32, cnt->glues); psm__alloc_field(gk->interp.blend_count, psm__i32, cnt->glues); psm__alloc_field(gk->interp.tmp, psm__f32, glue_tmp_total); @@ -378,7 +408,7 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, /* Offscreen rendering (v5.3+) */ if (ver >= csmMocVersion_53) { - struct psm__offscreens *os = &m->offscreens; + struct psm__offscreens *os = &m->offscreens; struct psm__offscreen_keydata *ok = &os->keydata; psm__alloc_field(os->surfaces, struct psm__offscreen, cnt->offscreens); psm__alloc_field(os->opacity, psm__f32, cnt->offscreens); @@ -406,7 +436,7 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, /* Render orders */ psm__i32 ro_count = cnt->art_meshes + - (ver >= csmMocVersion_53 ? cnt->offscreens : 0); + (ver >= csmMocVersion_53 ? cnt->offscreens : 0); psm__alloc_field(m->render_order, psm__i32, ro_count); #undef psm__alloc_field @@ -484,7 +514,7 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, w->pos[i] = pos; psm__i32 vc = src->warp_src.vertex_count[i]; pos = (psm__f32 *)((psm__u8 *)pos + - psm__align_to_16(2 * sizeof(psm__f32) * vc)); + psm__align_to_16(2 * sizeof(psm__f32) * vc)); } } @@ -495,28 +525,23 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, am->pos[i] = pos; psm__i32 vc = src->art_mesh_src.vertex_count[i]; pos = (psm__f32 *)((psm__u8 *)pos + - psm__align_to_16(2 * sizeof(psm__f32) * vc)); + psm__align_to_16(2 * sizeof(psm__f32) * vc)); } } /* Set up keyform binding internal pointers */ if (m && kb_ptrs && m->bindings.items) { struct psm__key_table **bp = kb_ptrs; - struct psm__key_table **bp_end = kb_ptrs + kb_ptr_total; + psm__i32 *ki = kb_indices; psm__f32 *kwt = kb_weights; for (psm__i32 i = 0; i < cnt->bindings; i++) { struct psm__binding *kc = &m->bindings.items[i]; - psm__i32 bc = src->binding_src.key_table_idx_len[i]; - bc = psm__clamp_i32(bc, 0, PSM__MAX_KEY_TABLES); + psm__i32 bc = src->binding_src.key_table_idx_len[i]; psm__u32 mc = 1 << bc; - /* Ensure bp doesn't exceed allocated buffer */ - psm__i32 remaining = (bp < bp_end) ? (psm__i32)(bp_end - bp) : 0; - bc = psm__clamp_i32(bc, 0, remaining); - kc->key_tables = bp; kc->keyform_idx = ki; kc->weights = kwt; @@ -534,6 +559,7 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, struct psm__draw_item *ip = do_items; for (psm__i32 i = 0; i < cnt->draw_groups; i++) { struct psm__draw_group *grp = &m->draw_groups.groups[i]; + psm__i32 count = src->draw_group_src.obj_len[i]; if (!psm__valid_range(ip - do_items, count, cnt->draw_items)) { grp->items = NULL; @@ -552,6 +578,7 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, struct psm__blend_constraint **ptr = bs_constr_ptrs; for (psm__i32 i = 0; i < cnt->blend_bindings; i++) { struct psm__blend_binding *bb = &m->blend_bindings.items[i]; + psm__i32 cc = src->blend_binding_src.bs_constraint_idx_len[i]; bb->constraints = ptr; bb->constraint_count = cc; @@ -562,7 +589,6 @@ psm__alloc_model(struct psm__arena *arena, psm__u8 ver, return m2; } - static void psm__init_mul_color(psm__f32 *buf, psm__i32 count) { @@ -585,41 +611,41 @@ static int psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) { psm__u8 ver = moc->header->version; - struct psm__sections *ms = moc->sections; + + struct psm__sections *ms = moc->sections; struct psm__count_info *cnt = ms->count_info; m->source = moc; + m->last_error = moc->header->last_error; /* inherit the revive outcome */ m->y_reversed = (ms->canvas_info->flag & PSM__CANVAS_FLAG_Y_REVERSED) != 0; m->force_update = 1; /* Parameter bindings */ - if (cnt->key_tables > 0 && m->key_tables.items && - ms->key_table_src.keys_len && ms->key_table_src.keys_off && - ms->keys_src.key) { + /* keys_src.key has count cnt->keys, which can be 0 while key_tables > 0 */ + if (cnt->key_tables > 0 && ms->keys_src.key) { for (psm__i32 i = 0; i < cnt->key_tables; i++) { struct psm__key_table *c = &m->key_tables.items[i]; + psm__i32 key_cnt = ms->key_table_src.keys_len[i]; psm__i32 keyform_off = ms->key_table_src.keys_off[i]; c->key_count = key_cnt; c->out_of_range = 1; - if (psm__valid_range(keyform_off, key_cnt, cnt->keys)) - c->keys = &ms->keys_src.key[keyform_off]; - else - c->keys = NULL; + /* keys_off + keys_len <= cnt->keys proved by verify_idx */ + c->keys = &ms->keys_src.key[keyform_off]; } } - /* Keyform bindings */ - if (cnt->bindings > 0 && m->bindings.items) { + /* + * Keyform bindings. The key_table_idx_off/len range and every + * key_table_idx_src.idx entry are already proved valid by verify_idx, + * so this just wires the pointers. + */ + if (cnt->bindings > 0) { for (psm__i32 i = 0; i < cnt->bindings; i++) { struct psm__binding *kc = &m->bindings.items[i]; - psm__i32 bc = ms->binding_src.key_table_idx_len[i]; - psm__i32 kt_off = ms->binding_src.key_table_idx_off[i]; - - PSM__FAIL(!psm__valid_range(kt_off, bc, - cnt->key_table_idx), PSM__ERR_FILE_CORRUPT, - "binding[%d] OOB: off=%d len=%d max=%d", i, kt_off, bc, cnt->key_table_idx); + psm__i32 bc = ms->binding_src.key_table_idx_len[i]; + psm__i32 kt_off = ms->binding_src.key_table_idx_off[i]; kc->idx_dirty = 1; kc->weight_dirty = 1; @@ -630,26 +656,22 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) for (psm__i32 j = 0; j < bc; j++) { psm__i32 idx = ms->key_table_idx_src.idx[kt_off + j]; - PSM__FAIL(!psm__valid_idx(idx, cnt->key_tables), - PSM__ERR_FILE_CORRUPT, "binding[%d] ptr[%d] OOB: idx=%d max=%d", - i, j, idx, cnt->key_tables); kc->key_tables[j] = &m->key_tables.items[idx]; } } } /* Parts */ - if (cnt->parts > 0 && m->parts.items && m->bindings.items && - m->parts.keydata.interp.max_blend) { + if (cnt->parts > 0) { psm__i32 tmp_len = 0; for (psm__i32 i = 0; i < cnt->parts; i++) { struct psm__part *part = &m->parts.items[i]; - psm__i32 bi = ms->part_src.binding_idx[i]; - if (!psm__valid_idx(bi, cnt->bindings)) - continue; + /* binding_idx valid by verify_idx */ + psm__i32 bi = ms->part_src.binding_idx[i]; struct psm__binding *binding = &m->bindings.items[bi]; part->binding = binding; + m->parts.bindings[i] = binding; part->parent_part_idx = ms->part_src.parent_part_idx[i]; part->local_enable = ms->part_src.enable[i]; @@ -659,11 +681,6 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) m->parts.offscreen_src_idx[i] = -1; psm__i32 mc = binding->max_blend; - psm__i32 kb = ms->part_src.keyform_off[i]; - if (!psm__valid_range(kb, mc, cnt->part_keyforms)) { - part->binding = NULL; - continue; - } m->parts.keydata.interp.max_blend[i] = mc; tmp_len += mc; } @@ -671,11 +688,7 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) } /* Parameters */ - if (cnt->parameters > 0 && m->params.items && m->params.input_value && - ms->param_src.minimum_value && ms->param_src.maximum_value && - ms->param_src.default_value && ms->param_src.repeat && - ms->param_src.decimal_places && ms->param_src.key_table_off && - ms->param_src.key_table_len) { + if (cnt->parameters > 0) { for (psm__i32 i = 0; i < cnt->parameters; i++) { struct psm__param *param = &m->params.items[i]; @@ -703,7 +716,8 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) int rc = psm__valid_opt_range(kt_off, kt_count, cnt->key_tables); if (rc < 0) PSM__LOGF("param[%d]: binding range " - "[%d, %d) OOB (max %d)", i, kt_off, kt_off + kt_count, + "[%d, %d) OOB (max %d)", + i, kt_off, kt_off + kt_count, cnt->key_tables); if (rc == 1) { param->key_tables = &m->key_tables.items[kt_off]; @@ -724,13 +738,11 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) } /* Deformers */ - if (cnt->deformers > 0 && m->deformers.nodes && m->bindings.items) { + if (cnt->deformers > 0) { for (psm__i32 i = 0; i < cnt->deformers; i++) { struct psm__deformer_node *node = &m->deformers.nodes[i]; - psm__i32 bi = ms->deformer_src.binding_idx[i]; - if (!psm__valid_idx(bi, cnt->bindings)) - continue; + psm__i32 bi = ms->deformer_src.binding_idx[i]; /* valid by verify_idx */ node->binding = &m->bindings.items[bi]; node->parent_part_idx = ms->deformer_src.parent_part_idx[i]; node->parent_deformer_idx = ms->deformer_src.parent_deformer_idx[i]; @@ -743,18 +755,17 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) /* Warp deformers */ { struct psm__warp_src *ws = &ms->warp_src; - struct psm__interp *interp = &m->deformers.warps.keydata.interp; - if (cnt->warps > 0 && m->deformers.warps.items && - m->bindings.items && interp->max_blend) { + struct psm__interp *interp = &m->deformers.warps.keydata.interp; + if (cnt->warps > 0) { psm__i32 tmp_len = 0; for (psm__i32 i = 0; i < cnt->warps; i++) { struct psm__warp *warp = &m->deformers.warps.items[i]; - psm__i32 bi = ws->binding_idx[i]; - if (!psm__valid_idx(bi, cnt->bindings)) - continue; + /* binding_idx valid by verify_idx */ + psm__i32 bi = ws->binding_idx[i]; struct psm__binding *binding = &m->bindings.items[bi]; warp->binding = binding; + m->deformers.warps.bindings[i] = binding; warp->row = ws->row[i]; warp->col = ws->col[i]; warp->vertex_count = ws->vertex_count[i]; @@ -762,11 +773,6 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) warp->quad_transform = ws->quad_transform[i]; psm__i32 mc = binding->max_blend; - psm__i32 kb = ws->keyform_off[i]; - if (!psm__valid_range(kb, mc, cnt->warp_keyforms)) { - warp->binding = NULL; - continue; - } interp->max_blend[i] = mc; tmp_len += mc; } @@ -777,26 +783,21 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) /* Rotation deformers */ { struct psm__rotation_src *rs = &ms->rotation_src; - struct psm__interp *interp = &m->deformers.rotations.keydata.interp; - if (cnt->rotations > 0 && m->deformers.rotations.items && - m->bindings.items && interp->max_blend) { + struct psm__interp *interp = &m->deformers.rotations.keydata.interp; + if (cnt->rotations > 0) { psm__i32 tmp_len = 0; for (psm__i32 i = 0; i < cnt->rotations; i++) { struct psm__rotation *rot = &m->deformers.rotations.items[i]; - psm__i32 bi = rs->binding_idx[i]; - if (!psm__valid_idx(bi, cnt->bindings)) - continue; + + /* binding_idx valid by verify_idx */ + psm__i32 bi = rs->binding_idx[i]; struct psm__binding *binding = &m->bindings.items[bi]; rot->binding = binding; + m->deformers.rotations.bindings[i] = binding; rot->base_angle = rs->base_angle[i]; psm__i32 mc = binding->max_blend; - psm__i32 kb = rs->keyform_off[i]; - if (!psm__valid_range(kb, mc, cnt->rotation_keyforms)) { - rot->binding = NULL; - continue; - } interp->max_blend[i] = mc; tmp_len += mc; } @@ -805,17 +806,17 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) } /* Art meshes */ - if (cnt->art_meshes > 0 && m->art_meshes.meshes && m->bindings.items && - m->art_meshes.keydata.interp.max_blend) { + if (cnt->art_meshes > 0) { psm__i32 tmp_len = 0; for (psm__i32 i = 0; i < cnt->art_meshes; i++) { struct psm__art_mesh *mesh = &m->art_meshes.meshes[i]; - psm__i32 bi = ms->art_mesh_src.binding_idx[i]; - if (!psm__valid_idx(bi, cnt->bindings)) - continue; + + /* binding_idx valid by verify_idx */ + psm__i32 bi = ms->art_mesh_src.binding_idx[i]; struct psm__binding *binding = &m->bindings.items[bi]; mesh->binding = binding; + m->art_meshes.bindings[i] = binding; mesh->parent_part_idx = ms->art_mesh_src.parent_part_idx[i]; mesh->parent_deformer_idx = ms->art_mesh_src.parent_deformer_idx[i]; mesh->vertex_count = ms->art_mesh_src.vertex_count[i]; @@ -824,38 +825,35 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) ms->art_mesh_src.drawable_flag[i]; if (ver < csmMocVersion_53) { - psm__u8 flag = ms->art_mesh_src.drawable_flag[i]; + psm__u8 flag = ms->art_mesh_src.drawable_flag[i]; psm__i32 blend_mode = (flag & csmBlendMultiplicative) - ? csmColorBlendType_MultiplyCompatible - : csmColorBlendType_Normal; + ? csmColorBlendType_MultiplyCompatible + : csmColorBlendType_Normal; if (flag & csmBlendAdditive) blend_mode = csmColorBlendType_AddCompatible; m->art_meshes.blend_mode[i] = blend_mode; } else { - psm__i32 blend_mode = psm__remap_blend_mode( - ms->art_mesh_src.blend_mode[i]); - m->art_meshes.blend_mode[i] = blend_mode; + psm__i32 raw = ms->art_mesh_src.blend_mode[i]; + m->art_meshes.blend_mode[i] = psm__remap_blend_mode(raw); #if PSM_COMPAT_VERSION < 0x06000000L /* * v5 callers read blend mode from constant flags * (csmGetDrawableBlendModes doesn't exist in v5). - * Bake the remapped mode into const_flags. + * Only the original compatible modes (raw 1=AddCompatible, + * 2=MultiplyCompatible) set a flag bit; v5.3 extended modes (raw >= 3) + * leave both bits unset, matching the v6 oracle -- keying off the + * remapped value here wrongly flagged every extended mode. */ psm__u8 *cf = &m->art_meshes.const_flags[i]; *cf &= ~(csmBlendAdditive | csmBlendMultiplicative); - if (blend_mode == csmColorBlendType_AddCompatible) + if (raw == csmColorBlendType_AddCompatible) *cf |= csmBlendAdditive; - else if (blend_mode == csmColorBlendType_MultiplyCompatible) + else if (raw == csmColorBlendType_MultiplyCompatible) *cf |= csmBlendMultiplicative; #endif } psm__i32 mc = binding->max_blend; - psm__i32 kb = ms->art_mesh_src.keyform_off[i]; - if (!psm__valid_range(kb, mc, cnt->art_mesh_keyforms)) { - mesh->binding = NULL; - continue; - } m->art_meshes.keydata.interp.max_blend[i] = mc; tmp_len += mc; } @@ -864,7 +862,7 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) /* Link keyform color data to MOC3 source (v4.2+). */ if (ver >= csmMocVersion_42) { - struct psm__warp_keydata *wk = &m->deformers.warps.keydata; + struct psm__warp_keydata *wk = &m->deformers.warps.keydata; struct psm__rotation_keydata *rk = &m->deformers.rotations.keydata; struct psm__art_mesh_keydata *ak = &m->art_meshes.keydata; { @@ -875,7 +873,7 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) if (mc->r && sc->r && wk->mul_color.r && ms->warp_key_src.key_mul_color_off && ms->warp_key_src.key_scr_color_off) { - psm__i32 n = wk->interp.tmp_len; + psm__i32 n = wk->interp.tmp_len; psm__i32 *mb = ms->warp_key_src.key_mul_color_off; psm__i32 *sb = ms->warp_key_src.key_scr_color_off; if (n > cnt->warp_keyforms) @@ -899,7 +897,7 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) if (mc->r && sc->r && rk->mul_color.r && ms->rotation_key_src.key_mul_color_off && ms->rotation_key_src.key_scr_color_off) { - psm__i32 n = rk->interp.tmp_len; + psm__i32 n = rk->interp.tmp_len; psm__i32 *mb = ms->rotation_key_src.key_mul_color_off; psm__i32 *sb = ms->rotation_key_src.key_scr_color_off; if (n > cnt->rotation_keyforms) @@ -923,7 +921,7 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) if (mc->r && sc->r && ak->mul_color.r && ms->art_mesh_key_src.key_mul_color_off && ms->art_mesh_key_src.key_scr_color_off) { - psm__i32 n = ak->interp.tmp_len; + psm__i32 n = ak->interp.tmp_len; psm__i32 *mb = ms->art_mesh_key_src.key_mul_color_off; psm__i32 *sb = ms->art_mesh_key_src.key_scr_color_off; if (n > cnt->art_mesh_keyforms) @@ -947,13 +945,14 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) /* Draw order groups */ { - struct psm__draw_group_src *gs = &ms->draw_group_src; + struct psm__draw_group_src *gs = &ms->draw_group_src; struct psm__draw_group_obj_src *os = &ms->draw_group_obj_src; - if (cnt->draw_groups > 0 && m->draw_groups.groups && - gs->obj_total_count && gs->max_order && gs->min_order && - gs->obj_off && os->type && os->idx && os->self_group_idx) { + /* os->* have count cnt->draw_items, which differs from cnt->draw_groups */ + if (cnt->draw_groups > 0 && + os->type && os->idx && os->self_group_idx) { for (psm__i32 i = 0; i < cnt->draw_groups; i++) { struct psm__draw_group *grp = &m->draw_groups.groups[i]; + psm__i32 max_order = gs->max_order[i]; psm__i32 min_order = gs->min_order[i]; psm__i32 base_idx = gs->obj_off[i]; @@ -964,19 +963,14 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) grp->order_level = psm__safe_order_level(max_order, min_order); grp->cursor = 0; - if (!psm__valid_range(base_idx, grp->count, cnt->draw_items)) { - PSM__LOGF("draw_order_group[%d]: " - "range [%d, %d) OOB (max %d)", i, base_idx, - base_idx + grp->count, cnt->draw_items); - grp->count = 0; - } else { - for (psm__i32 j = 0; j < grp->count; j++) { - struct psm__draw_item *item = &grp->items[j]; - item->object_type = os->type[base_idx + j]; - item->object_idx = os->idx[base_idx + j]; - item->group_idx = os->self_group_idx[base_idx + j]; - item->draw_order = 0; - } + /* obj_off + obj_len <= cnt->draw_items proved by verify_idx; + * count>0 implies grp->items != NULL (set together in alloc) */ + for (psm__i32 j = 0; j < grp->count; j++) { + struct psm__draw_item *item = &grp->items[j]; + item->object_type = os->type[base_idx + j]; + item->object_idx = os->idx[base_idx + j]; + item->group_idx = os->self_group_idx[base_idx + j]; + item->draw_order = 0; } } } @@ -984,48 +978,31 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) /* Glues */ { - struct psm__glue_src *gls = &ms->glue_src; + struct psm__glue_src *gls = &ms->glue_src; struct psm__glue_info_src *gis = &ms->glue_info_src; + psm__i32 *max_combs = m->glues.keydata.interp.max_blend; - if (cnt->glues > 0 && m->glues.items && m->bindings.items && max_combs && - gls->binding_idx && gls->art_mesh_idx_a && - gls->art_mesh_idx_b && gls->info_len && gls->info_off && - gis->weight && gis->pos_idx) { + /* gis->* have count cnt->glue_info, which differs from cnt->glues */ + if (cnt->glues > 0 && gis->weight && gis->pos_idx) { psm__i32 tmp_len = 0; for (psm__i32 i = 0; i < cnt->glues; i++) { struct psm__glue *glue = &m->glues.items[i]; - psm__i32 bi = gls->binding_idx[i]; - if (!psm__valid_idx(bi, cnt->bindings)) { - glue->binding = NULL; - continue; - } + psm__i32 bi = gls->binding_idx[i]; /* valid by verify_idx */ + struct psm__binding *binding = &m->bindings.items[bi]; - psm__i32 info_off = gls->info_off[i]; + psm__i32 info_off = gls->info_off[i]; glue->binding = binding; + m->glues.bindings[i] = binding; glue->mesh_idx0 = gls->art_mesh_idx_a[i]; glue->mesh_idx1 = gls->art_mesh_idx_b[i]; glue->glue_info_count = gls->info_len[i]; - if (!psm__valid_range(info_off, glue->glue_info_count, - cnt->glue_info)) { - PSM__LOGF("glue[%d]: info range " - "[%d, %d) OOB (max %d)", i, info_off, - info_off + glue->glue_info_count, cnt->glue_info); - glue->weights = NULL; - glue->pos_idx = NULL; - glue->glue_info_count = 0; - } else { - glue->weights = &gis->weight[info_off]; - glue->pos_idx = &gis->pos_idx[info_off]; - } + /* info_off + info_len <= cnt->glue_info proved by verify_idx */ + glue->weights = &gis->weight[info_off]; + glue->pos_idx = &gis->pos_idx[info_off]; psm__i32 mc = binding->max_blend; - psm__i32 kb = gls->keyform_off[i]; - if (!psm__valid_range(kb, mc, cnt->glue_keyforms)) { - glue->binding = NULL; - continue; - } max_combs[i] = mc; tmp_len += mc; } @@ -1042,6 +1019,7 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) bscs->value_len && bsvs->key && bsvs->weight) { for (psm__i32 i = 0; i < cnt->bs_constraints; i++) { struct psm__blend_constraint *constr = &m->blend_constraints.items[i]; + psm__i32 pi = bscs->parameter_idx[i]; psm__i32 vb = bscs->value_off[i]; psm__i32 vc = bscs->value_len[i]; @@ -1070,9 +1048,11 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) } struct psm__blend_key_table_src *ba_src = &ms->blend_key_table_src; - if (ba_src->keys_len && ba_src->keys_off && ba_src->base_key_idx) { + if (ba_src->keys_len && ba_src->keys_off && ba_src->base_key_idx && + ms->keys_src.key) { for (psm__i32 i = 0; i < cnt->blend_key_tables; i++) { struct psm__blend_key_table *ba = &m->blend_key_tables.items[i]; + psm__i32 kc = ba_src->keys_len[i]; psm__i32 kb = ba_src->keys_off[i]; if (psm__valid_range(kb, kc, cnt->keys)) { @@ -1080,7 +1060,8 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) ba->keys = &ms->keys_src.key[kb]; } else { PSM__LOGF("blend_pb[%d]: key range " - "[%d, %d) OOB (max %d)", i, kb, kb + kc, cnt->keys); + "[%d, %d) OOB (max %d)", + i, kb, kb + kc, cnt->keys); ba->keys = NULL; ba->key_count = 0; } @@ -1111,9 +1092,10 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) if (bb_src->key_table_idx && bb_src->key_bs_off) { for (psm__i32 i = 0; i < cnt->blend_bindings; i++) { struct psm__blend_binding *bb = &m->blend_bindings.items[i]; + psm__i32 bpi = bb_src->key_table_idx[i]; if (m->blend_key_tables.items && psm__valid_idx(bpi, - cnt->blend_key_tables)) + cnt->blend_key_tables)) bb->key_table = &m->blend_key_tables.items[bpi]; else bb->key_table = NULL; @@ -1217,11 +1199,12 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) /* Offscreen rendering (v5.3+) */ if (ver >= csmMocVersion_53) { if (cnt->offscreens > 0 && ms->offscreen_src.owner_idx) { - psm__i32 tmp_len = 0; + psm__i32 tmp_len = 0; psm__i32 *os_key_idx = ms->part_key_src.key_idx; for (psm__i32 i = 0; i < cnt->offscreens; i++) { struct psm__offscreen *surf = &m->offscreens.surfaces[i]; + psm__i32 oi = ms->offscreen_src.owner_idx[i]; if (!psm__valid_idx(oi, cnt->parts)) { @@ -1326,8 +1309,8 @@ psm__init_model_data(struct psm__model *m, const struct psm__moc3_data *moc) psm__i32 best = kt_off, best_kc = 0; for (psm__i32 j = 0; j < kt_count; j++) { psm__i32 idx = kt_off + j; - if (idx < cnt->key_tables && ms->key_table_src.keys_len[idx] - > best_kc) { + if (idx < cnt->key_tables && + ms->key_table_src.keys_len[idx] > best_kc) { best_kc = ms->key_table_src.keys_len[idx]; best = idx; } @@ -1426,7 +1409,6 @@ psm__init_model(struct psm__model **out, return PSM__OK; } - PSMDEF unsigned int csmGetSizeofModel(const csmMoc *moc) { @@ -1440,8 +1422,17 @@ PSMDEF csmModel * csmInitializeModelInPlace(const csmMoc *moc, void *address, unsigned int size) { struct psm__model *model; - PSM__FAILM(psm__init_model(&model, psm__moc_to_data(moc), - address, size) != PSM__OK, NULL, "could not init model"); + + int err = psm__init_model(&model, psm__moc_to_data(moc), address, size); + + /* + * Record the outcome in the moc header so csmGetMocError can explain an + * init failure, just as it does for a failed revive. Written on success + * too (PSM__OK), so a later good init clears a prior failure's code. + */ + ((struct psm__moc3_header *)moc)->last_error = err; + + PSM__FAILM(err != PSM__OK, NULL, "could not init model"); return (csmModel *)model; } @@ -1449,7 +1440,7 @@ PSMDEF void csmReadCanvasInfo(const csmModel *model, csmVector2 *outSizeInPixels, csmVector2 *outOriginInPixels, float *outPixelsPerUnit) { - const struct psm__model *m = (const struct psm__model *)model; + const struct psm__model *m = (const struct psm__model *)model; const struct psm__canvas_info *c = m->source->sections->canvas_info; outSizeInPixels->X = c->width; outSizeInPixels->Y = c->height; @@ -1458,3 +1449,34 @@ csmReadCanvasInfo(const csmModel *model, csmVector2 *outSizeInPixels, *outPixelsPerUnit = c->pix_per_unit; } +/* Purism Core extension: see PurismCore.h. */ +PSMDEF csmError +csmGetLastError(const csmModel *model) +{ + if (!model) + return csmError_NoError; + return (csmError)((const struct psm__model *)model)->last_error; +} + +PSMDEF const char * +csmGetErrorString(csmError error) +{ + switch (error) { + case csmError_NoError: + return "no error"; + case csmError_Failed: + return "operation failed"; + case csmError_ParameterRange: + return "parameter out of range"; + case csmError_FileUnrecognized: + return "unrecognized MOC3 file"; + case csmError_FileCorrupt: + return "corrupt MOC3 file"; + case csmError_InvalidData: + return "invalid data"; + case csmError_InvalidParameter: + return "invalid parameter"; + default: + return "unknown error"; + } +} diff --git a/src/model.h b/src/model.h index 06df80e..3b69f89 100644 --- a/src/model.h +++ b/src/model.h @@ -18,205 +18,209 @@ struct psm__color3 { struct psm__model; struct psm__interp { - psm__i32 object_count; + psm__i32 object_count; psm__i32 *max_blend; psm__i32 *blend_count; - psm__i32 tmp_len; + psm__i32 tmp_len; psm__f32 *tmp; psm__f32 *weights; }; struct psm__key_table { - psm__i32 key_count; + psm__i32 key_count; psm__f32 *keys; - psm__i32 idx; - psm__f32 weight; - bool out_of_range; - bool idx_dirty; - bool weight_dirty; + psm__i32 idx; + psm__f32 weight; + bool out_of_range; + bool idx_dirty; + bool weight_dirty; }; struct psm__blend_key_table { - psm__i32 key_count; + psm__i32 key_count; psm__f32 *keys; - psm__i32 base_key_idx; - psm__i32 idx; - psm__f32 weight; - bool idx_dirty; - bool weight_dirty; + psm__i32 base_key_idx; + psm__i32 idx; + psm__f32 weight; + bool idx_dirty; + bool weight_dirty; }; struct psm__binding { struct psm__key_table **key_tables; - psm__i32 key_table_len; - psm__i32 max_blend; - psm__i32 blend_count; - psm__i32 *keyform_idx; - psm__f32 *weights; - bool idx_dirty; - bool weight_dirty; - bool out_of_range; + psm__i32 key_table_len; + psm__i32 max_blend; + psm__i32 blend_count; + psm__i32 *keyform_idx; + psm__f32 *weights; + bool idx_dirty; + bool weight_dirty; + bool out_of_range; }; struct psm__blend_constraint { struct psm__param *param; - psm__f32 *keys; - psm__f32 *weights; - psm__i32 count; - psm__f32 weight; + psm__f32 *keys; + psm__f32 *weights; + psm__i32 count; + psm__f32 weight; }; struct psm__blend_binding { - struct psm__blend_key_table *key_table; - psm__i32 key_src_off; - psm__i32 blend_count; - psm__i32 keyform_idx[2]; - psm__f32 weights[2]; - bool idx_dirty; - bool weight_dirty; - psm__i32 constraint_count; + struct psm__blend_key_table *key_table; + psm__i32 key_src_off; + psm__i32 blend_count; + psm__i32 keyform_idx[2]; + psm__f32 weights[2]; + bool idx_dirty; + bool weight_dirty; + psm__i32 constraint_count; struct psm__blend_constraint **constraints; - psm__f32 weight; + psm__f32 weight; }; struct psm__part { struct psm__binding *binding; - psm__i32 parent_part_idx; - bool local_enable; + psm__i32 parent_part_idx; + bool local_enable; }; struct psm__part_keydata { struct psm__interp interp; - psm__f32 *draw_order; + psm__f32 *draw_order; }; struct psm__parts { - psm__i32 count; - struct psm__part *items; + psm__i32 count; + struct psm__part *items; + struct psm__binding **bindings; /* items[i].binding, gathered once at load */ struct psm__part_keydata keydata; - bool *enable; - psm__i32 *draw_order; - psm__f32 *opacity; - psm__f32 *input_opacity; - psm__i32 *offscreen_src_idx; + bool *enable; + psm__i32 *draw_order; + psm__f32 *opacity; + psm__f32 *input_opacity; + psm__i32 *offscreen_src_idx; }; struct psm__deformer_node { struct psm__binding *binding; - psm__i32 parent_part_idx; - psm__i32 parent_deformer_idx; - psm__i32 type; - psm__i32 local_idx; - bool local_enable; + psm__i32 parent_part_idx; + psm__i32 parent_deformer_idx; + psm__i32 type; + psm__i32 local_idx; + bool local_enable; }; struct psm__warp { struct psm__binding *binding; - psm__i32 row; - psm__i32 col; - bool quad_transform; - psm__i32 vertex_count; + psm__i32 row; + psm__i32 col; + bool quad_transform; + psm__i32 vertex_count; }; struct psm__rotation { struct psm__binding *binding; - psm__f32 base_angle; + psm__f32 base_angle; }; struct psm__warp_keydata { struct psm__interp interp; - psm__f32 *opacity; - psm__f32 **pos; + psm__f32 *opacity; + psm__f32 **pos; struct psm__color3 mul_color; struct psm__color3 scr_color; }; struct psm__warps { - psm__i32 count; - struct psm__warp *items; + psm__i32 count; + struct psm__warp *items; + struct psm__binding **bindings; /* items[i].binding, gathered once at load */ struct psm__warp_keydata keydata; - bool *enable; - psm__f32 *opacity; - psm__f32 **pos; - psm__f32 *mul_color; - psm__f32 *scr_color; + bool *enable; + psm__f32 *opacity; + psm__f32 **pos; + psm__f32 *mul_color; + psm__f32 *scr_color; }; struct psm__rotation_keydata { struct psm__interp interp; - psm__f32 *opacity; - psm__f32 *angle; - psm__f32 *origin_x; - psm__f32 *origin_y; - psm__f32 *scale; + psm__f32 *opacity; + psm__f32 *angle; + psm__f32 *origin_x; + psm__f32 *origin_y; + psm__f32 *scale; struct psm__color3 mul_color; struct psm__color3 scr_color; }; struct psm__rotations { - psm__i32 count; - struct psm__rotation *items; + psm__i32 count; + struct psm__rotation *items; + struct psm__binding **bindings; /* items[i].binding, gathered once at load */ struct psm__rotation_keydata keydata; - bool *enable; - psm__f32 *opacity; - psm__f32 *scale; - psm__f32 *origin_x; - psm__f32 *origin_y; - psm__f32 *angle; - psm__i32 *reflect_x; - psm__i32 *reflect_y; - psm__f32 *mul_color; - psm__f32 *scr_color; + bool *enable; + psm__f32 *opacity; + psm__f32 *scale; + psm__f32 *origin_x; + psm__f32 *origin_y; + psm__f32 *angle; + psm__i32 *reflect_x; + psm__i32 *reflect_y; + psm__f32 *mul_color; + psm__f32 *scr_color; }; struct psm__deformers { - struct psm__warps warps; - struct psm__rotations rotations; - psm__i32 count; + struct psm__warps warps; + struct psm__rotations rotations; + psm__i32 count; struct psm__deformer_node *nodes; - bool *enable; - psm__f32 *opacity; - psm__f32 *scale; - psm__f32 *mul_color; - psm__f32 *scr_color; + bool *enable; + psm__f32 *opacity; + psm__f32 *scale; + psm__f32 *mul_color; + psm__f32 *scr_color; }; struct psm__art_mesh { struct psm__binding *binding; - psm__i32 parent_part_idx; - psm__i32 parent_deformer_idx; - bool local_enable; - psm__i32 vertex_count; + psm__i32 parent_part_idx; + psm__i32 parent_deformer_idx; + bool local_enable; + psm__i32 vertex_count; }; struct psm__art_mesh_keydata { struct psm__interp interp; - psm__f32 *opacity; - psm__f32 *draw_order; - psm__f32 **pos; + psm__f32 *opacity; + psm__f32 *draw_order; + psm__f32 **pos; struct psm__color3 mul_color; struct psm__color3 scr_color; }; struct psm__art_meshes { - psm__i32 count; - struct psm__art_mesh *meshes; + psm__i32 count; + struct psm__art_mesh *meshes; + struct psm__binding **bindings; /* meshes[i].binding, gathered once at load */ struct psm__art_mesh_keydata keydata; - bool *enable; - bool state_changed; - psm__u8 *const_flags; - psm__u8 *change_flags; - psm__i32 *blend_mode; - psm__i32 *draw_order; - psm__f32 **pos; - psm__f32 *opacity; - psm__f32 *mul_color; - psm__f32 *scr_color; - psm__i32 *last_render_order; - psm__i32 *last_draw_order; - psm__f32 *last_opacity; - psm__f32 *last_mul_color; - psm__f32 *last_scr_color; + bool *enable; + bool state_changed; + psm__u8 *const_flags; + psm__u8 *change_flags; + psm__i32 *blend_mode; + psm__i32 *draw_order; + psm__f32 **pos; + psm__f32 *opacity; + psm__f32 *mul_color; + psm__f32 *scr_color; + psm__i32 *last_render_order; + psm__i32 *last_draw_order; + psm__f32 *last_opacity; + psm__f32 *last_mul_color; + psm__f32 *last_scr_color; }; struct psm__draw_item { @@ -227,12 +231,12 @@ struct psm__draw_item { }; struct psm__draw_group { - psm__i32 total_count; - psm__i32 count; - psm__i32 cursor; - psm__i32 max_order; - psm__i32 min_order; - psm__i32 order_level; + psm__i32 total_count; + psm__i32 count; + psm__i32 cursor; + psm__i32 max_order; + psm__i32 min_order; + psm__i32 order_level; struct psm__draw_item *items; }; @@ -243,143 +247,146 @@ struct psm__draw_sort { }; struct psm__draw_groups { - psm__i32 count; + psm__i32 count; struct psm__draw_group *groups; - struct psm__draw_sort sort; + struct psm__draw_sort sort; }; struct psm__glue { struct psm__binding *binding; - psm__i32 mesh_idx0; - psm__i32 mesh_idx1; - psm__i32 glue_info_count; - bool local_enable; - psm__f32 *weights; - psm__u16 *pos_idx; + psm__i32 mesh_idx0; + psm__i32 mesh_idx1; + psm__i32 glue_info_count; + bool local_enable; + psm__f32 *weights; + psm__u16 *pos_idx; }; struct psm__glue_keydata { struct psm__interp interp; - psm__f32 *intensity; + psm__f32 *intensity; }; struct psm__glues { - psm__i32 count; - struct psm__glue *items; + psm__i32 count; + struct psm__glue *items; + struct psm__binding **bindings; /* items[i].binding, gathered once at load */ struct psm__glue_keydata keydata; - psm__f32 *intensity; + psm__f32 *intensity; }; struct psm__offscreen { struct psm__binding *binding; - bool *owner_enable; - psm__i32 *keyform_idx; + bool *owner_enable; + psm__i32 *keyform_idx; }; struct psm__offscreen_keydata { struct psm__interp interp; - psm__f32 *opacity; + psm__f32 *opacity; struct psm__color3 mul_color; struct psm__color3 scr_color; }; struct psm__offscreens { - psm__i32 count; - struct psm__offscreen *surfaces; + psm__i32 count; + struct psm__offscreen *surfaces; struct psm__offscreen_keydata keydata; - bool *enable; - psm__f32 *opacity; - psm__f32 *mul_color; - psm__f32 *scr_color; + bool *enable; + psm__f32 *opacity; + psm__f32 *mul_color; + psm__f32 *scr_color; }; struct psm__param { - psm__i32 type; - psm__f32 range[2]; - psm__f32 range_length; - bool repeat; - psm__f32 snap_eps; - psm__f32 interp_eps; - psm__f32 value; - bool dirty; - struct psm__key_table *key_tables; - psm__i32 key_table_len; + psm__i32 type; + psm__f32 range[2]; + psm__f32 range_length; + bool repeat; + psm__f32 snap_eps; + psm__f32 interp_eps; + psm__f32 value; + bool dirty; + struct psm__key_table *key_tables; + psm__i32 key_table_len; struct psm__blend_key_table *blend_key_tables; - psm__i32 blend_key_table_len; + psm__i32 blend_key_table_len; }; struct psm__params { - psm__i32 count; + psm__i32 count; struct psm__param *items; - psm__i32 *type; - psm__f32 *input_value; + psm__i32 *type; + psm__f32 *input_value; }; struct psm__key_tables { - psm__i32 count; + psm__i32 count; struct psm__key_table *items; }; struct psm__bindings { - psm__i32 count; + psm__i32 count; struct psm__binding *items; }; struct psm__blend_shape { - psm__i32 target_idx; - psm__i32 binding_count; + psm__i32 target_idx; + psm__i32 binding_count; struct psm__blend_binding *bindings; }; struct psm__blend_shapes { - psm__i32 count; + psm__i32 count; struct psm__blend_shape *items; }; struct psm__blend_constraints { - psm__i32 count; + psm__i32 count; struct psm__blend_constraint *items; }; struct psm__blend_key_tables { - psm__i32 count; + psm__i32 count; struct psm__blend_key_table *items; }; struct psm__blend_bindings { - psm__i32 count; + psm__i32 count; struct psm__blend_binding *items; }; struct psm__param_keys { psm__f32 **keys; - psm__i32 *key_counts; + psm__i32 *key_counts; }; struct psm__model { - const struct psm__moc3_data *source; - struct psm__parts parts; - struct psm__deformers deformers; - struct psm__art_meshes art_meshes; - struct psm__draw_groups draw_groups; - struct psm__glues glues; - struct psm__offscreens offscreens; - struct psm__params params; - struct psm__key_tables key_tables; - struct psm__bindings bindings; + const struct psm__moc3_data *source; + struct psm__parts parts; + struct psm__deformers deformers; + struct psm__art_meshes art_meshes; + struct psm__draw_groups draw_groups; + struct psm__glues glues; + struct psm__offscreens offscreens; + struct psm__params params; + struct psm__key_tables key_tables; + struct psm__bindings bindings; struct psm__blend_constraints blend_constraints; - struct psm__blend_key_tables blend_key_tables; - struct psm__blend_bindings blend_bindings; - struct psm__blend_shapes bs_parts; - struct psm__blend_shapes bs_warps; - struct psm__blend_shapes bs_rotations; - struct psm__blend_shapes bs_art_meshes; - struct psm__blend_shapes bs_glues; - struct psm__blend_shapes bs_offscreens; - struct psm__param_keys param_keys; - psm__i32 *render_order; - bool force_update; - bool y_reversed; + struct psm__blend_key_tables blend_key_tables; + struct psm__blend_bindings blend_bindings; + struct psm__blend_shapes bs_parts; + struct psm__blend_shapes bs_warps; + struct psm__blend_shapes bs_rotations; + struct psm__blend_shapes bs_art_meshes; + struct psm__blend_shapes bs_glues; + struct psm__blend_shapes bs_offscreens; + struct psm__param_keys param_keys; + psm__i32 *render_order; + bool force_update; + bool y_reversed; + /* csmGetLastError: code from the most recent update */ + psm__i32 last_error; }; #endif /* PSM__MODEL_H */ diff --git a/src/offscreen.c b/src/offscreen.c index c53a85a..97feda4 100644 --- a/src/offscreen.c +++ b/src/offscreen.c @@ -24,6 +24,7 @@ psm__enable_offscreens(struct psm__model *m) return; struct psm__offscreen *surfaces = m->offscreens.surfaces; + bool *enable = m->offscreens.enable; for (psm__i32 i = 0; i < count; i++) { @@ -47,12 +48,12 @@ psm__gather_offscreens(struct psm__model *m) return; struct psm__sections *ms = m->source->sections; - psm__f32 *opa_src = ms->offscreen_key_src.opacity; + psm__f32 *opa_src = ms->offscreen_key_src.opacity; if (!opa_src) return; - psm__i32 max_keyforms = ms->count_info->offscreen_keyforms; struct psm__offscreen_keydata *kd = &m->offscreens.keydata; + psm__i32 off = 0; /* Opacity and weights */ @@ -70,12 +71,13 @@ psm__gather_offscreens(struct psm__model *m) if (b->idx_dirty && nc > 0) { psm__i32 *kp = surfaces[i].keyform_idx; - psm__i32 ki = kp ? *kp : -1; - if (ki != -1) { + psm__i32 ki = kp ? *kp : -1; + /* ki < 0 means "no keyforms"; any negative (not just -1) must be + * skipped, else idx goes out of bounds (matches the color loop and + * the psm__verify_offscreen_window load check). */ + if (ki >= 0) { for (psm__i32 j = 0; j < nc; j++) { psm__i32 idx = b->keyform_idx[j] + ki; - if ((psm__u32)idx >= (psm__u32)max_keyforms) - continue; kd->opacity[off + j] = opa_src[idx]; } } @@ -100,7 +102,6 @@ psm__gather_offscreens(struct psm__model *m) if (!col_off || !mr || !mg || !mb || !sr || !sg || !sb) return; - psm__i32 max_kf_colors = ms->count_info->keyform_mul_colors; off = 0; for (psm__i32 i = 0; i < count; i++) { @@ -112,17 +113,15 @@ psm__gather_offscreens(struct psm__model *m) } psm__i32 *kp = surfaces[i].keyform_idx; - psm__i32 ki = kp ? *kp : -1; - psm__i32 nc = b->blend_count; + psm__i32 ki = kp ? *kp : -1; + psm__i32 nc = b->blend_count; if (ki >= 0 && nc > 0) { - if ((psm__u32)ki >= (psm__u32)max_keyforms) - goto skip_color; psm__i32 cb = col_off[ki]; + if (cb < 0) /* offscreen keyform has no color override */ + goto skip_color; for (psm__i32 j = 0; j < nc; j++) { psm__i32 idx = b->keyform_idx[j] + cb; - if ((psm__u32)idx >= (psm__u32)max_kf_colors) - continue; kd->mul_color.r[off + j] = mr[idx]; kd->mul_color.g[off + j] = mg[idx]; kd->mul_color.b[off + j] = mb[idx]; @@ -131,7 +130,7 @@ psm__gather_offscreens(struct psm__model *m) kd->scr_color.b[off + j] = sb[idx]; } } -skip_color: + skip_color: off += b->max_blend; } } @@ -148,8 +147,7 @@ PSMDEF const int * csmGetOffscreenBlendModes(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - const struct psm__sections *ms = m->source->sections; - return ms->offscreen_src.blend_mode; + return m->source->sections->offscreen_src.blend_mode; } PSMDEF const float * @@ -163,8 +161,7 @@ PSMDEF const int * csmGetOffscreenOwnerIndices(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - const struct psm__sections *ms = m->source->sections; - return ms->offscreen_src.owner_idx; + return m->source->sections->offscreen_src.owner_idx; } PSMDEF const csmVector4 * @@ -185,23 +182,20 @@ PSMDEF const int * csmGetOffscreenMaskCounts(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - const struct psm__sections *ms = m->source->sections; - return ms->offscreen_src.mask_len; + return m->source->sections->offscreen_src.mask_len; } PSMDEF const int ** csmGetOffscreenMasks(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - const struct psm__sections *ms = m->source->sections; - return ms->offscreen_src.drawable_mask_runtime; + return m->source->sections->offscreen_src.drawable_mask_runtime; } PSMDEF const csmFlags * csmGetOffscreenConstantFlags(const csmModel *model) { const struct psm__model *m = (const struct psm__model *)model; - const struct psm__sections *ms = m->source->sections; - return (const csmFlags *)ms->offscreen_src.drawable_flag; + return (const csmFlags *)m->source->sections->offscreen_src.drawable_flag; } #endif /* PSM_COMPAT_VERSION >= 0x06000000L */ diff --git a/src/param.c b/src/param.c index 3eef237..f743bc8 100644 --- a/src/param.c +++ b/src/param.c @@ -18,15 +18,15 @@ struct psm__key_search_result { psm__i32 index; /* Key index (lower bound of segment) */ psm__f32 weight; /* Interpolation weight within segment [0,1] */ - bool is_outside; /* Value is outside key range */ - bool needs_check; /* Need to check previous out_of_range state */ + bool is_outside; /* Value is outside key range */ + bool needs_check; /* Need to check previous out_of_range state */ }; static struct psm__key_search_result psm__find_key_segment(psm__f32 value, const psm__f32 *keys, psm__i32 key_count, - psm__f32 snap_eps, psm__f32 interp_epsilon) + psm__f32 snap_eps, psm__f32 interp_eps) { - struct psm__key_search_result r = {0, 0.0f, false, false}; + struct psm__key_search_result r = { 0, 0.0f, false, false }; if (key_count <= 0) { r.needs_check = true; @@ -60,7 +60,7 @@ psm__find_key_segment(psm__f32 value, const psm__f32 *keys, psm__i32 key_count, if (value < key1 - snap_eps) { /* Between first and second key */ psm__f32 key_diff = key1 - key0; - if (key_diff >= interp_epsilon) + if (key_diff >= interp_eps) r.weight = (value - key0) / key_diff; return r; } @@ -79,7 +79,7 @@ psm__find_key_segment(psm__f32 value, const psm__f32 *keys, psm__i32 key_count, /* Between key0 and key1 */ r.index = k - 1; psm__f32 key_diff = key1 - key0; - if (key_diff >= interp_epsilon) + if (key_diff >= interp_eps) r.weight = (value - key0) / key_diff; return r; } @@ -97,15 +97,17 @@ psm__find_key_segment(psm__f32 value, const psm__f32 *keys, psm__i32 key_count, return r; } -PSM__DEF void +PSM__DEF bool psm__resolve_params(struct psm__params *parameters) { psm__i32 count = parameters->count; if (count <= 0) - return; + return false; struct psm__param *params = parameters->items; + psm__f32 *input_value = parameters->input_value; + bool range_error = false; for (psm__i32 i = 0; i < count; i++) { psm__f32 user_value = input_value[i]; @@ -127,6 +129,8 @@ psm__resolve_params(struct psm__params *parameters) } } else { psm__f32 range_min = params[i].range[0], range_max = params[i].range[1]; + if (user_value < range_min || user_value > range_max) + range_error = true; new_value = psm__clamp_f32(user_value, range_min, range_max); if (params[i].value != new_value) { @@ -139,12 +143,14 @@ psm__resolve_params(struct psm__params *parameters) input_value[i] = new_value; } } + + return range_error; } PSM__DEF void psm__resolve_key_tables(struct psm__model *m) { - psm__i32 param_count = m->params.count; + psm__i32 param_count = m->params.count; struct psm__param *param_items = m->params.items; if (!param_items || param_count <= 0) return; @@ -160,6 +166,7 @@ psm__resolve_key_tables(struct psm__model *m) * so downstream keyform updates don't re-evaluate them. */ psm__i32 bc = param->key_table_len; + struct psm__key_table *bs = param->key_tables; if (bs) { for (psm__i32 j = 0; j < bc; j++) { @@ -171,6 +178,7 @@ psm__resolve_key_tables(struct psm__model *m) } psm__i32 binding_count = param->key_table_len; + struct psm__key_table *bindings = param->key_tables; if (!bindings || binding_count <= 0) continue; @@ -232,7 +240,8 @@ psm__resolve_blend_key_tables(struct psm__model *m) if (bs_count <= 0) continue; - struct psm__blend_key_table *blend_key_tables = params[param_i].blend_key_tables; + struct psm__blend_key_table *blend_key_tables = + params[param_i].blend_key_tables; if (!blend_key_tables) continue; psm__f32 value = params[param_i].value; @@ -248,7 +257,8 @@ psm__resolve_blend_key_tables(struct psm__model *m) if (keys && value > keys[0]) { /* Find upper bound: first key > value */ for (index = 1; - index < (psm__u32)key_count && value >= keys[index]; index++) {} + index < (psm__u32)key_count && value >= keys[index]; index++) { + } index--; if (index < (psm__u32)key_count - 1) weight = (value - keys[index]) / (keys[index + 1] - keys[index]); @@ -257,11 +267,11 @@ psm__resolve_blend_key_tables(struct psm__model *m) psm__u32 old_index = blend_key_tables[bs_i].idx; psm__f32 old_weight = blend_key_tables[bs_i].weight; - bool idx_dirty = (old_index != index), - weight_dirty = (old_weight != weight); + bool idx_dirty = (old_index != index), + weight_dirty = (old_weight != weight); if (weight_dirty) idx_dirty = weight == 0.0f || - old_weight == 0.0f || old_index != index; + old_weight == 0.0f || old_index != index; blend_key_tables[bs_i].idx_dirty = idx_dirty; blend_key_tables[bs_i].weight_dirty = weight_dirty; @@ -294,11 +304,12 @@ psm__resolve_bindings(struct psm__model *m) for (psm__i32 bi = 0; bi < count; bi++) { psm__i32 binding_count = binds[bi].key_table_len; + struct psm__key_table **bindings = binds[bi].key_tables; - bool idx_dirty = false; - bool weight_dirty = false; - bool out_of_range = false; + bool idx_dirty = false; + bool weight_dirty = false; + bool out_of_range = false; psm__u32 active_binding_count = 0; /* Skip if no bindings */ @@ -355,13 +366,6 @@ psm__resolve_bindings(struct psm__model *m) psm__u32 blend_count = 1u << active_binding_count; binds[bi].blend_count = blend_count; - if (active_binding_count == 31) { - binds[bi].idx_dirty = idx_dirty; - binds[bi].weight_dirty = weight_dirty; - binds[bi].out_of_range = false; - continue; - } - /* Skip if keyform_idx or weights arrays are missing */ if (!binds[bi].keyform_idx || !binds[bi].weights) { binds[bi].idx_dirty = 0; @@ -386,6 +390,7 @@ psm__resolve_bindings(struct psm__model *m) for (psm__i32 i = 0; i < binding_count; i++) { struct psm__key_table *binding = bindings[i]; + psm__i32 index = binding->idx; psm__i32 key_count = binding->key_count; psm__f32 weight = binding->weight; @@ -488,11 +493,12 @@ psm__resolve_blend_bindings(struct psm__model *m) for (psm__i32 i = 0; i < constraint_count; i++) { struct psm__blend_constraint *constraint = binds[bi].constraints[i]; - struct psm__param *param = constraint->param; + struct psm__param *param = constraint->param; + psm__f32 constraint_weight = 1.0f; if (param != NULL && (force_update || param->dirty)) { - psm__i32 key_count = constraint->count; + psm__i32 key_count = constraint->count; psm__f32 *keys = constraint->keys, *weights = constraint->weights; if (key_count >= 2) { @@ -500,12 +506,13 @@ psm__resolve_blend_bindings(struct psm__model *m) if (value > keys[0]) { /* Find upper bound */ psm__i32 idx; - for (idx = 1; idx < key_count && value >= keys[idx]; idx++) {} + for (idx = 1; idx < key_count && value >= keys[idx]; idx++) { + } idx--; if (idx < key_count - 1) { psm__f32 t = (value - keys[idx]) / (keys[idx + 1] - keys[idx]); constraint_weight = weights[idx] * (1.0f - t) + - weights[idx + 1] * t; + weights[idx + 1] * t; } else { constraint_weight = weights[key_count - 1]; } diff --git a/src/param.h b/src/param.h index e73678b..841e5d7 100644 --- a/src/param.h +++ b/src/param.h @@ -11,7 +11,8 @@ #include "private.h" #include "model.h" -PSM__DEF void psm__resolve_params(struct psm__params *); +/* returns true if any non-repeat parameter was outside [min,max] (clamped) */ +PSM__DEF bool psm__resolve_params(struct psm__params *); PSM__DEF void psm__resolve_key_tables(struct psm__model *); PSM__DEF void psm__resolve_blend_key_tables(struct psm__model *); PSM__DEF void psm__resolve_bindings(struct psm__model *); diff --git a/src/part.c b/src/part.c index 365e123..4ceef98 100644 --- a/src/part.c +++ b/src/part.c @@ -22,19 +22,16 @@ psm__enable_parts(struct psm__model *m) return; struct psm__part *items = m->parts.items; - bool *enable = m->parts.enable; + bool *enable = m->parts.enable; for (psm__i32 i = 0; i < count; i++) { struct psm__part *part = &items[i]; - bool en = part->local_enable; + + bool en = part->local_enable; psm__i32 parent_part_idx = part->parent_part_idx; if (en && parent_part_idx != -1) en = enable[parent_part_idx]; -#ifndef PSM_FAST_AND_DANGEROUS - if (en && !part->binding) - en = 0; -#endif if (en) en = !part->binding->out_of_range; @@ -53,15 +50,13 @@ psm__gather_parts(struct psm__model *m) if (!items) return; struct psm__sections *ms = m->source->sections; - psm__i32 *keyform_off = ms->part_src.keyform_off; - psm__f32 *draw_order_src = ms->part_key_src.draw_order; + psm__i32 *keyform_off = ms->part_src.keyform_off; + psm__f32 *draw_order_src = ms->part_key_src.draw_order; if (!keyform_off || !draw_order_src) return; - struct psm__binding *bindings[count]; - for (psm__i32 i = 0; i < count; i++) - bindings[i] = items[i].binding; + struct psm__binding *const *bindings = m->parts.bindings; struct psm__gather_channel ch[] = { { draw_order_src, m->parts.keydata.draw_order }, @@ -78,10 +73,11 @@ psm__apply_part_opacity(struct psm__model *m) return; struct psm__part *items = m->parts.items; + psm__i32 *offscreen_indices = m->parts.offscreen_src_idx; - bool *enable = m->parts.enable; + bool *enable = m->parts.enable; psm__f32 *input_opacity = m->parts.input_opacity, - *part_opa = m->parts.opacity; + *part_opa = m->parts.opacity; for (psm__i32 i = 0; i < count; i++) { if (!enable[i]) diff --git a/src/private.h b/src/private.h index d46243c..8326a23 100644 --- a/src/private.h +++ b/src/private.h @@ -10,7 +10,7 @@ /* Internal code always needs the full v6 blend type set. */ #ifndef PSM__BLENDTYPE_V6 -#define PSM__BLENDTYPE_V6 +# define PSM__BLENDTYPE_V6 #endif #include "../include/PurismCore.h" @@ -19,13 +19,13 @@ #if defined(__cplusplus) /* C++ has bool natively */ #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L -# include +# include #elif defined(_MSC_VER) && _MSC_VER >= 1800 -# include +# include #else - typedef unsigned char bool; -# define true 1 -# define false 0 +typedef unsigned char bool; +# define true 1 +# define false 0 #endif #ifndef PSM__DEF @@ -41,26 +41,28 @@ #endif enum { - PSM__FLAG_IS_VISIBLE = 0x01, - PSM__FLAG_VISIBILITY_CHANGED = 0x02, - PSM__FLAG_OPACITY_CHANGED = 0x04, - PSM__FLAG_DRAW_ORDER_CHANGED = 0x08, + PSM__FLAG_IS_VISIBLE = 0x01, + PSM__FLAG_VISIBILITY_CHANGED = 0x02, + PSM__FLAG_OPACITY_CHANGED = 0x04, + PSM__FLAG_DRAW_ORDER_CHANGED = 0x08, PSM__FLAG_RENDER_ORDER_CHANGED = 0x10, - PSM__FLAG_VERTEX_CHANGED = 0x20, - PSM__FLAG_BLEND_COLOR_CHANGED = 0x40, - PSM__FLAG_ALL_CHANGED = 0x7E, - PSM__FLAG_ALL = 0x7F, + PSM__FLAG_VERTEX_CHANGED = 0x20, + PSM__FLAG_BLEND_COLOR_CHANGED = 0x40, + PSM__FLAG_ALL_CHANGED = 0x7E, + PSM__FLAG_ALL = 0x7F, }; enum { PSM__CANVAS_FLAG_Y_REVERSED = 0x01, }; -#define PSM__VERFMT "%d.%d.%d" -#define PSM__VERARG(x) ((x) >> 24), (((x) >> 16) & 0xFF), ((x) & 0xFFFF) +/* The version constants are `long` (e.g. 0x06000001L); cast the extracted + components to int so they match PSM__VERFMT's %d. */ +#define PSM__VERFMT "%d.%d.%d" +#define PSM__VERARG(x) (int)((x) >> 24), (int)(((x) >> 16) & 0xFF), (int)((x) & 0xFFFF) -static inline -psm__i32 psm__clamp_i32(psm__i32 v, psm__i32 lo, psm__i32 hi) +static inline psm__i32 +psm__clamp_i32(psm__i32 v, psm__i32 lo, psm__i32 hi) { if (v < lo) return lo; if (v > hi) return hi; @@ -73,9 +75,13 @@ psm__i32 psm__clamp_i32(psm__i32 v, psm__i32 lo, psm__i32 hi) * floats. On 64-bit platforms this is 2 (one pointer = two floats). */ #define PSM__PTR_FLOAT_RATIO (sizeof(void *) / sizeof(psm__f32)) -#define PSM__MAX_KEY_TABLES 20 +#define PSM__MAX_KEY_TABLES 20 -static inline psm__u32 psm__align_to_16(psm__u32 n) { return (n + 15) & ~15u; } +static inline psm__u32 +psm__align_to_16(psm__u32 n) +{ + return (n + 15) & ~15u; +} #define psm__nop_predicate(...) diff --git a/src/render.c b/src/render.c index dc96ccf..dffd481 100644 --- a/src/render.c +++ b/src/render.c @@ -17,6 +17,7 @@ PSM__DEF void psm__sort_render_order(struct psm__model *m) { struct psm__draw_groups *dog = &m->draw_groups; + psm__i32 group_count = dog->count; if (group_count <= 0) return; @@ -26,16 +27,18 @@ psm__sort_render_order(struct psm__model *m) return; struct psm__art_meshes *am = &m->art_meshes; - struct psm__parts *pt = &m->parts; + struct psm__parts *pt = &m->parts; + psm__i32 *am_draw = am->draw_order; psm__i32 *pt_draw = pt->draw_order; - bool *am_en = am->enable; - bool *pt_en = pt->enable; - psm__i32 am_cnt = am->count; + bool *am_en = am->enable; + bool *pt_en = pt->enable; + psm__i32 am_cnt = am->count; /* First assign draw orders to items */ for (psm__i32 gi = 0; gi < group_count; gi++) { struct psm__draw_group *c = &groups[gi]; + psm__i32 n = c->count; if (n <= 0) continue; @@ -45,6 +48,7 @@ psm__sort_render_order(struct psm__model *m) for (psm__i32 j = 0; j < n; j++) { struct psm__draw_item *item = &it[j]; + psm__i32 oi = item->object_idx; if (item->object_type == 1) { @@ -63,18 +67,21 @@ psm__sort_render_order(struct psm__model *m) /* Now time for sorting and render order assignment */ psm__i32 *render_order = m->render_order; - psm__u8 ver = m->source->header->version; + psm__u8 ver = m->source->header->version; + struct psm__draw_sort *srt = &dog->sort; + psm__i32 *first = srt->first; - psm__i32 *last = srt->last; - psm__i32 *next = srt->next; + psm__i32 *last = srt->last; + psm__i32 *next = srt->next; if (!first || !last || !next) return; /* Compute max values from source for bounds checking */ psm__i32 max_level = 0, max_items = 0; - struct psm__sections *ms = m->source->sections; + + struct psm__sections *ms = m->source->sections; struct psm__count_info *cnt = ms->count_info; if (cnt->draw_groups > 0 && ms->draw_group_src.obj_len && @@ -84,8 +91,8 @@ psm__sort_render_order(struct psm__model *m) psm__i32 hi = ms->draw_group_src.max_order[i]; psm__i32 lo = ms->draw_group_src.min_order[i]; psm__i32 lv = psm__safe_order_level(hi, lo); - if (lv > max_level) max_level = lv; - if (gc > max_items) max_items = gc; + if (lv > max_level) max_level = lv; + if (gc > max_items) max_items = gc; } } @@ -94,8 +101,9 @@ psm__sort_render_order(struct psm__model *m) for (psm__i32 gi = 0; gi < group_count; gi++) { struct psm__draw_group *c = &groups[gi]; + psm__i32 olevel = c->order_level; - psm__i32 n = c->count; + psm__i32 n = c->count; if (olevel <= 0 || n <= 0) continue; @@ -107,7 +115,7 @@ psm__sort_render_order(struct psm__model *m) continue; psm_size olsz = (psm_size)olevel * sizeof(psm__i32); memset(first, 0xFF, olsz); - memset(last, 0xFF, olsz); + memset(last, 0xFF, olsz); if ((psm_size)n > (psm_size)-1 / sizeof(psm__i32)) continue; @@ -118,8 +126,8 @@ psm__sort_render_order(struct psm__model *m) if (!it) continue; for (psm__i32 j = 0; j < n; j++) { - psm__i32 rel = (psm__i32)((psm__u32)it[j].draw_order - - (psm__u32)c->min_order); + psm__i32 rel = + (psm__i32)((psm__u32)it[j].draw_order - (psm__u32)c->min_order); rel = psm__clamp_idx(rel, olevel); if (last[rel] == -1) @@ -131,12 +139,13 @@ psm__sort_render_order(struct psm__model *m) /* Walk buckets in order and assign render positions */ psm__i32 pos = c->cursor; - psm__i32 has_offscr = (ver >= csmMocVersion_53); + bool has_offscr = (ver >= csmMocVersion_53); for (psm__i32 oi = 0; oi < olevel; oi++) { psm__i32 di = first[oi]; while (di != -1 && di < n) { struct psm__draw_item *item = &it[di]; + psm__i32 obj = item->object_idx; psm__i32 grp = item->group_idx; @@ -146,6 +155,8 @@ psm__sort_render_order(struct psm__model *m) if (oidx >= 0) render_order[am_cnt + oidx] = pos++; } + /* F3: a part draw item (type 1) always has a valid child group; + * self_group_idx >= 0 is proved at load (psm__verify_idx). */ if (grp < group_count) { struct psm__draw_group *nc = &groups[grp]; nc->cursor = pos; diff --git a/src/samples/.clang-format b/src/samples/.clang-format new file mode 100644 index 0000000..885a4c3 --- /dev/null +++ b/src/samples/.clang-format @@ -0,0 +1,5 @@ +# src/samples is excluded from the library house style: +# - src/tests: includes vendored (partcl.h) and generated (refdata.h) headers +# - src/samples: standalone example code (the viewer keeps its own .clang-format) +DisableFormat: true +SortIncludes: Never diff --git a/src/samples/benchmark.c b/src/samples/benchmark.c new file mode 100644 index 0000000..64e1991 --- /dev/null +++ b/src/samples/benchmark.c @@ -0,0 +1,266 @@ +/* + * Purism Core: micro-benchmark + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include + +#include "../../include/PurismCore.h" +#include "common.h" + +static uint64_t +now_ns(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; +} + +/* + * Run `fn(ctx)` repeatedly (in batches, to amortize the clock read) until at + * least target_ns has elapsed, then return mean nanoseconds per call. + * `*iters_out` receives the iteration count actually run. + */ +static double +time_loop(uint64_t target_ns, void (*fn)(void *), void *ctx, + uint64_t *iters_out) +{ + const int batch = 8; + /* warm up */ + for (int i = 0; i < 4; i++) fn(ctx); + + uint64_t iters = 0; + uint64_t start = now_ns(), elapsed = 0; + do { + for (int b = 0; b < batch; b++) fn(ctx); + iters += batch; + elapsed = now_ns() - start; + } while (elapsed < target_ns && iters < 200000000ull); + + if (iters_out) *iters_out = iters; + return (double)elapsed / (double)iters; +} + +struct ctx { + const uint8_t *raw; /* pristine file bytes */ + size_t n; /* file size */ + void *work; /* reusable revive buffer (n bytes, aligned) */ + csmMoc *moc; /* live moc (for init/update phases) */ + void *model_buf; + unsigned model_sz; + csmModel *model; /* live model (for update phases) */ + uint64_t counter; /* drives param animation */ +}; + +static void +do_memcpy(void *p) +{ + struct ctx *c = p; + memcpy(c->work, c->raw, c->n); +} + +static void +do_revive(void *p) +{ + struct ctx *c = p; + memcpy(c->work, c->raw, c->n); + volatile csmMoc *m = csmReviveMocInPlace(c->work, (unsigned)c->n); + (void)m; +} + +static void +do_init(void *p) +{ + struct ctx *c = p; + unsigned sz = csmGetSizeofModel(c->moc); + volatile csmModel *m = + csmInitializeModelInPlace(c->moc, c->model_buf, sz); + (void)m; +} + +static void +do_animate(void *p) +{ + struct ctx *c = p; + int pc = csmGetParameterCount(c->model); + float *v = csmGetParameterValues(c->model); + const float *mn = csmGetParameterMinimumValues(c->model); + const float *mx = csmGetParameterMaximumValues(c->model); + uint64_t t = c->counter++; + for (int i = 0; i < pc; i++) { + /* cheap deterministic sweep across [min,max] */ + float f = (float)(((t + (uint64_t)i) * 2654435761u) % 1000) / 999.0f; + v[i] = mn[i] + f * (mx[i] - mn[i]); + } + csmResetDrawableDynamicFlags(c->model); + csmUpdateModel(c->model); +} + +static void +do_idle(void *p) +{ + struct ctx *c = p; + csmUpdateModel(c->model); +} + +static int g_models; +static double g_sum_revive, g_sum_init, g_sum_anim, g_sum_idle; +static uint64_t g_total_bytes, g_total_verts; + +static void +fmt_time(double ns, char *out, size_t cap) +{ + if (ns < 1000.0) snprintf(out, cap, "%.0f ns", ns); + else if (ns < 1000000.0) snprintf(out, cap, "%.2f us", ns / 1e3); + else snprintf(out, cap, "%.3f ms", ns / 1e6); +} + +static void +run_one(const char *path, uint64_t budget_ns) +{ + size_t n = 0; + void *raw = psm_read_file(path, &n, csmAlignofMoc, 0); + if (!raw) return; + + void *work = psm_aligned_alloc(csmAlignofMoc, n); + void *probe = psm_aligned_alloc(csmAlignofMoc, n); + memcpy(probe, raw, n); + csmMoc *moc = csmReviveMocInPlace(probe, (unsigned)n); + if (!moc) { + fprintf(stderr, " skip (revive failed): %s\n", path); + psm_aligned_free(raw); psm_aligned_free(work); psm_aligned_free(probe); + return; + } + unsigned model_sz = csmGetSizeofModel(moc); + void *model_buf = psm_aligned_alloc(csmAlignofModel, model_sz); + csmModel *model = csmInitializeModelInPlace(moc, model_buf, model_sz); + if (!model) { + fprintf(stderr, " skip (init failed): %s\n", path); + psm_aligned_free(raw); psm_aligned_free(work); + psm_aligned_free(probe); psm_aligned_free(model_buf); + return; + } + + int dc = csmGetDrawableCount(model); + int pc = csmGetParameterCount(model); + int parts = csmGetPartCount(model); + const int *vcs = csmGetDrawableVertexCounts(model); + uint64_t verts = 0; + for (int i = 0; i < dc; i++) verts += (vcs[i] > 0) ? (uint64_t)vcs[i] : 0; + + struct ctx c = {0}; + c.raw = raw; c.n = n; c.work = work; + c.moc = moc; c.model_buf = model_buf; c.model_sz = model_sz; c.model = model; + + uint64_t it; + double t_copy = time_loop(budget_ns / 2, do_memcpy, &c, &it); + double t_revive = time_loop(budget_ns, do_revive, &c, &it); + double revive = t_revive - t_copy; if (revive < 0) revive = 0; + double init = time_loop(budget_ns, do_init, &c, &it); + double anim = time_loop(budget_ns, do_animate, &c, &it); + double idle = time_loop(budget_ns, do_idle, &c, &it); + + char b1[32], b2[32], b3[32], b4[32]; + fmt_time(revive, b1, sizeof b1); + fmt_time(init, b2, sizeof b2); + fmt_time(anim, b3, sizeof b3); + fmt_time(idle, b4, sizeof b4); + + const char *base = strrchr(path, '/'); + base = base ? base + 1 : path; + + printf("\n%s\n", base); + printf(" size %zu KB | drawables %d | params %d | parts %d | vertices %llu\n", + n / 1024, dc, pc, parts, (unsigned long long)verts); + printf(" revive (parse+validate) : %-10s %.2f GB/s\n", + b1, revive > 0 ? (double)n / revive : 0.0); /* bytes/ns == GB/s */ + printf(" init (build model) : %-10s\n", b2); + printf(" animate (full recompute): %-10s %.0f fps\n", + b3, anim > 0 ? 1e9 / anim : 0.0); + printf(" idle (no change) : %-10s %.0f fps\n", + b4, idle > 0 ? 1e9 / idle : 0.0); + + g_models++; + g_sum_revive += revive; g_sum_init += init; + g_sum_anim += anim; g_sum_idle += idle; + g_total_bytes += n; g_total_verts += verts; + + psm_aligned_free(raw); psm_aligned_free(work); + psm_aligned_free(probe); psm_aligned_free(model_buf); +} + +static int +ends_moc3(const char *s) +{ + size_t l = strlen(s); + return l > 5 && strcmp(s + l - 5, ".moc3") == 0; +} + +static void +run_path(const char *path, uint64_t budget_ns) +{ + DIR *d = opendir(path); + if (d) { + struct dirent *e; char p[4096]; + while ((e = readdir(d))) { + if (!ends_moc3(e->d_name)) continue; + snprintf(p, sizeof p, "%s/%s", path, e->d_name); + run_one(p, budget_ns); + } + closedir(d); + } else { + run_one(path, budget_ns); + } +} + +static void +null_log(const char *m) { (void)m; } + +int +main(int argc, char **argv) +{ + uint64_t budget_ns = 200ull * 1000000ull; /* 200 ms per phase */ + + csmSetLogFunction(null_log); + printf("Purism Core benchmark (ABI %#08x)\n", (unsigned)csmGetVersion()); + + int any = 0; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-t") == 0 && i + 1 < argc) { + budget_ns = (uint64_t)strtoull(argv[++i], NULL, 10) * 1000000ull; + } else if (strcmp(argv[i], "-h") == 0) { + printf("usage: benchmark [-t ms_per_phase] ...\n"); + return 0; + } else { + run_path(argv[i], budget_ns); + any = 1; + } + } + + if (!any) { + fprintf(stderr, "no input; usage: benchmark [-t ms] ...\n"); + return 1; + } + + if (g_models > 1) { + char b1[32], b2[32], b3[32], b4[32]; + fmt_time(g_sum_revive / g_models, b1, sizeof b1); + fmt_time(g_sum_init / g_models, b2, sizeof b2); + fmt_time(g_sum_anim / g_models, b3, sizeof b3); + fmt_time(g_sum_idle / g_models, b4, sizeof b4); + printf("\n=== mean over %d models ===\n", g_models); + printf(" revive %-10s init %-10s\n", b1, b2); + printf(" animate %-10s idle %-10s\n", b3, b4); + printf(" total %llu KB parsed, %llu vertices\n", + (unsigned long long)(g_total_bytes / 1024), + (unsigned long long)g_total_verts); + } + return 0; +} diff --git a/src/samples/cascadia.fnt b/src/samples/cascadia.fnt new file mode 100755 index 0000000..4d1eaa4 --- /dev/null +++ b/src/samples/cascadia.fnt @@ -0,0 +1,99 @@ +info face="Cascadia Code" size=32 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=2 padding=0,0,0,0 spacing=1,1 outline=0 +common lineHeight=32 base=27 scaleW=256 scaleH=128 pages=1 packed=0 alphaChnl=1 redChnl=0 greenChnl=0 blueChnl=0 +page id=0 file="cascadia_0.png" +chars count=95 +char id=32 x=126 y=21 width=2 height=1 xoffset=0 yoffset=31 xadvance=14 page=0 chnl=15 +char id=33 x=251 y=0 width=4 height=17 xoffset=5 yoffset=10 xadvance=14 page=0 chnl=15 +char id=34 x=44 y=80 width=8 height=8 xoffset=3 yoffset=10 xadvance=14 page=0 chnl=15 +char id=35 x=0 y=45 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=36 x=4 y=0 width=11 height=24 xoffset=1 yoffset=6 xadvance=14 page=0 chnl=15 +char id=37 x=82 y=23 width=14 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=38 x=67 y=23 width=14 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=39 x=53 y=78 width=3 height=8 xoffset=5 yoffset=10 xadvance=14 page=0 chnl=15 +char id=40 x=88 y=0 width=9 height=22 xoffset=3 yoffset=7 xadvance=14 page=0 chnl=15 +char id=41 x=98 y=0 width=9 height=22 xoffset=2 yoffset=7 xadvance=14 page=0 chnl=15 +char id=42 x=0 y=81 width=12 height=12 xoffset=1 yoffset=12 xadvance=14 page=0 chnl=15 +char id=43 x=141 y=59 width=12 height=13 xoffset=1 yoffset=12 xadvance=14 page=0 chnl=15 +char id=44 x=39 y=80 width=4 height=9 xoffset=4 yoffset=23 xadvance=14 page=0 chnl=15 +char id=45 x=105 y=73 width=12 height=3 xoffset=1 yoffset=17 xadvance=14 page=0 chnl=15 +char id=46 x=87 y=73 width=4 height=4 xoffset=5 yoffset=23 xadvance=14 page=0 chnl=15 +char id=47 x=74 y=0 width=13 height=22 xoffset=1 yoffset=7 xadvance=14 page=0 chnl=15 +char id=48 x=26 y=44 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=49 x=208 y=37 width=11 height=17 xoffset=2 yoffset=10 xadvance=14 page=0 chnl=15 +char id=50 x=39 y=42 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=51 x=48 y=60 width=11 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=52 x=139 y=20 width=13 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=53 x=52 y=41 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=54 x=156 y=37 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=55 x=195 y=19 width=13 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=56 x=65 y=41 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=57 x=78 y=41 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=58 x=249 y=19 width=4 height=13 xoffset=5 yoffset=14 xadvance=14 page=0 chnl=15 +char id=59 x=245 y=0 width=5 height=18 xoffset=4 yoffset=14 xadvance=14 page=0 chnl=15 +char id=60 x=240 y=55 width=12 height=12 xoffset=1 yoffset=12 xadvance=14 page=0 chnl=15 +char id=61 x=26 y=80 width=12 height=9 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=62 x=13 y=81 width=12 height=12 xoffset=1 yoffset=12 xadvance=14 page=0 chnl=15 +char id=63 x=36 y=62 width=11 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=64 x=126 y=0 width=12 height=20 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=65 x=153 y=19 width=13 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=66 x=91 y=41 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=67 x=111 y=23 width=13 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=68 x=13 y=44 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=69 x=169 y=37 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=70 x=182 y=37 width=12 height=17 xoffset=2 yoffset=10 xadvance=14 page=0 chnl=15 +char id=71 x=97 y=23 width=13 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=72 x=0 y=63 width=11 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=73 x=12 y=63 width=11 height=17 xoffset=2 yoffset=10 xadvance=14 page=0 chnl=15 +char id=74 x=117 y=41 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=75 x=195 y=37 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=76 x=223 y=19 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=77 x=236 y=19 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=78 x=220 y=37 width=11 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=79 x=209 y=19 width=13 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=80 x=130 y=41 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=81 x=60 y=0 width=13 height=22 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=82 x=104 y=41 width=12 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=83 x=24 y=62 width=11 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=84 x=181 y=19 width=13 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=85 x=232 y=37 width=11 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=86 x=125 y=23 width=13 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=87 x=37 y=24 width=14 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=88 x=167 y=19 width=13 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=89 x=52 y=23 width=14 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=90 x=244 y=37 width=11 height=17 xoffset=1 yoffset=10 xadvance=14 page=0 chnl=15 +char id=91 x=117 y=0 width=8 height=22 xoffset=4 yoffset=7 xadvance=14 page=0 chnl=15 +char id=92 x=46 y=0 width=13 height=22 xoffset=0 yoffset=7 xadvance=14 page=0 chnl=15 +char id=93 x=108 y=0 width=8 height=22 xoffset=2 yoffset=7 xadvance=14 page=0 chnl=15 +char id=94 x=57 y=78 width=10 height=7 xoffset=2 yoffset=10 xadvance=14 page=0 chnl=15 +char id=95 x=92 y=73 width=12 height=3 xoffset=1 yoffset=26 xadvance=14 page=0 chnl=15 +char id=96 x=81 y=73 width=5 height=5 xoffset=4 yoffset=7 xadvance=14 page=0 chnl=15 +char id=97 x=60 y=59 width=13 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=98 x=0 y=26 width=12 height=18 xoffset=1 yoffset=9 xadvance=14 page=0 chnl=15 +char id=99 x=102 y=59 width=12 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=100 x=219 y=0 width=12 height=18 xoffset=1 yoffset=9 xadvance=14 page=0 chnl=15 +char id=101 x=216 y=55 width=11 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=102 x=152 y=0 width=13 height=18 xoffset=0 yoffset=9 xadvance=14 page=0 chnl=15 +char id=103 x=232 y=0 width=12 height=18 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=104 x=13 y=25 width=11 height=18 xoffset=2 yoffset=9 xadvance=14 page=0 chnl=15 +char id=105 x=139 y=0 width=12 height=19 xoffset=1 yoffset=8 xadvance=14 page=0 chnl=15 +char id=106 x=16 y=0 width=9 height=24 xoffset=2 yoffset=8 xadvance=14 page=0 chnl=15 +char id=107 x=206 y=0 width=12 height=18 xoffset=1 yoffset=9 xadvance=14 page=0 chnl=15 +char id=108 x=193 y=0 width=12 height=18 xoffset=1 yoffset=9 xadvance=14 page=0 chnl=15 +char id=109 x=128 y=59 width=12 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=110 x=180 y=55 width=11 height=13 xoffset=2 yoffset=14 xadvance=14 page=0 chnl=15 +char id=111 x=192 y=55 width=11 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=112 x=25 y=25 width=11 height=18 xoffset=2 yoffset=14 xadvance=14 page=0 chnl=15 +char id=113 x=180 y=0 width=12 height=18 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=114 x=74 y=59 width=13 height=13 xoffset=0 yoffset=14 xadvance=14 page=0 chnl=15 +char id=115 x=204 y=55 width=11 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=116 x=143 y=38 width=12 height=17 xoffset=0 yoffset=10 xadvance=14 page=0 chnl=15 +char id=117 x=154 y=56 width=12 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=118 x=167 y=55 width=12 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=119 x=88 y=59 width=13 height=13 xoffset=0 yoffset=14 xadvance=14 page=0 chnl=15 +char id=120 x=115 y=59 width=12 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=121 x=166 y=0 width=13 height=18 xoffset=0 yoffset=14 xadvance=14 page=0 chnl=15 +char id=122 x=228 y=55 width=11 height=13 xoffset=1 yoffset=14 xadvance=14 page=0 chnl=15 +char id=123 x=36 y=0 width=9 height=23 xoffset=2 yoffset=7 xadvance=14 page=0 chnl=15 +char id=124 x=0 y=0 width=3 height=25 xoffset=5 yoffset=6 xadvance=14 page=0 chnl=15 +char id=125 x=26 y=0 width=9 height=23 xoffset=2 yoffset=7 xadvance=14 page=0 chnl=15 +char id=126 x=68 y=73 width=12 height=6 xoffset=1 yoffset=15 xadvance=14 page=0 chnl=15 diff --git a/src/samples/cascadia_0.png b/src/samples/cascadia_0.png new file mode 100755 index 0000000000000000000000000000000000000000..f338477ec28e4da5a6b74a8b7d3a9383312814ac GIT binary patch literal 8501 zcmV-5AuS5LSH6EF z=Q+=L&U4Q9xz9QGeV_XbtAgtR@c6I9hP!eCjPp+!lgwhw6#u;pFCRw=N_IvSts9$? zS5RK=F;cl@QzuX|bzGit`B)$j2m}JFk_q7Ux|ljv71$L{$#YhXWh$X;8kblYdQo#{#ywtNI=r&0;747tSm`E;6)=D8)Q7|lbVt>6~-h3LvS;(91A+g9gL>Ujb*a=)>Mhm@)U|5QLwu;BLN2F*WrUZl)B%rV<0VAbFxr}VEx?L&7?V3ChC93cA-8~2n zYa;-4+6(z{MJ5a$zU-LsY4QCx2pF7D_*dT1pGGAwo~UAZry`&@dSoEL4n@tJRfQs`lySOW-4O-Z+?YQF6;KUU<&~6!X;MbsGJke z0Ir+)bgEs^;x`$VOK1F)UG;PCI`5+QKH-eccl*(1C3kM&R^aEftc89w0B}4*jRvZf z04Up^NbDO0!1?AgJ5$QV4FawoP5o1>{*U81=VMMPDhK!p2hEZ#Ru7*!ku%DA{7n9| z4uE|fTy14O$m*TIzveL0@unqY|7(VZp3kfJXuALX;drc92QqeP4&S&5u44_5#fdJv zZop{x!WS;e%KGNQ?{!`7utC5N;1?gVrR0ZY<5Tbd6Ln_l4_sh;_?>U?pmp0z%uF4% zKj7HDEHavKJULe2wPOs+&;T#t5jiKXd*;Rjo(#7gs0wj2L(bwIAV#lK7j|F7M=-dU zW8t(NdA!(92azX>#xn{0e0+?&l>xG{_37uGKO z{&9c<=KIq>Ll!^;9w!r889fGoq6r*ZYvlcQr!4|eyWl@qi6NKr3;^fQl2TO*XJvRn z59mP@sPC#THfEg6x$#ck1z1S0R{`$mqUgXfnis-PX?_#nHGoB&RR!GqBLF7&rua$w;l|Of#=HUl@row7m>K&$My+F+DT4 z_C)3zmz;LmX?xjw+G(eqW_-GxpYh;7@eaVs1B^JlVN=KX8J_z;zPpzMa!yVpZQgt3 z2zML^)UDV9fPdOJIMLB@!XTjj=YQlo+*#DAwoT72ruW5M*oURh)LNNUt=w;<4w7{< z0N3Zv4VaZ%RMWdO~;IKa7se)z|g8A|nD^ioj| z<}XjrZ}uMM8-?^!dR=drBcGzRKdW*@&DfO~UJJZm+c3sb7Pm;-c13)eC6b56dvsUwqsq@Z{n zZ*I?c=kt@?3E(!bC;y43J?ZnFfx;j04x9@OFsFB~U#Hpvbuu+CO9EQ;nfc(yN>2IY z#WmeX4Q!Mo9lYGQJd`vGCpu||JZZ9#N|69}T*Yv;LkGw#0V6Bs?vvWmlu1hh_*{wv zo8lFgD*TkR5?t-lfk0j2Gsz1+-@WA%SHu-7fJur1^QFb91Z=W2X^Vh$vc5~Joa8Sz z4PIb$P6CQ{wmg{ZTdg{r=0-2>48r8dC>=gMfh@ zrrB)S0*%s07aWFS_Ji#FtyN^_mdlD~+N9-;rSVNN;E^G{|7+u$$8)kDg*yuYxOf_L z)B?=xW0^Lgj?f?e@N%XK0NC{X>(3{0=pz=aHOA~M*K)o+@8$mC!+MDJ2%n4}T=ZH! z&{DGwU^*BQ(586frwS(|b0r)s-3w4sHCZomr$xL^<>QUf5l%i-qqP9mI9a83zXY(w?fipBiU#STk3Sk)inWe zH>rNQbONHl5lLRQ%UGz>Jwjv4r*4c(k3(3BJZV4taEH@bMQL!4wdmlfs1A&T$AY_( zA_73(?A@X5tPR!AGsePK5-?E-my>`F{a)rmJlvqSL@G8#>2pJj1`{+AK3_~Vr1|O6 z3D9t^ktDCV26QICAo3;qb0l%KH?4nvepIUk2LVHra7SgXF)I~z!y&z86}J0kEf%*) zcai3&OCz90lc~@a$!n|ubSB_vsCE0Kbk|rKrrRo6UffXq2Gm3p-&N5^&-F?C1wmB-Dr&0&3cRcsOiTC z3ZhE9HpV-=IuR>`JEYix5#Hi8s&i9pIa9sTJuuvu>Ma613EhfUy2h+%k77<<18H+e zsWb-g+oXo{ekTH6(3`4GAt1$%no7Wegd}MxOie1nAyol~v>af*!Y;rqtpW=Deh(1R zov>9?z*skT7#0&eFc3TuOZ9ug4X&7}AWu}n1NTa92La>FIeVh{>H!JKHc2}ZP-CdA z$qqFt(20Os6;;W9mw;n?ioJKWNjEv>WfH0c#w(r&o~U6Hc%9GDs_{^#Nw}7T^T8`E zP2Rjuayry@$gr|DYcEi3Sd7-&YP1npCfi2eufvXK(+F_Iv?R4d!3+ctRov|)pi8$J zplcSdo%geo+(bKg84?N=csjrrC;3kDvG;iR?pc@e*`QB~=mVEw zcu5VEaCE-o6$|;M8{j+q7WfH!o$oJTIq)&xGFP9ni#T@%U13NM!(EiYPv=A(jfN7N zRD`;efMo#eR11TpBFdym+#rnr7^`@mCu(UvpDSih&g8aA(&E_(y^{;9Pws92p;VRn z8bf=&Rk|67sS<1n7%jB~gTY2i!S+P7$N8Q#3D{;@|5J@E0U)BP&JqCNb0|WcB%pY| z5_=7u&~Wo@k)dq4&e6}K;57RY8F5JFTXnR z*uT~RE{-^=KbyCmZ~Nd7KLIY`*ebZL2WNS`Uhk#g^}tndLI3{!FSh(;Pxd} z(;IBU{awTPnZZw&8K1yLfPg}In_~(s&j+LxaY;%PC8fcy5QH_6^=%EGaj(U1k>W9H zIJ+0dr8N$Uc{ZlH!G_{wtkNaHU@#aQo+4ua3)KSD>tmoSMZsmiKJxqhe*f=v1Yo38 z%rJ+&l_~+_EqgmRVa_=Vq`Xr|KvEG3)f$%2K#6$xq|1O>IoJ zM@*f)k?pEe;)t}IE2NwgRuI!&1Rh38=Ft$P_=_DG0Eui5+|9h8t!jR0lE~9ZMt^lCdjvSi51n0z}@Q zo1n_But@rqk(#5#&F(ts`LRm4fYONe4_cvkFs>C)t%Msc_xt^lj~3IWvEYhn93@IP z_l1l(ph5|=t1(T6xr!G6c~Ns}O6yy0LUYYzepqS@)Jw4e5KuH|qNFPV3_+eRc`>~> z8EqpjpZ2$$c7X10oIiAt+lW( zStr5Y+;PBt5yxi27xcM|jwr4RSxY+3VmH)ZwXUCw8NlkfA1}w7#bOp$XaC=Q@1Ds1 z^}BYpYe!}Md&(~9!l%E_wfweIrM$M<+Lyfds*@Bv$`Sw`ZOQ6qWG`6jSihpv;&#I} zeRK3Td&Zg;8-?s5wM@fsQEi{*aPnHqpx2JF^nQZ60VUdj78AK<^I%(@-GXEfE%Q;bOIPN zb!w>#ph%jkt6i#&*os_|fKgI>d!WghNewho zz=zeMQ@O-PzP$TrJ88lkN?v#gn#0m7-3_@Fd#w38Xc~s)3K25%$`x{WP=d+*s{X0s zZ(yO7XoFv#{E%Z;_2O`+Qp*b__{G^BZ`T7f-~Qlr{dy8^{iqqJ zAK~`)?a7h-=H}FlFW>{`yDsdX3^ZZlM=0}q(F8-X`0Cv#HSaX~HpvyQ+0 zljN@HKaN~MCrs=Oz(1F<$s!={xeJ)wb^l??>4e~ke#t$e`bUy?>t8hf*VH)j9BO;B zbBlm|PHTWqKI!_cMH4tW(JmczYJuB3tAtn3UeA^%*E9zm+r}d@5MWY2jXypF0mTA= zhPYv}rOyFf!5&0DfPpn3DO6(w5auswlftzlu|{pQemvNbH?7r{tH@_$_^o1%vrJte zX`Q$_?A^+=RWIahoH+-$qSlsW(4GU9#pAi1=KupUq*BO&9)*SO?6$E*z_e-W0BWaA z>m0C9x;d#hH2~yX zJbqgt1f)XFHkH8}&*Fid8B1q^Kj-B1^t>-NXP0$Ryx<19l1~Rn6O}{F>3ow zKIa#uFJp|_p4ScU{mQoh2;2_5&>M_meToP6emAc<<)vddI{$@R&O5%(-)XzQGYU(2 zz4PAbbFYjp&s=_ak3ac3Wridv(qIWA(_0d1o#O)5N_F>mZ+%0Wh2d+l;FY0-R!Ac# zH;t8IX_6(h`EgYzh4z)Ciwh&ylI@qwZH?p*Xw%k}vWzoZsT~f4^t#~*2XhEK8dIAk zoyj5K=YUEw4uMCdN0Y6%0UC6NJx~=f?BUOit2!63#+c_0s1XR+iZ^(rI{|#s2&%0m zJ*@S1&q7+CB;XEPeMlz&a||a5h^uM;J>H=&By`B32lz-fR7OAdR2`5birGoPecG6b zfV`+a2aIL#Y&xJ19RP2<$$3SW^{TF~g}HgWxsWl#E`8#5xWTAT;4RAf4u2hv7=ecwVgx)M zfGbybzQd}=FU#cQe>&&59q#CnfVjN&XSzS3CLp3c09zGH&V27H1ca23r0^pEg<;iJ zRJ@*u7DBb@J@v#DFn0haw&^pH60q12YLX@ak#q@=Ijs+s#-4H&NVKgO)nqtmjf|uO zuv~LHOF*e)YDih&;TX8G9ahfMRk%h|)XUgEo=FcoLX2E<2R-Y^-apd&;ZmUH2QU6; z`t%!L&aZ(jS&IGtRq=SL2kym4S-yXGxt38hQbU4Et91<`xtau(kwT_y1+stuVAr1d(J)q9m zq#;cL*r)vpvD>VxRxR+F;scUZWgvi7Z8gOC-g1{SYGsZ65`Y=faNMn`8w`2BQ#cn2 zJ0z)YuvBR^dGs!|dhGXnmNL!_0;0iS@Q92B2K>hE0I*z|1{8D{li*hnP$bi3kVe3- zB!!r3)b(IORg98mI~q};4REJ|&U$w%NI)SLO0UvlhsQkUSUWJs<|OmOd1872aFzL| zjA4Q_#BQ=Hta)bDpC<>Zg8&0zGrE49eN3YuChZUGh^H=t!C*;70;p3wk6}XP@Uf-jPoKspCQIK7?YDZ zXEh|lh}`XJa(uI`3Yt|HNz(yq|B|RC5>UI}@2@Pjm~nMT(S~5#J}5k7P*Y4Nb2tM5 zMbb)~rA$evl0tS}hYB5p^-?AGnojOqsSp_LkusJx;rz#E8VoxC(qK<#Uax zCx%Kx|J`aHV-~0T@h@{}AQ9~WE>ypPbhzO}Y7z|0;?*=YU zD86-TT!~Rokc{CgNpIX=2q_XkvoQv0)B(~V7qCKdF;BW37%nYgs&s?(^gJ_|=5sp= z=Xb`;g=@l>lH#>@nRD|M5+GfHJsx6<4NastEJ;zz71tRG%8nl_mDF<+^*SzbwrM zwMzj-xYKX-Y*&C4= z4b=q!dD3_&k*-hm+=a}r{0)lXX^|AGf8lx{q~H7fe!u^9GumQ?S$|0SGBpUSz&{B* z_8FS5LQ%xglRTfVykbS%4xJ9FDmH8IZ4ot6p%5O0!w^>mo(?5myW3aUuGkRBjc6Ba zQzQ?9isj}@r5v=w)iM&m9&LjaY9^fkBj6w)fvciEE=g~=9Fuzjpg1AJSPrS9BGU=R z3~-z#^~`9DDOO(w>{7h?R&Qym17*PFF~h9`FGr-r zwqGT6GpC)LToJvn)J#SnPJZ>p;Qp|{>~wNX4I7ai5Hw;{uk`yFDtX4KQbTW$u4cDV z_fu9X9yQ{uf(o9RfICeBJaOGMM#@VgV58z5z!gh|5)sjERw$e_yYj9uG(FF4k9LGka^9IRm(@#d^K3cLK;OP2Kb(I7hYfeyx+wswS9^xt|Bc?p z_DPzv(BAU&CnMu5Of&>b~eCM=E8G_LlbnI zOTMl4UdViy4=_%Xrf!zyP3G`QV8HC$^G?ipY9IWJg)@On2frb7UaxcK(=rG)!yj(B zc$nkFh7ZTu!NUuA8F5`|4#cu|< zPLkrUs z0EH1ryX-?0k0j5A;U2yqs#XunrBD6UN8;6L50p^i_qVIv@1I~4^6P+xGfoF}2e4RD zBp7Uo__2JlPf1Umnt;CwLTV2NgTdgzbOOj-8ke-qocKGm$EgwyjB6ITK}m`~=9d(( zN17=F<&xZtkl7z=LjY%OIp<`)Raf-LzunfmVBHmUz}08*gfoC* zQ&!kO zm`1&QL~QM~8N!hMf(V)6i0^ii008F8&{wVwg+`@3cQEZ}?B6T0VcjDD<=17rKlaK? ze+KZO!Co4hR2R2}S06jIi3JIs`^2a+ZGC$`*{T*PM$fnKGfE8ZSLoJVizJ9a*we4mYck_=dnFg*FHulfm;zbd+tfHuV& zHv}52)1>QV>T>R)a=(9SS&BuyOYL`0G!6#Uz)}UVLy`aXkrF=_3^p1;`d6f%CY_%4 zdtoVP(_C;bib>IdkQYz=y|KS-qzr-n09*i^LI2#Jp3{>^i+SATzQ!{gVJa;bbo2q= zBDm)zpgH_G?7RJ8@9FQ?!OqkFN^SqM;Izt~>(&EYer@c@q{g}A)W7Ny?BCG`KKU{7rTn_y-y^aC@)vP?J;435w>)o0V5Y8HXVQduBr8f7@Z8J{o}V=|@4p|(_csf# zwV@D{bZ~m6U-j}xlZ_8={Q$yr#dC4RtPRt@Uo077SYv~ixX7D*SW^7e&I5adG?wiO z!gd7-Xgi0@A0tSOc=-E6iL|RUA>XHFQ5H6Q0002+Nkl;|j!1qEKVK_6lu-3c z<`MV2SqCM>mS6u>4}Fbrc|r}aQ;A~uLqgJl`@Y8NZ3ehMjTJzCWLGW(6|Wuj)sKAL zFa%%_z~)(B@$)I&=teiX(T#3&qZ{4mMmM_Ajc# StatusBar, Panel +* - GroupBox --> Line +* - Line +* - Panel --> StatusBar +* - ScrollPanel --> StatusBar +* - TabBar --> Button +* +* # Basic Controls +* - Label +* - LabelButton --> Label +* - Button +* - Toggle +* - ToggleGroup --> Toggle +* - ToggleSlider +* - CheckBox +* - ComboBox +* - DropdownBox +* - TextBox +* - ValueBox --> TextBox +* - Spinner --> Button, ValueBox +* - Slider +* - SliderBar --> Slider +* - ProgressBar +* - StatusBar +* - DummyRec +* - Grid +* +* # Advance Controls +* - ListView +* - ColorPicker --> ColorPanel, ColorBarHue +* - MessageBox --> Window, Label, Button +* - TextInputBox --> Window, Label, TextBox, Button +* +* It also provides a set of functions for styling the controls based on its properties (size, color) +* +* +* RAYGUI STYLE (guiStyle): +* raygui uses a global data array for all gui style properties (allocated on data segment by default), +* when a new style is loaded, it is loaded over the global style... but a default gui style could always be +* recovered with GuiLoadStyleDefault() function, that overwrites the current style to the default one +* +* The global style array size is fixed and depends on the number of controls and properties: +* +* static unsigned int guiStyle[RAYGUI_MAX_CONTROLS*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED)]; +* +* guiStyle size is by default: 16*(16 + 8) = 384 int = 384*4 bytes = 1536 bytes = 1.5 KB +* +* Note that the first set of BASE properties (by default guiStyle[0..15]) belong to the generic style +* used for all controls, when any of those base values is set, it is automatically populated to all +* controls, so, specific control values overwriting generic style should be set after base values +* +* After the first BASE properties set, the EXTENDED properties set is defined (by default guiStyle[16..23]), +* those properties are actually common to all controls and can not be overwritten individually (like BASE ones) +* Some of those properties are: TEXT_SIZE, TEXT_SPACING, LINE_COLOR, BACKGROUND_COLOR +* +* Custom control properties can be defined using the EXTENDED properties for each independent control. +* +* TOOL: rGuiStyler is a visual tool to customize raygui style: github.com/raysan5/rguistyler +* +* +* RAYGUI ICONS (guiIcons): +* raygui could use a global array containing icons data (allocated on data segment by default), +* a custom icons set could be loaded over this array using GuiLoadIcons(), but loaded icons set +* must be same RAYGUI_ICON_SIZE and no more than RAYGUI_ICON_MAX_ICONS will be loaded +* +* Every icon is codified in binary form, using 1 bit per pixel, so, every 16x16 icon +* requires 8 integers (16*16/32) to be stored in memory. +* +* When the icon is draw, actually one quad per pixel is drawn if the bit for that pixel is set +* +* The global icons array size is fixed and depends on the number of icons and size: +* +* static unsigned int guiIcons[RAYGUI_ICON_MAX_ICONS*RAYGUI_ICON_DATA_ELEMENTS]; +* +* guiIcons size is by default: 256*(16*16/32) = 2048*4 = 8192 bytes = 8 KB +* +* TOOL: rGuiIcons is a visual tool to customize/create raygui icons: github.com/raysan5/rguiicons +* +* RAYGUI LAYOUT: +* raygui currently does not provide an auto-layout mechanism like other libraries, +* layouts must be defined manually on controls drawing, providing the right bounds Rectangle for it +* +* TOOL: rGuiLayout is a visual tool to create raygui layouts: github.com/raysan5/rguilayout +* +* CONFIGURATION: +* #define RAYGUI_IMPLEMENTATION +* Generates the implementation of the library into the included file +* If not defined, the library is in header only mode and can be included in other headers +* or source files without problems. But only ONE file should hold the implementation +* +* #define RAYGUI_STANDALONE +* Avoid raylib.h header inclusion in this file. Data types defined on raylib are defined +* internally in the library and input management and drawing functions must be provided by +* the user (check library implementation for further details) +* +* #define RAYGUI_NO_ICONS +* Avoid including embedded ricons data (256 icons, 16x16 pixels, 1-bit per pixel, 2KB) +* +* #define RAYGUI_CUSTOM_ICONS +* Includes custom ricons.h header defining a set of custom icons, +* this file can be generated using rGuiIcons tool +* +* #define RAYGUI_DEBUG_RECS_BOUNDS +* Draw control bounds rectangles for debug +* +* #define RAYGUI_DEBUG_TEXT_BOUNDS +* Draw text bounds rectangles for debug +* +* VERSIONS HISTORY: +* 5.0 (xx-May-2026) ADDED: Support up to 32 controls (v500) +* ADDED: Support up to 512 icons (v500) +* ADDED: guiControlExclusiveMode and guiControlExclusiveRec for exclusive modes +* ADDED: GuiValueBoxFloat() +* ADDED: GuiDropdonwBox() properties: DROPDOWN_ARROW_HIDDEN, DROPDOWN_ROLL_UP +* ADDED: GuiListView() property: LIST_ITEMS_BORDER_WIDTH +* ADDED: GuiLoadIconsFromMemory() +* ADDED: Multiple new icons +* ADDED: Macros for inputs customization, raylib decoupling +* REMOVED: GuiSpinner() from controls list, using BUTTON + VALUEBOX properties +* REMOVED: GuiSliderPro(), functionality was redundant +* REVIEWED: Controls using text labels to use LABEL properties +* REVIEWED: Replaced sprintf() by snprintf() for more safety +* REVIEWED: GuiTabBar(), close tab with mouse middle button +* REVIEWED: GuiScrollPanel(), scroll speed proportional to content +* REVIEWED: GuiDropdownBox(), support roll up and hidden arrow +* REVIEWED: GuiTextBox(), cursor position initialization +* REVIEWED: GuiSliderPro(), control value change check +* REVIEWED: GuiGrid(), simplified implementation +* REVIEWED: GuiIconText(), increase buffer size and reviewed padding +* REVIEWED: GuiDrawText(), improved wrap mode drawing +* REVIEWED: GuiScrollBar(), minor tweaks +* REVIEWED: GuiProgressBar(), improved borders computing +* REVIEWED: GuiTextBox(), multiple improvements: autocursor and more +* REVIEWED: Functions descriptions, removed wrong return value reference +* REDESIGNED: GuiColorPanel(), improved HSV <-> RGBA convertion +* REDESIGNED: WARNING: TEXT_LINE_SPACING does not consider text height, only lines spacing +* +* 4.0 (12-Sep-2023) ADDED: GuiToggleSlider() +* ADDED: GuiColorPickerHSV() and GuiColorPanelHSV() +* ADDED: Multiple new icons, mostly compiler related +* ADDED: New DEFAULT properties: TEXT_LINE_SPACING, TEXT_ALIGNMENT_VERTICAL, TEXT_WRAP_MODE +* ADDED: New enum values: GuiTextAlignment, GuiTextAlignmentVertical, GuiTextWrapMode +* ADDED: Support loading styles with custom font charset from external file +* REDESIGNED: GuiTextBox(), support mouse cursor positioning +* REDESIGNED: GuiDrawText(), support multiline and word-wrap modes (read only) +* REDESIGNED: GuiProgressBar() to be more visual, progress affects border color +* REDESIGNED: Global alpha consideration moved to GuiDrawRectangle() and GuiDrawText() +* REDESIGNED: GuiScrollPanel(), get parameters by reference and return result value +* REDESIGNED: GuiToggleGroup(), get parameters by reference and return result value +* REDESIGNED: GuiComboBox(), get parameters by reference and return result value +* REDESIGNED: GuiCheckBox(), get parameters by reference and return result value +* REDESIGNED: GuiSlider(), get parameters by reference and return result value +* REDESIGNED: GuiSliderBar(), get parameters by reference and return result value +* REDESIGNED: GuiProgressBar(), get parameters by reference and return result value +* REDESIGNED: GuiListView(), get parameters by reference and return result value +* REDESIGNED: GuiColorPicker(), get parameters by reference and return result value +* REDESIGNED: GuiColorPanel(), get parameters by reference and return result value +* REDESIGNED: GuiColorBarAlpha(), get parameters by reference and return result value +* REDESIGNED: GuiColorBarHue(), get parameters by reference and return result value +* REDESIGNED: GuiGrid(), get parameters by reference and return result value +* REDESIGNED: GuiGrid(), added extra parameter +* REDESIGNED: GuiListViewEx(), change parameters order +* REDESIGNED: All controls return result as int value +* REVIEWED: GuiScrollPanel() to avoid smallish scroll-bars +* REVIEWED: All examples and specially controls_test_suite +* RENAMED: gui_file_dialog module to gui_window_file_dialog +* UPDATED: All styles to include ISO-8859-15 charset (as much as possible) +* +* 3.6 (10-May-2023) ADDED: New icon: SAND_TIMER +* ADDED: GuiLoadStyleFromMemory() (binary only) +* REVIEWED: GuiScrollBar() horizontal movement key +* REVIEWED: GuiTextBox() crash on cursor movement +* REVIEWED: GuiTextBox(), additional inputs support +* REVIEWED: GuiLabelButton(), avoid text cut +* REVIEWED: GuiTextInputBox(), password input +* REVIEWED: Local GetCodepointNext(), aligned with raylib +* REDESIGNED: GuiSlider*()/GuiScrollBar() to support out-of-bounds +* +* 3.5 (20-Apr-2023) ADDED: GuiTabBar(), based on GuiToggle() +* ADDED: Helper functions to split text in separate lines +* ADDED: Multiple new icons, useful for code editing tools +* REMOVED: Unneeded icon editing functions +* REMOVED: GuiTextBoxMulti(), very limited and broken +* REMOVED: MeasureTextEx() dependency, logic directly implemented +* REMOVED: DrawTextEx() dependency, logic directly implemented +* REVIEWED: GuiScrollBar(), improve mouse-click behaviour +* REVIEWED: Library header info, more info, better organized +* REDESIGNED: GuiTextBox() to support cursor movement +* REDESIGNED: GuiDrawText() to divide drawing by lines +* +* 3.2 (22-May-2022) RENAMED: Some enum values, for unification, avoiding prefixes +* REMOVED: GuiScrollBar(), only internal +* REDESIGNED: GuiPanel() to support text parameter +* REDESIGNED: GuiScrollPanel() to support text parameter +* REDESIGNED: GuiColorPicker() to support text parameter +* REDESIGNED: GuiColorPanel() to support text parameter +* REDESIGNED: GuiColorBarAlpha() to support text parameter +* REDESIGNED: GuiColorBarHue() to support text parameter +* REDESIGNED: GuiTextInputBox() to support password +* +* 3.1 (12-Jan-2022) REVIEWED: Default style for consistency (aligned with rGuiLayout v2.5 tool) +* REVIEWED: GuiLoadStyle() to support compressed font atlas image data and unload previous textures +* REVIEWED: External icons usage logic +* REVIEWED: GuiLine() for centered alignment when including text +* RENAMED: Multiple controls properties definitions to prepend RAYGUI_ +* RENAMED: RICON_ references to RAYGUI_ICON_ for library consistency +* Projects updated and multiple tweaks +* +* 3.0 (04-Nov-2021) Integrated ricons data to avoid external file +* REDESIGNED: GuiTextBoxMulti() +* REMOVED: GuiImageButton*() +* Multiple minor tweaks and bugs corrected +* +* 2.9 (17-Mar-2021) REMOVED: Tooltip API +* 2.8 (03-May-2020) Centralized rectangles drawing to GuiDrawRectangle() +* 2.7 (20-Feb-2020) ADDED: Possible tooltips API +* 2.6 (09-Sep-2019) ADDED: GuiTextInputBox() +* REDESIGNED: GuiListView*(), GuiDropdownBox(), GuiSlider*(), GuiProgressBar(), GuiMessageBox() +* REVIEWED: GuiTextBox(), GuiSpinner(), GuiValueBox(), GuiLoadStyle() +* Replaced property INNER_PADDING by TEXT_PADDING, renamed some properties +* ADDED: 8 new custom styles ready to use +* Multiple minor tweaks and bugs corrected +* +* 2.5 (28-May-2019) Implemented extended GuiTextBox(), GuiValueBox(), GuiSpinner() +* 2.3 (29-Apr-2019) ADDED: rIcons auxiliar library and support for it, multiple controls reviewed +* Refactor all controls drawing mechanism to use control state +* 2.2 (05-Feb-2019) ADDED: GuiScrollBar(), GuiScrollPanel(), reviewed GuiListView(), removed Gui*Ex() controls +* 2.1 (26-Dec-2018) REDESIGNED: GuiCheckBox(), GuiComboBox(), GuiDropdownBox(), GuiToggleGroup() > Use combined text string +* REDESIGNED: Style system (breaking change) +* 2.0 (08-Nov-2018) ADDED: Support controls guiLock and custom fonts +* REVIEWED: GuiComboBox(), GuiListView()... +* 1.9 (09-Oct-2018) REVIEWED: GuiGrid(), GuiTextBox(), GuiTextBoxMulti(), GuiValueBox()... +* 1.8 (01-May-2018) Lot of rework and redesign to align with rGuiStyler and rGuiLayout +* 1.5 (21-Jun-2017) Working in an improved styles system +* 1.4 (15-Jun-2017) Rewritten all GUI functions (removed useless ones) +* 1.3 (12-Jun-2017) Complete redesign of style system +* 1.1 (01-Jun-2017) Complete review of the library +* 1.0 (07-Jun-2016) Converted to header-only by Ramon Santamaria +* 0.9 (07-Mar-2016) Reviewed and tested by Albert Martos, Ian Eito, Sergio Martinez and Ramon Santamaria +* 0.8 (27-Aug-2015) Initial release. Implemented by Kevin Gato, Daniel Nicolás and Ramon Santamaria +* +* DEPENDENCIES: +* raylib 5.6-dev - Inputs reading (keyboard/mouse), shapes drawing, font loading and text drawing +* +* STANDALONE MODE: +* By default raygui depends on raylib mostly for the inputs and the drawing functionality but that dependency can be disabled +* with the config flag RAYGUI_STANDALONE. In that case is up to the user to provide another backend to cover library needs +* +* The following functions should be redefined for a custom backend: +* +* - Vector2 GetMousePosition(void); +* - float GetMouseWheelMove(void); +* - bool IsMouseButtonDown(int button); +* - bool IsMouseButtonPressed(int button); +* - bool IsMouseButtonReleased(int button); +* - bool IsKeyDown(int key); +* - bool IsKeyPressed(int key); +* - int GetCharPressed(void); // -- GuiTextBox(), GuiValueBox() +* +* - void DrawRectangle(int x, int y, int width, int height, Color color); // -- GuiDrawRectangle() +* - void DrawRectangleGradientEx(Rectangle rec, Color col1, Color col2, Color col3, Color col4); // -- GuiColorPicker() +* +* - Font GetFontDefault(void); // -- GuiLoadStyleDefault() +* - Font LoadFontEx(const char *fileName, int fontSize, int *codepoints, int codepointCount); // -- GuiLoadStyle() +* - Texture2D LoadTextureFromImage(Image image); // -- GuiLoadStyle(), required to load texture from embedded font atlas image +* - void SetShapesTexture(Texture2D tex, Rectangle rec); // -- GuiLoadStyle(), required to set shapes rec to font white rec (optimization) +* - char *LoadFileText(const char *fileName); // -- GuiLoadStyle(), required to load charset data +* - void UnloadFileText(char *text); // -- GuiLoadStyle(), required to unload charset data +* - const char *GetDirectoryPath(const char *filePath); // -- GuiLoadStyle(), required to find charset/font file from text .rgs +* - int *LoadCodepoints(const char *text, int *count); // -- GuiLoadStyle(), required to load required font codepoints list +* - void UnloadCodepoints(int *codepoints); // -- GuiLoadStyle(), required to unload codepoints list +* - unsigned char *DecompressData(const unsigned char *compData, int compDataSize, int *dataSize); // -- GuiLoadStyle() +* +* CONTRIBUTORS: +* Ramon Santamaria: Supervision, review, redesign, update and maintenance +* Vlad Adrian: Complete rewrite of GuiTextBox() to support extended features (2019) +* Sergio Martinez: Review, testing (2015) and redesign of multiple controls (2018) +* Adria Arranz: Testing and implementation of additional controls (2018) +* Jordi Jorba: Testing and implementation of additional controls (2018) +* Albert Martos: Review and testing of the library (2015) +* Ian Eito: Review and testing of the library (2015) +* Kevin Gato: Initial implementation of basic components (2014) +* Daniel Nicolas: Initial implementation of basic components (2014) +* +* +* LICENSE: zlib/libpng +* +* Copyright (c) 2014-2026 Ramon Santamaria (@raysan5) +* +* This software is provided "as-is", without any express or implied warranty. In no event +* will the authors be held liable for any damages arising from the use of this software. +* +* Permission is granted to anyone to use this software for any purpose, including commercial +* applications, and to alter it and redistribute it freely, subject to the following restrictions: +* +* 1. The origin of this software must not be misrepresented; you must not claim that you +* wrote the original software. If you use this software in a product, an acknowledgment +* in the product documentation would be appreciated but is not required. +* +* 2. Altered source versions must be plainly marked as such, and must not be misrepresented +* as being the original software. +* +* 3. This notice may not be removed or altered from any source distribution. +* +**********************************************************************************************/ + +#ifndef RAYGUI_H +#define RAYGUI_H + +#define RAYGUI_VERSION_MAJOR 5 +#define RAYGUI_VERSION_MINOR 0 +#define RAYGUI_VERSION_PATCH 0 +#define RAYGUI_VERSION "5.0-dev" + +#if !defined(RAYGUI_STANDALONE) + #include "raylib.h" +#endif + +// Function specifiers in case library is build/used as a shared library (Windows) +// NOTE: Microsoft specifiers to tell compiler that symbols are imported/exported from a .dll +#if defined(_WIN32) + #if defined(BUILD_LIBTYPE_SHARED) + #define RAYGUIAPI __declspec(dllexport) // Building the library as a Win32 shared library (.dll) + #elif defined(USE_LIBTYPE_SHARED) + #define RAYGUIAPI __declspec(dllimport) // Using the library as a Win32 shared library (.dll) + #endif + #if !defined(_CRT_SECURE_NO_WARNINGS) + #define _CRT_SECURE_NO_WARNINGS // Disable unsafe warnings on scanf() functions in MSVC + #endif +#else + #if defined(BUILD_LIBTYPE_SHARED) + #define RAYGUIAPI __attribute__((visibility("default"))) // Building as a Unix shared library (.so/.dylib) + #endif +#endif + +// Function specifiers definition +#ifndef RAYGUIAPI + #define RAYGUIAPI // Functions defined as 'extern' by default (implicit specifiers) +#endif + +//---------------------------------------------------------------------------------- +// Defines and Macros +//---------------------------------------------------------------------------------- +// Simple log system to avoid printf() calls if required +// NOTE: Avoiding those calls, also avoids const strings memory usage +#define RAYGUI_SUPPORT_LOG_INFO +#if defined(RAYGUI_SUPPORT_LOG_INFO) + #define RAYGUI_LOG(...) printf(__VA_ARGS__) +#else + #define RAYGUI_LOG(...) +#endif + +// Macros to define required UI inputs, including mapping to gamepad controls +// TODO: Define additionally required macros for missing inputs +#if !defined(GUI_BUTTON_DOWN) + #define GUI_BUTTON_DOWN (IsMouseButtonDown(MOUSE_LEFT_BUTTON) || IsGamepadButtonDown(0, GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) +#endif +#if !defined(GUI_BUTTON_DOWN_ALT) + // Mapping to alternative button down pressed + #define GUI_BUTTON_DOWN_ALT (IsMouseButtonDown(MOUSE_RIGHT_BUTTON) || IsGamepadButtonDown(0, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)) +#endif +#if !defined(GUI_BUTTON_PRESSED) + #define GUI_BUTTON_PRESSED (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) +#endif +// TODO: WARNING: GuiTabBar() still requires IsMouseButtonPressed(MOUSE_MIDDLE_BUTTON) +#if !defined(GUI_BUTTON_RELEASED) + #define GUI_BUTTON_RELEASED (IsMouseButtonReleased(MOUSE_LEFT_BUTTON) || IsGamepadButtonReleased(0, GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) +#endif +#if !defined(GUI_SCROLL_DELTA) + // Mapping to scroll delta changes + // TODO: Review inconsistencies between platforms + #if defined(PLATFORM_WEB) + // NOTE: Gamepad axis triggers not detected on web platform + #define GUI_SCROLL_DELTA ((float)IsGamepadButtonDown(0, GAMEPAD_BUTTON_RIGHT_TRIGGER_2) - (float)IsGamepadButtonDown(0, GAMEPAD_BUTTON_LEFT_TRIGGER_2)) + #else + #define GUI_SCROLL_DELTA (GetMouseWheelMove() + (GetGamepadAxisMovement(0, GAMEPAD_AXIS_RIGHT_TRIGGER) + 1) - (GetGamepadAxisMovement(0, GAMEPAD_AXIS_LEFT_TRIGGER) + 1)) + #endif +#endif +#if !defined(GUI_POINTER_POSITION) + #define GUI_POINTER_POSITION GetMousePosition() +#endif +#if !defined(GUI_KEY_DOWN) + #define GUI_KEY_DOWN(key) IsKeyDown(key) +#endif +#if !defined(GUI_KEY_PRESSED) + #define GUI_KEY_PRESSED(key) IsKeyPressed(key) +#endif +#if !defined(GUI_INPUT_KEY) + #define GUI_INPUT_KEY GetCharPressed() +#endif + +//---------------------------------------------------------------------------------- +// Types and Structures Definition +// NOTE: Some types are required for RAYGUI_STANDALONE usage +//---------------------------------------------------------------------------------- +#if defined(RAYGUI_STANDALONE) + #ifndef __cplusplus + // Boolean type + #ifndef true + typedef enum { false, true } bool; + #endif + #endif + + // Vector2 type + typedef struct Vector2 { + float x; + float y; + } Vector2; + + // Vector3 type // -- ConvertHSVtoRGB(), ConvertRGBtoHSV() + typedef struct Vector3 { + float x; + float y; + float z; + } Vector3; + + // Color type, RGBA (32bit) + typedef struct Color { + unsigned char r; + unsigned char g; + unsigned char b; + unsigned char a; + } Color; + + // Rectangle type + typedef struct Rectangle { + float x; + float y; + float width; + float height; + } Rectangle; + + // TODO: Texture2D type is very coupled to raylib, required by Font type + // It should be redesigned to be provided by user + typedef struct Texture { + unsigned int id; // OpenGL texture id + int width; // Texture base width + int height; // Texture base height + int mipmaps; // Mipmap levels, 1 by default + int format; // Data format (PixelFormat type) + } Texture; + + // Texture2D, same as Texture + typedef Texture Texture2D; + + // Image, pixel data stored in CPU memory (RAM) + typedef struct Image { + void *data; // Image raw data + int width; // Image base width + int height; // Image base height + int mipmaps; // Mipmap levels, 1 by default + int format; // Data format (PixelFormat type) + } Image; + + // GlyphInfo, font characters glyphs info + typedef struct GlyphInfo { + int value; // Character value (Unicode) + int offsetX; // Character offset X when drawing + int offsetY; // Character offset Y when drawing + int advanceX; // Character advance position X + Image image; // Character image data + } GlyphInfo; + + // TODO: Font type is very coupled to raylib, mostly required by GuiLoadStyle() + // It should be redesigned to be provided by user + typedef struct Font { + int baseSize; // Base size (default chars height) + int glyphCount; // Number of glyph characters + int glyphPadding; // Padding around the glyph characters + Texture2D texture; // Texture atlas containing the glyphs + Rectangle *recs; // Rectangles in texture for the glyphs + GlyphInfo *glyphs; // Glyphs info data + } Font; +#endif + +// Style property +// NOTE: Used when exporting style as code for convenience +typedef struct GuiStyleProp { + unsigned short controlId; // Control identifier + unsigned short propertyId; // Property identifier + int propertyValue; // Property value +} GuiStyleProp; + +/* +// Controls text style -NOT USED- +// NOTE: Text style is defined by control +typedef struct GuiTextStyle { + unsigned int size; + int charSpacing; + int lineSpacing; + int alignmentH; + int alignmentV; + int padding; +} GuiTextStyle; +*/ + +// Gui control state +typedef enum { + STATE_NORMAL = 0, + STATE_FOCUSED, + STATE_PRESSED, + STATE_DISABLED +} GuiState; + +// Gui control text alignment +typedef enum { + TEXT_ALIGN_LEFT = 0, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT +} GuiTextAlignment; + +// Gui control text alignment vertical +// NOTE: Text vertical position inside the text bounds +typedef enum { + TEXT_ALIGN_TOP = 0, + TEXT_ALIGN_MIDDLE, + TEXT_ALIGN_BOTTOM +} GuiTextAlignmentVertical; + +// Gui control text wrap mode +// NOTE: Useful for multiline text +typedef enum { + TEXT_WRAP_NONE = 0, + TEXT_WRAP_CHAR, + TEXT_WRAP_WORD +} GuiTextWrapMode; + +// Gui controls +// NOTE: Up to 16 controls supported or 32 controls (v500) +typedef enum { + // Default -> populates to all controls when set + DEFAULT = 0, + + // Basic controls + LABEL, // Used also for: LABELBUTTON + BUTTON, + TOGGLE, // Used also for: TOGGLEGROUP + SLIDER, // Used also for: SLIDERBAR, TOGGLESLIDER + PROGRESSBAR, + CHECKBOX, + COMBOBOX, + DROPDOWNBOX, + TEXTBOX, // Used also for: TEXTBOXMULTI + VALUEBOX, + CONTROL11, + LISTVIEW, + COLORPICKER, + SCROLLBAR, + STATUSBAR + // NOTE: More controls can be added if required +} GuiControl; + +// Gui base properties for every control +// NOTE: RAYGUI_MAX_PROPS_BASE properties (by default 16 properties) +typedef enum { + BORDER_COLOR_NORMAL = 0, // Control border color in STATE_NORMAL + BASE_COLOR_NORMAL, // Control base color in STATE_NORMAL + TEXT_COLOR_NORMAL, // Control text color in STATE_NORMAL + BORDER_COLOR_FOCUSED, // Control border color in STATE_FOCUSED + BASE_COLOR_FOCUSED, // Control base color in STATE_FOCUSED + TEXT_COLOR_FOCUSED, // Control text color in STATE_FOCUSED + BORDER_COLOR_PRESSED, // Control border color in STATE_PRESSED + BASE_COLOR_PRESSED, // Control base color in STATE_PRESSED + TEXT_COLOR_PRESSED, // Control text color in STATE_PRESSED + BORDER_COLOR_DISABLED, // Control border color in STATE_DISABLED + BASE_COLOR_DISABLED, // Control base color in STATE_DISABLED + TEXT_COLOR_DISABLED, // Control text color in STATE_DISABLED + BORDER_WIDTH = 12, // Control border size, 0 for no border + //TEXT_SIZE, // Control text size (glyphs max height) -> GLOBAL for all controls + //TEXT_SPACING, // Control text spacing between glyphs -> GLOBAL for all controls + //TEXT_LINE_SPACING, // Control text spacing between lines -> GLOBAL for all controls + TEXT_PADDING = 13, // Control text padding, not considering border + TEXT_ALIGNMENT = 14, // Control text horizontal alignment inside control text bound (after border and padding) + //TEXT_WRAP_MODE // Control text wrap-mode inside text bounds -> GLOBAL for all controls +} GuiControlProperty; + +// TODO: Which text styling properties should be global or per-control? +// At this moment TEXT_PADDING and TEXT_ALIGNMENT is configured and saved per control while +// TEXT_SIZE, TEXT_SPACING, TEXT_LINE_SPACING, TEXT_ALIGNMENT_VERTICAL, TEXT_WRAP_MODE are global and +// should be configured by user as needed while defining the UI layout + +// Gui extended properties depend on control +// NOTE: RAYGUI_MAX_PROPS_EXTENDED properties (by default, max 8 properties) +//---------------------------------------------------------------------------------- +// DEFAULT extended properties +// NOTE: Those properties are common to all controls or global +// WARNING: Only 8 slots vailable for those properties by default +typedef enum { + TEXT_SIZE = 16, // Text size (glyphs max height) + TEXT_SPACING, // Text spacing between glyphs + LINE_COLOR, // Line control color + BACKGROUND_COLOR, // Background color + TEXT_LINE_SPACING, // Text spacing between lines + TEXT_ALIGNMENT_VERTICAL, // Text vertical alignment inside text bounds (after border and padding) + TEXT_WRAP_MODE // Text wrap-mode inside text bounds + //TEXT_DECORATION // Text decoration: 0-None, 1-Underline, 2-Line-through, 3-Overline + //TEXT_DECORATION_THICK // Text decoration line thickness +} GuiDefaultProperty; + +// Other possible text properties: +// TEXT_WEIGHT // Normal, Italic, Bold -> Requires specific font change +// TEXT_INDENT // Text indentation -> Now using TEXT_PADDING... + +// Label +//typedef enum { } GuiLabelProperty; + +// Button/Spinner +//typedef enum { } GuiButtonProperty; + +// Toggle/ToggleGroup +typedef enum { + GROUP_PADDING = 16, // ToggleGroup separation between toggles +} GuiToggleProperty; + +// Slider/SliderBar +typedef enum { + SLIDER_WIDTH = 16, // Slider size of internal bar + SLIDER_PADDING // Slider/SliderBar internal bar padding +} GuiSliderProperty; + +// ProgressBar +typedef enum { + PROGRESS_PADDING = 16, // ProgressBar internal padding + PROGRESS_SIDE, // ProgressBar increment side: 0-left->right, 1-right-left +} GuiProgressBarProperty; + +// ScrollBar +typedef enum { + ARROWS_SIZE = 16, // ScrollBar arrows size + ARROWS_VISIBLE, // ScrollBar arrows visible + SCROLL_SLIDER_PADDING, // ScrollBar slider internal padding + SCROLL_SLIDER_SIZE, // ScrollBar slider size + SCROLL_PADDING, // ScrollBar scroll padding from arrows + SCROLL_SPEED, // ScrollBar scrolling speed +} GuiScrollBarProperty; + +// CheckBox +typedef enum { + CHECK_PADDING = 16 // CheckBox internal check padding +} GuiCheckBoxProperty; + +// ComboBox +typedef enum { + COMBO_BUTTON_WIDTH = 16, // ComboBox right button width + COMBO_BUTTON_SPACING // ComboBox button separation +} GuiComboBoxProperty; + +// DropdownBox +typedef enum { + ARROW_PADDING = 16, // DropdownBox arrow separation from border and items + DROPDOWN_ITEMS_SPACING, // DropdownBox items separation + DROPDOWN_ARROW_HIDDEN, // DropdownBox arrow hidden + DROPDOWN_ROLL_UP // DropdownBox roll up flag (default rolls down) +} GuiDropdownBoxProperty; + +// TextBox/TextBoxMulti/ValueBox/Spinner +typedef enum { + TEXT_READONLY = 16, // TextBox in read-only mode: 0-text editable, 1-text no-editable +} GuiTextBoxProperty; + +// ValueBox/Spinner +typedef enum { + SPINNER_BUTTON_WIDTH = 16, // Spinner left/right buttons width + SPINNER_BUTTON_SPACING, // Spinner buttons separation +} GuiValueBoxProperty; + +// Control11 +//typedef enum { } GuiControl11Property; + +// ListView +typedef enum { + LIST_ITEMS_HEIGHT = 16, // ListView items height + LIST_ITEMS_SPACING, // ListView items separation + SCROLLBAR_WIDTH, // ListView scrollbar size (usually width) + SCROLLBAR_SIDE, // ListView scrollbar side (0-SCROLLBAR_LEFT_SIDE, 1-SCROLLBAR_RIGHT_SIDE) + LIST_ITEMS_BORDER_NORMAL, // ListView items border enabled in normal state + LIST_ITEMS_BORDER_WIDTH // ListView items border width +} GuiListViewProperty; + +// ColorPicker +typedef enum { + COLOR_SELECTOR_SIZE = 16, + HUEBAR_WIDTH, // ColorPicker right hue bar width + HUEBAR_PADDING, // ColorPicker right hue bar separation from panel + HUEBAR_SELECTOR_HEIGHT, // ColorPicker right hue bar selector height + HUEBAR_SELECTOR_OVERFLOW // ColorPicker right hue bar selector overflow +} GuiColorPickerProperty; + +#define SCROLLBAR_LEFT_SIDE 0 +#define SCROLLBAR_RIGHT_SIDE 1 + +//---------------------------------------------------------------------------------- +// Global Variables Definition +//---------------------------------------------------------------------------------- +// ... + +//---------------------------------------------------------------------------------- +// Module Functions Declaration +//---------------------------------------------------------------------------------- + +#if defined(__cplusplus) +extern "C" { // Prevents name mangling of functions +#endif + +// Global gui state control functions +RAYGUIAPI void GuiEnable(void); // Enable gui controls (global state) +RAYGUIAPI void GuiDisable(void); // Disable gui controls (global state) +RAYGUIAPI void GuiLock(void); // Lock gui controls (global state) +RAYGUIAPI void GuiUnlock(void); // Unlock gui controls (global state) +RAYGUIAPI bool GuiIsLocked(void); // Check if gui is locked (global state) +RAYGUIAPI void GuiSetAlpha(float alpha); // Set gui controls alpha (global state), alpha goes from 0.0f to 1.0f +RAYGUIAPI void GuiSetState(int state); // Set gui state (global state) +RAYGUIAPI int GuiGetState(void); // Get gui state (global state) + +// Font set/get functions +RAYGUIAPI void GuiSetFont(Font font); // Set gui custom font (global state) +RAYGUIAPI Font GuiGetFont(void); // Get gui custom font (global state) + +// Style set/get functions +RAYGUIAPI void GuiSetStyle(int control, int property, int value); // Set one style property +RAYGUIAPI int GuiGetStyle(int control, int property); // Get one style property + +// Styles loading functions +RAYGUIAPI void GuiLoadStyle(const char *fileName); // Load style file over global style variable (.rgs) +RAYGUIAPI void GuiLoadStyleFromMemory(const unsigned char *fileData, int dataSize); // Load style from memory (binary only) +RAYGUIAPI void GuiLoadStyleDefault(void); // Load style default over global style + +// Tooltips management functions +RAYGUIAPI void GuiEnableTooltip(void); // Enable gui tooltips (global state) +RAYGUIAPI void GuiDisableTooltip(void); // Disable gui tooltips (global state) +RAYGUIAPI void GuiSetTooltip(const char *tooltip); // Set tooltip string + +// Icons functionality +RAYGUIAPI const char *GuiIconText(int iconId, const char *text); // Get text with icon id prepended (if supported) +#if !defined(RAYGUI_NO_ICONS) +RAYGUIAPI void GuiSetIconScale(int scale); // Set default icon drawing size +RAYGUIAPI unsigned int *GuiGetIcons(void); // Get raygui icons data pointer +RAYGUIAPI char **GuiLoadIcons(const char *fileName, bool loadIconsName); // Load raygui icons file (.rgi) into internal icons data +RAYGUIAPI void GuiDrawIcon(int iconId, int posX, int posY, int pixelSize, Color color); // Draw icon using pixel size at specified position +#endif + +// Utility functions +RAYGUIAPI int GuiGetTextWidth(const char *text); // Get text width considering gui style and icon size (if required) + +// Controls +//---------------------------------------------------------------------------------------------------------- +// Container/separator controls, useful for controls organization +RAYGUIAPI int GuiWindowBox(Rectangle bounds, const char *title); // Window Box control, shows a window that can be closed +RAYGUIAPI int GuiGroupBox(Rectangle bounds, const char *text); // Group Box control with text name +RAYGUIAPI int GuiLine(Rectangle bounds, const char *text); // Line separator control, could contain text +RAYGUIAPI int GuiPanel(Rectangle bounds, const char *text); // Panel control, useful to group controls +RAYGUIAPI int GuiTabBar(Rectangle bounds, char **text, int count, int *active); // Tab Bar control, returns TAB to be closed or -1 +RAYGUIAPI int GuiScrollPanel(Rectangle bounds, const char *text, Rectangle content, Vector2 *scroll, Rectangle *view); // Scroll Panel control + +// Basic controls set +RAYGUIAPI int GuiLabel(Rectangle bounds, const char *text); // Label control +RAYGUIAPI int GuiButton(Rectangle bounds, const char *text); // Button control, returns true when clicked +RAYGUIAPI int GuiLabelButton(Rectangle bounds, const char *text); // Label button control, returns true when clicked +RAYGUIAPI int GuiToggle(Rectangle bounds, const char *text, bool *active); // Toggle Button control +RAYGUIAPI int GuiToggleGroup(Rectangle bounds, const char *text, int *active); // Toggle Group control +RAYGUIAPI int GuiToggleSlider(Rectangle bounds, const char *text, int *active); // Toggle Slider control +RAYGUIAPI int GuiCheckBox(Rectangle bounds, const char *text, bool *checked); // Check Box control, returns true when active +RAYGUIAPI int GuiComboBox(Rectangle bounds, const char *text, int *active); // Combo Box control + +RAYGUIAPI int GuiDropdownBox(Rectangle bounds, const char *text, int *active, bool editMode); // Dropdown Box control +RAYGUIAPI int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Spinner control +RAYGUIAPI int GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode); // Value Box control, updates input text with numbers +RAYGUIAPI int GuiValueBoxFloat(Rectangle bounds, const char *text, char *textValue, float *value, bool editMode); // Value box control for float values +RAYGUIAPI int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode); // Text Box control, updates input text + +RAYGUIAPI int GuiSlider(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue); // Slider control +RAYGUIAPI int GuiSliderBar(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue); // Slider Bar control +RAYGUIAPI int GuiProgressBar(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue); // Progress Bar control +RAYGUIAPI int GuiStatusBar(Rectangle bounds, const char *text); // Status Bar control, shows info text +RAYGUIAPI int GuiDummyRec(Rectangle bounds, const char *text); // Dummy control for placeholders +RAYGUIAPI int GuiGrid(Rectangle bounds, const char *text, float spacing, int subdivs, Vector2 *mouseCell); // Grid control + +// Advance controls set +RAYGUIAPI int GuiListView(Rectangle bounds, const char *text, int *scrollIndex, int *active); // List View control +RAYGUIAPI int GuiListViewEx(Rectangle bounds, char **text, int count, int *scrollIndex, int *active, int *focus); // List View using text entries list and returning focus entry +RAYGUIAPI int GuiMessageBox(Rectangle bounds, const char *title, const char *message, const char *buttons); // Message Box control, displays a message +RAYGUIAPI int GuiTextInputBox(Rectangle bounds, const char *title, const char *message, const char *buttons, char *text, int textMaxSize, bool *secretViewActive); // Text Input Box control, ask for text, supports secret +RAYGUIAPI int GuiColorPicker(Rectangle bounds, const char *text, Color *color); // Color Picker control (multiple color controls) +RAYGUIAPI int GuiColorPanel(Rectangle bounds, const char *text, Color *color); // Color Panel control +RAYGUIAPI int GuiColorBarAlpha(Rectangle bounds, const char *text, float *alpha); // Color Bar Alpha control +RAYGUIAPI int GuiColorBarHue(Rectangle bounds, const char *text, float *value); // Color Bar Hue control +RAYGUIAPI int GuiColorPickerHSV(Rectangle bounds, const char *text, Vector3 *colorHsv); // Color Picker control that avoids conversion to RGB on each call (multiple color controls) +RAYGUIAPI int GuiColorPanelHSV(Rectangle bounds, const char *text, Vector3 *colorHsv); // Color Panel control that updates Hue-Saturation-Value color value, used by GuiColorPickerHSV() +//---------------------------------------------------------------------------------------------------------- + +#if !defined(RAYGUI_NO_ICONS) + +#if !defined(RAYGUI_CUSTOM_ICONS) +//---------------------------------------------------------------------------------- +// Icons enumeration +//---------------------------------------------------------------------------------- +typedef enum { + ICON_NONE = 0, + ICON_FOLDER_FILE_OPEN = 1, + ICON_FILE_SAVE_CLASSIC = 2, + ICON_FOLDER_OPEN = 3, + ICON_FOLDER_SAVE = 4, + ICON_FILE_OPEN = 5, + ICON_FILE_SAVE = 6, + ICON_FILE_EXPORT = 7, + ICON_FILE_ADD = 8, + ICON_FILE_DELETE = 9, + ICON_FILETYPE_TEXT = 10, + ICON_FILETYPE_AUDIO = 11, + ICON_FILETYPE_IMAGE = 12, + ICON_FILETYPE_PLAY = 13, + ICON_FILETYPE_VIDEO = 14, + ICON_FILETYPE_INFO = 15, + ICON_FILE_COPY = 16, + ICON_FILE_CUT = 17, + ICON_FILE_PASTE = 18, + ICON_CURSOR_HAND = 19, + ICON_CURSOR_POINTER = 20, + ICON_CURSOR_CLASSIC = 21, + ICON_PENCIL = 22, + ICON_PENCIL_BIG = 23, + ICON_BRUSH_CLASSIC = 24, + ICON_BRUSH_PAINTER = 25, + ICON_WATER_DROP = 26, + ICON_COLOR_PICKER = 27, + ICON_RUBBER = 28, + ICON_COLOR_BUCKET = 29, + ICON_TEXT_T = 30, + ICON_TEXT_A = 31, + ICON_SCALE = 32, + ICON_RESIZE = 33, + ICON_FILTER_POINT = 34, + ICON_FILTER_BILINEAR = 35, + ICON_CROP = 36, + ICON_CROP_ALPHA = 37, + ICON_SQUARE_TOGGLE = 38, + ICON_SYMMETRY = 39, + ICON_SYMMETRY_HORIZONTAL = 40, + ICON_SYMMETRY_VERTICAL = 41, + ICON_LENS = 42, + ICON_LENS_BIG = 43, + ICON_EYE_ON = 44, + ICON_EYE_OFF = 45, + ICON_FILTER_TOP = 46, + ICON_FILTER = 47, + ICON_TARGET_POINT = 48, + ICON_TARGET_SMALL = 49, + ICON_TARGET_BIG = 50, + ICON_TARGET_MOVE = 51, + ICON_CURSOR_MOVE = 52, + ICON_CURSOR_SCALE = 53, + ICON_CURSOR_SCALE_RIGHT = 54, + ICON_CURSOR_SCALE_LEFT = 55, + ICON_UNDO = 56, + ICON_REDO = 57, + ICON_REREDO = 58, + ICON_MUTATE = 59, + ICON_ROTATE = 60, + ICON_REPEAT = 61, + ICON_SHUFFLE = 62, + ICON_EMPTYBOX = 63, + ICON_TARGET = 64, + ICON_TARGET_SMALL_FILL = 65, + ICON_TARGET_BIG_FILL = 66, + ICON_TARGET_MOVE_FILL = 67, + ICON_CURSOR_MOVE_FILL = 68, + ICON_CURSOR_SCALE_FILL = 69, + ICON_CURSOR_SCALE_RIGHT_FILL = 70, + ICON_CURSOR_SCALE_LEFT_FILL = 71, + ICON_UNDO_FILL = 72, + ICON_REDO_FILL = 73, + ICON_REREDO_FILL = 74, + ICON_MUTATE_FILL = 75, + ICON_ROTATE_FILL = 76, + ICON_REPEAT_FILL = 77, + ICON_SHUFFLE_FILL = 78, + ICON_EMPTYBOX_SMALL = 79, + ICON_BOX = 80, + ICON_BOX_TOP = 81, + ICON_BOX_TOP_RIGHT = 82, + ICON_BOX_RIGHT = 83, + ICON_BOX_BOTTOM_RIGHT = 84, + ICON_BOX_BOTTOM = 85, + ICON_BOX_BOTTOM_LEFT = 86, + ICON_BOX_LEFT = 87, + ICON_BOX_TOP_LEFT = 88, + ICON_BOX_CENTER = 89, + ICON_BOX_CIRCLE_MASK = 90, + ICON_POT = 91, + ICON_ALPHA_MULTIPLY = 92, + ICON_ALPHA_CLEAR = 93, + ICON_DITHERING = 94, + ICON_MIPMAPS = 95, + ICON_BOX_GRID = 96, + ICON_GRID = 97, + ICON_BOX_CORNERS_SMALL = 98, + ICON_BOX_CORNERS_BIG = 99, + ICON_FOUR_BOXES = 100, + ICON_GRID_FILL = 101, + ICON_BOX_MULTISIZE = 102, + ICON_ZOOM_SMALL = 103, + ICON_ZOOM_MEDIUM = 104, + ICON_ZOOM_BIG = 105, + ICON_ZOOM_ALL = 106, + ICON_ZOOM_CENTER = 107, + ICON_BOX_DOTS_SMALL = 108, + ICON_BOX_DOTS_BIG = 109, + ICON_BOX_CONCENTRIC = 110, + ICON_BOX_GRID_BIG = 111, + ICON_OK_TICK = 112, + ICON_CROSS = 113, + ICON_ARROW_LEFT = 114, + ICON_ARROW_RIGHT = 115, + ICON_ARROW_DOWN = 116, + ICON_ARROW_UP = 117, + ICON_ARROW_LEFT_FILL = 118, + ICON_ARROW_RIGHT_FILL = 119, + ICON_ARROW_DOWN_FILL = 120, + ICON_ARROW_UP_FILL = 121, + ICON_AUDIO = 122, + ICON_FX = 123, + ICON_WAVE = 124, + ICON_WAVE_SINUS = 125, + ICON_WAVE_SQUARE = 126, + ICON_WAVE_TRIANGULAR = 127, + ICON_CROSS_SMALL = 128, + ICON_PLAYER_PREVIOUS = 129, + ICON_PLAYER_PLAY_BACK = 130, + ICON_PLAYER_PLAY = 131, + ICON_PLAYER_PAUSE = 132, + ICON_PLAYER_STOP = 133, + ICON_PLAYER_NEXT = 134, + ICON_PLAYER_RECORD = 135, + ICON_MAGNET = 136, + ICON_LOCK_CLOSE = 137, + ICON_LOCK_OPEN = 138, + ICON_CLOCK = 139, + ICON_TOOLS = 140, + ICON_GEAR = 141, + ICON_GEAR_BIG = 142, + ICON_BIN = 143, + ICON_HAND_POINTER = 144, + ICON_LASER = 145, + ICON_COIN = 146, + ICON_EXPLOSION = 147, + ICON_1UP = 148, + ICON_PLAYER = 149, + ICON_PLAYER_JUMP = 150, + ICON_KEY = 151, + ICON_DEMON = 152, + ICON_TEXT_POPUP = 153, + ICON_GEAR_EX = 154, + ICON_CRACK = 155, + ICON_CRACK_POINTS = 156, + ICON_STAR = 157, + ICON_DOOR = 158, + ICON_EXIT = 159, + ICON_MODE_2D = 160, + ICON_MODE_3D = 161, + ICON_CUBE = 162, + ICON_CUBE_FACE_TOP = 163, + ICON_CUBE_FACE_LEFT = 164, + ICON_CUBE_FACE_FRONT = 165, + ICON_CUBE_FACE_BOTTOM = 166, + ICON_CUBE_FACE_RIGHT = 167, + ICON_CUBE_FACE_BACK = 168, + ICON_CAMERA = 169, + ICON_SPECIAL = 170, + ICON_LINK_NET = 171, + ICON_LINK_BOXES = 172, + ICON_LINK_MULTI = 173, + ICON_LINK = 174, + ICON_LINK_BROKE = 175, + ICON_TEXT_NOTES = 176, + ICON_NOTEBOOK = 177, + ICON_SUITCASE = 178, + ICON_SUITCASE_ZIP = 179, + ICON_MAILBOX = 180, + ICON_MONITOR = 181, + ICON_PRINTER = 182, + ICON_PHOTO_CAMERA = 183, + ICON_PHOTO_CAMERA_FLASH = 184, + ICON_HOUSE = 185, + ICON_HEART = 186, + ICON_CORNER = 187, + ICON_VERTICAL_BARS = 188, + ICON_VERTICAL_BARS_FILL = 189, + ICON_LIFE_BARS = 190, + ICON_INFO = 191, + ICON_CROSSLINE = 192, + ICON_HELP = 193, + ICON_FILETYPE_ALPHA = 194, + ICON_FILETYPE_HOME = 195, + ICON_LAYERS_VISIBLE = 196, + ICON_LAYERS = 197, + ICON_WINDOW = 198, + ICON_HIDPI = 199, + ICON_FILETYPE_BINARY = 200, + ICON_HEX = 201, + ICON_SHIELD = 202, + ICON_FILE_NEW = 203, + ICON_FOLDER_ADD = 204, + ICON_ALARM = 205, + ICON_CPU = 206, + ICON_ROM = 207, + ICON_STEP_OVER = 208, + ICON_STEP_INTO = 209, + ICON_STEP_OUT = 210, + ICON_RESTART = 211, + ICON_BREAKPOINT_ON = 212, + ICON_BREAKPOINT_OFF = 213, + ICON_BURGER_MENU = 214, + ICON_CASE_SENSITIVE = 215, + ICON_REG_EXP = 216, + ICON_FOLDER = 217, + ICON_FILE = 218, + ICON_SAND_TIMER = 219, + ICON_WARNING = 220, + ICON_HELP_BOX = 221, + ICON_INFO_BOX = 222, + ICON_PRIORITY = 223, + ICON_LAYERS_ISO = 224, + ICON_LAYERS2 = 225, + ICON_MLAYERS = 226, + ICON_MAPS = 227, + ICON_HOT = 228, + ICON_LABEL = 229, + ICON_NAME_ID = 230, + ICON_SLICING = 231, + ICON_MANUAL_CONTROL = 232, + ICON_COLLISION = 233, + ICON_CIRCLE_ADD = 234, + ICON_CIRCLE_ADD_FILL = 235, + ICON_CIRCLE_WARNING = 236, + ICON_CIRCLE_WARNING_FILL = 237, + ICON_BOX_MORE = 238, + ICON_BOX_MORE_FILL = 239, + ICON_BOX_MINUS = 240, + ICON_BOX_MINUS_FILL = 241, + ICON_UNION = 242, + ICON_INTERSECTION = 243, + ICON_DIFFERENCE = 244, + ICON_SPHERE = 245, + ICON_CYLINDER = 246, + ICON_CONE = 247, + ICON_ELLIPSOID = 248, + ICON_CAPSULE = 249, + ICON_FILETYPE_FONT = 250, + ICON_FILETYPE_3D = 251, + ICON_FILETYPE_CODE_XML = 252, + ICON_FILETYPE_CODE_C = 253, + ICON_FILETYPE_CODE_PYTHON = 254, + ICON_FILETYPE_CODE_JS = 255, + ICON_FILETYPE_ICON = 256, +} GuiIconName; +#endif + +#endif + +#if defined(__cplusplus) +} // Prevents name mangling of functions +#endif + +#endif // RAYGUI_H + +/*********************************************************************************** +* +* RAYGUI IMPLEMENTATION +* +************************************************************************************/ + +#if defined(RAYGUI_IMPLEMENTATION) + +#include // Required for: FILE, fopen(), fclose(), fprintf(), feof(), fscanf(), snprintf(), vsprintf() [GuiLoadStyle(), GuiLoadIcons()] +#include // Required for: strlen() [GuiTextBox(), GuiValueBox()], memset(), memcpy() +#include // Required for: va_list, va_start(), vfprintf(), va_end() [TextFormat()] +#include // Required for: roundf() [GuiColorPicker()] +#include // Required for: isspace() [GuiTextBox()] + +// Allow custom memory allocators +#if defined(RAYGUI_MALLOC) || defined(RAYGUI_CALLOC) || defined(RAYGUI_FREE) + #if !defined(RAYGUI_MALLOC) || !defined(RAYGUI_CALLOC) || !defined(RAYGUI_FREE) + #error "RAYGUI: if RAYGUI_MALLOC, RAYGUI_CALLOC, or RAYGUI_FREE is customized, all three must be customized" + #endif +#else + #include // Required for: malloc(), calloc(), free() [GuiLoadStyle(), GuiLoadIcons()] + + #define RAYGUI_MALLOC(sz) malloc(sz) + #define RAYGUI_CALLOC(n,sz) calloc(n,sz) + #define RAYGUI_FREE(p) free(p) +#endif + +#ifdef __cplusplus + #define RAYGUI_CLITERAL(name) name +#else + #define RAYGUI_CLITERAL(name) (name) +#endif + +// Check if two rectangles are equal, used to validate a slider bounds as an id +#ifndef CHECK_BOUNDS_ID + #define CHECK_BOUNDS_ID(src, dst) (((int)src.x == (int)dst.x) && ((int)src.y == (int)dst.y) && ((int)src.width == (int)dst.width) && ((int)src.height == (int)dst.height)) +#endif + +#if !defined(RAYGUI_NO_ICONS) && !defined(RAYGUI_CUSTOM_ICONS) + +// Embedded icons, no external file provided +#define RAYGUI_ICON_SIZE 16 // Size of icons in pixels (squared) +#define RAYGUI_ICON_MAX_ICONS 512 // Maximum number of icons +#define RAYGUI_ICON_MAX_NAME_LENGTH 32 // Maximum length of icon name id + +// Icons data is defined by bit array (every bit represents one pixel) +// Those arrays are stored as unsigned int data arrays, so, +// every array element defines 32 pixels (bits) of information +// One icon is defined by 8 int, (8 int*32 bit = 256 bit = 16*16 pixels) +// NOTE: Number of elemens depend on RAYGUI_ICON_SIZE (by default 16x16 pixels) +#define RAYGUI_ICON_DATA_ELEMENTS (RAYGUI_ICON_SIZE*RAYGUI_ICON_SIZE/32) + +//---------------------------------------------------------------------------------- +// Icons data for all gui possible icons (allocated on data segment by default) +// +// NOTE 1: Every icon is codified in binary form, using 1 bit per pixel, so, +// every 16x16 icon requires 8 integers (16*16/32) to be stored +// +// NOTE 2: A different icon set could be loaded over this array using GuiLoadIcons(), +// but loaded icons set must be same RAYGUI_ICON_SIZE and no more than RAYGUI_ICON_MAX_ICONS +// +// guiIcons size is by default: 256*(16*16/32) = 2048*4 = 8192 bytes = 8 KB +//---------------------------------------------------------------------------------- +static unsigned int guiIcons[RAYGUI_ICON_MAX_ICONS*RAYGUI_ICON_DATA_ELEMENTS] = { + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, // ICON_NONE + 0x3ff80000, 0x2f082008, 0x2042207e, 0x40027fc2, 0x40024002, 0x40024002, 0x40024002, 0x00007ffe, // ICON_FOLDER_FILE_OPEN + 0x3ffe0000, 0x44226422, 0x400247e2, 0x5ffa4002, 0x57ea500a, 0x500a500a, 0x40025ffa, 0x00007ffe, // ICON_FILE_SAVE_CLASSIC + 0x00000000, 0x0042007e, 0x40027fc2, 0x40024002, 0x41024002, 0x44424282, 0x793e4102, 0x00000100, // ICON_FOLDER_OPEN + 0x00000000, 0x0042007e, 0x40027fc2, 0x40024002, 0x41024102, 0x44424102, 0x793e4282, 0x00000000, // ICON_FOLDER_SAVE + 0x3ff00000, 0x201c2010, 0x20042004, 0x21042004, 0x24442284, 0x21042104, 0x20042104, 0x00003ffc, // ICON_FILE_OPEN + 0x3ff00000, 0x201c2010, 0x20042004, 0x21042004, 0x21042104, 0x22842444, 0x20042104, 0x00003ffc, // ICON_FILE_SAVE + 0x3ff00000, 0x201c2010, 0x00042004, 0x20041004, 0x20844784, 0x00841384, 0x20042784, 0x00003ffc, // ICON_FILE_EXPORT + 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x22042204, 0x22042f84, 0x20042204, 0x00003ffc, // ICON_FILE_ADD + 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x25042884, 0x25042204, 0x20042884, 0x00003ffc, // ICON_FILE_DELETE + 0x3ff00000, 0x201c2010, 0x20042004, 0x20042ff4, 0x20042ff4, 0x20042ff4, 0x20042004, 0x00003ffc, // ICON_FILETYPE_TEXT + 0x3ff00000, 0x201c2010, 0x27042004, 0x244424c4, 0x26442444, 0x20642664, 0x20042004, 0x00003ffc, // ICON_FILETYPE_AUDIO + 0x3ff00000, 0x201c2010, 0x26042604, 0x20042004, 0x35442884, 0x2414222c, 0x20042004, 0x00003ffc, // ICON_FILETYPE_IMAGE + 0x3ff00000, 0x201c2010, 0x20c42004, 0x22442144, 0x22442444, 0x20c42144, 0x20042004, 0x00003ffc, // ICON_FILETYPE_PLAY + 0x3ff00000, 0x3ffc2ff0, 0x3f3c2ff4, 0x3dbc2eb4, 0x3dbc2bb4, 0x3f3c2eb4, 0x3ffc2ff4, 0x00002ff4, // ICON_FILETYPE_VIDEO + 0x3ff00000, 0x201c2010, 0x21842184, 0x21842004, 0x21842184, 0x21842184, 0x20042184, 0x00003ffc, // ICON_FILETYPE_INFO + 0x0ff00000, 0x381c0810, 0x28042804, 0x28042804, 0x28042804, 0x28042804, 0x20102ffc, 0x00003ff0, // ICON_FILE_COPY + 0x00000000, 0x701c0000, 0x079c1e14, 0x55a000f0, 0x079c00f0, 0x701c1e14, 0x00000000, 0x00000000, // ICON_FILE_CUT + 0x01c00000, 0x13e41bec, 0x3f841004, 0x204420c4, 0x20442044, 0x20442044, 0x207c2044, 0x00003fc0, // ICON_FILE_PASTE + 0x00000000, 0x3aa00fe0, 0x2abc2aa0, 0x2aa42aa4, 0x20042aa4, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_CURSOR_HAND + 0x00000000, 0x003c000c, 0x030800c8, 0x30100c10, 0x10202020, 0x04400840, 0x01800280, 0x00000000, // ICON_CURSOR_POINTER + 0x00000000, 0x00180000, 0x01f00078, 0x03e007f0, 0x07c003e0, 0x04000e40, 0x00000000, 0x00000000, // ICON_CURSOR_CLASSIC + 0x00000000, 0x04000000, 0x11000a00, 0x04400a80, 0x01100220, 0x00580088, 0x00000038, 0x00000000, // ICON_PENCIL + 0x04000000, 0x15000a00, 0x50402880, 0x14102820, 0x05040a08, 0x015c028c, 0x007c00bc, 0x00000000, // ICON_PENCIL_BIG + 0x01c00000, 0x01400140, 0x01400140, 0x0ff80140, 0x0ff80808, 0x0aa80808, 0x0aa80aa8, 0x00000ff8, // ICON_BRUSH_CLASSIC + 0x1ffc0000, 0x5ffc7ffe, 0x40004000, 0x00807f80, 0x01c001c0, 0x01c001c0, 0x01c001c0, 0x00000080, // ICON_BRUSH_PAINTER + 0x00000000, 0x00800000, 0x01c00080, 0x03e001c0, 0x07f003e0, 0x036006f0, 0x000001c0, 0x00000000, // ICON_WATER_DROP + 0x00000000, 0x3e003800, 0x1f803f80, 0x0c201e40, 0x02080c10, 0x00840104, 0x00380044, 0x00000000, // ICON_COLOR_PICKER + 0x00000000, 0x07800300, 0x1fe00fc0, 0x3f883fd0, 0x0e021f04, 0x02040402, 0x00f00108, 0x00000000, // ICON_RUBBER + 0x00c00000, 0x02800140, 0x08200440, 0x20081010, 0x2ffe3004, 0x03f807fc, 0x00e001f0, 0x00000040, // ICON_COLOR_BUCKET + 0x00000000, 0x21843ffc, 0x01800180, 0x01800180, 0x01800180, 0x01800180, 0x03c00180, 0x00000000, // ICON_TEXT_T + 0x00800000, 0x01400180, 0x06200340, 0x0c100620, 0x1ff80c10, 0x380c1808, 0x70067004, 0x0000f80f, // ICON_TEXT_A + 0x78000000, 0x50004000, 0x00004800, 0x03c003c0, 0x03c003c0, 0x00100000, 0x0002000a, 0x0000000e, // ICON_SCALE + 0x75560000, 0x5e004002, 0x54001002, 0x41001202, 0x408200fe, 0x40820082, 0x40820082, 0x00006afe, // ICON_RESIZE + 0x00000000, 0x3f003f00, 0x3f003f00, 0x3f003f00, 0x00400080, 0x001c0020, 0x001c001c, 0x00000000, // ICON_FILTER_POINT + 0x6d800000, 0x00004080, 0x40804080, 0x40800000, 0x00406d80, 0x001c0020, 0x001c001c, 0x00000000, // ICON_FILTER_BILINEAR + 0x40080000, 0x1ffe2008, 0x14081008, 0x11081208, 0x10481088, 0x10081028, 0x10047ff8, 0x00001002, // ICON_CROP + 0x00100000, 0x3ffc0010, 0x2ab03550, 0x22b02550, 0x20b02150, 0x20302050, 0x2000fff0, 0x00002000, // ICON_CROP_ALPHA + 0x40000000, 0x1ff82000, 0x04082808, 0x01082208, 0x00482088, 0x00182028, 0x35542008, 0x00000002, // ICON_SQUARE_TOGGLE + 0x00000000, 0x02800280, 0x06c006c0, 0x0ea00ee0, 0x1e901eb0, 0x3e883e98, 0x7efc7e8c, 0x00000000, // ICON_SYMMETRY + 0x01000000, 0x05600100, 0x1d480d50, 0x7d423d44, 0x3d447d42, 0x0d501d48, 0x01000560, 0x00000100, // ICON_SYMMETRY_HORIZONTAL + 0x01800000, 0x04200240, 0x10080810, 0x00001ff8, 0x00007ffe, 0x0ff01ff8, 0x03c007e0, 0x00000180, // ICON_SYMMETRY_VERTICAL + 0x00000000, 0x010800f0, 0x02040204, 0x02040204, 0x07f00308, 0x1c000e00, 0x30003800, 0x00000000, // ICON_LENS + 0x00000000, 0x061803f0, 0x08240c0c, 0x08040814, 0x0c0c0804, 0x23f01618, 0x18002400, 0x00000000, // ICON_LENS_BIG + 0x00000000, 0x00000000, 0x1c7007c0, 0x638e3398, 0x1c703398, 0x000007c0, 0x00000000, 0x00000000, // ICON_EYE_ON + 0x00000000, 0x10002000, 0x04700fc0, 0x610e3218, 0x1c703098, 0x001007a0, 0x00000008, 0x00000000, // ICON_EYE_OFF + 0x00000000, 0x00007ffc, 0x40047ffc, 0x10102008, 0x04400820, 0x02800280, 0x02800280, 0x00000100, // ICON_FILTER_TOP + 0x00000000, 0x40027ffe, 0x10082004, 0x04200810, 0x02400240, 0x02400240, 0x01400240, 0x000000c0, // ICON_FILTER + 0x00800000, 0x00800080, 0x00000080, 0x3c9e0000, 0x00000000, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_POINT + 0x00800000, 0x00800080, 0x00800080, 0x3f7e01c0, 0x008001c0, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_SMALL + 0x00800000, 0x00800080, 0x03e00080, 0x3e3e0220, 0x03e00220, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_BIG + 0x01000000, 0x04400280, 0x01000100, 0x43842008, 0x43849ab2, 0x01002008, 0x04400100, 0x01000280, // ICON_TARGET_MOVE + 0x01000000, 0x04400280, 0x01000100, 0x41042108, 0x41049ff2, 0x01002108, 0x04400100, 0x01000280, // ICON_CURSOR_MOVE + 0x781e0000, 0x500a4002, 0x04204812, 0x00000240, 0x02400000, 0x48120420, 0x4002500a, 0x0000781e, // ICON_CURSOR_SCALE + 0x00000000, 0x20003c00, 0x24002800, 0x01000200, 0x00400080, 0x00140024, 0x003c0004, 0x00000000, // ICON_CURSOR_SCALE_RIGHT + 0x00000000, 0x0004003c, 0x00240014, 0x00800040, 0x02000100, 0x28002400, 0x3c002000, 0x00000000, // ICON_CURSOR_SCALE_LEFT + 0x00000000, 0x00100020, 0x10101fc8, 0x10001020, 0x10001000, 0x10001000, 0x00001fc0, 0x00000000, // ICON_UNDO + 0x00000000, 0x08000400, 0x080813f8, 0x00080408, 0x00080008, 0x00080008, 0x000003f8, 0x00000000, // ICON_REDO + 0x00000000, 0x3ffc0000, 0x20042004, 0x20002000, 0x20402000, 0x3f902020, 0x00400020, 0x00000000, // ICON_REREDO + 0x00000000, 0x3ffc0000, 0x20042004, 0x27fc2004, 0x20202000, 0x3fc82010, 0x00200010, 0x00000000, // ICON_MUTATE + 0x00000000, 0x0ff00000, 0x10081818, 0x11801008, 0x10001180, 0x18101020, 0x00100fc8, 0x00000020, // ICON_ROTATE + 0x00000000, 0x04000200, 0x240429fc, 0x20042204, 0x20442004, 0x3f942024, 0x00400020, 0x00000000, // ICON_REPEAT + 0x00000000, 0x20001000, 0x22104c0e, 0x00801120, 0x11200040, 0x4c0e2210, 0x10002000, 0x00000000, // ICON_SHUFFLE + 0x7ffe0000, 0x50024002, 0x44024802, 0x41024202, 0x40424082, 0x40124022, 0x4002400a, 0x00007ffe, // ICON_EMPTYBOX + 0x00800000, 0x03e00080, 0x08080490, 0x3c9e0808, 0x08080808, 0x03e00490, 0x00800080, 0x00000000, // ICON_TARGET + 0x00800000, 0x00800080, 0x00800080, 0x3ffe01c0, 0x008001c0, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_SMALL_FILL + 0x00800000, 0x00800080, 0x03e00080, 0x3ffe03e0, 0x03e003e0, 0x00800080, 0x00800080, 0x00000000, // ICON_TARGET_BIG_FILL + 0x01000000, 0x07c00380, 0x01000100, 0x638c2008, 0x638cfbbe, 0x01002008, 0x07c00100, 0x01000380, // ICON_TARGET_MOVE_FILL + 0x01000000, 0x07c00380, 0x01000100, 0x610c2108, 0x610cfffe, 0x01002108, 0x07c00100, 0x01000380, // ICON_CURSOR_MOVE_FILL + 0x781e0000, 0x6006700e, 0x04204812, 0x00000240, 0x02400000, 0x48120420, 0x700e6006, 0x0000781e, // ICON_CURSOR_SCALE_FILL + 0x00000000, 0x38003c00, 0x24003000, 0x01000200, 0x00400080, 0x000c0024, 0x003c001c, 0x00000000, // ICON_CURSOR_SCALE_RIGHT_FILL + 0x00000000, 0x001c003c, 0x0024000c, 0x00800040, 0x02000100, 0x30002400, 0x3c003800, 0x00000000, // ICON_CURSOR_SCALE_LEFT_FILL + 0x00000000, 0x00300020, 0x10301ff8, 0x10001020, 0x10001000, 0x10001000, 0x00001fc0, 0x00000000, // ICON_UNDO_FILL + 0x00000000, 0x0c000400, 0x0c081ff8, 0x00080408, 0x00080008, 0x00080008, 0x000003f8, 0x00000000, // ICON_REDO_FILL + 0x00000000, 0x3ffc0000, 0x20042004, 0x20002000, 0x20402000, 0x3ff02060, 0x00400060, 0x00000000, // ICON_REREDO_FILL + 0x00000000, 0x3ffc0000, 0x20042004, 0x27fc2004, 0x20202000, 0x3ff82030, 0x00200030, 0x00000000, // ICON_MUTATE_FILL + 0x00000000, 0x0ff00000, 0x10081818, 0x11801008, 0x10001180, 0x18301020, 0x00300ff8, 0x00000020, // ICON_ROTATE_FILL + 0x00000000, 0x06000200, 0x26042ffc, 0x20042204, 0x20442004, 0x3ff42064, 0x00400060, 0x00000000, // ICON_REPEAT_FILL + 0x00000000, 0x30001000, 0x32107c0e, 0x00801120, 0x11200040, 0x7c0e3210, 0x10003000, 0x00000000, // ICON_SHUFFLE_FILL + 0x00000000, 0x30043ffc, 0x24042804, 0x21042204, 0x20442084, 0x20142024, 0x3ffc200c, 0x00000000, // ICON_EMPTYBOX_SMALL + 0x00000000, 0x20043ffc, 0x20042004, 0x20042004, 0x20042004, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX + 0x00000000, 0x23c43ffc, 0x23c423c4, 0x200423c4, 0x20042004, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_TOP + 0x00000000, 0x3e043ffc, 0x3e043e04, 0x20043e04, 0x20042004, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_TOP_RIGHT + 0x00000000, 0x20043ffc, 0x20042004, 0x3e043e04, 0x3e043e04, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_RIGHT + 0x00000000, 0x20043ffc, 0x20042004, 0x20042004, 0x3e042004, 0x3e043e04, 0x3ffc3e04, 0x00000000, // ICON_BOX_BOTTOM_RIGHT + 0x00000000, 0x20043ffc, 0x20042004, 0x20042004, 0x23c42004, 0x23c423c4, 0x3ffc23c4, 0x00000000, // ICON_BOX_BOTTOM + 0x00000000, 0x20043ffc, 0x20042004, 0x20042004, 0x207c2004, 0x207c207c, 0x3ffc207c, 0x00000000, // ICON_BOX_BOTTOM_LEFT + 0x00000000, 0x20043ffc, 0x20042004, 0x207c207c, 0x207c207c, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_LEFT + 0x00000000, 0x207c3ffc, 0x207c207c, 0x2004207c, 0x20042004, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_TOP_LEFT + 0x00000000, 0x20043ffc, 0x20042004, 0x23c423c4, 0x23c423c4, 0x20042004, 0x3ffc2004, 0x00000000, // ICON_BOX_CENTER + 0x7ffe0000, 0x40024002, 0x47e24182, 0x4ff247e2, 0x47e24ff2, 0x418247e2, 0x40024002, 0x00007ffe, // ICON_BOX_CIRCLE_MASK + 0x7fff0000, 0x40014001, 0x40014001, 0x49555ddd, 0x4945495d, 0x400149c5, 0x40014001, 0x00007fff, // ICON_POT + 0x7ffe0000, 0x53327332, 0x44ce4cce, 0x41324332, 0x404e40ce, 0x48125432, 0x4006540e, 0x00007ffe, // ICON_ALPHA_MULTIPLY + 0x7ffe0000, 0x53327332, 0x44ce4cce, 0x41324332, 0x5c4e40ce, 0x44124432, 0x40065c0e, 0x00007ffe, // ICON_ALPHA_CLEAR + 0x7ffe0000, 0x42fe417e, 0x42fe417e, 0x42fe417e, 0x42fe417e, 0x42fe417e, 0x42fe417e, 0x00007ffe, // ICON_DITHERING + 0x07fe0000, 0x1ffa0002, 0x7fea000a, 0x402a402a, 0x5b2a512a, 0x5128552a, 0x40205128, 0x00007fe0, // ICON_MIPMAPS + 0x00000000, 0x1ff80000, 0x12481248, 0x12481ff8, 0x1ff81248, 0x12481248, 0x00001ff8, 0x00000000, // ICON_BOX_GRID + 0x12480000, 0x7ffe1248, 0x12481248, 0x12487ffe, 0x7ffe1248, 0x12481248, 0x12487ffe, 0x00001248, // ICON_GRID + 0x00000000, 0x1c380000, 0x1c3817e8, 0x08100810, 0x08100810, 0x17e81c38, 0x00001c38, 0x00000000, // ICON_BOX_CORNERS_SMALL + 0x700e0000, 0x700e5ffa, 0x20042004, 0x20042004, 0x20042004, 0x20042004, 0x5ffa700e, 0x0000700e, // ICON_BOX_CORNERS_BIG + 0x3f7e0000, 0x21422142, 0x21422142, 0x00003f7e, 0x21423f7e, 0x21422142, 0x3f7e2142, 0x00000000, // ICON_FOUR_BOXES + 0x00000000, 0x3bb80000, 0x3bb83bb8, 0x3bb80000, 0x3bb83bb8, 0x3bb80000, 0x3bb83bb8, 0x00000000, // ICON_GRID_FILL + 0x7ffe0000, 0x7ffe7ffe, 0x77fe7000, 0x77fe77fe, 0x777e7700, 0x777e777e, 0x777e777e, 0x0000777e, // ICON_BOX_MULTISIZE + 0x781e0000, 0x40024002, 0x00004002, 0x01800000, 0x00000180, 0x40020000, 0x40024002, 0x0000781e, // ICON_ZOOM_SMALL + 0x781e0000, 0x40024002, 0x00004002, 0x03c003c0, 0x03c003c0, 0x40020000, 0x40024002, 0x0000781e, // ICON_ZOOM_MEDIUM + 0x781e0000, 0x40024002, 0x07e04002, 0x07e007e0, 0x07e007e0, 0x400207e0, 0x40024002, 0x0000781e, // ICON_ZOOM_BIG + 0x781e0000, 0x5ffa4002, 0x1ff85ffa, 0x1ff81ff8, 0x1ff81ff8, 0x5ffa1ff8, 0x40025ffa, 0x0000781e, // ICON_ZOOM_ALL + 0x00000000, 0x2004381c, 0x00002004, 0x00000000, 0x00000000, 0x20040000, 0x381c2004, 0x00000000, // ICON_ZOOM_CENTER + 0x00000000, 0x1db80000, 0x10081008, 0x10080000, 0x00001008, 0x10081008, 0x00001db8, 0x00000000, // ICON_BOX_DOTS_SMALL + 0x35560000, 0x00002002, 0x00002002, 0x00002002, 0x00002002, 0x00002002, 0x35562002, 0x00000000, // ICON_BOX_DOTS_BIG + 0x7ffe0000, 0x40024002, 0x48124ff2, 0x49924812, 0x48124992, 0x4ff24812, 0x40024002, 0x00007ffe, // ICON_BOX_CONCENTRIC + 0x00000000, 0x10841ffc, 0x10841084, 0x1ffc1084, 0x10841084, 0x10841084, 0x00001ffc, 0x00000000, // ICON_BOX_GRID_BIG + 0x00000000, 0x00000000, 0x10000000, 0x04000800, 0x01040200, 0x00500088, 0x00000020, 0x00000000, // ICON_OK_TICK + 0x00000000, 0x10080000, 0x04200810, 0x01800240, 0x02400180, 0x08100420, 0x00001008, 0x00000000, // ICON_CROSS + 0x00000000, 0x02000000, 0x00800100, 0x00200040, 0x00200010, 0x00800040, 0x02000100, 0x00000000, // ICON_ARROW_LEFT + 0x00000000, 0x00400000, 0x01000080, 0x04000200, 0x04000800, 0x01000200, 0x00400080, 0x00000000, // ICON_ARROW_RIGHT + 0x00000000, 0x00000000, 0x00000000, 0x08081004, 0x02200410, 0x00800140, 0x00000000, 0x00000000, // ICON_ARROW_DOWN + 0x00000000, 0x00000000, 0x01400080, 0x04100220, 0x10040808, 0x00000000, 0x00000000, 0x00000000, // ICON_ARROW_UP + 0x00000000, 0x02000000, 0x03800300, 0x03e003c0, 0x03e003f0, 0x038003c0, 0x02000300, 0x00000000, // ICON_ARROW_LEFT_FILL + 0x00000000, 0x00400000, 0x01c000c0, 0x07c003c0, 0x07c00fc0, 0x01c003c0, 0x004000c0, 0x00000000, // ICON_ARROW_RIGHT_FILL + 0x00000000, 0x00000000, 0x00000000, 0x0ff81ffc, 0x03e007f0, 0x008001c0, 0x00000000, 0x00000000, // ICON_ARROW_DOWN_FILL + 0x00000000, 0x00000000, 0x01c00080, 0x07f003e0, 0x1ffc0ff8, 0x00000000, 0x00000000, 0x00000000, // ICON_ARROW_UP_FILL + 0x00000000, 0x18a008c0, 0x32881290, 0x24822686, 0x26862482, 0x12903288, 0x08c018a0, 0x00000000, // ICON_AUDIO + 0x00000000, 0x04800780, 0x004000c0, 0x662000f0, 0x08103c30, 0x130a0e18, 0x0000318e, 0x00000000, // ICON_FX + 0x00000000, 0x00800000, 0x08880888, 0x2aaa0a8a, 0x0a8a2aaa, 0x08880888, 0x00000080, 0x00000000, // ICON_WAVE + 0x00000000, 0x00600000, 0x01080090, 0x02040108, 0x42044204, 0x24022402, 0x00001800, 0x00000000, // ICON_WAVE_SINUS + 0x00000000, 0x07f80000, 0x04080408, 0x04080408, 0x04080408, 0x7c0e0408, 0x00000000, 0x00000000, // ICON_WAVE_SQUARE + 0x00000000, 0x00000000, 0x00a00040, 0x22084110, 0x08021404, 0x00000000, 0x00000000, 0x00000000, // ICON_WAVE_TRIANGULAR + 0x00000000, 0x00000000, 0x04200000, 0x01800240, 0x02400180, 0x00000420, 0x00000000, 0x00000000, // ICON_CROSS_SMALL + 0x00000000, 0x18380000, 0x12281428, 0x10a81128, 0x112810a8, 0x14281228, 0x00001838, 0x00000000, // ICON_PLAYER_PREVIOUS + 0x00000000, 0x18000000, 0x11801600, 0x10181060, 0x10601018, 0x16001180, 0x00001800, 0x00000000, // ICON_PLAYER_PLAY_BACK + 0x00000000, 0x00180000, 0x01880068, 0x18080608, 0x06081808, 0x00680188, 0x00000018, 0x00000000, // ICON_PLAYER_PLAY + 0x00000000, 0x1e780000, 0x12481248, 0x12481248, 0x12481248, 0x12481248, 0x00001e78, 0x00000000, // ICON_PLAYER_PAUSE + 0x00000000, 0x1ff80000, 0x10081008, 0x10081008, 0x10081008, 0x10081008, 0x00001ff8, 0x00000000, // ICON_PLAYER_STOP + 0x00000000, 0x1c180000, 0x14481428, 0x15081488, 0x14881508, 0x14281448, 0x00001c18, 0x00000000, // ICON_PLAYER_NEXT + 0x00000000, 0x03c00000, 0x08100420, 0x10081008, 0x10081008, 0x04200810, 0x000003c0, 0x00000000, // ICON_PLAYER_RECORD + 0x00000000, 0x0c3007e0, 0x13c81818, 0x14281668, 0x14281428, 0x1c381c38, 0x08102244, 0x00000000, // ICON_MAGNET + 0x07c00000, 0x08200820, 0x3ff80820, 0x23882008, 0x21082388, 0x20082108, 0x1ff02008, 0x00000000, // ICON_LOCK_CLOSE + 0x07c00000, 0x08000800, 0x3ff80800, 0x23882008, 0x21082388, 0x20082108, 0x1ff02008, 0x00000000, // ICON_LOCK_OPEN + 0x01c00000, 0x0c180770, 0x3086188c, 0x60832082, 0x60034781, 0x30062002, 0x0c18180c, 0x01c00770, // ICON_CLOCK + 0x0a200000, 0x1b201b20, 0x04200e20, 0x04200420, 0x04700420, 0x0e700e70, 0x0e700e70, 0x04200e70, // ICON_TOOLS + 0x01800000, 0x3bdc318c, 0x0ff01ff8, 0x7c3e1e78, 0x1e787c3e, 0x1ff80ff0, 0x318c3bdc, 0x00000180, // ICON_GEAR + 0x01800000, 0x3ffc318c, 0x1c381ff8, 0x781e1818, 0x1818781e, 0x1ff81c38, 0x318c3ffc, 0x00000180, // ICON_GEAR_BIG + 0x00000000, 0x08080ff8, 0x08081ffc, 0x0aa80aa8, 0x0aa80aa8, 0x0aa80aa8, 0x08080aa8, 0x00000ff8, // ICON_BIN + 0x00000000, 0x00000000, 0x20043ffc, 0x08043f84, 0x04040f84, 0x04040784, 0x000007fc, 0x00000000, // ICON_HAND_POINTER + 0x00000000, 0x24400400, 0x00001480, 0x6efe0e00, 0x00000e00, 0x24401480, 0x00000400, 0x00000000, // ICON_LASER + 0x00000000, 0x03c00000, 0x08300460, 0x11181118, 0x11181118, 0x04600830, 0x000003c0, 0x00000000, // ICON_COIN + 0x00000000, 0x10880080, 0x06c00810, 0x366c07e0, 0x07e00240, 0x00001768, 0x04200240, 0x00000000, // ICON_EXPLOSION + 0x00000000, 0x3d280000, 0x2528252c, 0x3d282528, 0x05280528, 0x05e80528, 0x00000000, 0x00000000, // ICON_1UP + 0x01800000, 0x03c003c0, 0x018003c0, 0x0ff007e0, 0x0bd00bd0, 0x0a500bd0, 0x02400240, 0x02400240, // ICON_PLAYER + 0x01800000, 0x03c003c0, 0x118013c0, 0x03c81ff8, 0x07c003c8, 0x04400440, 0x0c080478, 0x00000000, // ICON_PLAYER_JUMP + 0x3ff80000, 0x30183ff8, 0x30183018, 0x3ff83ff8, 0x03000300, 0x03c003c0, 0x03e00300, 0x000003e0, // ICON_KEY + 0x3ff80000, 0x3ff83ff8, 0x33983ff8, 0x3ff83398, 0x3ff83ff8, 0x00000540, 0x0fe00aa0, 0x00000fe0, // ICON_DEMON + 0x00000000, 0x0ff00000, 0x20041008, 0x25442004, 0x10082004, 0x06000bf0, 0x00000300, 0x00000000, // ICON_TEXT_POPUP + 0x00000000, 0x11440000, 0x07f00be8, 0x1c1c0e38, 0x1c1c0c18, 0x07f00e38, 0x11440be8, 0x00000000, // ICON_GEAR_EX + 0x00000000, 0x20080000, 0x0c601010, 0x07c00fe0, 0x07c007c0, 0x0c600fe0, 0x20081010, 0x00000000, // ICON_CRACK + 0x00000000, 0x20080000, 0x0c601010, 0x04400fe0, 0x04405554, 0x0c600fe0, 0x20081010, 0x00000000, // ICON_CRACK_POINTS + 0x00000000, 0x00800080, 0x01c001c0, 0x1ffc3ffe, 0x03e007f0, 0x07f003e0, 0x0c180770, 0x00000808, // ICON_STAR + 0x0ff00000, 0x08180810, 0x08100818, 0x0a100810, 0x08180810, 0x08100818, 0x08100810, 0x00001ff8, // ICON_DOOR + 0x0ff00000, 0x08100810, 0x08100810, 0x10100010, 0x4f902010, 0x10102010, 0x08100010, 0x00000ff0, // ICON_EXIT + 0x00040000, 0x001f000e, 0x0ef40004, 0x12f41284, 0x0ef41214, 0x10040004, 0x7ffc3004, 0x10003000, // ICON_MODE_2D + 0x78040000, 0x501f600e, 0x0ef44004, 0x12f41284, 0x0ef41284, 0x10140004, 0x7ffc300c, 0x10003000, // ICON_MODE_3D + 0x7fe00000, 0x50286030, 0x47fe4804, 0x44224402, 0x44224422, 0x241275e2, 0x0c06140a, 0x000007fe, // ICON_CUBE + 0x7fe00000, 0x5ff87ff0, 0x47fe4ffc, 0x44224402, 0x44224422, 0x241275e2, 0x0c06140a, 0x000007fe, // ICON_CUBE_FACE_TOP + 0x7fe00000, 0x50386030, 0x47c2483c, 0x443e443e, 0x443e443e, 0x241e75fe, 0x0c06140e, 0x000007fe, // ICON_CUBE_FACE_LEFT + 0x7fe00000, 0x50286030, 0x47fe4804, 0x47fe47fe, 0x47fe47fe, 0x27fe77fe, 0x0ffe17fe, 0x000007fe, // ICON_CUBE_FACE_FRONT + 0x7fe00000, 0x50286030, 0x47fe4804, 0x44224402, 0x44224422, 0x3bf27be2, 0x0bfe1bfa, 0x000007fe, // ICON_CUBE_FACE_BOTTOM + 0x7fe00000, 0x70286030, 0x7ffe7804, 0x7c227c02, 0x7c227c22, 0x3c127de2, 0x0c061c0a, 0x000007fe, // ICON_CUBE_FACE_RIGHT + 0x7fe00000, 0x6fe85ff0, 0x781e77e4, 0x7be27be2, 0x7be27be2, 0x24127be2, 0x0c06140a, 0x000007fe, // ICON_CUBE_FACE_BACK + 0x00000000, 0x2a0233fe, 0x22022602, 0x22022202, 0x2a022602, 0x00a033fe, 0x02080110, 0x00000000, // ICON_CAMERA + 0x00000000, 0x200c3ffc, 0x000c000c, 0x3ffc000c, 0x30003000, 0x30003000, 0x3ffc3004, 0x00000000, // ICON_SPECIAL + 0x00000000, 0x0022003e, 0x012201e2, 0x0100013e, 0x01000100, 0x79000100, 0x4f004900, 0x00007800, // ICON_LINK_NET + 0x00000000, 0x44007c00, 0x45004600, 0x00627cbe, 0x00620022, 0x45007cbe, 0x44004600, 0x00007c00, // ICON_LINK_BOXES + 0x00000000, 0x0044007c, 0x0010007c, 0x3f100010, 0x3f1021f0, 0x3f100010, 0x3f0021f0, 0x00000000, // ICON_LINK_MULTI + 0x00000000, 0x0044007c, 0x00440044, 0x0010007c, 0x00100010, 0x44107c10, 0x440047f0, 0x00007c00, // ICON_LINK + 0x00000000, 0x0044007c, 0x00440044, 0x0000007c, 0x00000010, 0x44007c10, 0x44004550, 0x00007c00, // ICON_LINK_BROKE + 0x02a00000, 0x22a43ffc, 0x20042004, 0x20042ff4, 0x20042ff4, 0x20042ff4, 0x20042004, 0x00003ffc, // ICON_TEXT_NOTES + 0x3ffc0000, 0x20042004, 0x245e27c4, 0x27c42444, 0x2004201e, 0x201e2004, 0x20042004, 0x00003ffc, // ICON_NOTEBOOK + 0x00000000, 0x07e00000, 0x04200420, 0x24243ffc, 0x24242424, 0x24242424, 0x3ffc2424, 0x00000000, // ICON_SUITCASE + 0x00000000, 0x0fe00000, 0x08200820, 0x40047ffc, 0x7ffc5554, 0x40045554, 0x7ffc4004, 0x00000000, // ICON_SUITCASE_ZIP + 0x00000000, 0x20043ffc, 0x3ffc2004, 0x13c81008, 0x100813c8, 0x10081008, 0x1ff81008, 0x00000000, // ICON_MAILBOX + 0x00000000, 0x40027ffe, 0x5ffa5ffa, 0x5ffa5ffa, 0x40025ffa, 0x03c07ffe, 0x1ff81ff8, 0x00000000, // ICON_MONITOR + 0x0ff00000, 0x6bfe7ffe, 0x7ffe7ffe, 0x68167ffe, 0x08106816, 0x08100810, 0x0ff00810, 0x00000000, // ICON_PRINTER + 0x3ff80000, 0xfffe2008, 0x870a8002, 0x904a888a, 0x904a904a, 0x870a888a, 0xfffe8002, 0x00000000, // ICON_PHOTO_CAMERA + 0x0fc00000, 0xfcfe0cd8, 0x8002fffe, 0x84428382, 0x84428442, 0x80028382, 0xfffe8002, 0x00000000, // ICON_PHOTO_CAMERA_FLASH + 0x00000000, 0x02400180, 0x08100420, 0x20041008, 0x23c42004, 0x22442244, 0x3ffc2244, 0x00000000, // ICON_HOUSE + 0x00000000, 0x1c700000, 0x3ff83ef8, 0x3ff83ff8, 0x0fe01ff0, 0x038007c0, 0x00000100, 0x00000000, // ICON_HEART + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x80000000, 0xe000c000, // ICON_CORNER + 0x00000000, 0x14001c00, 0x15c01400, 0x15401540, 0x155c1540, 0x15541554, 0x1ddc1554, 0x00000000, // ICON_VERTICAL_BARS + 0x00000000, 0x03000300, 0x1b001b00, 0x1b601b60, 0x1b6c1b60, 0x1b6c1b6c, 0x1b6c1b6c, 0x00000000, // ICON_VERTICAL_BARS_FILL + 0x00000000, 0x00000000, 0x403e7ffe, 0x7ffe403e, 0x7ffe0000, 0x43fe43fe, 0x00007ffe, 0x00000000, // ICON_LIFE_BARS + 0x7ffc0000, 0x43844004, 0x43844284, 0x43844004, 0x42844284, 0x42844284, 0x40044384, 0x00007ffc, // ICON_INFO + 0x40008000, 0x10002000, 0x04000800, 0x01000200, 0x00400080, 0x00100020, 0x00040008, 0x00010002, // ICON_CROSSLINE + 0x00000000, 0x1ff01ff0, 0x18301830, 0x1f001830, 0x03001f00, 0x00000300, 0x03000300, 0x00000000, // ICON_HELP + 0x3ff00000, 0x2abc3550, 0x2aac3554, 0x2aac3554, 0x2aac3554, 0x2aac3554, 0x2aac3554, 0x00003ffc, // ICON_FILETYPE_ALPHA + 0x3ff00000, 0x201c2010, 0x22442184, 0x28142424, 0x29942814, 0x2ff42994, 0x20042004, 0x00003ffc, // ICON_FILETYPE_HOME + 0x07fe0000, 0x04020402, 0x7fe20402, 0x44224422, 0x44224422, 0x402047fe, 0x40204020, 0x00007fe0, // ICON_LAYERS_VISIBLE + 0x07fe0000, 0x04020402, 0x7c020402, 0x44024402, 0x44024402, 0x402047fe, 0x40204020, 0x00007fe0, // ICON_LAYERS + 0x00000000, 0x40027ffe, 0x7ffe4002, 0x40024002, 0x40024002, 0x40024002, 0x7ffe4002, 0x00000000, // ICON_WINDOW + 0x09100000, 0x09f00910, 0x09100910, 0x00000910, 0x24a2779e, 0x27a224a2, 0x709e20a2, 0x00000000, // ICON_HIDPI + 0x3ff00000, 0x201c2010, 0x2a842e84, 0x2e842a84, 0x2ba42004, 0x2aa42aa4, 0x20042ba4, 0x00003ffc, // ICON_FILETYPE_BINARY + 0x00000000, 0x00000000, 0x00120012, 0x4a5e4bd2, 0x485233d2, 0x00004bd2, 0x00000000, 0x00000000, // ICON_HEX + 0x01800000, 0x381c0660, 0x23c42004, 0x23c42044, 0x13c82204, 0x08101008, 0x02400420, 0x00000180, // ICON_SHIELD + 0x007e0000, 0x20023fc2, 0x40227fe2, 0x400a403a, 0x400a400a, 0x400a400a, 0x4008400e, 0x00007ff8, // ICON_FILE_NEW + 0x00000000, 0x0042007e, 0x40027fc2, 0x44024002, 0x5f024402, 0x44024402, 0x7ffe4002, 0x00000000, // ICON_FOLDER_ADD + 0x44220000, 0x12482244, 0xf3cf0000, 0x14280420, 0x48122424, 0x08100810, 0x1ff81008, 0x03c00420, // ICON_ALARM + 0x0aa00000, 0x1ff80aa0, 0x1068700e, 0x1008706e, 0x1008700e, 0x1008700e, 0x0aa01ff8, 0x00000aa0, // ICON_CPU + 0x07e00000, 0x04201db8, 0x04a01c38, 0x04a01d38, 0x04a01d38, 0x04a01d38, 0x04201d38, 0x000007e0, // ICON_ROM + 0x00000000, 0x03c00000, 0x3c382ff0, 0x3c04380c, 0x01800000, 0x03c003c0, 0x00000180, 0x00000000, // ICON_STEP_OVER + 0x01800000, 0x01800180, 0x01800180, 0x03c007e0, 0x00000180, 0x01800000, 0x03c003c0, 0x00000180, // ICON_STEP_INTO + 0x01800000, 0x07e003c0, 0x01800180, 0x01800180, 0x00000180, 0x01800000, 0x03c003c0, 0x00000180, // ICON_STEP_OUT + 0x00000000, 0x0ff003c0, 0x181c1c34, 0x303c301c, 0x30003000, 0x1c301800, 0x03c00ff0, 0x00000000, // ICON_RESTART + 0x00000000, 0x00000000, 0x07e003c0, 0x0ff00ff0, 0x0ff00ff0, 0x03c007e0, 0x00000000, 0x00000000, // ICON_BREAKPOINT_ON + 0x00000000, 0x00000000, 0x042003c0, 0x08100810, 0x08100810, 0x03c00420, 0x00000000, 0x00000000, // ICON_BREAKPOINT_OFF + 0x00000000, 0x00000000, 0x1ff81ff8, 0x1ff80000, 0x00001ff8, 0x1ff81ff8, 0x00000000, 0x00000000, // ICON_BURGER_MENU + 0x00000000, 0x00000000, 0x00880070, 0x0c880088, 0x1e8810f8, 0x3e881288, 0x00000000, 0x00000000, // ICON_CASE_SENSITIVE + 0x00000000, 0x02000000, 0x07000a80, 0x07001fc0, 0x02000a80, 0x00300030, 0x00000000, 0x00000000, // ICON_REG_EXP + 0x00000000, 0x0042007e, 0x40027fc2, 0x40024002, 0x40024002, 0x40024002, 0x7ffe4002, 0x00000000, // ICON_FOLDER + 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x20042004, 0x20042004, 0x20042004, 0x00003ffc, // ICON_FILE + 0x1ff00000, 0x20082008, 0x17d02fe8, 0x05400ba0, 0x09200540, 0x23881010, 0x2fe827c8, 0x00001ff0, // ICON_SAND_TIMER + 0x01800000, 0x02400240, 0x05a00420, 0x09900990, 0x11881188, 0x21842004, 0x40024182, 0x00003ffc, // ICON_WARNING + 0x7ffe0000, 0x4ff24002, 0x4c324ff2, 0x4f824c02, 0x41824f82, 0x41824002, 0x40024182, 0x00007ffe, // ICON_HELP_BOX + 0x7ffe0000, 0x41824002, 0x40024182, 0x41824182, 0x41824182, 0x41824182, 0x40024182, 0x00007ffe, // ICON_INFO_BOX + 0x01800000, 0x04200240, 0x10080810, 0x7bde2004, 0x0a500a50, 0x08500bd0, 0x08100850, 0x00000ff0, // ICON_PRIORITY + 0x01800000, 0x18180660, 0x80016006, 0x98196006, 0x99996666, 0x19986666, 0x01800660, 0x00000000, // ICON_LAYERS_ISO + 0x07fe0000, 0x1c020402, 0x74021402, 0x54025402, 0x54025402, 0x500857fe, 0x40205ff8, 0x00007fe0, // ICON_LAYERS2 + 0x0ffe0000, 0x3ffa0802, 0x7fea200a, 0x402a402a, 0x422a422a, 0x422e422a, 0x40384e28, 0x00007fe0, // ICON_MLAYERS + 0x0ffe0000, 0x3ffa0802, 0x7fea200a, 0x402a402a, 0x5b2a512a, 0x512e552a, 0x40385128, 0x00007fe0, // ICON_MAPS + 0x04200000, 0x1cf00c60, 0x11f019f0, 0x0f3807b8, 0x1e3c0f3c, 0x1c1c1e1c, 0x1e3c1c1c, 0x00000f70, // ICON_HOT + 0x00000000, 0x20803f00, 0x2a202e40, 0x20082e10, 0x08021004, 0x02040402, 0x00900108, 0x00000060, // ICON_LABEL + 0x00000000, 0x042007e0, 0x47e27c3e, 0x4ffa4002, 0x47fa4002, 0x4ffa4002, 0x7ffe4002, 0x00000000, // ICON_NAME_ID + 0x7fe00000, 0x402e4020, 0x43ce5e0a, 0x40504078, 0x438e4078, 0x402e5e0a, 0x7fe04020, 0x00000000, // ICON_SLICING + 0x00000000, 0x40027ffe, 0x47c24002, 0x55425d42, 0x55725542, 0x50125552, 0x10105016, 0x00001ff0, // ICON_MANUAL_CONTROL + 0x7ffe0000, 0x43c24002, 0x48124422, 0x500a500a, 0x500a500a, 0x44224812, 0x400243c2, 0x00007ffe, // ICON_COLLISION + 0x03c00000, 0x10080c30, 0x21842184, 0x4ff24182, 0x41824ff2, 0x21842184, 0x0c301008, 0x000003c0, // ICON_CIRCLE_ADD + 0x03c00000, 0x1ff80ff0, 0x3e7c3e7c, 0x700e7e7e, 0x7e7e700e, 0x3e7c3e7c, 0x0ff01ff8, 0x000003c0, // ICON_CIRCLE_ADD_FILL + 0x03c00000, 0x10080c30, 0x21842184, 0x41824182, 0x40024182, 0x21842184, 0x0c301008, 0x000003c0, // ICON_CIRCLE_WARNING + 0x03c00000, 0x1ff80ff0, 0x3e7c3e7c, 0x7e7e7e7e, 0x7ffe7e7e, 0x3e7c3e7c, 0x0ff01ff8, 0x000003c0, // ICON_CIRCLE_WARNING_FILL + 0x00000000, 0x10041ffc, 0x10841004, 0x13e41084, 0x10841084, 0x10041004, 0x00001ffc, 0x00000000, // ICON_BOX_MORE + 0x00000000, 0x1ffc1ffc, 0x1f7c1ffc, 0x1c1c1f7c, 0x1f7c1f7c, 0x1ffc1ffc, 0x00001ffc, 0x00000000, // ICON_BOX_MORE_FILL + 0x00000000, 0x1ffc1ffc, 0x1ffc1ffc, 0x1c1c1ffc, 0x1ffc1ffc, 0x1ffc1ffc, 0x00001ffc, 0x00000000, // ICON_BOX_MINUS + 0x00000000, 0x10041ffc, 0x10041004, 0x13e41004, 0x10041004, 0x10041004, 0x00001ffc, 0x00000000, // ICON_BOX_MINUS_FILL + 0x07fe0000, 0x055606aa, 0x7ff606aa, 0x55766eba, 0x55766eaa, 0x55606ffe, 0x55606aa0, 0x00007fe0, // ICON_UNION + 0x07fe0000, 0x04020402, 0x7fe20402, 0x456246a2, 0x456246a2, 0x402047fe, 0x40204020, 0x00007fe0, // ICON_INTERSECTION + 0x07fe0000, 0x055606aa, 0x7ff606aa, 0x4436442a, 0x4436442a, 0x402047fe, 0x40204020, 0x00007fe0, // ICON_DIFFERENCE + 0x03c00000, 0x10080c30, 0x20042004, 0x60064002, 0x47e2581a, 0x20042004, 0x0c301008, 0x000003c0, // ICON_SPHERE + 0x03e00000, 0x08080410, 0x0c180808, 0x08080be8, 0x08080808, 0x08080808, 0x04100808, 0x000003e0, // ICON_CYLINDER + 0x00800000, 0x01400140, 0x02200220, 0x04100410, 0x08080808, 0x1c1c13e4, 0x08081004, 0x000007f0, // ICON_CONE + 0x00000000, 0x07e00000, 0x20841918, 0x40824082, 0x40824082, 0x19182084, 0x000007e0, 0x00000000, // ICON_ELLIPSOID + 0x00000000, 0x00000000, 0x20041ff8, 0x40024002, 0x40024002, 0x1ff82004, 0x00000000, 0x00000000, // ICON_CAPSULE + 0x3ff00000, 0x201c2010, 0x21042004, 0x22842384, 0x264426c4, 0x2c242fe4, 0x20043e74, 0x00003ffc, // ICON_FILETYPE_FONT + 0x3ff00000, 0x201c2010, 0x20042004, 0x27742004, 0x29742944, 0x27742944, 0x20042004, 0x00003ffc, // ICON_FILETYPE_3D + 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x24242244, 0x24242814, 0x20042244, 0x00003ffc, // ICON_FILETYPE_XML + 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x21042f04, 0x21042104, 0x20042f04, 0x00003ffc, // ICON_FILETYPE_C + 0x3ff00000, 0x201c2010, 0x23842004, 0x2bf42a04, 0x2fd42814, 0x21c42054, 0x20042004, 0x00003ffc, // ICON_FILETYPE_PYTHON + 0x3ff00000, 0x201c2010, 0x20042004, 0x20042004, 0x22842ee4, 0x28a42e84, 0x20042e64, 0x00003ffc, // ICON_FILETYPE_JS + 0x3ff00000, 0x241c2010, 0x2a8c3104, 0x28242454, 0x2ed42004, 0x2a542a54, 0x20042ed4, 0x00003ffc, // ICON_FILETYPE_ICON + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, // ICON_257 +}; + +// NOTE: A pointer to current icons array should be defined +static unsigned int *guiIconsPtr = guiIcons; + +#endif // !RAYGUI_NO_ICONS && !RAYGUI_CUSTOM_ICONS + +#ifndef RAYGUI_ICON_SIZE + #define RAYGUI_ICON_SIZE 0 +#endif + +// WARNING: Those values define the total size of the style data array, +// if changed, previous saved styles could become incompatible +#define RAYGUI_MAX_CONTROLS 16 // Maximum number of controls +#define RAYGUI_MAX_PROPS_BASE 16 // Maximum number of base properties +#define RAYGUI_MAX_PROPS_EXTENDED 8 // Maximum number of extended properties + +//---------------------------------------------------------------------------------- +// Module Types and Structures Definition +//---------------------------------------------------------------------------------- +// Gui control property style color element +typedef enum { BORDER = 0, BASE, TEXT, OTHER } GuiPropertyElement; + +//---------------------------------------------------------------------------------- +// Global Variables Definition +//---------------------------------------------------------------------------------- +static GuiState guiState = STATE_NORMAL; // Gui global state, if !STATE_NORMAL, forces defined state + +static Font guiFont = { 0 }; // Gui current font (WARNING: highly coupled to raylib) +static bool guiLocked = false; // Gui lock state (no inputs processed) +static float guiAlpha = 1.0f; // Gui controls transparency + +static unsigned int guiIconScale = 1; // Gui icon default scale (if icons enabled) + +static bool guiTooltip = false; // Tooltip enabled/disabled +static const char *guiTooltipPtr = NULL; // Tooltip string pointer (string provided by user) + +static bool guiControlExclusiveMode = false; // Gui control exclusive mode (no inputs processed except current control) +static Rectangle guiControlExclusiveRec = { 0 }; // Gui control exclusive bounds rectangle, used as an unique identifier + +static int textBoxCursorIndex = 0; // Cursor index, shared by all GuiTextBox*() +//static int blinkCursorFrameCounter = 0; // Frame counter for cursor blinking +static int autoCursorCounter = 0; // Frame counter for automatic repeated cursor movement on key-down (cooldown and delay) + +//---------------------------------------------------------------------------------- +// Style data array for all gui style properties (allocated on data segment by default) +// +// NOTE 1: First set of BASE properties are generic to all controls but could be individually +// overwritten per control, first set of EXTENDED properties are generic to all controls and +// can not be overwritten individually but custom EXTENDED properties can be used by control +// +// NOTE 2: A new style set could be loaded over this array using GuiLoadStyle(), +// but default gui style could always be recovered with GuiLoadStyleDefault() +// +// guiStyle size is by default: 16*(16 + 8) = 384*4 = 1536 bytes = 1.5 KB +//---------------------------------------------------------------------------------- +static unsigned int guiStyle[RAYGUI_MAX_CONTROLS*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED)] = { 0 }; + +static bool guiStyleLoaded = false; // Style loaded flag for lazy style initialization + +//---------------------------------------------------------------------------------- +// Standalone Mode Functions Declaration +// +// NOTE: raygui depend on some raylib input and drawing functions +// To use raygui as standalone library, below functions must be defined by the user +//---------------------------------------------------------------------------------- +#if defined(RAYGUI_STANDALONE) + +#define KEY_RIGHT 262 +#define KEY_LEFT 263 +#define KEY_DOWN 264 +#define KEY_UP 265 +#define KEY_BACKSPACE 259 +#define KEY_ENTER 257 + +#define MOUSE_LEFT_BUTTON 0 + +// Input required functions +//------------------------------------------------------------------------------- +static Vector2 GetMousePosition(void); +static float GetMouseWheelMove(void); +static bool IsMouseButtonDown(int button); +static bool IsMouseButtonPressed(int button); +static bool IsMouseButtonReleased(int button); + +static bool IsKeyDown(int key); +static bool IsKeyPressed(int key); +static int GetCharPressed(void); // -- GuiTextBox(), GuiValueBox() +//------------------------------------------------------------------------------- + +// Drawing required functions +//------------------------------------------------------------------------------- +static void DrawRectangle(int x, int y, int width, int height, Color color); // -- GuiDrawRectangle() +static void DrawRectangleGradientEx(Rectangle rec, Color col1, Color col2, Color col3, Color col4); // -- GuiColorPicker() +//------------------------------------------------------------------------------- + +// Text required functions +//------------------------------------------------------------------------------- +static Font GetFontDefault(void); // -- GuiLoadStyleDefault() +static Font LoadFontEx(const char *fileName, int fontSize, int *codepoints, int codepointCount); // -- GuiLoadStyle(), load font + +static Texture2D LoadTextureFromImage(Image image); // -- GuiLoadStyle(), required to load texture from embedded font atlas image +static void SetShapesTexture(Texture2D tex, Rectangle rec); // -- GuiLoadStyle(), required to set shapes rec to font white rec (optimization) + +static char *LoadFileText(const char *fileName); // -- GuiLoadStyle(), required to load charset data +static void UnloadFileText(char *text); // -- GuiLoadStyle(), required to unload charset data + +static const char *GetDirectoryPath(const char *filePath); // -- GuiLoadStyle(), required to find charset/font file from text .rgs + +static int *LoadCodepoints(const char *text, int *count); // -- GuiLoadStyle(), required to load required font codepoints list +static void UnloadCodepoints(int *codepoints); // -- GuiLoadStyle(), required to unload codepoints list + +static unsigned char *DecompressData(const unsigned char *compData, int compDataSize, int *dataSize); // -- GuiLoadStyle() +//------------------------------------------------------------------------------- + +// raylib functions already implemented in raygui +//------------------------------------------------------------------------------- +static Color GetColor(int hexValue); // Returns a Color struct from hexadecimal value +static int ColorToInt(Color color); // Returns hexadecimal value for a Color +static bool CheckCollisionPointRec(Vector2 point, Rectangle rec); // Check if point is inside rectangle +static const char *TextFormat(const char *text, ...); // Formatting of text with variables to 'embed' +static char **TextSplit(const char *text, char delimiter, int *count); // Split text into multiple strings +static int TextToInteger(const char *text); // Get integer value from text +static float TextToFloat(const char *text); // Get float value from text + +static int GetCodepointNext(const char *text, int *codepointSize); // Get next codepoint in a UTF-8 encoded text +static const char *CodepointToUTF8(int codepoint, int *byteSize); // Encode codepoint into UTF-8 text (char array size returned as parameter) + +static void DrawRectangleGradientV(int posX, int posY, int width, int height, Color color1, Color color2); // Draw rectangle vertical gradient +//------------------------------------------------------------------------------- + +#endif // RAYGUI_STANDALONE + +//---------------------------------------------------------------------------------- +// Module Internal Functions Declaration +//---------------------------------------------------------------------------------- +static Rectangle GetTextBounds(int control, Rectangle bounds); // Get text bounds considering control bounds +static const char *GetTextIcon(const char *text, int *iconId); // Get text icon if provided and move text cursor + +static void GuiDrawText(const char *text, Rectangle textBounds, int alignment, Color tint); // Gui draw text using default font +static void GuiDrawRectangle(Rectangle rec, int borderWidth, Color borderColor, Color color); // Gui draw rectangle using default raygui style + +static char **GuiTextSplit(const char *text, char delimiter, int *count, int *textRow); // Split controls text into multiple strings +static Vector3 ConvertHSVtoRGB(Vector3 hsv); // Convert color data from HSV to RGB +static Vector3 ConvertRGBtoHSV(Vector3 rgb); // Convert color data from RGB to HSV + +static int GuiScrollBar(Rectangle bounds, int value, int minValue, int maxValue); // Scroll bar control, used by GuiScrollPanel() +static void GuiTooltip(Rectangle controlRec); // Draw tooltip using control rec position + +static Color GuiFade(Color color, float alpha); // Fade color by an alpha factor + +//---------------------------------------------------------------------------------- +// Gui Setup Functions Definition +//---------------------------------------------------------------------------------- +// Enable gui global state +// NOTE: Checking for STATE_DISABLED to avoid messing custom global state setups +void GuiEnable(void) { if (guiState == STATE_DISABLED) guiState = STATE_NORMAL; } + +// Disable gui global state +// NOTE: Checking for STATE_NORMAL to avoid messing custom global state setups +void GuiDisable(void) { if (guiState == STATE_NORMAL) guiState = STATE_DISABLED; } + +// Lock gui global state +void GuiLock(void) { guiLocked = true; } + +// Unlock gui global state +void GuiUnlock(void) { guiLocked = false; } + +// Check if gui is locked (global state) +bool GuiIsLocked(void) { return guiLocked; } + +// Set gui controls alpha global state +void GuiSetAlpha(float alpha) +{ + if (alpha < 0.0f) alpha = 0.0f; + else if (alpha > 1.0f) alpha = 1.0f; + + guiAlpha = alpha; +} + +// Set gui state (global state) +void GuiSetState(int state) { guiState = (GuiState)state; } + +// Get gui state (global state) +int GuiGetState(void) { return guiState; } + +// Set custom gui font +// NOTE: Font loading/unloading is external to raygui +void GuiSetFont(Font font) +{ + if (font.texture.id > 0) + { + // NOTE: If a font is tried to be set but default style has not been lazily loaded first, + // it will be overwritten, so default style loading needs to be forced first + if (!guiStyleLoaded) GuiLoadStyleDefault(); + + guiFont = font; + } +} + +// Get custom gui font +Font GuiGetFont(void) +{ + return guiFont; +} + +// Set control style property value +void GuiSetStyle(int control, int property, int value) +{ + if (!guiStyleLoaded) GuiLoadStyleDefault(); + guiStyle[control*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED) + property] = value; + + // Default properties are propagated to all controls + if ((control == 0) && (property < RAYGUI_MAX_PROPS_BASE)) + { + for (int i = 1; i < RAYGUI_MAX_CONTROLS; i++) guiStyle[i*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED) + property] = value; + } +} + +// Get control style property value +int GuiGetStyle(int control, int property) +{ + if (!guiStyleLoaded) GuiLoadStyleDefault(); + return guiStyle[control*(RAYGUI_MAX_PROPS_BASE + RAYGUI_MAX_PROPS_EXTENDED) + property]; +} + +//---------------------------------------------------------------------------------- +// Gui Controls Functions Definition +//---------------------------------------------------------------------------------- + +// Window Box control +int GuiWindowBox(Rectangle bounds, const char *title) +{ + // Window title bar height (including borders) + // NOTE: This define is also used by GuiMessageBox() and GuiTextInputBox() + #if !defined(RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT) + #define RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT 24 + #endif + + #if !defined(RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT) + #define RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT 18 + #endif + + int result = 0; + //GuiState state = guiState; + + int statusBarHeight = RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT; + int statusBorderWidth = GuiGetStyle(STATUSBAR, BORDER_WIDTH); + + Rectangle statusBar = { bounds.x, bounds.y, bounds.width, (float)statusBarHeight }; + if (bounds.height < statusBarHeight*2.0f) bounds.height = statusBarHeight*2.0f; + + const float vPadding = statusBarHeight/2.0f - RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT/2.0f; + Rectangle windowPanel = { bounds.x, bounds.y + (float)statusBarHeight - (float)statusBorderWidth, bounds.width, bounds.height - (float)statusBarHeight + (float)statusBorderWidth }; + Rectangle closeButtonRec = { statusBar.x + statusBar.width - (float)statusBorderWidth - RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT - vPadding, + statusBar.y + vPadding, RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT, RAYGUI_WINDOWBOX_CLOSEBUTTON_HEIGHT }; + + // Update control + //-------------------------------------------------------------------- + // NOTE: Logic is directly managed by button + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiPanel(windowPanel, NULL); // Draw window base + GuiStatusBar(statusBar, title); // Draw window header as status bar + + // Draw window close button + int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH); + int tempTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT); + GuiSetStyle(BUTTON, BORDER_WIDTH, 1); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); +#if defined(RAYGUI_NO_ICONS) + result = GuiButton(closeButtonRec, "x"); +#else + result = GuiButton(closeButtonRec, GuiIconText(ICON_CROSS_SMALL, NULL)); +#endif + GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlignment); + //-------------------------------------------------------------------- + + return result; // Window close button clicked: result = 1 +} + +// Group Box control with text name +int GuiGroupBox(Rectangle bounds, const char *text) +{ + #if !defined(RAYGUI_GROUPBOX_LINE_THICK) + #define RAYGUI_GROUPBOX_LINE_THICK 1 + #endif + + int result = 0; + GuiState state = guiState; + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y, RAYGUI_GROUPBOX_LINE_THICK, bounds.height }, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR))); + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height - 1, bounds.width, RAYGUI_GROUPBOX_LINE_THICK }, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR))); + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - 1, bounds.y, RAYGUI_GROUPBOX_LINE_THICK, bounds.height }, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR))); + + GuiLine(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y - GuiGetStyle(DEFAULT, TEXT_SIZE)/2, bounds.width, (float)GuiGetStyle(DEFAULT, TEXT_SIZE) }, text); + //-------------------------------------------------------------------- + + return result; +} + +// Line control +int GuiLine(Rectangle bounds, const char *text) +{ + #if !defined(RAYGUI_LINE_MARGIN_TEXT) + #define RAYGUI_LINE_MARGIN_TEXT 12 + #endif + #if !defined(RAYGUI_LINE_TEXT_PADDING) + #define RAYGUI_LINE_TEXT_PADDING 4 + #endif + + int result = 0; + GuiState state = guiState; + + Color color = GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR)); + + // Draw control + //-------------------------------------------------------------------- + if (text == NULL) GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height/2, bounds.width, 1 }, 0, BLANK, color); + else + { + Rectangle textBounds = { 0 }; + textBounds.width = (float)GuiGetTextWidth(text) + 2; + textBounds.height = bounds.height; + textBounds.x = bounds.x + RAYGUI_LINE_MARGIN_TEXT; + textBounds.y = bounds.y; + + // Draw line with embedded text label: "--- text --------------" + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height/2, RAYGUI_LINE_MARGIN_TEXT - RAYGUI_LINE_TEXT_PADDING, 1 }, 0, BLANK, color); + GuiDrawText(text, textBounds, TEXT_ALIGN_LEFT, color); + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + 12 + textBounds.width + 4, bounds.y + bounds.height/2, bounds.width - textBounds.width - RAYGUI_LINE_MARGIN_TEXT - RAYGUI_LINE_TEXT_PADDING, 1 }, 0, BLANK, color); + } + //-------------------------------------------------------------------- + + return result; +} + +// Panel control +int GuiPanel(Rectangle bounds, const char *text) +{ + #if !defined(RAYGUI_PANEL_BORDER_WIDTH) + #define RAYGUI_PANEL_BORDER_WIDTH 1 + #endif + + int result = 0; + GuiState state = guiState; + + // Text will be drawn as a header bar (if provided) + Rectangle statusBar = { bounds.x, bounds.y, bounds.width, (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT }; + if ((text != NULL) && (bounds.height < RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT*2.0f)) bounds.height = RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT*2.0f; + + if (text != NULL) + { + // Move panel bounds after the header bar + bounds.y += (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - 1; + bounds.height -= (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - 1; + } + + // Draw control + //-------------------------------------------------------------------- + if (text != NULL) GuiStatusBar(statusBar, text); // Draw panel header as status bar + + GuiDrawRectangle(bounds, RAYGUI_PANEL_BORDER_WIDTH, GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BORDER_COLOR_DISABLED : (int)LINE_COLOR)), + GetColor(GuiGetStyle(DEFAULT, (state == STATE_DISABLED)? (int)BASE_COLOR_DISABLED : (int)BACKGROUND_COLOR))); + //-------------------------------------------------------------------- + + return result; +} + +// Tab Bar control +// NOTE: Using GuiToggle() for the TABS +int GuiTabBar(Rectangle bounds, char **text, int count, int *active) +{ + #if !defined(RAYGUI_TABBAR_ITEM_WIDTH) + #define RAYGUI_TABBAR_ITEM_WIDTH 148 + #endif + + int result = -1; + //GuiState state = guiState; + + Rectangle tabBounds = { bounds.x, bounds.y, RAYGUI_TABBAR_ITEM_WIDTH, bounds.height }; + + if (*active < 0) *active = 0; + else if (*active > count - 1) *active = count - 1; + + int offsetX = 0; // Required in case tabs go out of screen + offsetX = (*active*RAYGUI_TABBAR_ITEM_WIDTH) - GetScreenWidth(); + if (offsetX < 0) offsetX = 0; + + bool toggle = false; // Required for individual toggles + + // Draw control + //-------------------------------------------------------------------- + for (int i = 0; i < count; i++) + { + tabBounds.x = bounds.x + (RAYGUI_TABBAR_ITEM_WIDTH + 4)*i + offsetX; + + if (tabBounds.x < GetScreenWidth()) + { + // Draw tabs as toggle controls + int textAlignment = GuiGetStyle(TOGGLE, TEXT_ALIGNMENT); + int textPadding = GuiGetStyle(TOGGLE, TEXT_PADDING); + GuiSetStyle(TOGGLE, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + GuiSetStyle(TOGGLE, TEXT_PADDING, 8); + + if (i == (*active)) + { + toggle = true; + GuiToggle(tabBounds, text[i], &toggle); + } + else + { + toggle = false; + GuiToggle(tabBounds, text[i], &toggle); + if (toggle) *active = i; + } + + // Close tab with middle mouse button pressed + if (CheckCollisionPointRec(GUI_POINTER_POSITION, tabBounds) && IsMouseButtonPressed(MOUSE_MIDDLE_BUTTON)) result = i; + + GuiSetStyle(TOGGLE, TEXT_PADDING, textPadding); + GuiSetStyle(TOGGLE, TEXT_ALIGNMENT, textAlignment); + + // Draw tab close button + // NOTE: Only draw close button for current tab: if (CheckCollisionPointRec(mousePosition, tabBounds)) + int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH); + int tempTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT); + GuiSetStyle(BUTTON, BORDER_WIDTH, 1); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); +#if defined(RAYGUI_NO_ICONS) + if (GuiButton(RAYGUI_CLITERAL(Rectangle){ tabBounds.x + tabBounds.width - 14 - 5, tabBounds.y + 5, 14, 14 }, "x")) result = i; +#else + if (GuiButton(RAYGUI_CLITERAL(Rectangle){ tabBounds.x + tabBounds.width - 14 - 5, tabBounds.y + 5, 14, 14 }, GuiIconText(ICON_CROSS_SMALL, NULL))) result = i; +#endif + GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlignment); + } + } + + // Draw tab-bar bottom line + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height - 1, bounds.width, 1 }, 0, BLANK, GetColor(GuiGetStyle(TOGGLE, BORDER_COLOR_NORMAL))); + //-------------------------------------------------------------------- + + return result; // Return as result the current TAB closing requested +} + +// Scroll Panel control +int GuiScrollPanel(Rectangle bounds, const char *text, Rectangle content, Vector2 *scroll, Rectangle *view) +{ + #define RAYGUI_MIN_SCROLLBAR_WIDTH 40 + #define RAYGUI_MIN_SCROLLBAR_HEIGHT 40 + #define RAYGUI_MIN_MOUSE_WHEEL_SPEED 20 + + int result = 0; + GuiState state = guiState; + + Rectangle temp = { 0 }; + if (view == NULL) view = &temp; + + Vector2 scrollPos = { 0.0f, 0.0f }; + if (scroll != NULL) scrollPos = *scroll; + + // Text will be drawn as a header bar (if provided) + Rectangle statusBar = { bounds.x, bounds.y, bounds.width, (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT }; + if (bounds.height < RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT*2.0f) bounds.height = RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT*2.0f; + + if (text != NULL) + { + // Move panel bounds after the header bar + bounds.y += (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - 1; + bounds.height -= (float)RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT + 1; + } + + bool hasHorizontalScrollBar = (content.width > bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH))? true : false; + bool hasVerticalScrollBar = (content.height > bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH))? true : false; + + // Recheck to account for the other scrollbar being visible + if (!hasHorizontalScrollBar) hasHorizontalScrollBar = (hasVerticalScrollBar && (content.width > (bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH))))? true : false; + if (!hasVerticalScrollBar) hasVerticalScrollBar = (hasHorizontalScrollBar && (content.height > (bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH))))? true : false; + + int horizontalScrollBarWidth = hasHorizontalScrollBar? GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH) : 0; + int verticalScrollBarWidth = hasVerticalScrollBar? GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH) : 0; + Rectangle horizontalScrollBar = { + (float)((GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)bounds.x + verticalScrollBarWidth : (float)bounds.x) + GuiGetStyle(DEFAULT, BORDER_WIDTH), + (float)bounds.y + bounds.height - horizontalScrollBarWidth - GuiGetStyle(DEFAULT, BORDER_WIDTH), + (float)bounds.width - verticalScrollBarWidth - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH), + (float)horizontalScrollBarWidth + }; + Rectangle verticalScrollBar = { + (float)((GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)bounds.x + GuiGetStyle(DEFAULT, BORDER_WIDTH) : (float)bounds.x + bounds.width - verticalScrollBarWidth - GuiGetStyle(DEFAULT, BORDER_WIDTH)), + (float)bounds.y + GuiGetStyle(DEFAULT, BORDER_WIDTH), + (float)verticalScrollBarWidth, + (float)bounds.height - horizontalScrollBarWidth - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) + }; + + // Make sure scroll bars have a minimum width/height + if (horizontalScrollBar.width < RAYGUI_MIN_SCROLLBAR_WIDTH) horizontalScrollBar.width = RAYGUI_MIN_SCROLLBAR_WIDTH; + if (verticalScrollBar.height < RAYGUI_MIN_SCROLLBAR_HEIGHT) verticalScrollBar.height = RAYGUI_MIN_SCROLLBAR_HEIGHT; + + // Calculate view area (area without the scrollbars) + *view = (GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? + RAYGUI_CLITERAL(Rectangle){ bounds.x + verticalScrollBarWidth + GuiGetStyle(DEFAULT, BORDER_WIDTH), bounds.y + GuiGetStyle(DEFAULT, BORDER_WIDTH), bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - verticalScrollBarWidth, bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - horizontalScrollBarWidth } : + RAYGUI_CLITERAL(Rectangle){ bounds.x + GuiGetStyle(DEFAULT, BORDER_WIDTH), bounds.y + GuiGetStyle(DEFAULT, BORDER_WIDTH), bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - verticalScrollBarWidth, bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - horizontalScrollBarWidth }; + + // Clip view area to the actual content size + if (view->width > content.width) view->width = content.width; + if (view->height > content.height) view->height = content.height; + + float horizontalMin = hasHorizontalScrollBar? ((GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)-verticalScrollBarWidth : 0) - (float)GuiGetStyle(DEFAULT, BORDER_WIDTH) : (((float)GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)-verticalScrollBarWidth : 0) - (float)GuiGetStyle(DEFAULT, BORDER_WIDTH); + float horizontalMax = hasHorizontalScrollBar? content.width - bounds.width + (float)verticalScrollBarWidth + GuiGetStyle(DEFAULT, BORDER_WIDTH) - (((float)GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (float)verticalScrollBarWidth : 0) : (float)-GuiGetStyle(DEFAULT, BORDER_WIDTH); + float verticalMin = -(float)GuiGetStyle(DEFAULT, BORDER_WIDTH); + float verticalMax = hasVerticalScrollBar? content.height - bounds.height + (float)horizontalScrollBarWidth + (float)GuiGetStyle(DEFAULT, BORDER_WIDTH) : (float)-GuiGetStyle(DEFAULT, BORDER_WIDTH); + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + // Check button state + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else state = STATE_FOCUSED; + +#if defined(SUPPORT_SCROLLBAR_KEY_INPUT) + if (hasHorizontalScrollBar) + { + if (GUI_KEY_DOWN(KEY_RIGHT)) scrollPos.x -= GuiGetStyle(SCROLLBAR, SCROLL_SPEED); + if (GUI_KEY_DOWN(KEY_LEFT)) scrollPos.x += GuiGetStyle(SCROLLBAR, SCROLL_SPEED); + } + + if (hasVerticalScrollBar) + { + if (GUI_KEY_DOWN(KEY_DOWN)) scrollPos.y -= GuiGetStyle(SCROLLBAR, SCROLL_SPEED); + if (GUI_KEY_DOWN(KEY_UP)) scrollPos.y += GuiGetStyle(SCROLLBAR, SCROLL_SPEED); + } +#endif + float scrollDelta = GUI_SCROLL_DELTA; + + // Set scrolling speed with mouse wheel based on ratio between bounds and content + Vector2 scrollSpeed = { content.width/bounds.width, content.height/bounds.height }; + if (scrollSpeed.x < RAYGUI_MIN_MOUSE_WHEEL_SPEED) scrollSpeed.x = RAYGUI_MIN_MOUSE_WHEEL_SPEED; + if (scrollSpeed.y < RAYGUI_MIN_MOUSE_WHEEL_SPEED) scrollSpeed.y = RAYGUI_MIN_MOUSE_WHEEL_SPEED; + + // Horizontal and vertical scrolling with mouse wheel + if (hasHorizontalScrollBar && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_LEFT_SHIFT))) scrollPos.x += scrollDelta*scrollSpeed.x; + else scrollPos.y += scrollDelta*scrollSpeed.y; // Vertical scroll + } + } + + // Normalize scroll values + if (scrollPos.x > -horizontalMin) scrollPos.x = -horizontalMin; + if (scrollPos.x < -horizontalMax) scrollPos.x = -horizontalMax; + if (scrollPos.y > -verticalMin) scrollPos.y = -verticalMin; + if (scrollPos.y < -verticalMax) scrollPos.y = -verticalMax; + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (text != NULL) GuiStatusBar(statusBar, text); // Draw panel header as status bar + + GuiDrawRectangle(bounds, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR))); // Draw background + + // Save size of the scrollbar slider + const int slider = GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE); + + // Draw horizontal scrollbar if visible + if (hasHorizontalScrollBar) + { + // Change scrollbar slider size to show the diff in size between the content width and the widget width + GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, (int)(((bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - verticalScrollBarWidth)/(int)content.width)*((int)bounds.width - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - verticalScrollBarWidth))); + scrollPos.x = (float)-GuiScrollBar(horizontalScrollBar, (int)-scrollPos.x, (int)horizontalMin, (int)horizontalMax); + } + else scrollPos.x = 0.0f; + + // Draw vertical scrollbar if visible + if (hasVerticalScrollBar) + { + // Change scrollbar slider size to show the diff in size between the content height and the widget height + GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, (int)(((bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - horizontalScrollBarWidth)/(int)content.height)*((int)bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) - horizontalScrollBarWidth))); + scrollPos.y = (float)-GuiScrollBar(verticalScrollBar, (int)-scrollPos.y, (int)verticalMin, (int)verticalMax); + } + else scrollPos.y = 0.0f; + + // Draw detail corner rectangle if both scroll bars are visible + if (hasHorizontalScrollBar && hasVerticalScrollBar) + { + Rectangle corner = { (GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE)? (bounds.x + GuiGetStyle(DEFAULT, BORDER_WIDTH) + 2) : (horizontalScrollBar.x + horizontalScrollBar.width + 2), verticalScrollBar.y + verticalScrollBar.height + 2, (float)horizontalScrollBarWidth - 4, (float)verticalScrollBarWidth - 4 }; + GuiDrawRectangle(corner, 0, BLANK, GetColor(GuiGetStyle(LISTVIEW, TEXT + (state*3)))); + } + + // Draw scrollbar lines depending on current state + GuiDrawRectangle(bounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER + (state*3))), BLANK); + + // Set scrollbar slider size back to the way it was before + GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, slider); + //-------------------------------------------------------------------- + + if (scroll != NULL) *scroll = scrollPos; + + return result; +} + +// Label control +int GuiLabel(Rectangle bounds, const char *text) +{ + int result = 0; + GuiState state = guiState; + + // Update control + //-------------------------------------------------------------------- + //... + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawText(text, GetTextBounds(LABEL, bounds), GuiGetStyle(LABEL, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + //-------------------------------------------------------------------- + + return result; +} + +// Button control, returns true when clicked +int GuiButton(Rectangle bounds, const char *text) +{ + int result = 0; + GuiState state = guiState; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + // Check button state + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else state = STATE_FOCUSED; + + if (GUI_BUTTON_RELEASED) result = 1; + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(bounds, GuiGetStyle(BUTTON, BORDER_WIDTH), GetColor(GuiGetStyle(BUTTON, BORDER + (state*3))), GetColor(GuiGetStyle(BUTTON, BASE + (state*3)))); + GuiDrawText(text, GetTextBounds(BUTTON, bounds), GuiGetStyle(BUTTON, TEXT_ALIGNMENT), GetColor(GuiGetStyle(BUTTON, TEXT + (state*3)))); + + if (state == STATE_FOCUSED) GuiTooltip(bounds); + //------------------------------------------------------------------ + + return result; // Button pressed: result = 1 +} + +// Label button control +int GuiLabelButton(Rectangle bounds, const char *text) +{ + GuiState state = guiState; + bool pressed = false; + + // NOTE: Force bounds.width to be all text + float textWidth = (float)GuiGetTextWidth(text); + if ((bounds.width - 2*GuiGetStyle(LABEL, BORDER_WIDTH) - 2*GuiGetStyle(LABEL, TEXT_PADDING)) < textWidth) bounds.width = textWidth + 2*GuiGetStyle(LABEL, BORDER_WIDTH) + 2*GuiGetStyle(LABEL, TEXT_PADDING) + 2; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + // Check checkbox state + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else state = STATE_FOCUSED; + + if (GUI_BUTTON_RELEASED) pressed = true; + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawText(text, GetTextBounds(LABEL, bounds), GuiGetStyle(LABEL, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + //-------------------------------------------------------------------- + + return pressed; +} + +// Toggle Button control +int GuiToggle(Rectangle bounds, const char *text, bool *active) +{ + int result = 0; + GuiState state = guiState; + + bool temp = false; + if (active == NULL) active = &temp; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + // Check toggle button state + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else if (GUI_BUTTON_RELEASED) + { + state = STATE_NORMAL; + *active = !(*active); + } + else state = STATE_FOCUSED; + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (state == STATE_NORMAL) + { + GuiDrawRectangle(bounds, GuiGetStyle(TOGGLE, BORDER_WIDTH), GetColor(GuiGetStyle(TOGGLE, ((*active)? BORDER_COLOR_PRESSED : (BORDER + state*3)))), GetColor(GuiGetStyle(TOGGLE, ((*active)? BASE_COLOR_PRESSED : (BASE + state*3))))); + GuiDrawText(text, GetTextBounds(TOGGLE, bounds), GuiGetStyle(TOGGLE, TEXT_ALIGNMENT), GetColor(GuiGetStyle(TOGGLE, ((*active)? TEXT_COLOR_PRESSED : (TEXT + state*3))))); + } + else + { + GuiDrawRectangle(bounds, GuiGetStyle(TOGGLE, BORDER_WIDTH), GetColor(GuiGetStyle(TOGGLE, BORDER + state*3)), GetColor(GuiGetStyle(TOGGLE, BASE + state*3))); + GuiDrawText(text, GetTextBounds(TOGGLE, bounds), GuiGetStyle(TOGGLE, TEXT_ALIGNMENT), GetColor(GuiGetStyle(TOGGLE, TEXT + state*3))); + } + + if (state == STATE_FOCUSED) GuiTooltip(bounds); + //-------------------------------------------------------------------- + + return result; +} + +// Toggle Group control +int GuiToggleGroup(Rectangle bounds, const char *text, int *active) +{ + #if !defined(RAYGUI_TOGGLEGROUP_MAX_ITEMS) + #define RAYGUI_TOGGLEGROUP_MAX_ITEMS 32 + #endif + + int result = 0; + float initBoundsX = bounds.x; + + int temp = 0; + if (active == NULL) active = &temp; + + bool toggle = false; // Required for individual toggles + + // Get substrings items from text (items pointers) + int rows[RAYGUI_TOGGLEGROUP_MAX_ITEMS] = { 0 }; + int itemCount = 0; + char **items = GuiTextSplit(text, ';', &itemCount, rows); + + int prevRow = rows[0]; + + for (int i = 0; i < itemCount; i++) + { + if (prevRow != rows[i]) + { + bounds.x = initBoundsX; + bounds.y += (bounds.height + GuiGetStyle(TOGGLE, GROUP_PADDING)); + prevRow = rows[i]; + } + + if (i == (*active)) + { + toggle = true; + GuiToggle(bounds, items[i], &toggle); + } + else + { + toggle = false; + GuiToggle(bounds, items[i], &toggle); + if (toggle) *active = i; + } + + bounds.x += (bounds.width + GuiGetStyle(TOGGLE, GROUP_PADDING)); + } + + return result; +} + +// Toggle Slider control extended +int GuiToggleSlider(Rectangle bounds, const char *text, int *active) +{ + int result = 0; + GuiState state = guiState; + + int temp = 0; + if (active == NULL) active = &temp; + + //bool toggle = false; // Required for individual toggles + + // Get substrings items from text (items pointers) + int itemCount = 0; + char **items = NULL; + + if (text != NULL) items = GuiTextSplit(text, ';', &itemCount, NULL); + + Rectangle slider = { + 0, // Calculated later depending on the active toggle + bounds.y + GuiGetStyle(SLIDER, BORDER_WIDTH) + GuiGetStyle(SLIDER, SLIDER_PADDING), + (bounds.width - 2*GuiGetStyle(SLIDER, BORDER_WIDTH) - (itemCount + 1)*GuiGetStyle(SLIDER, SLIDER_PADDING))/itemCount, + bounds.height - 2*GuiGetStyle(SLIDER, BORDER_WIDTH) - 2*GuiGetStyle(SLIDER, SLIDER_PADDING) }; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else if (GUI_BUTTON_RELEASED) + { + state = STATE_PRESSED; + (*active)++; + result = 1; + } + else state = STATE_FOCUSED; + } + + if ((*active) && (state != STATE_FOCUSED)) state = STATE_PRESSED; + } + + if (*active >= itemCount) *active = 0; + slider.x = bounds.x + GuiGetStyle(SLIDER, BORDER_WIDTH) + (*active + 1)*GuiGetStyle(SLIDER, SLIDER_PADDING) + (*active)*slider.width; + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(bounds, GuiGetStyle(SLIDER, BORDER_WIDTH), GetColor(GuiGetStyle(TOGGLE, BORDER + (state*3))), + GetColor(GuiGetStyle(TOGGLE, BASE_COLOR_NORMAL))); + + // Draw internal slider + if (state == STATE_NORMAL) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BASE_COLOR_PRESSED))); + else if (state == STATE_FOCUSED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BASE_COLOR_FOCUSED))); + else if (state == STATE_PRESSED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BASE_COLOR_PRESSED))); + + // Draw text in slider + if (text != NULL) + { + Rectangle textBounds = { 0 }; + textBounds.width = (float)GuiGetTextWidth(text); + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = slider.x + slider.width/2 - textBounds.width/2; + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + + GuiDrawText(items[*active], textBounds, GuiGetStyle(TOGGLE, TEXT_ALIGNMENT), Fade(GetColor(GuiGetStyle(TOGGLE, TEXT + (state*3))), guiAlpha)); + } + //-------------------------------------------------------------------- + + return result; +} + +// Check Box control, returns 1 when state changed +int GuiCheckBox(Rectangle bounds, const char *text, bool *checked) +{ + int result = 0; + GuiState state = guiState; + + bool temp = false; + if (checked == NULL) checked = &temp; + + Rectangle textBounds = { 0 }; + + if (text != NULL) + { + textBounds.width = (float)GuiGetTextWidth(text) + 2; + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = bounds.x + bounds.width + GuiGetStyle(CHECKBOX, TEXT_PADDING); + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + if (GuiGetStyle(CHECKBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT) textBounds.x = bounds.x - textBounds.width - GuiGetStyle(CHECKBOX, TEXT_PADDING); + } + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + Rectangle totalBounds = { + (GuiGetStyle(CHECKBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT)? textBounds.x : bounds.x, + bounds.y, + bounds.width + textBounds.width + GuiGetStyle(CHECKBOX, TEXT_PADDING), + bounds.height, + }; + + // Check checkbox state + if (CheckCollisionPointRec(mousePoint, totalBounds)) + { + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else state = STATE_FOCUSED; + + if (GUI_BUTTON_RELEASED) + { + *checked = !(*checked); + result = 1; + } + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(bounds, GuiGetStyle(CHECKBOX, BORDER_WIDTH), GetColor(GuiGetStyle(CHECKBOX, BORDER + (state*3))), BLANK); + + if (*checked) + { + Rectangle check = { bounds.x + GuiGetStyle(CHECKBOX, BORDER_WIDTH) + GuiGetStyle(CHECKBOX, CHECK_PADDING), + bounds.y + GuiGetStyle(CHECKBOX, BORDER_WIDTH) + GuiGetStyle(CHECKBOX, CHECK_PADDING), + bounds.width - 2*(GuiGetStyle(CHECKBOX, BORDER_WIDTH) + GuiGetStyle(CHECKBOX, CHECK_PADDING)), + bounds.height - 2*(GuiGetStyle(CHECKBOX, BORDER_WIDTH) + GuiGetStyle(CHECKBOX, CHECK_PADDING)) }; + GuiDrawRectangle(check, 0, BLANK, GetColor(GuiGetStyle(CHECKBOX, TEXT + state*3))); + } + + GuiDrawText(text, textBounds, (GuiGetStyle(CHECKBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT)? TEXT_ALIGN_LEFT : TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + //-------------------------------------------------------------------- + + return result; +} + +// Combo Box control +int GuiComboBox(Rectangle bounds, const char *text, int *active) +{ + int result = 0; + GuiState state = guiState; + + int temp = 0; + if (active == NULL) active = &temp; + + bounds.width -= (GuiGetStyle(COMBOBOX, COMBO_BUTTON_WIDTH) + GuiGetStyle(COMBOBOX, COMBO_BUTTON_SPACING)); + + Rectangle selector = { (float)bounds.x + bounds.width + GuiGetStyle(COMBOBOX, COMBO_BUTTON_SPACING), + (float)bounds.y, (float)GuiGetStyle(COMBOBOX, COMBO_BUTTON_WIDTH), (float)bounds.height }; + + // Get substrings items from text (items pointers, lengths and count) + int itemCount = 0; + char **items = GuiTextSplit(text, ';', &itemCount, NULL); + + if (*active < 0) *active = 0; + else if (*active > (itemCount - 1)) *active = itemCount - 1; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && (itemCount > 1) && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + if (CheckCollisionPointRec(mousePoint, bounds) || + CheckCollisionPointRec(mousePoint, selector)) + { + if (GUI_BUTTON_PRESSED) + { + *active += 1; + if (*active >= itemCount) *active = 0; // Cyclic combobox + } + + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else state = STATE_FOCUSED; + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + // Draw combo box main + GuiDrawRectangle(bounds, GuiGetStyle(COMBOBOX, BORDER_WIDTH), GetColor(GuiGetStyle(COMBOBOX, BORDER + (state*3))), GetColor(GuiGetStyle(COMBOBOX, BASE + (state*3)))); + GuiDrawText(items[*active], GetTextBounds(COMBOBOX, bounds), GuiGetStyle(COMBOBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(COMBOBOX, TEXT + (state*3)))); + + // Draw selector using a custom button + // NOTE: BORDER_WIDTH and TEXT_ALIGNMENT forced values + int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH); + int tempTextAlign = GuiGetStyle(BUTTON, TEXT_ALIGNMENT); + GuiSetStyle(BUTTON, BORDER_WIDTH, 1); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + + GuiButton(selector, TextFormat("%i/%i", *active + 1, itemCount)); + + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlign); + GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth); + //-------------------------------------------------------------------- + + return result; +} + +// Dropdown Box control +// NOTE: Returns mouse click +int GuiDropdownBox(Rectangle bounds, const char *text, int *active, bool editMode) +{ + int result = 0; + GuiState state = guiState; + + int temp = 0; + if (active == NULL) active = &temp; + + int itemSelected = *active; + int itemFocused = -1; + + int direction = 0; // Dropdown box open direction: down (default) + if (GuiGetStyle(DROPDOWNBOX, DROPDOWN_ROLL_UP) == 1) direction = 1; // Up + + // Get substrings items from text (items pointers, lengths and count) + int itemCount = 0; + char **items = GuiTextSplit(text, ';', &itemCount, NULL); + + Rectangle boundsOpen = bounds; + boundsOpen.height = (itemCount + 1)*(bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + if (direction == 1) boundsOpen.y -= itemCount*(bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)) + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING); + + Rectangle itemBounds = bounds; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && (editMode || !guiLocked) && (itemCount > 1) && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + if (editMode) + { + state = STATE_PRESSED; + + // Check if mouse has been pressed or released outside limits + if (!CheckCollisionPointRec(mousePoint, boundsOpen)) + { + if (GUI_BUTTON_PRESSED || GUI_BUTTON_RELEASED) result = 1; + } + + // Check if already selected item has been pressed again + if (CheckCollisionPointRec(mousePoint, bounds) && GUI_BUTTON_PRESSED) result = 1; + + // Check focused and selected item + for (int i = 0; i < itemCount; i++) + { + // Update item rectangle y position for next item + if (direction == 0) itemBounds.y += (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + else itemBounds.y -= (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + + if (CheckCollisionPointRec(mousePoint, itemBounds)) + { + itemFocused = i; + if (GUI_BUTTON_RELEASED) + { + itemSelected = i; + result = 1; // Item selected + } + break; + } + } + + itemBounds = bounds; + } + else + { + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_PRESSED) + { + result = 1; + state = STATE_PRESSED; + } + else state = STATE_FOCUSED; + } + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (editMode) GuiPanel(boundsOpen, NULL); + + GuiDrawRectangle(bounds, GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH), GetColor(GuiGetStyle(DROPDOWNBOX, BORDER + state*3)), GetColor(GuiGetStyle(DROPDOWNBOX, BASE + state*3))); + GuiDrawText(items[itemSelected], GetTextBounds(DROPDOWNBOX, bounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + state*3))); + + if (editMode) + { + // Draw visible items + for (int i = 0; i < itemCount; i++) + { + // Update item rectangle y position for next item + if (direction == 0) itemBounds.y += (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + else itemBounds.y -= (bounds.height + GuiGetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING)); + + if (i == itemSelected) + { + GuiDrawRectangle(itemBounds, GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH), GetColor(GuiGetStyle(DROPDOWNBOX, BORDER_COLOR_PRESSED)), GetColor(GuiGetStyle(DROPDOWNBOX, BASE_COLOR_PRESSED))); + GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_PRESSED))); + } + else if (i == itemFocused) + { + GuiDrawRectangle(itemBounds, GuiGetStyle(DROPDOWNBOX, BORDER_WIDTH), GetColor(GuiGetStyle(DROPDOWNBOX, BORDER_COLOR_FOCUSED)), GetColor(GuiGetStyle(DROPDOWNBOX, BASE_COLOR_FOCUSED))); + GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_FOCUSED))); + } + else GuiDrawText(items[i], GetTextBounds(DROPDOWNBOX, itemBounds), GuiGetStyle(DROPDOWNBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(DROPDOWNBOX, TEXT_COLOR_NORMAL))); + } + } + + if (!GuiGetStyle(DROPDOWNBOX, DROPDOWN_ARROW_HIDDEN)) + { + // Draw arrows (using icon if available) +#if defined(RAYGUI_NO_ICONS) + GuiDrawText("v", RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - GuiGetStyle(DROPDOWNBOX, ARROW_PADDING), bounds.y + bounds.height/2 - 2, 10, 10 }, + TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3)))); +#else + GuiDrawText(direction? GuiIconText(ICON_ARROW_UP_FILL, NULL) : GuiIconText(ICON_ARROW_DOWN_FILL, NULL), + RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - GuiGetStyle(DROPDOWNBOX, ARROW_PADDING), bounds.y + bounds.height/2 - 6, 10, 10 }, + TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3)))); // ICON_ARROW_DOWN_FILL +#endif + } + //-------------------------------------------------------------------- + + *active = itemSelected; + + // TODO: Use result to return more internal states: mouse-press out-of-bounds, mouse-press over selected-item... + return result; // Mouse click: result = 1 +} + +// Text Box control +// NOTE: Returns true on ENTER pressed (useful for data validation) +int GuiTextBox(Rectangle bounds, char *text, int textSize, bool editMode) +{ + #if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN) + #define RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN 20 // Frames to wait for autocursor movement + #endif + #if !defined(RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) + #define RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY 1 // Frames delay for autocursor movement + #endif + + int result = 0; + GuiState state = guiState; + + bool multiline = false; // TODO: Consider multiline text input + int wrapMode = GuiGetStyle(DEFAULT, TEXT_WRAP_MODE); + + Rectangle textBounds = GetTextBounds(TEXTBOX, bounds); + int textLength = (text != NULL)? (int)strlen(text) : 0; // Get current text length + int thisCursorIndex = textBoxCursorIndex; + if (thisCursorIndex > textLength) thisCursorIndex = textLength; + int textWidth = GuiGetTextWidth(text) - GuiGetTextWidth(text + thisCursorIndex); + int textIndexOffset = 0; // Text index offset to start drawing in the box + + // Cursor rectangle + // NOTE: Position X value should be updated + Rectangle cursor = { + textBounds.x + textWidth + GuiGetStyle(DEFAULT, TEXT_SPACING), + textBounds.y + textBounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE), + 2, + (float)GuiGetStyle(DEFAULT, TEXT_SIZE)*2 + }; + + if (cursor.height >= bounds.height) cursor.height = bounds.height - GuiGetStyle(TEXTBOX, BORDER_WIDTH)*2; + if (cursor.y < (bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH))) cursor.y = bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH); + + // Mouse cursor rectangle + // NOTE: Initialized outside of screen + Rectangle mouseCursor = cursor; + mouseCursor.x = -1; + mouseCursor.width = 1; + + // Blink-cursor frame counter + //if (!autoCursorMode) blinkCursorFrameCounter++; + //else blinkCursorFrameCounter = 0; + + // Update control + //-------------------------------------------------------------------- + // WARNING: Text editing is only supported under certain conditions: + if ((state != STATE_DISABLED) && // Control not disabled + !GuiGetStyle(TEXTBOX, TEXT_READONLY) && // TextBox not on read-only mode + !guiLocked && // Gui not locked + !guiControlExclusiveMode && // No gui slider on dragging + (wrapMode == TEXT_WRAP_NONE)) // No wrap mode + { + Vector2 mousePosition = GUI_POINTER_POSITION; + + if (editMode) + { + // GLOBAL: Auto-cursor movement logic + // NOTE: Keystrokes are handled repeatedly when button is held down for some time + if (GUI_KEY_DOWN(KEY_LEFT) || GUI_KEY_DOWN(KEY_RIGHT) || GUI_KEY_DOWN(KEY_UP) || GUI_KEY_DOWN(KEY_DOWN) || GUI_KEY_DOWN(KEY_BACKSPACE) || GUI_KEY_DOWN(KEY_DELETE)) autoCursorCounter++; + else autoCursorCounter = 0; + + bool autoCursorShouldTrigger = (autoCursorCounter > RAYGUI_TEXTBOX_AUTO_CURSOR_COOLDOWN) && ((autoCursorCounter % RAYGUI_TEXTBOX_AUTO_CURSOR_DELAY) == 0); + + state = STATE_PRESSED; + + if (textBoxCursorIndex > textLength) textBoxCursorIndex = textLength; + + // If text does not fit in the textbox and current cursor position is out of bounds, + // adding an index offset to text for drawing only what requires depending on cursor + while (textWidth >= textBounds.width) + { + int nextCodepointSize = 0; + GetCodepointNext(text + textIndexOffset, &nextCodepointSize); + + textIndexOffset += nextCodepointSize; + + textWidth = GuiGetTextWidth(text + textIndexOffset) - GuiGetTextWidth(text + textBoxCursorIndex); + } + + int codepoint = GUI_INPUT_KEY; // Get Unicode codepoint + if (multiline && GUI_KEY_PRESSED(KEY_ENTER)) codepoint = (int)'\n'; + + // Encode codepoint as UTF-8 + int codepointSize = 0; + const char *charEncoded = CodepointToUTF8(codepoint, &codepointSize); + + // Handle text paste action + if (GUI_KEY_PRESSED(KEY_V) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL))) + { + const char *pasteText = GetClipboardText(); + if (pasteText != NULL) + { + int pasteLength = 0; + int pasteCodepoint; + int pasteCodepointSize; + + // Count how many codepoints to copy, stopping at the first unwanted control character + while (true) + { + pasteCodepoint = GetCodepointNext(pasteText + pasteLength, &pasteCodepointSize); + if (textLength + pasteLength + pasteCodepointSize >= textSize) break; + if (!(multiline && (pasteCodepoint == (int)'\n')) && !(pasteCodepoint >= 32)) break; + pasteLength += pasteCodepointSize; + } + + if (pasteLength > 0) + { + // Move forward data from cursor position + for (int i = textLength + pasteLength; i > textBoxCursorIndex; i--) text[i] = text[i - pasteLength]; + + // Paste data in at cursor + for (int i = 0; i < pasteLength; i++) text[textBoxCursorIndex + i] = pasteText[i]; + + textBoxCursorIndex += pasteLength; + textLength += pasteLength; + text[textLength] = '\0'; + } + } + } + else if (((multiline && (codepoint == (int)'\n')) || (codepoint >= 32)) && ((textLength + codepointSize) < textSize)) + { + // Adding codepoint to text, at current cursor position + + // Move forward data from cursor position + for (int i = (textLength + codepointSize); i > textBoxCursorIndex; i--) text[i] = text[i - codepointSize]; + + // Add new codepoint in current cursor position + for (int i = 0; i < codepointSize; i++) text[textBoxCursorIndex + i] = charEncoded[i]; + + textBoxCursorIndex += codepointSize; + textLength += codepointSize; + + // Make sure text last character is EOL + text[textLength] = '\0'; + } + + // Move cursor to start + if ((textLength > 0) && GUI_KEY_PRESSED(KEY_HOME)) textBoxCursorIndex = 0; + + // Move cursor to end + if ((textLength > textBoxCursorIndex) && GUI_KEY_PRESSED(KEY_END)) textBoxCursorIndex = textLength; + + // Delete related codepoints from text, after current cursor position + if ((textLength > textBoxCursorIndex) && GUI_KEY_PRESSED(KEY_DELETE) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL))) + { + int offset = textBoxCursorIndex; + int accCodepointSize = 0; + int nextCodepointSize; + int nextCodepoint; + + // Check characters of the same type to delete (either ASCII punctuation or anything non-whitespace) + // Not using isalnum() since it only works on ASCII characters + nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize); + bool puctuation = ispunct(nextCodepoint & 0xff); + while (offset < textLength) + { + if ((puctuation && !ispunct(nextCodepoint & 0xff)) || (!puctuation && (isspace(nextCodepoint & 0xff) || ispunct(nextCodepoint & 0xff)))) + break; + offset += nextCodepointSize; + accCodepointSize += nextCodepointSize; + nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize); + } + + // Check whitespace to delete (ASCII only) + while (offset < textLength) + { + if (!isspace(nextCodepoint & 0xff)) break; + + offset += nextCodepointSize; + accCodepointSize += nextCodepointSize; + nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize); + } + + // Move text after cursor forward (including final null terminator) + for (int i = offset; i <= textLength; i++) text[i - accCodepointSize] = text[i]; + + textLength -= accCodepointSize; + } + else if ((textLength > textBoxCursorIndex) && (GUI_KEY_PRESSED(KEY_DELETE) || (GUI_KEY_DOWN(KEY_DELETE) && autoCursorShouldTrigger))) + { + // Delete single codepoint from text, after current cursor position + + int nextCodepointSize = 0; + GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize); + + // Move text after cursor forward (including final null terminator) + for (int i = textBoxCursorIndex + nextCodepointSize; i <= textLength; i++) text[i - nextCodepointSize] = text[i]; + + textLength -= nextCodepointSize; + } + + // Delete related codepoints from text, before current cursor position + if ((textBoxCursorIndex > 0) && GUI_KEY_PRESSED(KEY_BACKSPACE) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL))) + { + int offset = textBoxCursorIndex; + int accCodepointSize = 0; + int prevCodepointSize = 0; + int prevCodepoint = 0; + + // Check whitespace to delete (ASCII only) + while (offset > 0) + { + prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize); + if (!isspace(prevCodepoint & 0xff)) break; + + offset -= prevCodepointSize; + accCodepointSize += prevCodepointSize; + } + + // Check characters of the same type to delete (either ASCII punctuation or anything non-whitespace) + // Not using isalnum() since it only works on ASCII characters + bool puctuation = ispunct(prevCodepoint & 0xff); + while (offset > 0) + { + prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize); + if ((puctuation && !ispunct(prevCodepoint & 0xff)) || (!puctuation && (isspace(prevCodepoint & 0xff) || ispunct(prevCodepoint & 0xff)))) break; + + offset -= prevCodepointSize; + accCodepointSize += prevCodepointSize; + } + + // Move text after cursor forward (including final null terminator) + for (int i = textBoxCursorIndex; i <= textLength; i++) text[i - accCodepointSize] = text[i]; + + textLength -= accCodepointSize; + textBoxCursorIndex -= accCodepointSize; + } + else if ((textBoxCursorIndex > 0) && (GUI_KEY_PRESSED(KEY_BACKSPACE) || (GUI_KEY_DOWN(KEY_BACKSPACE) && autoCursorShouldTrigger))) + { + // Delete single codepoint from text, before current cursor position + + int prevCodepointSize = 0; + + GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize); + + // Move text after cursor forward (including final null terminator) + for (int i = textBoxCursorIndex; i <= textLength; i++) text[i - prevCodepointSize] = text[i]; + + textLength -= prevCodepointSize; + textBoxCursorIndex -= prevCodepointSize; + } + + // Move cursor position with keys + if ((textBoxCursorIndex > 0) && GUI_KEY_PRESSED(KEY_LEFT) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL))) + { + int offset = textBoxCursorIndex; + //int accCodepointSize = 0; + int prevCodepointSize = 0; + int prevCodepoint = 0; + + // Check whitespace to skip (ASCII only) + while (offset > 0) + { + prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize); + if (!isspace(prevCodepoint & 0xff)) break; + + offset -= prevCodepointSize; + //accCodepointSize += prevCodepointSize; + } + + // Check characters of the same type to skip (either ASCII punctuation or anything non-whitespace) + // Not using isalnum() since it only works on ASCII characters + bool puctuation = ispunct(prevCodepoint & 0xff); + while (offset > 0) + { + prevCodepoint = GetCodepointPrevious(text + offset, &prevCodepointSize); + if ((puctuation && !ispunct(prevCodepoint & 0xff)) || (!puctuation && (isspace(prevCodepoint & 0xff) || ispunct(prevCodepoint & 0xff)))) break; + + offset -= prevCodepointSize; + //accCodepointSize += prevCodepointSize; + } + + textBoxCursorIndex = offset; + } + else if ((textBoxCursorIndex > 0) && (GUI_KEY_PRESSED(KEY_LEFT) || (GUI_KEY_DOWN(KEY_LEFT) && autoCursorShouldTrigger))) + { + int prevCodepointSize = 0; + GetCodepointPrevious(text + textBoxCursorIndex, &prevCodepointSize); + + textBoxCursorIndex -= prevCodepointSize; + } + else if ((textLength > textBoxCursorIndex) && GUI_KEY_PRESSED(KEY_RIGHT) && (GUI_KEY_DOWN(KEY_LEFT_CONTROL) || GUI_KEY_DOWN(KEY_RIGHT_CONTROL))) + { + int offset = textBoxCursorIndex; + //int accCodepointSize = 0; + int nextCodepointSize; + int nextCodepoint; + + // Check characters of the same type to skip (either ASCII punctuation or anything non-whitespace) + // Not using isalnum() since it only works on ASCII characters + nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize); + bool puctuation = ispunct(nextCodepoint & 0xff); + while (offset < textLength) + { + if ((puctuation && !ispunct(nextCodepoint & 0xff)) || (!puctuation && (isspace(nextCodepoint & 0xff) || ispunct(nextCodepoint & 0xff)))) break; + + offset += nextCodepointSize; + //accCodepointSize += nextCodepointSize; + nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize); + } + + // Check whitespace to skip (ASCII only) + while (offset < textLength) + { + if (!isspace(nextCodepoint & 0xff)) break; + + offset += nextCodepointSize; + //accCodepointSize += nextCodepointSize; + nextCodepoint = GetCodepointNext(text + offset, &nextCodepointSize); + } + + textBoxCursorIndex = offset; + } + else if ((textLength > textBoxCursorIndex) && (GUI_KEY_PRESSED(KEY_RIGHT) || (GUI_KEY_DOWN(KEY_RIGHT) && autoCursorShouldTrigger))) + { + int nextCodepointSize = 0; + GetCodepointNext(text + textBoxCursorIndex, &nextCodepointSize); + + textBoxCursorIndex += nextCodepointSize; + } + + // Move cursor position with mouse + if (CheckCollisionPointRec(mousePosition, textBounds)) // Mouse hover text + { + float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/(float)guiFont.baseSize; + int codepointIndex = 0; + float glyphWidth = 0.0f; + float widthToMouseX = 0; + int mouseCursorIndex = 0; + + for (int i = textIndexOffset; i < textLength; i += codepointSize) + { + codepoint = GetCodepointNext(&text[i], &codepointSize); + codepointIndex = GetGlyphIndex(guiFont, codepoint); + + if (guiFont.glyphs[codepointIndex].advanceX == 0) glyphWidth = ((float)guiFont.recs[codepointIndex].width*scaleFactor); + else glyphWidth = ((float)guiFont.glyphs[codepointIndex].advanceX*scaleFactor); + + if (mousePosition.x <= (textBounds.x + (widthToMouseX + glyphWidth/2))) + { + mouseCursor.x = textBounds.x + widthToMouseX; + mouseCursorIndex = i; + break; + } + + widthToMouseX += (glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); + } + + // Check if mouse cursor is at the last position + int textEndWidth = GuiGetTextWidth(text + textIndexOffset); + if (GUI_POINTER_POSITION.x >= (textBounds.x + textEndWidth - glyphWidth/2)) + { + mouseCursor.x = textBounds.x + textEndWidth; + mouseCursorIndex = textLength; + } + + // Place cursor at required index on mouse click + if ((mouseCursor.x >= 0) && GUI_BUTTON_PRESSED) + { + cursor.x = mouseCursor.x; + textBoxCursorIndex = mouseCursorIndex; + } + } + else mouseCursor.x = -1; + + // Recalculate cursor position.y depending on textBoxCursorIndex + cursor.x = bounds.x + GuiGetStyle(TEXTBOX, TEXT_PADDING) + GuiGetTextWidth(text + textIndexOffset) - GuiGetTextWidth(text + textBoxCursorIndex) + GuiGetStyle(DEFAULT, TEXT_SPACING); + //if (multiline) cursor.y = GetTextLines() + + // Finish text editing on ENTER or mouse click outside bounds + if ((!multiline && GUI_KEY_PRESSED(KEY_ENTER)) || + (!CheckCollisionPointRec(mousePosition, bounds) && GUI_BUTTON_PRESSED)) + { + textBoxCursorIndex = 0; // GLOBAL: Reset the shared cursor index + autoCursorCounter = 0; // GLOBAL: Reset counter for repeated keystrokes + result = 1; + } + } + else + { + if (CheckCollisionPointRec(mousePosition, bounds)) + { + state = STATE_FOCUSED; + + if (GUI_BUTTON_PRESSED) + { + textBoxCursorIndex = textLength; // GLOBAL: Place cursor index to the end of current text + autoCursorCounter = 0; // GLOBAL: Reset counter for repeated keystrokes + result = 1; + } + } + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (state == STATE_PRESSED) + { + GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_PRESSED))); + } + else if (state == STATE_DISABLED) + { + GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), GetColor(GuiGetStyle(TEXTBOX, BASE_COLOR_DISABLED))); + } + else GuiDrawRectangle(bounds, GuiGetStyle(TEXTBOX, BORDER_WIDTH), GetColor(GuiGetStyle(TEXTBOX, BORDER + (state*3))), BLANK); + + // Draw text considering index offset if required + // NOTE: Text index offset depends on cursor position + GuiDrawText(text + textIndexOffset, textBounds, GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT), GetColor(GuiGetStyle(TEXTBOX, TEXT + (state*3)))); + + // Draw cursor + if (editMode && !GuiGetStyle(TEXTBOX, TEXT_READONLY)) + { + //if (autoCursorMode || ((blinkCursorFrameCounter/40)%2 == 0)) + GuiDrawRectangle(cursor, 0, BLANK, GetColor(GuiGetStyle(TEXTBOX, BORDER_COLOR_PRESSED))); + + // Draw mouse position cursor (if required) + if (mouseCursor.x >= 0) GuiDrawRectangle(mouseCursor, 0, BLANK, GetColor(GuiGetStyle(TEXTBOX, BORDER_COLOR_PRESSED))); + } + else if (state == STATE_FOCUSED) GuiTooltip(bounds); + //-------------------------------------------------------------------- + + return result; // Mouse button pressed: result = 1 +} + +/* +// Text Box control with multiple lines and word-wrap +// NOTE: This text-box is readonly, no editing supported by default +bool GuiTextBoxMulti(Rectangle bounds, char *text, int textSize, bool editMode) +{ + bool pressed = false; + + GuiSetStyle(TEXTBOX, TEXT_READONLY, 1); + GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, TEXT_WRAP_WORD); // WARNING: If wrap mode enabled, text editing is not supported + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, TEXT_ALIGN_TOP); + + // TODO: Implement methods to calculate cursor position properly + pressed = GuiTextBox(bounds, text, textSize, editMode); + + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, TEXT_ALIGN_MIDDLE); + GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, TEXT_WRAP_NONE); + GuiSetStyle(TEXTBOX, TEXT_READONLY, 0); + + return pressed; +} +*/ + +// Spinner control, returns selected value +int GuiSpinner(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode) +{ + int result = 1; + GuiState state = guiState; + + int tempValue = *value; + + Rectangle valueBoxBounds = { + bounds.x + GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH) + GuiGetStyle(VALUEBOX, SPINNER_BUTTON_SPACING), + bounds.y, + bounds.width - 2*(GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH) + GuiGetStyle(VALUEBOX, SPINNER_BUTTON_SPACING)), bounds.height }; + Rectangle leftButtonBound = { (float)bounds.x, (float)bounds.y, (float)GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH), (float)bounds.height }; + Rectangle rightButtonBound = { (float)bounds.x + bounds.width - GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH), (float)bounds.y, + (float)GuiGetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH), (float)bounds.height }; + + Rectangle textBounds = { 0 }; + if (text != NULL) + { + textBounds.width = (float)GuiGetTextWidth(text) + 2; + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = bounds.x + bounds.width + GuiGetStyle(VALUEBOX, TEXT_PADDING); + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + if (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT) textBounds.x = bounds.x - textBounds.width - GuiGetStyle(VALUEBOX, TEXT_PADDING); + } + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + // Check spinner state + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else state = STATE_FOCUSED; + } + } + +#if defined(RAYGUI_NO_ICONS) + if (GuiButton(leftButtonBound, "<")) tempValue--; + if (GuiButton(rightButtonBound, ">")) tempValue++; +#else + if (GuiButton(leftButtonBound, GuiIconText(ICON_ARROW_LEFT_FILL, NULL))) tempValue--; + if (GuiButton(rightButtonBound, GuiIconText(ICON_ARROW_RIGHT_FILL, NULL))) tempValue++; +#endif + + if (!editMode) + { + if (tempValue < minValue) tempValue = minValue; + if (tempValue > maxValue) tempValue = maxValue; + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + result = GuiValueBox(valueBoxBounds, NULL, &tempValue, minValue, maxValue, editMode); + + // Draw value selector custom buttons + // NOTE: BORDER_WIDTH and TEXT_ALIGNMENT forced values + int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH); + int tempTextAlign = GuiGetStyle(BUTTON, TEXT_ALIGNMENT); + GuiSetStyle(BUTTON, BORDER_WIDTH, GuiGetStyle(VALUEBOX, BORDER_WIDTH)); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlign); + GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth); + + // Draw text label if provided + GuiDrawText(text, textBounds, (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT)? TEXT_ALIGN_LEFT : TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + //-------------------------------------------------------------------- + + *value = tempValue; + return result; +} + +// Value Box control, updates input text with numbers +// NOTE: Requires static variables: frameCounter +int GuiValueBox(Rectangle bounds, const char *text, int *value, int minValue, int maxValue, bool editMode) +{ + #if !defined(RAYGUI_VALUEBOX_MAX_CHARS) + #define RAYGUI_VALUEBOX_MAX_CHARS 32 + #endif + + int result = 0; + GuiState state = guiState; + + char textValue[RAYGUI_VALUEBOX_MAX_CHARS + 1] = { 0 }; + snprintf(textValue, RAYGUI_VALUEBOX_MAX_CHARS + 1, "%i", *value); + + Rectangle textBounds = { 0 }; + if (text != NULL) + { + textBounds.width = (float)GuiGetTextWidth(text) + 2; + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = bounds.x + bounds.width + GuiGetStyle(VALUEBOX, TEXT_PADDING); + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + if (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT) textBounds.x = bounds.x - textBounds.width - GuiGetStyle(VALUEBOX, TEXT_PADDING); + } + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + bool valueHasChanged = false; + + if (editMode) + { + state = STATE_PRESSED; + + int keyCount = (int)strlen(textValue); + + // Add or remove minus symbol + if (GUI_KEY_PRESSED(KEY_MINUS)) + { + if (textValue[0] == '-') + { + for (int i = 0 ; i < keyCount; i++) textValue[i] = textValue[i + 1]; + + keyCount--; + valueHasChanged = true; + } + else if (keyCount < RAYGUI_VALUEBOX_MAX_CHARS) + { + if (keyCount == 0) + { + textValue[0] = '0'; + textValue[1] = '\0'; + keyCount++; + } + + for (int i = keyCount ; i > -1; i--) textValue[i + 1] = textValue[i]; + + textValue[0] = '-'; + keyCount++; + valueHasChanged = true; + } + } + + // Add new digit to text value + if ((keyCount >= 0) && (keyCount < RAYGUI_VALUEBOX_MAX_CHARS) && (GuiGetTextWidth(textValue) < bounds.width)) + { + int key = GUI_INPUT_KEY; + + // Only allow keys in range [48..57] + if ((key >= 48) && (key <= 57)) + { + textValue[keyCount] = (char)key; + keyCount++; + valueHasChanged = true; + } + } + + // Delete text + if ((keyCount > 0) && GUI_KEY_PRESSED(KEY_BACKSPACE)) + { + keyCount--; + textValue[keyCount] = '\0'; + valueHasChanged = true; + } + + if (valueHasChanged) *value = TextToInteger(textValue); + + // NOTE: Values are not clamped until user input finishes + //if (*value > maxValue) *value = maxValue; + //else if (*value < minValue) *value = minValue; + + if ((GUI_KEY_PRESSED(KEY_ENTER) || GUI_KEY_PRESSED(KEY_KP_ENTER)) || (!CheckCollisionPointRec(mousePoint, bounds) && GUI_BUTTON_PRESSED)) + { + if (*value > maxValue) *value = maxValue; + else if (*value < minValue) *value = minValue; + + result = 1; + } + } + else + { + if (*value > maxValue) *value = maxValue; + else if (*value < minValue) *value = minValue; + + if (CheckCollisionPointRec(mousePoint, bounds)) + { + state = STATE_FOCUSED; + if (GUI_BUTTON_PRESSED) result = 1; + } + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + Color baseColor = BLANK; + if (state == STATE_PRESSED) baseColor = GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_PRESSED)); + else if (state == STATE_DISABLED) baseColor = GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_DISABLED)); + + GuiDrawRectangle(bounds, GuiGetStyle(VALUEBOX, BORDER_WIDTH), GetColor(GuiGetStyle(VALUEBOX, BORDER + (state*3))), baseColor); + GuiDrawText(textValue, GetTextBounds(VALUEBOX, bounds), TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(VALUEBOX, TEXT + (state*3)))); + + // Draw cursor rectangle + if (editMode) + { + // NOTE: ValueBox internal text is always centered + Rectangle cursor = { bounds.x + GuiGetTextWidth(textValue)/2 + bounds.width/2 + 1, + bounds.y + GuiGetStyle(TEXTBOX, BORDER_WIDTH) + 2, + 2, bounds.height - GuiGetStyle(TEXTBOX, BORDER_WIDTH)*2 - 4 }; + if (cursor.height > bounds.height) cursor.height = bounds.height - GuiGetStyle(TEXTBOX, BORDER_WIDTH)*2; + GuiDrawRectangle(cursor, 0, BLANK, GetColor(GuiGetStyle(VALUEBOX, BORDER_COLOR_PRESSED))); + } + + // Draw text label if provided + GuiDrawText(text, textBounds, (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT)? TEXT_ALIGN_LEFT : TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + //-------------------------------------------------------------------- + + return result; +} + +// Floating point Value Box control, updates input val_str with numbers +// NOTE: Requires static variables: frameCounter +int GuiValueBoxFloat(Rectangle bounds, const char *text, char *textValue, float *value, bool editMode) +{ + #if !defined(RAYGUI_VALUEBOX_MAX_CHARS) + #define RAYGUI_VALUEBOX_MAX_CHARS 32 + #endif + + int result = 0; + GuiState state = guiState; + + //char textValue[RAYGUI_VALUEBOX_MAX_CHARS + 1] = "\0"; + //snprintf(textValue, sizeof(textValue), "%2.2f", *value); + + Rectangle textBounds = { 0 }; + if (text != NULL) + { + textBounds.width = (float)GuiGetTextWidth(text) + 2; + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = bounds.x + bounds.width + GuiGetStyle(VALUEBOX, TEXT_PADDING); + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + if (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_LEFT) textBounds.x = bounds.x - textBounds.width - GuiGetStyle(VALUEBOX, TEXT_PADDING); + } + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + bool valueHasChanged = false; + + if (editMode) + { + state = STATE_PRESSED; + + int keyCount = (int)strlen(textValue); + + // Add or remove minus symbol + if (GUI_KEY_PRESSED(KEY_MINUS)) + { + if (textValue[0] == '-') + { + for (int i = 0; i < keyCount; i++) textValue[i] = textValue[i + 1]; + + keyCount--; + valueHasChanged = true; + } + else if (keyCount < (RAYGUI_VALUEBOX_MAX_CHARS - 1)) + { + if (keyCount == 0) + { + textValue[0] = '0'; + textValue[1] = '\0'; + keyCount++; + } + + for (int i = keyCount; i > -1; i--) textValue[i + 1] = textValue[i]; + + textValue[0] = '-'; + keyCount++; + valueHasChanged = true; + } + } + + // Only allow keys in range [48..57] + if (keyCount < RAYGUI_VALUEBOX_MAX_CHARS) + { + if (GuiGetTextWidth(textValue) < bounds.width) + { + int key = GUI_INPUT_KEY; + if (((key >= 48) && (key <= 57)) || + (key == '.') || + ((keyCount == 0) && (key == '+')) || // NOTE: Sign can only be in first position + ((keyCount == 0) && (key == '-'))) + { + textValue[keyCount] = (char)key; + keyCount++; + + valueHasChanged = true; + } + } + } + + // Pressed backspace + if (GUI_KEY_PRESSED(KEY_BACKSPACE)) + { + if (keyCount > 0) + { + keyCount--; + textValue[keyCount] = '\0'; + valueHasChanged = true; + } + } + + if (valueHasChanged) *value = TextToFloat(textValue); + + if ((GUI_KEY_PRESSED(KEY_ENTER) || GUI_KEY_PRESSED(KEY_KP_ENTER)) || (!CheckCollisionPointRec(mousePoint, bounds) && GUI_BUTTON_PRESSED)) result = 1; + } + else + { + if (CheckCollisionPointRec(mousePoint, bounds)) + { + state = STATE_FOCUSED; + if (GUI_BUTTON_PRESSED) result = 1; + } + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + Color baseColor = BLANK; + if (state == STATE_PRESSED) baseColor = GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_PRESSED)); + else if (state == STATE_DISABLED) baseColor = GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_DISABLED)); + + GuiDrawRectangle(bounds, GuiGetStyle(VALUEBOX, BORDER_WIDTH), GetColor(GuiGetStyle(VALUEBOX, BORDER + (state*3))), baseColor); + GuiDrawText(textValue, GetTextBounds(VALUEBOX, bounds), TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(VALUEBOX, TEXT + (state*3)))); + + // Draw cursor + if (editMode) + { + // NOTE: ValueBox internal text is always centered + Rectangle cursor = {bounds.x + GuiGetTextWidth(textValue)/2 + bounds.width/2 + 1, + bounds.y + 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH), 4, + bounds.height - 4*GuiGetStyle(VALUEBOX, BORDER_WIDTH)}; + GuiDrawRectangle(cursor, 0, BLANK, GetColor(GuiGetStyle(VALUEBOX, BORDER_COLOR_PRESSED))); + } + + // Draw text label if provided + GuiDrawText(text, textBounds, + (GuiGetStyle(VALUEBOX, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT)? TEXT_ALIGN_LEFT : TEXT_ALIGN_RIGHT, + GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + //-------------------------------------------------------------------- + + return result; +} + +// Slider control with pro parameters +// NOTE: Other GuiSlider*() controls use this one +int GuiSlider(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue) +{ + int result = 0; + GuiState state = guiState; + + float temp = (maxValue - minValue)/2.0f; + if (value == NULL) value = &temp; + float oldValue = *value; + + int sliderWidth = GuiGetStyle(SLIDER, SLIDER_WIDTH); + + Rectangle slider = { bounds.x, bounds.y + GuiGetStyle(SLIDER, BORDER_WIDTH) + GuiGetStyle(SLIDER, SLIDER_PADDING), + 0, bounds.height - 2*GuiGetStyle(SLIDER, BORDER_WIDTH) - 2*GuiGetStyle(SLIDER, SLIDER_PADDING) }; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds + { + if (GUI_BUTTON_DOWN) + { + if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec)) + { + state = STATE_PRESSED; + // Get equivalent value and slider position from mousePosition.x + *value = (maxValue - minValue)*((mousePoint.x - bounds.x - sliderWidth/2)/(bounds.width - sliderWidth)) + minValue; + } + } + else + { + guiControlExclusiveMode = false; + guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 }; + } + } + else if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) + { + state = STATE_PRESSED; + guiControlExclusiveMode = true; + guiControlExclusiveRec = bounds; // Store bounds as an identifier when dragging starts + + if (!CheckCollisionPointRec(mousePoint, slider)) + { + // Get equivalent value and slider position from mousePosition.x + *value = (maxValue - minValue)*((mousePoint.x - bounds.x - sliderWidth/2)/(bounds.width - sliderWidth)) + minValue; + } + } + else state = STATE_FOCUSED; + } + + if (*value > maxValue) *value = maxValue; + else if (*value < minValue) *value = minValue; + } + + // Control value change check + if (oldValue == *value) result = 0; + else result = 1; + + // Slider bar limits check + float sliderValue = (((*value - minValue)/(maxValue - minValue))*(bounds.width - sliderWidth - 2*GuiGetStyle(SLIDER, BORDER_WIDTH))); + if (sliderWidth > 0) // Slider + { + slider.x += sliderValue; + slider.width = (float)sliderWidth; + if (slider.x <= (bounds.x + GuiGetStyle(SLIDER, BORDER_WIDTH))) slider.x = bounds.x + GuiGetStyle(SLIDER, BORDER_WIDTH); + else if ((slider.x + slider.width) >= (bounds.x + bounds.width)) slider.x = bounds.x + bounds.width - slider.width - GuiGetStyle(SLIDER, BORDER_WIDTH); + } + else if (sliderWidth == 0) // SliderBar + { + slider.x += GuiGetStyle(SLIDER, BORDER_WIDTH); + slider.width = sliderValue; + if (slider.width > bounds.width) slider.width = bounds.width - 2*GuiGetStyle(SLIDER, BORDER_WIDTH); + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(bounds, GuiGetStyle(SLIDER, BORDER_WIDTH), GetColor(GuiGetStyle(SLIDER, BORDER + (state*3))), GetColor(GuiGetStyle(SLIDER, (state != STATE_DISABLED)? BASE_COLOR_NORMAL : BASE_COLOR_DISABLED))); + + // Draw slider internal bar (depends on state) + if (state == STATE_NORMAL) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BASE_COLOR_PRESSED))); + else if (state == STATE_FOCUSED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, TEXT_COLOR_FOCUSED))); + else if (state == STATE_PRESSED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, TEXT_COLOR_PRESSED))); + else if (state == STATE_DISABLED) GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, TEXT_COLOR_DISABLED))); + + // Draw left/right text if provided + if (textLeft != NULL) + { + Rectangle textBounds = { 0 }; + textBounds.width = (float)GuiGetTextWidth(textLeft); + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = bounds.x - textBounds.width - GuiGetStyle(SLIDER, TEXT_PADDING); + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + + GuiDrawText(textLeft, textBounds, TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + } + + if (textRight != NULL) + { + Rectangle textBounds = { 0 }; + textBounds.width = (float)GuiGetTextWidth(textRight); + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = bounds.x + bounds.width + GuiGetStyle(SLIDER, TEXT_PADDING); + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + + GuiDrawText(textRight, textBounds, TEXT_ALIGN_LEFT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + } + //-------------------------------------------------------------------- + + return result; +} + +// Slider Bar control extended, returns selected value +int GuiSliderBar(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue) +{ + int result = 0; + int preSliderWidth = GuiGetStyle(SLIDER, SLIDER_WIDTH); + GuiSetStyle(SLIDER, SLIDER_WIDTH, 0); + result = GuiSlider(bounds, textLeft, textRight, value, minValue, maxValue); + GuiSetStyle(SLIDER, SLIDER_WIDTH, preSliderWidth); + + return result; +} + +// Progress Bar control extended, shows current progress value +int GuiProgressBar(Rectangle bounds, const char *textLeft, const char *textRight, float *value, float minValue, float maxValue) +{ + int result = 0; + GuiState state = guiState; + + float temp = (maxValue - minValue)/2.0f; + if (value == NULL) value = &temp; + + // Progress bar + Rectangle progress = { bounds.x + GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), + bounds.y + GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) + GuiGetStyle(PROGRESSBAR, PROGRESS_PADDING), 0, + bounds.height - GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) - 2*GuiGetStyle(PROGRESSBAR, PROGRESS_PADDING) -1 }; + + // Update control + //-------------------------------------------------------------------- + if (*value > maxValue) *value = maxValue; + + // WARNING: Working with floats could lead to rounding issues + if ((state != STATE_DISABLED)) progress.width = ((float)*value/(maxValue - minValue))*(bounds.width - 2*GuiGetStyle(PROGRESSBAR, BORDER_WIDTH)); + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (state == STATE_DISABLED) + { + GuiDrawRectangle(bounds, GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), GetColor(GuiGetStyle(PROGRESSBAR, BORDER + (state*3))), BLANK); + } + else + { + if (*value > minValue) + { + // Draw progress bar with colored border, more visual + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y, (int)progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_FOCUSED))); + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + 1, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.height - 2 }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_FOCUSED))); + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y + bounds.height - 1, (int)progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_FOCUSED))); + } + else GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x, bounds.y, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.height+GuiGetStyle(PROGRESSBAR, BORDER_WIDTH)-1 }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_NORMAL))); + + if (*value >= maxValue) GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.y, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.height+GuiGetStyle(PROGRESSBAR, BORDER_WIDTH)-1}, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_FOCUSED))); + else + { + // Draw borders not yet reached by value + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + (int)progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.y, bounds.width - (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) - (int)progress.width - 1, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_NORMAL))); + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + (int)progress.width + (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.y + bounds.height - 1, bounds.width - (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) - (int)progress.width - 1, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH) }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_NORMAL))); + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ bounds.x + bounds.width - (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.y, (float)GuiGetStyle(PROGRESSBAR, BORDER_WIDTH), bounds.height+GuiGetStyle(PROGRESSBAR, BORDER_WIDTH)-1 }, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BORDER_COLOR_NORMAL))); + } + + // Draw slider internal progress bar (depends on state) + if (GuiGetStyle(PROGRESSBAR, PROGRESS_SIDE) == 0) // Left-->Right + { + GuiDrawRectangle(progress, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BASE_COLOR_PRESSED))); + } + else // Right-->Left + { + progress.x = bounds.x + bounds.width - progress.width - GuiGetStyle(PROGRESSBAR, BORDER_WIDTH); + GuiDrawRectangle(progress, 0, BLANK, GetColor(GuiGetStyle(PROGRESSBAR, BASE_COLOR_PRESSED))); + } + } + + // Draw left/right text if provided + if (textLeft != NULL) + { + Rectangle textBounds = { 0 }; + textBounds.width = (float)GuiGetTextWidth(textLeft); + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = bounds.x - textBounds.width - GuiGetStyle(PROGRESSBAR, TEXT_PADDING); + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + + GuiDrawText(textLeft, textBounds, TEXT_ALIGN_RIGHT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + } + + if (textRight != NULL) + { + Rectangle textBounds = { 0 }; + textBounds.width = (float)GuiGetTextWidth(textRight); + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + textBounds.x = bounds.x + bounds.width + GuiGetStyle(PROGRESSBAR, TEXT_PADDING); + textBounds.y = bounds.y + bounds.height/2 - GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + + GuiDrawText(textRight, textBounds, TEXT_ALIGN_LEFT, GetColor(GuiGetStyle(LABEL, TEXT + (state*3)))); + } + //-------------------------------------------------------------------- + + return result; +} + +// Status Bar control +int GuiStatusBar(Rectangle bounds, const char *text) +{ + int result = 0; + GuiState state = guiState; + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(bounds, GuiGetStyle(STATUSBAR, BORDER_WIDTH), GetColor(GuiGetStyle(STATUSBAR, BORDER + (state*3))), GetColor(GuiGetStyle(STATUSBAR, BASE + (state*3)))); + GuiDrawText(text, GetTextBounds(STATUSBAR, bounds), GuiGetStyle(STATUSBAR, TEXT_ALIGNMENT), GetColor(GuiGetStyle(STATUSBAR, TEXT + (state*3)))); + //-------------------------------------------------------------------- + + return result; +} + +// Dummy rectangle control, intended for placeholding +int GuiDummyRec(Rectangle bounds, const char *text) +{ + int result = 0; + GuiState state = guiState; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + // Check button state + if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) state = STATE_PRESSED; + else state = STATE_FOCUSED; + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(bounds, 0, BLANK, GetColor(GuiGetStyle(DEFAULT, (state != STATE_DISABLED)? BASE_COLOR_NORMAL : BASE_COLOR_DISABLED))); + GuiDrawText(text, GetTextBounds(DEFAULT, bounds), TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(BUTTON, (state != STATE_DISABLED)? TEXT_COLOR_NORMAL : TEXT_COLOR_DISABLED))); + //------------------------------------------------------------------ + + return result; +} + +// List View control +int GuiListView(Rectangle bounds, const char *text, int *scrollIndex, int *active) +{ + int result = 0; + int itemCount = 0; + char **items = NULL; + + if (text != NULL) items = GuiTextSplit(text, ';', &itemCount, NULL); + + result = GuiListViewEx(bounds, items, itemCount, scrollIndex, active, NULL); + + return result; +} + +// List View control using text entries list and returning focus entry +int GuiListViewEx(Rectangle bounds, char **text, int count, int *scrollIndex, int *active, int *focus) +{ + int result = 0; + GuiState state = guiState; + + int itemFocused = (focus == NULL)? -1 : *focus; + int itemSelected = (active == NULL)? -1 : *active; + + // Check if scroll bar is needed + bool useScrollBar = false; + if ((GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING))*count > bounds.height) useScrollBar = true; + + // Define base item rectangle [0] + Rectangle itemBounds = { 0 }; + itemBounds.x = bounds.x + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING); + itemBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING) + GuiGetStyle(DEFAULT, BORDER_WIDTH); + itemBounds.width = bounds.width - 2*GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING) - GuiGetStyle(DEFAULT, BORDER_WIDTH); + itemBounds.height = (float)GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); + if (useScrollBar) itemBounds.width -= GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); + + // Get items on the list + int visibleItems = (int)bounds.height/(GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING)); + if (visibleItems > count) visibleItems = count; + + int startIndex = (scrollIndex == NULL)? 0 : *scrollIndex; + if ((startIndex < 0) || (startIndex > (count - visibleItems))) startIndex = 0; + int endIndex = startIndex + visibleItems; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + // Check mouse inside list view + if (CheckCollisionPointRec(mousePoint, bounds)) + { + state = STATE_FOCUSED; + + // Check focused and selected item + for (int i = 0; i < visibleItems; i++) + { + if (CheckCollisionPointRec(mousePoint, itemBounds)) + { + itemFocused = startIndex + i; + if (GUI_BUTTON_PRESSED) + { + if (itemSelected == (startIndex + i)) itemSelected = -1; + else itemSelected = startIndex + i; + } + break; + } + + // Update item rectangle y position for next item + itemBounds.y += (GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING)); + } + + if (useScrollBar) + { + float scrollDelta = GUI_SCROLL_DELTA; + startIndex -= (int)scrollDelta; + + if (startIndex < 0) startIndex = 0; + else if (startIndex > (count - visibleItems)) startIndex = count - visibleItems; + + endIndex = startIndex + visibleItems; + if (endIndex > count) endIndex = count; + } + } + else itemFocused = -1; + + // Reset item rectangle y to [0] + itemBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING) + GuiGetStyle(DEFAULT, BORDER_WIDTH); + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(bounds, GuiGetStyle(LISTVIEW, BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER + state*3)), GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR))); // Draw background + + // Draw visible items + for (int i = 0; ((i < visibleItems) && (text != NULL)); i++) + { + if (GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_NORMAL)) GuiDrawRectangle(itemBounds, GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_NORMAL)), BLANK); + + if (state == STATE_DISABLED) + { + if ((startIndex + i) == itemSelected) GuiDrawRectangle(itemBounds, GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_DISABLED)), GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_DISABLED))); + + GuiDrawText(text[startIndex + i], GetTextBounds(LISTVIEW, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_DISABLED))); + } + else + { + if (((startIndex + i) == itemSelected) && (active != NULL)) + { + // Draw item selected + GuiDrawRectangle(itemBounds, GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_PRESSED)), GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_PRESSED))); + GuiDrawText(text[startIndex + i], GetTextBounds(LISTVIEW, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_PRESSED))); + } + else if (((startIndex + i) == itemFocused)) // && (focus != NULL)) // NOTE: Items focused, despite not returned + { + // Draw item focused + GuiDrawRectangle(itemBounds, GuiGetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER_COLOR_FOCUSED)), GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_FOCUSED))); + GuiDrawText(text[startIndex + i], GetTextBounds(LISTVIEW, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_FOCUSED))); + } + else + { + // Draw item normal (no rectangle) + GuiDrawText(text[startIndex + i], GetTextBounds(LISTVIEW, itemBounds), GuiGetStyle(LISTVIEW, TEXT_ALIGNMENT), GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_NORMAL))); + } + } + + // Update item rectangle y position for next item + itemBounds.y += (GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) + GuiGetStyle(LISTVIEW, LIST_ITEMS_SPACING)); + } + + if (useScrollBar) + { + Rectangle scrollBarBounds = { + bounds.x + bounds.width - GuiGetStyle(LISTVIEW, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH), + bounds.y + GuiGetStyle(LISTVIEW, BORDER_WIDTH), (float)GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH), + bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) + }; + + // Calculate percentage of visible items and apply same percentage to scrollbar + float percentVisible = (float)(endIndex - startIndex)/count; + float sliderSize = bounds.height*percentVisible; + + int prevSliderSize = GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE); // Save default slider size + int prevScrollSpeed = GuiGetStyle(SCROLLBAR, SCROLL_SPEED); // Save default scroll speed + GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, (int)sliderSize); // Change slider size + GuiSetStyle(SCROLLBAR, SCROLL_SPEED, count - visibleItems); // Change scroll speed + + startIndex = GuiScrollBar(scrollBarBounds, startIndex, 0, count - visibleItems); + + GuiSetStyle(SCROLLBAR, SCROLL_SPEED, prevScrollSpeed); // Reset scroll speed to default + GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, prevSliderSize); // Reset slider size to default + } + //-------------------------------------------------------------------- + + if (active != NULL) *active = itemSelected; + if (focus != NULL) *focus = itemFocused; + if (scrollIndex != NULL) *scrollIndex = startIndex; + + return result; +} + +// Color Panel control - Color (RGBA) variant +int GuiColorPanel(Rectangle bounds, const char *text, Color *color) +{ + int result = 0; + + Vector3 vcolor = { (float)color->r/255.0f, (float)color->g/255.0f, (float)color->b/255.0f }; + Vector3 hsv = ConvertRGBtoHSV(vcolor); + Vector3 prevHsv = hsv; // workaround to see if GuiColorPanelHSV modifies the hsv + + GuiColorPanelHSV(bounds, text, &hsv); + + // Check if the hsv was changed, only then change the color + // This is required, because the Color->HSV->Color conversion has precision errors + // Thus the assignment from HSV to Color should only be made, if the HSV has a new user-entered value + // Otherwise GuiColorPanel would often modify it's color without user input + // TODO: GuiColorPanelHSV could return 1 if the slider was dragged, to simplify this check + if (hsv.x != prevHsv.x || hsv.y != prevHsv.y || hsv.z != prevHsv.z) + { + Vector3 rgb = ConvertHSVtoRGB(hsv); + + // NOTE: Vector3ToColor() only available on raylib 1.8.1 + *color = RAYGUI_CLITERAL(Color){ (unsigned char)(255.0f*rgb.x), + (unsigned char)(255.0f*rgb.y), + (unsigned char)(255.0f*rgb.z), + color->a }; + } + return result; +} + +// Color Bar Alpha control +// NOTE: Returns alpha value normalized [0..1] +int GuiColorBarAlpha(Rectangle bounds, const char *text, float *alpha) +{ + #if !defined(RAYGUI_COLORBARALPHA_CHECKED_SIZE) + #define RAYGUI_COLORBARALPHA_CHECKED_SIZE 10 + #endif + + int result = 0; + GuiState state = guiState; + Rectangle selector = { (float)bounds.x + (*alpha)*bounds.width - GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT)/2, + (float)bounds.y - GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW), + (float)GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT), + (float)bounds.height + GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW)*2 }; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds + { + if (GUI_BUTTON_DOWN) + { + if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec)) + { + state = STATE_PRESSED; + + *alpha = (mousePoint.x - bounds.x)/bounds.width; + if (*alpha <= 0.0f) *alpha = 0.0f; + if (*alpha >= 1.0f) *alpha = 1.0f; + } + } + else + { + guiControlExclusiveMode = false; + guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 }; + } + } + else if (CheckCollisionPointRec(mousePoint, bounds) || CheckCollisionPointRec(mousePoint, selector)) + { + if (GUI_BUTTON_DOWN) + { + state = STATE_PRESSED; + guiControlExclusiveMode = true; + guiControlExclusiveRec = bounds; // Store bounds as an identifier when dragging starts + + *alpha = (mousePoint.x - bounds.x)/bounds.width; + if (*alpha <= 0.0f) *alpha = 0.0f; + if (*alpha >= 1.0f) *alpha = 1.0f; + //selector.x = bounds.x + (int)(((alpha - 0)/(100 - 0))*(bounds.width - 2*GuiGetStyle(SLIDER, BORDER_WIDTH))) - selector.width/2; + } + else state = STATE_FOCUSED; + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + // Draw alpha bar: checked background + if (state != STATE_DISABLED) + { + int checksX = (int)bounds.width/RAYGUI_COLORBARALPHA_CHECKED_SIZE; + int checksY = (int)bounds.height/RAYGUI_COLORBARALPHA_CHECKED_SIZE; + + for (int x = 0; x < checksX; x++) + { + for (int y = 0; y < checksY; y++) + { + Rectangle check = { bounds.x + x*RAYGUI_COLORBARALPHA_CHECKED_SIZE, bounds.y + y*RAYGUI_COLORBARALPHA_CHECKED_SIZE, RAYGUI_COLORBARALPHA_CHECKED_SIZE, RAYGUI_COLORBARALPHA_CHECKED_SIZE }; + GuiDrawRectangle(check, 0, BLANK, ((x + y)%2)? Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), 0.4f) : Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.4f)); + } + } + + DrawRectangleGradientEx(bounds, RAYGUI_CLITERAL(Color){ 255, 255, 255, 0 }, RAYGUI_CLITERAL(Color){ 255, 255, 255, 0 }, Fade(RAYGUI_CLITERAL(Color){ 0, 0, 0, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 0, 0, 0, 255 }, guiAlpha)); + } + else DrawRectangleGradientEx(bounds, Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.1f), Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.1f), Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), guiAlpha), Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), guiAlpha)); + + GuiDrawRectangle(bounds, GuiGetStyle(COLORPICKER, BORDER_WIDTH), GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3)), BLANK); + + // Draw alpha bar: selector + GuiDrawRectangle(selector, 0, BLANK, GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3))); + //-------------------------------------------------------------------- + + return result; +} + +// Color Bar Hue control +// Returns hue value normalized [0..1] +// NOTE: Other similar bars (for reference): +// Color GuiColorBarSat() [WHITE->color] +// Color GuiColorBarValue() [BLACK->color], HSV/HSL +// float GuiColorBarLuminance() [BLACK->WHITE] +int GuiColorBarHue(Rectangle bounds, const char *text, float *hue) +{ + int result = 0; + GuiState state = guiState; + Rectangle selector = { (float)bounds.x - GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW), (float)bounds.y + (*hue)/360.0f*bounds.height - GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT)/2, (float)bounds.width + GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW)*2, (float)GuiGetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT) }; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds + { + if (GUI_BUTTON_DOWN) + { + if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec)) + { + state = STATE_PRESSED; + + *hue = (mousePoint.y - bounds.y)*360/bounds.height; + if (*hue <= 0.0f) *hue = 0.0f; + if (*hue >= 359.0f) *hue = 359.0f; + } + } + else + { + guiControlExclusiveMode = false; + guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 }; + } + } + else if (CheckCollisionPointRec(mousePoint, bounds) || CheckCollisionPointRec(mousePoint, selector)) + { + if (GUI_BUTTON_DOWN) + { + state = STATE_PRESSED; + guiControlExclusiveMode = true; + guiControlExclusiveRec = bounds; // Store bounds as an identifier when dragging starts + + *hue = (mousePoint.y - bounds.y)*360/bounds.height; + if (*hue <= 0.0f) *hue = 0.0f; + if (*hue >= 359.0f) *hue = 359.0f; + + } + else state = STATE_FOCUSED; + + /*if (GUI_KEY_DOWN(KEY_UP)) + { + hue -= 2.0f; + if (hue <= 0.0f) hue = 0.0f; + } + else if (GUI_KEY_DOWN(KEY_DOWN)) + { + hue += 2.0f; + if (hue >= 360.0f) hue = 360.0f; + }*/ + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (state != STATE_DISABLED) + { + // Draw hue bar:color bars + // TODO: Use directly DrawRectangleGradientEx(bounds, color1, color2, color2, color1); + DrawRectangleGradientV((int)bounds.x, (int)(bounds.y), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 255, 0, 0, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 255, 255, 0, 255 }, guiAlpha)); + DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + bounds.height/6), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 255, 255, 0, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 0, 255, 0, 255 }, guiAlpha)); + DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + 2*(bounds.height/6)), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 0, 255, 0, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 0, 255, 255, 255 }, guiAlpha)); + DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + 3*(bounds.height/6)), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 0, 255, 255, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 0, 0, 255, 255 }, guiAlpha)); + DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + 4*(bounds.height/6)), (int)bounds.width, (int)ceilf(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 0, 0, 255, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 255, 0, 255, 255 }, guiAlpha)); + DrawRectangleGradientV((int)bounds.x, (int)(bounds.y + 5*(bounds.height/6)), (int)bounds.width, (int)(bounds.height/6), Fade(RAYGUI_CLITERAL(Color){ 255, 0, 255, 255 }, guiAlpha), Fade(RAYGUI_CLITERAL(Color){ 255, 0, 0, 255 }, guiAlpha)); + } + else DrawRectangleGradientV((int)bounds.x, (int)bounds.y, (int)bounds.width, (int)bounds.height, Fade(Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.1f), guiAlpha), Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), guiAlpha)); + + GuiDrawRectangle(bounds, GuiGetStyle(COLORPICKER, BORDER_WIDTH), GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3)), BLANK); + + // Draw hue bar: selector + GuiDrawRectangle(selector, 0, BLANK, GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3))); + //-------------------------------------------------------------------- + + return result; +} + +// Color Picker control +// NOTE: It's divided in multiple controls: +// Color GuiColorPanel(Rectangle bounds, Color color) +// float GuiColorBarAlpha(Rectangle bounds, float alpha) +// float GuiColorBarHue(Rectangle bounds, float value) +// NOTE: bounds define GuiColorPanel() size +// NOTE: this picker converts RGB to HSV, which can cause the Hue control to jump. If you have this problem, consider using the HSV variant instead +int GuiColorPicker(Rectangle bounds, const char *text, Color *color) +{ + int result = 0; + + Color temp = { 200, 0, 0, 255 }; + if (color == NULL) color = &temp; + + GuiColorPanel(bounds, NULL, color); + + Rectangle boundsHue = { (float)bounds.x + bounds.width + GuiGetStyle(COLORPICKER, HUEBAR_PADDING), (float)bounds.y, (float)GuiGetStyle(COLORPICKER, HUEBAR_WIDTH), (float)bounds.height }; + //Rectangle boundsAlpha = { bounds.x, bounds.y + bounds.height + GuiGetStyle(COLORPICKER, BARS_PADDING), bounds.width, GuiGetStyle(COLORPICKER, BARS_THICK) }; + + // NOTE: this conversion can cause low hue-resolution, if the r, g and b value are very similar, which causes the hue bar to shift around when only the GuiColorPanel is used + Vector3 hsv = ConvertRGBtoHSV(RAYGUI_CLITERAL(Vector3){ (*color).r/255.0f, (*color).g/255.0f, (*color).b/255.0f }); + + GuiColorBarHue(boundsHue, NULL, &hsv.x); + + //color.a = (unsigned char)(GuiColorBarAlpha(boundsAlpha, (float)color.a/255.0f)*255.0f); + Vector3 rgb = ConvertHSVtoRGB(hsv); + + *color = RAYGUI_CLITERAL(Color){ (unsigned char)roundf(rgb.x*255.0f), (unsigned char)roundf(rgb.y*255.0f), (unsigned char)roundf(rgb.z*255.0f), (*color).a }; + + return result; +} + +// Color Picker control that avoids conversion to RGB and back to HSV on each call, thus avoiding jittering +// The user can call ConvertHSVtoRGB() to convert *colorHsv value to RGB +// NOTE: It's divided in multiple controls: +// int GuiColorPanelHSV(Rectangle bounds, const char *text, Vector3 *colorHsv) +// int GuiColorBarAlpha(Rectangle bounds, const char *text, float *alpha) +// float GuiColorBarHue(Rectangle bounds, float value) +// NOTE: bounds define GuiColorPanelHSV() size +int GuiColorPickerHSV(Rectangle bounds, const char *text, Vector3 *colorHsv) +{ + int result = 0; + + Vector3 tempHsv = { 0 }; + + if (colorHsv == NULL) + { + const Vector3 tempColor = { 200.0f/255.0f, 0.0f, 0.0f }; + tempHsv = ConvertRGBtoHSV(tempColor); + colorHsv = &tempHsv; + } + + GuiColorPanelHSV(bounds, NULL, colorHsv); + + const Rectangle boundsHue = { (float)bounds.x + bounds.width + GuiGetStyle(COLORPICKER, HUEBAR_PADDING), (float)bounds.y, (float)GuiGetStyle(COLORPICKER, HUEBAR_WIDTH), (float)bounds.height }; + + GuiColorBarHue(boundsHue, NULL, &colorHsv->x); + + return result; +} + +// Color Panel control - HSV variant +int GuiColorPanelHSV(Rectangle bounds, const char *text, Vector3 *colorHsv) +{ + int result = 0; + GuiState state = guiState; + Vector2 pickerSelector = { 0 }; + + const Color colWhite = { 255, 255, 255, 255 }; + const Color colBlack = { 0, 0, 0, 255 }; + + pickerSelector.x = bounds.x + (float)colorHsv->y*bounds.width; // HSV: Saturation + pickerSelector.y = bounds.y + (1.0f - (float)colorHsv->z)*bounds.height; // HSV: Value + + Vector3 maxHue = { colorHsv->x, 1.0f, 1.0f }; + Vector3 rgbHue = ConvertHSVtoRGB(maxHue); + Color maxHueCol = { (unsigned char)(255.0f*rgbHue.x), + (unsigned char)(255.0f*rgbHue.y), + (unsigned char)(255.0f*rgbHue.z), 255 }; + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds + { + if (GUI_BUTTON_DOWN) + { + if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec)) + { + pickerSelector = mousePoint; + + if (pickerSelector.x < bounds.x) pickerSelector.x = bounds.x; + if (pickerSelector.x > bounds.x + bounds.width) pickerSelector.x = bounds.x + bounds.width; + if (pickerSelector.y < bounds.y) pickerSelector.y = bounds.y; + if (pickerSelector.y > bounds.y + bounds.height) pickerSelector.y = bounds.y + bounds.height; + + // Calculate color from picker + Vector2 colorPick = { pickerSelector.x - bounds.x, pickerSelector.y - bounds.y }; + + colorPick.x /= (float)bounds.width; // Get normalized value on x + colorPick.y /= (float)bounds.height; // Get normalized value on y + + colorHsv->y = colorPick.x; + colorHsv->z = 1.0f - colorPick.y; + + } + } + else + { + guiControlExclusiveMode = false; + guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 }; + } + } + else if (CheckCollisionPointRec(mousePoint, bounds)) + { + if (GUI_BUTTON_DOWN) + { + state = STATE_PRESSED; + guiControlExclusiveMode = true; + guiControlExclusiveRec = bounds; + pickerSelector = mousePoint; + + // Calculate color from picker + Vector2 colorPick = { pickerSelector.x - bounds.x, pickerSelector.y - bounds.y }; + + colorPick.x /= (float)bounds.width; // Get normalized value on x + colorPick.y /= (float)bounds.height; // Get normalized value on y + + colorHsv->y = colorPick.x; + colorHsv->z = 1.0f - colorPick.y; + } + else state = STATE_FOCUSED; + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (state != STATE_DISABLED) + { + DrawRectangleGradientEx(bounds, Fade(colWhite, guiAlpha), Fade(colWhite, guiAlpha), Fade(maxHueCol, guiAlpha), Fade(maxHueCol, guiAlpha)); + DrawRectangleGradientEx(bounds, Fade(colBlack, 0), Fade(colBlack, guiAlpha), Fade(colBlack, guiAlpha), Fade(colBlack, 0)); + + // Draw color picker: selector + Rectangle selector = { pickerSelector.x - GuiGetStyle(COLORPICKER, COLOR_SELECTOR_SIZE)/2, pickerSelector.y - GuiGetStyle(COLORPICKER, COLOR_SELECTOR_SIZE)/2, (float)GuiGetStyle(COLORPICKER, COLOR_SELECTOR_SIZE), (float)GuiGetStyle(COLORPICKER, COLOR_SELECTOR_SIZE) }; + GuiDrawRectangle(selector, 0, BLANK, colWhite); + } + else + { + DrawRectangleGradientEx(bounds, Fade(Fade(GetColor(GuiGetStyle(COLORPICKER, BASE_COLOR_DISABLED)), 0.1f), guiAlpha), Fade(Fade(colBlack, 0.6f), guiAlpha), Fade(Fade(colBlack, 0.6f), guiAlpha), Fade(Fade(GetColor(GuiGetStyle(COLORPICKER, BORDER_COLOR_DISABLED)), 0.6f), guiAlpha)); + } + + GuiDrawRectangle(bounds, GuiGetStyle(COLORPICKER, BORDER_WIDTH), GetColor(GuiGetStyle(COLORPICKER, BORDER + state*3)), BLANK); + //-------------------------------------------------------------------- + + return result; +} + +// Message Box control +int GuiMessageBox(Rectangle bounds, const char *title, const char *message, const char *buttons) +{ + #if !defined(RAYGUI_MESSAGEBOX_BUTTON_HEIGHT) + #define RAYGUI_MESSAGEBOX_BUTTON_HEIGHT 24 + #endif + #if !defined(RAYGUI_MESSAGEBOX_BUTTON_PADDING) + #define RAYGUI_MESSAGEBOX_BUTTON_PADDING 12 + #endif + + int result = -1; // Returns clicked button from buttons list, 0 refers to closed window button + + int buttonCount = 0; + char **buttonsText = GuiTextSplit(buttons, ';', &buttonCount, NULL); + Rectangle buttonBounds = { 0 }; + buttonBounds.x = bounds.x + RAYGUI_MESSAGEBOX_BUTTON_PADDING; + buttonBounds.y = bounds.y + bounds.height - RAYGUI_MESSAGEBOX_BUTTON_HEIGHT - RAYGUI_MESSAGEBOX_BUTTON_PADDING; + buttonBounds.width = (bounds.width - RAYGUI_MESSAGEBOX_BUTTON_PADDING*(buttonCount + 1))/buttonCount; + buttonBounds.height = RAYGUI_MESSAGEBOX_BUTTON_HEIGHT; + + //int textWidth = GuiGetTextWidth(message) + 2; + + Rectangle textBounds = { 0 }; + textBounds.x = bounds.x + RAYGUI_MESSAGEBOX_BUTTON_PADDING; + textBounds.y = bounds.y + RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT + RAYGUI_MESSAGEBOX_BUTTON_PADDING; + textBounds.width = bounds.width - RAYGUI_MESSAGEBOX_BUTTON_PADDING*2; + textBounds.height = bounds.height - RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - 3*RAYGUI_MESSAGEBOX_BUTTON_PADDING - RAYGUI_MESSAGEBOX_BUTTON_HEIGHT; + + // Draw control + //-------------------------------------------------------------------- + if (GuiWindowBox(bounds, title)) result = 0; + + int prevTextAlignment = GuiGetStyle(LABEL, TEXT_ALIGNMENT); + GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + GuiLabel(textBounds, message); + GuiSetStyle(LABEL, TEXT_ALIGNMENT, prevTextAlignment); + + prevTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + + for (int i = 0; i < buttonCount; i++) + { + if (GuiButton(buttonBounds, buttonsText[i])) result = i + 1; + buttonBounds.x += (buttonBounds.width + RAYGUI_MESSAGEBOX_BUTTON_PADDING); + } + + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, prevTextAlignment); + //-------------------------------------------------------------------- + + return result; +} + +// Text Input Box control, ask for text +int GuiTextInputBox(Rectangle bounds, const char *title, const char *message, const char *buttons, char *text, int textMaxSize, bool *secretViewActive) +{ + #if !defined(RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT) + #define RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT 24 + #endif + #if !defined(RAYGUI_TEXTINPUTBOX_BUTTON_PADDING) + #define RAYGUI_TEXTINPUTBOX_BUTTON_PADDING 12 + #endif + #if !defined(RAYGUI_TEXTINPUTBOX_HEIGHT) + #define RAYGUI_TEXTINPUTBOX_HEIGHT 26 + #endif + + // Used to enable text edit mode + // WARNING: No more than one GuiTextInputBox() should be open at the same time + static bool textEditMode = false; + + int result = -1; + + int buttonCount = 0; + char **buttonsText = GuiTextSplit(buttons, ';', &buttonCount, NULL); + Rectangle buttonBounds = { 0 }; + buttonBounds.x = bounds.x + RAYGUI_TEXTINPUTBOX_BUTTON_PADDING; + buttonBounds.y = bounds.y + bounds.height - RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT - RAYGUI_TEXTINPUTBOX_BUTTON_PADDING; + buttonBounds.width = (bounds.width - RAYGUI_TEXTINPUTBOX_BUTTON_PADDING*(buttonCount + 1))/buttonCount; + buttonBounds.height = RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT; + + int messageInputHeight = (int)bounds.height - RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - GuiGetStyle(STATUSBAR, BORDER_WIDTH) - RAYGUI_TEXTINPUTBOX_BUTTON_HEIGHT - 2*RAYGUI_TEXTINPUTBOX_BUTTON_PADDING; + + Rectangle textBounds = { 0 }; + if (message != NULL) + { + int textSize = GuiGetTextWidth(message) + 2; + + textBounds.x = bounds.x + bounds.width/2 - textSize/2; + textBounds.y = bounds.y + RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT + messageInputHeight/4 - (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/2; + textBounds.width = (float)textSize; + textBounds.height = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + } + + Rectangle textBoxBounds = { 0 }; + textBoxBounds.x = bounds.x + RAYGUI_TEXTINPUTBOX_BUTTON_PADDING; + textBoxBounds.y = bounds.y + RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT - RAYGUI_TEXTINPUTBOX_HEIGHT/2; + if (message == NULL) textBoxBounds.y = bounds.y + 24 + RAYGUI_TEXTINPUTBOX_BUTTON_PADDING; + else textBoxBounds.y += (messageInputHeight/2 + messageInputHeight/4); + textBoxBounds.width = bounds.width - RAYGUI_TEXTINPUTBOX_BUTTON_PADDING*2; + textBoxBounds.height = RAYGUI_TEXTINPUTBOX_HEIGHT; + + // Draw control + //-------------------------------------------------------------------- + if (GuiWindowBox(bounds, title)) result = 0; + + // Draw message if available + if (message != NULL) + { + int prevTextAlignment = GuiGetStyle(LABEL, TEXT_ALIGNMENT); + GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + GuiLabel(textBounds, message); + GuiSetStyle(LABEL, TEXT_ALIGNMENT, prevTextAlignment); + } + + int prevTextBoxAlignment = GuiGetStyle(TEXTBOX, TEXT_ALIGNMENT); + GuiSetStyle(TEXTBOX, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + + if (secretViewActive != NULL) + { + static char stars[] = "****************"; + if (GuiTextBox(RAYGUI_CLITERAL(Rectangle){ textBoxBounds.x, textBoxBounds.y, textBoxBounds.width - 4 - RAYGUI_TEXTINPUTBOX_HEIGHT, textBoxBounds.height }, + ((*secretViewActive == 1) || textEditMode)? text : stars, textMaxSize, textEditMode)) textEditMode = !textEditMode; + +#if defined(RAYGUI_NO_ICONS) + GuiToggle(RAYGUI_CLITERAL(Rectangle){ textBoxBounds.x + textBoxBounds.width - RAYGUI_TEXTINPUTBOX_HEIGHT, textBoxBounds.y, RAYGUI_TEXTINPUTBOX_HEIGHT, RAYGUI_TEXTINPUTBOX_HEIGHT }, + (*secretViewActive == 1)? "O" : "*", secretViewActive); +#else + GuiToggle(RAYGUI_CLITERAL(Rectangle){ textBoxBounds.x + textBoxBounds.width - RAYGUI_TEXTINPUTBOX_HEIGHT, textBoxBounds.y, RAYGUI_TEXTINPUTBOX_HEIGHT, RAYGUI_TEXTINPUTBOX_HEIGHT }, + (*secretViewActive == 1)? GuiIconText(ICON_EYE_ON, NULL) : GuiIconText(ICON_EYE_OFF, NULL), secretViewActive); +#endif + } + else + { + if (GuiTextBox(textBoxBounds, text, textMaxSize, textEditMode)) textEditMode = !textEditMode; + } + + GuiSetStyle(TEXTBOX, TEXT_ALIGNMENT, prevTextBoxAlignment); + + int prevBtnTextAlignment = GuiGetStyle(BUTTON, TEXT_ALIGNMENT); + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + + for (int i = 0; i < buttonCount; i++) + { + if (GuiButton(buttonBounds, buttonsText[i])) result = i + 1; + buttonBounds.x += (buttonBounds.width + RAYGUI_MESSAGEBOX_BUTTON_PADDING); + } + + if (result >= 0) textEditMode = false; + + GuiSetStyle(BUTTON, TEXT_ALIGNMENT, prevBtnTextAlignment); + //-------------------------------------------------------------------- + + return result; // Result is the pressed button index +} + +// Grid control +// NOTE: Returns grid mouse-hover selected cell +// About drawing lines at subpixel spacing, simple put, not easy solution: +// REF: https://stackoverflow.com/questions/4435450/2d-opengl-drawing-lines-that-dont-exactly-fit-pixel-raster +int GuiGrid(Rectangle bounds, const char *text, float spacing, int subdivs, Vector2 *mouseCell) +{ + // Grid lines alpha amount + #if !defined(RAYGUI_GRID_ALPHA) + #define RAYGUI_GRID_ALPHA 0.15f + #endif + + int result = 0; + GuiState state = guiState; + + Vector2 mousePoint = GUI_POINTER_POSITION; + Vector2 currentMouseCell = { -1, -1 }; + + float spaceWidth = spacing/(float)subdivs; + int linesV = (int)(bounds.width/spaceWidth) + 1; + int linesH = (int)(bounds.height/spaceWidth) + 1; + + int color = GuiGetStyle(DEFAULT, LINE_COLOR); + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked && !guiControlExclusiveMode) + { + if (CheckCollisionPointRec(mousePoint, bounds)) + { + // NOTE: Cell values must be the upper left of the cell the mouse is in + currentMouseCell.x = floorf((mousePoint.x - bounds.x)/spacing); + currentMouseCell.y = floorf((mousePoint.y - bounds.y)/spacing); + } + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + if (state == STATE_DISABLED) color = GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED); + + if (subdivs > 0) + { + // Draw vertical grid lines + for (int i = 0; i < linesV; i++) + { + Rectangle lineV = { bounds.x + spacing*i/subdivs, bounds.y, 1, bounds.height + 1 }; + GuiDrawRectangle(lineV, 0, BLANK, ((i%subdivs) == 0)? GuiFade(GetColor(color), RAYGUI_GRID_ALPHA*4) : GuiFade(GetColor(color), RAYGUI_GRID_ALPHA)); + } + + // Draw horizontal grid lines + for (int i = 0; i < linesH; i++) + { + Rectangle lineH = { bounds.x, bounds.y + spacing*i/subdivs, bounds.width + 1, 1 }; + GuiDrawRectangle(lineH, 0, BLANK, ((i%subdivs) == 0)? GuiFade(GetColor(color), RAYGUI_GRID_ALPHA*4) : GuiFade(GetColor(color), RAYGUI_GRID_ALPHA)); + } + } + + if (mouseCell != NULL) *mouseCell = currentMouseCell; + return result; +} + +//---------------------------------------------------------------------------------- +// Tooltip management functions +// NOTE: Tooltips requires some global variables: tooltipPtr +//---------------------------------------------------------------------------------- +// Enable gui tooltips (global state) +void GuiEnableTooltip(void) { guiTooltip = true; } + +// Disable gui tooltips (global state) +void GuiDisableTooltip(void) { guiTooltip = false; } + +// Set tooltip string +void GuiSetTooltip(const char *tooltip) { guiTooltipPtr = tooltip; } + +//---------------------------------------------------------------------------------- +// Styles loading functions +//---------------------------------------------------------------------------------- + +// Load raygui style file (.rgs) +// NOTE: By default a binary file is expected, that file could contain a custom font, +// in that case, custom font image atlas is GRAY+ALPHA and pixel data can be compressed (DEFLATE) +void GuiLoadStyle(const char *fileName) +{ + #define MAX_LINE_BUFFER_SIZE 256 + + bool tryBinary = false; + if (!guiStyleLoaded) GuiLoadStyleDefault(); + + // Try reading the files as text file first + FILE *rgsFile = fopen(fileName, "rt"); + + if (rgsFile != NULL) + { + char buffer[MAX_LINE_BUFFER_SIZE] = { 0 }; + fgets(buffer, MAX_LINE_BUFFER_SIZE, rgsFile); + + if (buffer[0] == '#') + { + int controlId = 0; + int propertyId = 0; + unsigned int propertyValue = 0; + + while (!feof(rgsFile)) + { + switch (buffer[0]) + { + case 'p': + { + // Style property: p + + sscanf(buffer, "p %d %d 0x%x", &controlId, &propertyId, &propertyValue); + GuiSetStyle(controlId, propertyId, (int)propertyValue); + + } break; + case 'f': + { + // Style font: f + + int fontSize = 0; + char charmapFileName[256] = { 0 }; + char fontFileName[256] = { 0 }; + sscanf(buffer, "f %d %s %[^\r\n]s", &fontSize, charmapFileName, fontFileName); + + Font font = { 0 }; + int *codepoints = NULL; + int codepointCount = 0; + + if (charmapFileName[0] != '0') + { + // Load text data from file + // NOTE: Expected an UTF-8 array of codepoints, no separation + char *textData = LoadFileText(TextFormat("%s/%s", GetDirectoryPath(fileName), charmapFileName)); + codepoints = LoadCodepoints(textData, &codepointCount); + UnloadFileText(textData); + } + + if (fontFileName[0] != '\0') + { + // In case a font is already loaded and it is not default internal font, unload it + if (font.texture.id != GetFontDefault().texture.id) UnloadTexture(font.texture); + + if (codepointCount > 0) font = LoadFontEx(TextFormat("%s/%s", GetDirectoryPath(fileName), fontFileName), fontSize, codepoints, codepointCount); + else font = LoadFontEx(TextFormat("%s/%s", GetDirectoryPath(fileName), fontFileName), fontSize, NULL, 0); // Default to 95 standard codepoints + } + + // If font texture not properly loaded, revert to default font and size/spacing + if (font.texture.id == 0) + { + font = GetFontDefault(); + GuiSetStyle(DEFAULT, TEXT_SIZE, 10); + GuiSetStyle(DEFAULT, TEXT_SPACING, 1); + } + + UnloadCodepoints(codepoints); + + if ((font.texture.id > 0) && (font.glyphCount > 0)) GuiSetFont(font); + + } break; + default: break; + } + + fgets(buffer, MAX_LINE_BUFFER_SIZE, rgsFile); + } + } + else tryBinary = true; + + fclose(rgsFile); + } + + if (tryBinary) + { + rgsFile = fopen(fileName, "rb"); + + if (rgsFile != NULL) + { + fseek(rgsFile, 0, SEEK_END); + int fileDataSize = ftell(rgsFile); + fseek(rgsFile, 0, SEEK_SET); + + if (fileDataSize > 0) + { + unsigned char *fileData = (unsigned char *)RAYGUI_CALLOC(fileDataSize, sizeof(unsigned char)); + if (fileData != NULL) + { + fread(fileData, sizeof(unsigned char), fileDataSize, rgsFile); + + GuiLoadStyleFromMemory(fileData, fileDataSize); + + RAYGUI_FREE(fileData); + } + } + + fclose(rgsFile); + } + } +} + +// Load style from memory +// WARNING: Binary files only +void GuiLoadStyleFromMemory(const unsigned char *fileData, int dataSize) +{ + unsigned char *fileDataPtr = (unsigned char *)fileData; + + char signature[5] = { 0 }; + short version = 0; + short reserved = 0; + int propertyCount = 0; + + memcpy(signature, fileDataPtr, 4); + memcpy(&version, fileDataPtr + 4, sizeof(short)); + memcpy(&reserved, fileDataPtr + 4 + 2, sizeof(short)); + memcpy(&propertyCount, fileDataPtr + 4 + 2 + 2, sizeof(int)); + fileDataPtr += 12; + + if ((signature[0] == 'r') && + (signature[1] == 'G') && + (signature[2] == 'S') && + (signature[3] == ' ')) + { + short controlId = 0; + short propertyId = 0; + unsigned int propertyValue = 0; + + for (int i = 0; i < propertyCount; i++) + { + memcpy(&controlId, fileDataPtr, sizeof(short)); + memcpy(&propertyId, fileDataPtr + 2, sizeof(short)); + memcpy(&propertyValue, fileDataPtr + 2 + 2, sizeof(unsigned int)); + fileDataPtr += 8; + + if (controlId == 0) // DEFAULT control + { + // If a DEFAULT property is loaded, it is propagated to all controls + // NOTE: All DEFAULT properties should be defined first in the file + GuiSetStyle(0, (int)propertyId, propertyValue); + + if (propertyId < RAYGUI_MAX_PROPS_BASE) for (int j = 1; j < RAYGUI_MAX_CONTROLS; j++) GuiSetStyle(j, (int)propertyId, propertyValue); + } + else GuiSetStyle((int)controlId, (int)propertyId, propertyValue); + } + + // Font loading is highly dependant on raylib API to load font data and image + +#if !defined(RAYGUI_STANDALONE) + // Load custom font if available + int fontDataSize = 0; + memcpy(&fontDataSize, fileDataPtr, sizeof(int)); + fileDataPtr += 4; + + if (fontDataSize > 0) + { + Font font = { 0 }; + int fontType = 0; // 0-Normal, 1-SDF + + memcpy(&font.baseSize, fileDataPtr, sizeof(int)); + memcpy(&font.glyphCount, fileDataPtr + 4, sizeof(int)); + memcpy(&fontType, fileDataPtr + 4 + 4, sizeof(int)); + fileDataPtr += 12; + + // Load font white rectangle + Rectangle fontWhiteRec = { 0 }; + memcpy(&fontWhiteRec, fileDataPtr, sizeof(Rectangle)); + fileDataPtr += 16; + + // Load font image parameters + int fontImageUncompSize = 0; + int fontImageCompSize = 0; + memcpy(&fontImageUncompSize, fileDataPtr, sizeof(int)); + memcpy(&fontImageCompSize, fileDataPtr + 4, sizeof(int)); + fileDataPtr += 8; + + Image imFont = { 0 }; + imFont.mipmaps = 1; + memcpy(&imFont.width, fileDataPtr, sizeof(int)); + memcpy(&imFont.height, fileDataPtr + 4, sizeof(int)); + memcpy(&imFont.format, fileDataPtr + 4 + 4, sizeof(int)); + fileDataPtr += 12; + + if ((fontImageCompSize > 0) && (fontImageCompSize != fontImageUncompSize)) + { + // Compressed font atlas image data (DEFLATE), it requires DecompressData() + int dataUncompSize = 0; + unsigned char *compData = (unsigned char *)RAYGUI_CALLOC(fontImageCompSize, sizeof(unsigned char)); + memcpy(compData, fileDataPtr, fontImageCompSize); + fileDataPtr += fontImageCompSize; + + imFont.data = DecompressData(compData, fontImageCompSize, &dataUncompSize); + + // Security check, dataUncompSize must match the provided fontImageUncompSize + if (dataUncompSize != fontImageUncompSize) RAYGUI_LOG("WARNING: Uncompressed font atlas image data could be corrupted"); + + RAYGUI_FREE(compData); + } + else + { + // Font atlas image data is not compressed + imFont.data = (unsigned char *)RAYGUI_CALLOC(fontImageUncompSize, sizeof(unsigned char)); + memcpy(imFont.data, fileDataPtr, fontImageUncompSize); + fileDataPtr += fontImageUncompSize; + } + + if (font.texture.id != GetFontDefault().texture.id) UnloadTexture(font.texture); + font.texture = LoadTextureFromImage(imFont); + + RAYGUI_FREE(imFont.data); + + // Validate font atlas texture was loaded correctly + if (font.texture.id != 0) + { + // Load font recs data + int recsDataSize = font.glyphCount*sizeof(Rectangle); + int recsDataCompressedSize = 0; + + // WARNING: Version 400 adds the compression size parameter + if (version >= 400) + { + // RGS files version 400 support compressed recs data + memcpy(&recsDataCompressedSize, fileDataPtr, sizeof(int)); + fileDataPtr += sizeof(int); + } + + if ((recsDataCompressedSize > 0) && (recsDataCompressedSize != recsDataSize)) + { + // Recs data is compressed, uncompress it + unsigned char *recsDataCompressed = (unsigned char *)RAYGUI_CALLOC(recsDataCompressedSize, sizeof(unsigned char)); + + memcpy(recsDataCompressed, fileDataPtr, recsDataCompressedSize); + fileDataPtr += recsDataCompressedSize; + + int recsDataUncompSize = 0; + font.recs = (Rectangle *)DecompressData(recsDataCompressed, recsDataCompressedSize, &recsDataUncompSize); + + // Security check, data uncompressed size must match the expected original data size + if (recsDataUncompSize != recsDataSize) RAYGUI_LOG("WARNING: Uncompressed font recs data could be corrupted"); + + RAYGUI_FREE(recsDataCompressed); + } + else + { + // Recs data is uncompressed + font.recs = (Rectangle *)RAYGUI_CALLOC(font.glyphCount, sizeof(Rectangle)); + for (int i = 0; i < font.glyphCount; i++) + { + memcpy(&font.recs[i], fileDataPtr, sizeof(Rectangle)); + fileDataPtr += sizeof(Rectangle); + } + } + + // Load font glyphs info data + int glyphsDataSize = font.glyphCount*16; // 16 bytes data per glyph + int glyphsDataCompressedSize = 0; + + // WARNING: Version 400 adds the compression size parameter + if (version >= 400) + { + // RGS files version 400 support compressed glyphs data + memcpy(&glyphsDataCompressedSize, fileDataPtr, sizeof(int)); + fileDataPtr += sizeof(int); + } + + // Allocate required glyphs space to fill with data + font.glyphs = (GlyphInfo *)RAYGUI_CALLOC(font.glyphCount, sizeof(GlyphInfo)); + + if ((glyphsDataCompressedSize > 0) && (glyphsDataCompressedSize != glyphsDataSize)) + { + // Glyphs data is compressed, uncompress it + unsigned char *glypsDataCompressed = (unsigned char *)RAYGUI_CALLOC(glyphsDataCompressedSize, sizeof(unsigned char)); + + memcpy(glypsDataCompressed, fileDataPtr, glyphsDataCompressedSize); + fileDataPtr += glyphsDataCompressedSize; + + int glyphsDataUncompSize = 0; + unsigned char *glyphsDataUncomp = DecompressData(glypsDataCompressed, glyphsDataCompressedSize, &glyphsDataUncompSize); + + // Security check, data uncompressed size must match the expected original data size + if (glyphsDataUncompSize != glyphsDataSize) RAYGUI_LOG("WARNING: Uncompressed font glyphs data could be corrupted"); + + unsigned char *glyphsDataUncompPtr = glyphsDataUncomp; + + for (int i = 0; i < font.glyphCount; i++) + { + memcpy(&font.glyphs[i].value, glyphsDataUncompPtr, sizeof(int)); + memcpy(&font.glyphs[i].offsetX, glyphsDataUncompPtr + 4, sizeof(int)); + memcpy(&font.glyphs[i].offsetY, glyphsDataUncompPtr + 8, sizeof(int)); + memcpy(&font.glyphs[i].advanceX, glyphsDataUncompPtr + 12, sizeof(int)); + glyphsDataUncompPtr += 16; + } + + RAYGUI_FREE(glypsDataCompressed); + RAYGUI_FREE(glyphsDataUncomp); + } + else + { + // Glyphs data is uncompressed + for (int i = 0; i < font.glyphCount; i++) + { + memcpy(&font.glyphs[i].value, fileDataPtr, sizeof(int)); + memcpy(&font.glyphs[i].offsetX, fileDataPtr + 4, sizeof(int)); + memcpy(&font.glyphs[i].offsetY, fileDataPtr + 8, sizeof(int)); + memcpy(&font.glyphs[i].advanceX, fileDataPtr + 12, sizeof(int)); + fileDataPtr += 16; + } + } + } + else font = GetFontDefault(); // Fallback in case of errors loading font atlas texture + + GuiSetFont(font); + + // Set font texture source rectangle to be used as white texture to draw shapes + // NOTE: It makes possible to draw shapes and text (full UI) in a single draw call + if ((fontWhiteRec.x > 0) && + (fontWhiteRec.y > 0) && + (fontWhiteRec.width > 0) && + (fontWhiteRec.height > 0)) SetShapesTexture(font.texture, fontWhiteRec); + } +#endif + } +} + +// Load style default over global style +void GuiLoadStyleDefault(void) +{ + // Setting this flag first to avoid cyclic function calls + // when calling GuiSetStyle() and GuiGetStyle() + guiStyleLoaded = true; + + // Initialize default LIGHT style property values + // WARNING: Default value are applied to all controls on set but + // they can be overwritten later on for every custom control + GuiSetStyle(DEFAULT, BORDER_COLOR_NORMAL, 0x838383ff); + GuiSetStyle(DEFAULT, BASE_COLOR_NORMAL, 0xc9c9c9ff); + GuiSetStyle(DEFAULT, TEXT_COLOR_NORMAL, 0x686868ff); + GuiSetStyle(DEFAULT, BORDER_COLOR_FOCUSED, 0x5bb2d9ff); + GuiSetStyle(DEFAULT, BASE_COLOR_FOCUSED, 0xc9effeff); + GuiSetStyle(DEFAULT, TEXT_COLOR_FOCUSED, 0x6c9bbcff); + GuiSetStyle(DEFAULT, BORDER_COLOR_PRESSED, 0x0492c7ff); + GuiSetStyle(DEFAULT, BASE_COLOR_PRESSED, 0x97e8ffff); + GuiSetStyle(DEFAULT, TEXT_COLOR_PRESSED, 0x368bafff); + GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, 0xb5c1c2ff); + GuiSetStyle(DEFAULT, BASE_COLOR_DISABLED, 0xe6e9e9ff); + GuiSetStyle(DEFAULT, TEXT_COLOR_DISABLED, 0xaeb7b8ff); + GuiSetStyle(DEFAULT, BORDER_WIDTH, 1); + GuiSetStyle(DEFAULT, TEXT_PADDING, 0); + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + + // Initialize default extended property values + // NOTE: By default, extended property values are initialized to 0 + GuiSetStyle(DEFAULT, TEXT_SIZE, 10); // DEFAULT, shared by all controls + GuiSetStyle(DEFAULT, TEXT_SPACING, 1); // DEFAULT, shared by all controls + GuiSetStyle(DEFAULT, LINE_COLOR, 0x90abb5ff); // DEFAULT specific property + GuiSetStyle(DEFAULT, BACKGROUND_COLOR, 0xf5f5f5ff); // DEFAULT specific property + GuiSetStyle(DEFAULT, TEXT_LINE_SPACING, 5); // DEFAULT, pixels between lines, from bottom of first line to top of second + GuiSetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL, TEXT_ALIGN_MIDDLE); // DEFAULT, text aligned vertically to middle of text-bounds + + // Initialize control-specific property values + // NOTE: Those properties are in default list but require specific values by control type + GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + GuiSetStyle(BUTTON, BORDER_WIDTH, 2); + GuiSetStyle(SLIDER, TEXT_PADDING, 4); + GuiSetStyle(PROGRESSBAR, TEXT_PADDING, 4); + GuiSetStyle(CHECKBOX, TEXT_PADDING, 4); + GuiSetStyle(CHECKBOX, TEXT_ALIGNMENT, TEXT_ALIGN_RIGHT); + GuiSetStyle(DROPDOWNBOX, TEXT_PADDING, 0); + GuiSetStyle(DROPDOWNBOX, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + GuiSetStyle(TEXTBOX, TEXT_PADDING, 4); + GuiSetStyle(TEXTBOX, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + GuiSetStyle(VALUEBOX, TEXT_PADDING, 0); + GuiSetStyle(VALUEBOX, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + GuiSetStyle(STATUSBAR, TEXT_PADDING, 8); + GuiSetStyle(STATUSBAR, TEXT_ALIGNMENT, TEXT_ALIGN_LEFT); + + // Initialize extended property values + // NOTE: By default, extended property values are initialized to 0 + GuiSetStyle(TOGGLE, GROUP_PADDING, 2); + GuiSetStyle(SLIDER, SLIDER_WIDTH, 16); + GuiSetStyle(SLIDER, SLIDER_PADDING, 1); + GuiSetStyle(PROGRESSBAR, PROGRESS_PADDING, 1); + GuiSetStyle(CHECKBOX, CHECK_PADDING, 1); + GuiSetStyle(COMBOBOX, COMBO_BUTTON_WIDTH, 32); + GuiSetStyle(COMBOBOX, COMBO_BUTTON_SPACING, 2); + GuiSetStyle(DROPDOWNBOX, ARROW_PADDING, 16); + GuiSetStyle(DROPDOWNBOX, DROPDOWN_ITEMS_SPACING, 2); + GuiSetStyle(VALUEBOX, SPINNER_BUTTON_WIDTH, 24); + GuiSetStyle(VALUEBOX, SPINNER_BUTTON_SPACING, 2); + GuiSetStyle(SCROLLBAR, BORDER_WIDTH, 0); + GuiSetStyle(SCROLLBAR, ARROWS_VISIBLE, 0); + GuiSetStyle(SCROLLBAR, ARROWS_SIZE, 6); + GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING, 0); + GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, 16); + GuiSetStyle(SCROLLBAR, SCROLL_PADDING, 0); + GuiSetStyle(SCROLLBAR, SCROLL_SPEED, 12); + GuiSetStyle(LISTVIEW, LIST_ITEMS_HEIGHT, 28); + GuiSetStyle(LISTVIEW, LIST_ITEMS_SPACING, 2); + GuiSetStyle(LISTVIEW, LIST_ITEMS_BORDER_WIDTH, 1); + GuiSetStyle(LISTVIEW, SCROLLBAR_WIDTH, 12); + GuiSetStyle(LISTVIEW, SCROLLBAR_SIDE, SCROLLBAR_RIGHT_SIDE); + GuiSetStyle(COLORPICKER, COLOR_SELECTOR_SIZE, 8); + GuiSetStyle(COLORPICKER, HUEBAR_WIDTH, 16); + GuiSetStyle(COLORPICKER, HUEBAR_PADDING, 8); + GuiSetStyle(COLORPICKER, HUEBAR_SELECTOR_HEIGHT, 8); + GuiSetStyle(COLORPICKER, HUEBAR_SELECTOR_OVERFLOW, 2); + + if (guiFont.texture.id != GetFontDefault().texture.id) + { + // Unload previous font texture + UnloadTexture(guiFont.texture); + RAYGUI_FREE(guiFont.recs); + RAYGUI_FREE(guiFont.glyphs); + guiFont.recs = NULL; + guiFont.glyphs = NULL; + + // Setup default raylib font + guiFont = GetFontDefault(); + + // NOTE: Default raylib font character 95 is a white square + Rectangle whiteChar = guiFont.recs[95]; + + // NOTE: Setting up a 1px padding on char rectangle to avoid pixel bleeding on MSAA filtering + SetShapesTexture(guiFont.texture, RAYGUI_CLITERAL(Rectangle){ whiteChar.x + 1, whiteChar.y + 1, whiteChar.width - 2, whiteChar.height - 2 }); + } +} + +// Get text with icon id prepended +// NOTE: Useful to add icons by name id (enum) instead of +// a number that can change between ricon versions +const char *GuiIconText(int iconId, const char *text) +{ +#if defined(RAYGUI_NO_ICONS) + return NULL; +#else + static char buffer[1024] = { 0 }; + static char iconBuffer[16] = { 0 }; + + if (text != NULL) + { + memset(buffer, 0, 1024); + snprintf(buffer, 1024, "#%03i#", iconId); + + for (int i = 5; i < 1024; i++) + { + buffer[i] = text[i - 5]; + if (text[i - 5] == '\0') break; + } + + return buffer; + } + else + { + snprintf(iconBuffer, 16, "#%03i#", iconId); + + return iconBuffer; + } +#endif +} + +#if !defined(RAYGUI_NO_ICONS) +// Get full icons data pointer +unsigned int *GuiGetIcons(void) { return guiIconsPtr; } + +// Load raygui icons file (.rgi) +// NOTE: In case nameIds are required, they can be requested with loadIconsName, +// they are returned as a guiIconsName[iconCount][RAYGUI_ICON_MAX_NAME_LENGTH], +// WARNING: guiIconsName[]][] memory should be manually freed! +char **GuiLoadIcons(const char *fileName, bool loadIconsName) +{ + // Style File Structure (.rgi) + // ------------------------------------------------------ + // Offset | Size | Type | Description + // ------------------------------------------------------ + // 0 | 4 | char | Signature: "rGI " + // 4 | 2 | short | Version: 100 + // 6 | 2 | short | reserved + + // 8 | 2 | short | Num icons (N) + // 10 | 2 | short | Icons size (Options: 16, 32, 64) (S) + + // Icons name id (32 bytes per name id) + // foreach (icon) + // { + // 12+32*i | 32 | char | Icon NameId + // } + + // Icons data: One bit per pixel, stored as unsigned int array (depends on icon size) + // S*S pixels/32bit per unsigned int = K unsigned int per icon + // foreach (icon) + // { + // ... | K | unsigned int | Icon Data + // } + + FILE *rgiFile = fopen(fileName, "rb"); + + char **guiIconsName = NULL; + + if (rgiFile != NULL) + { + char signature[5] = { 0 }; + short version = 0; + short reserved = 0; + short iconCount = 0; + short iconSize = 0; + + fread(signature, 1, 4, rgiFile); + fread(&version, sizeof(short), 1, rgiFile); + fread(&reserved, sizeof(short), 1, rgiFile); + fread(&iconCount, sizeof(short), 1, rgiFile); + fread(&iconSize, sizeof(short), 1, rgiFile); + + if ((signature[0] == 'r') && + (signature[1] == 'G') && + (signature[2] == 'I') && + (signature[3] == ' ')) + { + if (loadIconsName) + { + guiIconsName = (char **)RAYGUI_CALLOC(iconCount, sizeof(char *)); + for (int i = 0; i < iconCount; i++) + { + guiIconsName[i] = (char *)RAYGUI_CALLOC(RAYGUI_ICON_MAX_NAME_LENGTH, sizeof(char)); + fread(guiIconsName[i], 1, RAYGUI_ICON_MAX_NAME_LENGTH, rgiFile); + } + } + else fseek(rgiFile, iconCount*RAYGUI_ICON_MAX_NAME_LENGTH, SEEK_CUR); + + // Read icons data directly over internal icons array + fread(guiIconsPtr, sizeof(unsigned int), (int)iconCount*((int)iconSize*(int)iconSize/32), rgiFile); + } + + fclose(rgiFile); + } + + return guiIconsName; +} + +// Load icons from memory +// WARNING: Binary files only +char **GuiLoadIconsFromMemory(const unsigned char *fileData, int dataSize, bool loadIconsName) +{ + unsigned char *fileDataPtr = (unsigned char *)fileData; + char **guiIconsName = NULL; + + char signature[5] = { 0 }; + short version = 0; + short reserved = 0; + short iconCount = 0; + short iconSize = 0; + + memcpy(signature, fileDataPtr, 4); + memcpy(&version, fileDataPtr + 4, sizeof(short)); + memcpy(&reserved, fileDataPtr + 4 + 2, sizeof(short)); + memcpy(&iconCount, fileDataPtr + 4 + 2 + 2, sizeof(short)); + memcpy(&iconSize, fileDataPtr + 4 + 2 + 2 + 2, sizeof(short)); + fileDataPtr += 12; + + if ((signature[0] == 'r') && + (signature[1] == 'G') && + (signature[2] == 'I') && + (signature[3] == ' ')) + { + if (loadIconsName) + { + guiIconsName = (char **)RAYGUI_CALLOC(iconCount, sizeof(char *)); + for (int i = 0; i < iconCount; i++) + { + guiIconsName[i] = (char *)RAYGUI_CALLOC(RAYGUI_ICON_MAX_NAME_LENGTH, sizeof(char)); + memcpy(guiIconsName[i], fileDataPtr, RAYGUI_ICON_MAX_NAME_LENGTH); + fileDataPtr += RAYGUI_ICON_MAX_NAME_LENGTH; + } + } + else + { + // Skip icon name data if not required + fileDataPtr += iconCount*RAYGUI_ICON_MAX_NAME_LENGTH; + } + + int iconDataSize = iconCount*((int)iconSize*(int)iconSize/32)*(int)sizeof(unsigned int); + guiIconsPtr = (unsigned int *)RAYGUI_CALLOC(iconDataSize, 1); + + memcpy(guiIconsPtr, fileDataPtr, iconDataSize); + } + + return guiIconsName; +} + +// Draw selected icon using rectangles pixel-by-pixel +void GuiDrawIcon(int iconId, int posX, int posY, int pixelSize, Color color) +{ + #define BIT_CHECK(a,b) ((a) & (1u<<(b))) + + for (int i = 0, y = 0; i < RAYGUI_ICON_SIZE*RAYGUI_ICON_SIZE/32; i++) + { + for (int k = 0; k < 32; k++) + { + if (BIT_CHECK(guiIconsPtr[iconId*RAYGUI_ICON_DATA_ELEMENTS + i], k)) + { + #if !defined(RAYGUI_STANDALONE) + GuiDrawRectangle(RAYGUI_CLITERAL(Rectangle){ (float)posX + (k%RAYGUI_ICON_SIZE)*pixelSize, (float)posY + y*pixelSize, (float)pixelSize, (float)pixelSize }, 0, BLANK, color); + #endif + } + + if ((k == 15) || (k == 31)) y++; + } + } +} + +// Set icon drawing size +void GuiSetIconScale(int scale) +{ + if (scale >= 1) guiIconScale = scale; +} + +#endif // !RAYGUI_NO_ICONS + +// Get text width considering gui style and icon size (if required) +int GuiGetTextWidth(const char *text) +{ + #if !defined(ICON_TEXT_PADDING) + #define ICON_TEXT_PADDING 4 + #endif + + Vector2 textSize = { 0 }; + int textIconOffset = 0; + + if ((text != NULL) && (text[0] != '\0')) + { + if (text[0] == '#') + { + for (int i = 1; (i < 5) && (text[i] != '\0'); i++) + { + if (text[i] == '#') + { + if(TextToInteger(&text[1]) < RAYGUI_ICON_MAX_ICONS) textIconOffset = i; + break; + } + } + } + + text += textIconOffset; + + // Make sure guiFont is set, GuiGetStyle() initializes it lazynessly + float fontSize = (float)GuiGetStyle(DEFAULT, TEXT_SIZE); + + // Custom MeasureText() implementation + if ((guiFont.texture.id > 0) && (text != NULL)) + { + // Get size in bytes of text, considering end of line and line break + int size = 0; + for (int i = 0; i < MAX_LINE_BUFFER_SIZE; i++) + { + if ((text[i] != '\0') && (text[i] != '\n')) size++; + else break; + } + + float scaleFactor = fontSize/(float)guiFont.baseSize; + textSize.y = (float)guiFont.baseSize*scaleFactor; + float glyphWidth = 0.0f; + + for (int i = 0, codepointSize = 0; i < size; i += codepointSize) + { + int codepoint = GetCodepointNext(&text[i], &codepointSize); + int codepointIndex = GetGlyphIndex(guiFont, codepoint); + + if (guiFont.glyphs[codepointIndex].advanceX == 0) glyphWidth = ((float)guiFont.recs[codepointIndex].width*scaleFactor); + else glyphWidth = ((float)guiFont.glyphs[codepointIndex].advanceX*scaleFactor); + + textSize.x += (glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); + } + } + + if (textIconOffset > 0) textSize.x += (RAYGUI_ICON_SIZE + ICON_TEXT_PADDING); + } + + return (int)textSize.x; +} + +//---------------------------------------------------------------------------------- +// Module Internal Functions Definition +//---------------------------------------------------------------------------------- +// Get text bounds considering control bounds +static Rectangle GetTextBounds(int control, Rectangle bounds) +{ + Rectangle textBounds = bounds; + + textBounds.x = bounds.x + GuiGetStyle(control, BORDER_WIDTH); + textBounds.y = bounds.y + GuiGetStyle(control, BORDER_WIDTH) + GuiGetStyle(control, TEXT_PADDING); + textBounds.width = bounds.width - 2*GuiGetStyle(control, BORDER_WIDTH) - 2*GuiGetStyle(control, TEXT_PADDING); + textBounds.height = bounds.height - 2*GuiGetStyle(control, BORDER_WIDTH) - 2*GuiGetStyle(control, TEXT_PADDING); // NOTE: Text is processed line per line! + + // Depending on control, TEXT_PADDING and TEXT_ALIGNMENT properties could affect the text-bounds + switch (control) + { + case COMBOBOX: + case DROPDOWNBOX: + case LISTVIEW: + // TODO: Special cases (no label): COMBOBOX, DROPDOWNBOX, LISTVIEW + case SLIDER: + case CHECKBOX: + case VALUEBOX: + case CONTROL11: + // TODO: More special cases (label on side): SLIDER, CHECKBOX, VALUEBOX, SPINNER + default: + { + // TODO: WARNING: TEXT_ALIGNMENT is already considered in GuiDrawText() + if (GuiGetStyle(control, TEXT_ALIGNMENT) == TEXT_ALIGN_RIGHT) textBounds.x -= GuiGetStyle(control, TEXT_PADDING); + else textBounds.x += GuiGetStyle(control, TEXT_PADDING); + } + break; + } + + return textBounds; +} + +// Get text icon if provided and move text cursor +// NOTE: Up to RAYGUI_ICON_MAX_ICONS supported for iconId +static const char *GetTextIcon(const char *text, int *iconId) +{ +#if !defined(RAYGUI_NO_ICONS) + *iconId = -1; + if (text[0] == '#') // Maybe an icon, if it starts with # but an ending # must be found + { + char iconValue[4] = { 0 }; // Maximum length for icon value: 3 digits + '\0' + + int pos = 1; + while ((pos < 4) && (text[pos] >= '0') && (text[pos] <= '9')) + { + iconValue[pos - 1] = text[pos]; + pos++; + } + + if (text[pos] == '#') + { + int rawIconId = TextToInteger(iconValue); + if (rawIconId < RAYGUI_ICON_MAX_ICONS) + { + *iconId = rawIconId; + + // Move text pointer after icon + // WARNING: If only icon provided, it could point to EOL character: '\0' + if (*iconId >= 0) text += (pos + 1); + } + } + } +#endif + + return text; +} + +// Get text divided into lines (by line-breaks '\n') +// WARNING: It returns pointers to new lines but it does not add NULL ('\0') terminator! +static const char **GetTextLines(const char *text, int *count) +{ + #define RAYGUI_MAX_TEXT_LINES 128 + + static const char *lines[RAYGUI_MAX_TEXT_LINES] = { 0 }; + for (int i = 0; i < RAYGUI_MAX_TEXT_LINES; i++) lines[i] = NULL; // Init NULL pointers to substrings + + int textLength = (int)strlen(text); + + lines[0] = text; + *count = 1; + + for (int i = 0; (i < textLength) && (*count < RAYGUI_MAX_TEXT_LINES); i++) + { + if ((text[i] == '\n') && ((i + 1) < textLength)) + { + lines[*count] = &text[i + 1]; + *count += 1; + } + } + + return lines; +} + +// Get text width to next space for provided string +static float GetNextSpaceWidth(const char *text, int *nextSpaceIndex) +{ + float width = 0; + int codepointByteCount = 0; + int codepoint = 0; + int index = 0; + float glyphWidth = 0; + float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/guiFont.baseSize; + + for (int i = 0; text[i] != '\0'; i++) + { + if (text[i] != ' ') + { + codepoint = GetCodepoint(&text[i], &codepointByteCount); + index = GetGlyphIndex(guiFont, codepoint); + glyphWidth = (guiFont.glyphs[index].advanceX == 0)? guiFont.recs[index].width*scaleFactor : guiFont.glyphs[index].advanceX*scaleFactor; + width += (glyphWidth + (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); + } + else + { + *nextSpaceIndex = i; + break; + } + } + + return width; +} + +// Gui draw text using default font +static void GuiDrawText(const char *text, Rectangle textBounds, int alignment, Color tint) +{ + #define TEXT_VALIGN_PIXEL_OFFSET(h) ((int)h%2) // Vertical alignment for pixel perfect + + #if !defined(ICON_TEXT_PADDING) + #define ICON_TEXT_PADDING 4 + #endif + + if ((text == NULL) || (text[0] == '\0')) return; // Security check + + // PROCEDURE: + // - Text is processed line per line + // - For every line, horizontal alignment is defined + // - For all text, vertical alignment is defined (multiline text only) + // - For every line, wordwrap mode is checked (useful for GuitextBox(), read-only) + + // Get text lines (using '\n' as delimiter) to be processed individually + // WARNING: GuiTextSplit() function can't be used now because it can have already been used + // before the GuiDrawText() call and its buffer is static, it would be overriden :( + int lineCount = 0; + const char **lines = GetTextLines(text, &lineCount); + + // Text style variables + //int alignment = GuiGetStyle(DEFAULT, TEXT_ALIGNMENT); + int alignmentVertical = GuiGetStyle(DEFAULT, TEXT_ALIGNMENT_VERTICAL); + int wrapMode = GuiGetStyle(DEFAULT, TEXT_WRAP_MODE); // Wrap-mode only available in read-only mode, no for text editing + + // TODO: WARNING: This totalHeight is not valid for vertical alignment in case of word-wrap + float totalHeight = (float)(lineCount*GuiGetStyle(DEFAULT, TEXT_SIZE) + (lineCount - 1)*GuiGetStyle(DEFAULT, TEXT_LINE_SPACING)); + float posOffsetY = 0.0f; + + for (int i = 0; i < lineCount; i++) + { + int iconId = 0; + lines[i] = GetTextIcon(lines[i], &iconId); // Check text for icon and move cursor + + // Get text position depending on alignment and iconId + //--------------------------------------------------------------------------------- + Vector2 textBoundsPosition = { textBounds.x, textBounds.y }; + float textBoundsWidthOffset = 0.0f; + + // NOTE: Get text size after icon has been processed + // WARNING: GuiGetTextWidth() also processes text icon to get width! -> Really needed? + int textSizeX = GuiGetTextWidth(lines[i]); + + // If text requires an icon, add size to measure + if (iconId >= 0) + { + textSizeX += RAYGUI_ICON_SIZE*guiIconScale; + + // WARNING: If only icon provided, text could be pointing to EOF character: '\0' +#if !defined(RAYGUI_NO_ICONS) + if ((lines[i] != NULL) && (lines[i][0] != '\0')) textSizeX += ICON_TEXT_PADDING; +#endif + } + + // Check guiTextAlign global variables + switch (alignment) + { + case TEXT_ALIGN_LEFT: textBoundsPosition.x = textBounds.x; break; + case TEXT_ALIGN_CENTER: textBoundsPosition.x = textBounds.x + textBounds.width/2 - textSizeX/2; break; + case TEXT_ALIGN_RIGHT: textBoundsPosition.x = textBounds.x + textBounds.width - textSizeX; break; + default: break; + } + + if (textSizeX > textBounds.width && (lines[i] != NULL) && (lines[i][0] != '\0')) textBoundsPosition.x = textBounds.x; + + switch (alignmentVertical) + { + // Only valid in case of wordWrap = 0; + case TEXT_ALIGN_TOP: textBoundsPosition.y = textBounds.y + posOffsetY; break; + case TEXT_ALIGN_MIDDLE: textBoundsPosition.y = textBounds.y + posOffsetY + textBounds.height/2 - totalHeight/2 + TEXT_VALIGN_PIXEL_OFFSET(textBounds.height); break; + case TEXT_ALIGN_BOTTOM: textBoundsPosition.y = textBounds.y + posOffsetY + textBounds.height - totalHeight + TEXT_VALIGN_PIXEL_OFFSET(textBounds.height); break; + default: break; + } + + // NOTE: Make sure getting pixel-perfect coordinates, + // In case of decimals, it could result in text positioning artifacts + textBoundsPosition.x = (float)((int)textBoundsPosition.x); + textBoundsPosition.y = (float)((int)textBoundsPosition.y); + //--------------------------------------------------------------------------------- + + // Draw text (with icon if available) + //--------------------------------------------------------------------------------- +#if !defined(RAYGUI_NO_ICONS) + if (iconId >= 0) + { + // NOTE: Considering icon height, probably different than text size + GuiDrawIcon(iconId, (int)textBoundsPosition.x, (int)(textBounds.y + textBounds.height/2 - RAYGUI_ICON_SIZE*guiIconScale/2 + TEXT_VALIGN_PIXEL_OFFSET(textBounds.height)), guiIconScale, tint); + textBoundsPosition.x += (float)(RAYGUI_ICON_SIZE*guiIconScale + ICON_TEXT_PADDING); + textBoundsWidthOffset = (float)(RAYGUI_ICON_SIZE*guiIconScale + ICON_TEXT_PADDING); + } +#endif + // Get size in bytes of text, + // considering end of line and line break + int lineSize = 0; + for (int c = 0; (lines[i][c] != '\0') && (lines[i][c] != '\n') && (lines[i][c] != '\r'); c++, lineSize++){ } + float scaleFactor = (float)GuiGetStyle(DEFAULT, TEXT_SIZE)/guiFont.baseSize; + + int lastSpaceIndex = 0; + bool tempWrapCharMode = false; + + int textOffsetY = 0; + float textOffsetX = 0.0f; + float glyphWidth = 0; + + int ellipsisWidth = GuiGetTextWidth("..."); + bool textOverflow = false; + for (int c = 0, codepointSize = 0; c < lineSize; c += codepointSize) + { + int codepoint = GetCodepointNext(&lines[i][c], &codepointSize); + int index = GetGlyphIndex(guiFont, codepoint); + + // NOTE: Normally, exiting the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but all of the bad bytes need to be drawn using the '?' symbol, moving one byte + if (codepoint == 0x3f) codepointSize = 1; // TODO: Review not recognized codepoints size + + // Get glyph width to check if it goes out of bounds + if (guiFont.glyphs[index].advanceX == 0) glyphWidth = ((float)guiFont.recs[index].width*scaleFactor); + else glyphWidth = (float)guiFont.glyphs[index].advanceX*scaleFactor; + + // Wrap mode text measuring, to validate if + // it can be drawn or a new line is required + if (wrapMode == TEXT_WRAP_CHAR) + { + // Jump to next line if current character reach end of the box limits + if ((textOffsetX + glyphWidth) > textBounds.width - textBoundsWidthOffset) + { + textOffsetX = 0.0f; + textOffsetY += (GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(DEFAULT, TEXT_LINE_SPACING)); + + if (tempWrapCharMode) // Wrap at char level when too long words + { + wrapMode = TEXT_WRAP_WORD; + tempWrapCharMode = false; + } + } + } + else if (wrapMode == TEXT_WRAP_WORD) + { + if (codepoint == 32) lastSpaceIndex = c; + + // Get width to next space in line + int nextSpaceIndex = 0; + float nextSpaceWidth = GetNextSpaceWidth(lines[i] + c, &nextSpaceIndex); + + int nextSpaceIndex2 = 0; + float nextWordSize = GetNextSpaceWidth(lines[i] + lastSpaceIndex + 1, &nextSpaceIndex2); + + if (nextWordSize > textBounds.width - textBoundsWidthOffset) + { + // Considering the case the next word is longer than bounds + tempWrapCharMode = true; + wrapMode = TEXT_WRAP_CHAR; + } + else if ((textOffsetX + nextSpaceWidth) > textBounds.width - textBoundsWidthOffset) + { + textOffsetX = 0.0f; + textOffsetY += (GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(DEFAULT, TEXT_LINE_SPACING)); + } + } + + if (codepoint == '\n') break; // WARNING: Lines are already processed manually, no need to keep drawing after this codepoint + else + { + // TODO: There are multiple types of spaces in Unicode, + // maybe it's a good idea to add support for more: http://jkorpela.fi/chars/spaces.html + if ((codepoint != ' ') && (codepoint != '\t')) // Do not draw codepoints with no glyph + { + if (wrapMode == TEXT_WRAP_NONE) + { + // Draw only required text glyphs fitting the textBounds.width + if (textSizeX > textBounds.width) + { + if (textOffsetX <= (textBounds.width - glyphWidth - textBoundsWidthOffset - ellipsisWidth)) + { + DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ textBoundsPosition.x + textOffsetX, textBoundsPosition.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha)); + } + else if (!textOverflow) + { + textOverflow = true; + + for (int j = 0; j < ellipsisWidth; j += ellipsisWidth/3) + { + DrawTextCodepoint(guiFont, '.', RAYGUI_CLITERAL(Vector2){ textBoundsPosition.x + textOffsetX + j, textBoundsPosition.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha)); + } + } + } + else + { + DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ textBoundsPosition.x + textOffsetX, textBoundsPosition.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha)); + } + } + else if ((wrapMode == TEXT_WRAP_CHAR) || (wrapMode == TEXT_WRAP_WORD)) + { + // Draw only glyphs inside the bounds + if ((textBoundsPosition.y + textOffsetY) <= (textBounds.y + textBounds.height - GuiGetStyle(DEFAULT, TEXT_SIZE))) + { + DrawTextCodepoint(guiFont, codepoint, RAYGUI_CLITERAL(Vector2){ textBoundsPosition.x + textOffsetX, textBoundsPosition.y + textOffsetY }, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), GuiFade(tint, guiAlpha)); + } + } + } + + if (guiFont.glyphs[index].advanceX == 0) textOffsetX += ((float)guiFont.recs[index].width*scaleFactor + (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); + else textOffsetX += ((float)guiFont.glyphs[index].advanceX*scaleFactor + (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); + } + } + + if (wrapMode == TEXT_WRAP_NONE) posOffsetY += (float)(GuiGetStyle(DEFAULT, TEXT_SIZE) + GuiGetStyle(DEFAULT, TEXT_LINE_SPACING)); + else if ((wrapMode == TEXT_WRAP_CHAR) || (wrapMode == TEXT_WRAP_WORD)) + posOffsetY += (textOffsetY + GuiGetStyle(DEFAULT, TEXT_SIZE)); + //--------------------------------------------------------------------------------- + } + +#if defined(RAYGUI_DEBUG_TEXT_BOUNDS) + GuiDrawRectangle(textBounds, 0, WHITE, Fade(BLUE, 0.4f)); +#endif +} + +// Gui draw rectangle using default raygui plain style with borders +static void GuiDrawRectangle(Rectangle rec, int borderWidth, Color borderColor, Color color) +{ + if (color.a > 0) + { + // Draw rectangle filled with color + DrawRectangle((int)rec.x, (int)rec.y, (int)rec.width, (int)rec.height, GuiFade(color, guiAlpha)); + } + + if (borderWidth > 0) + { + // Draw rectangle border lines with color + DrawRectangle((int)rec.x, (int)rec.y, (int)rec.width, borderWidth, GuiFade(borderColor, guiAlpha)); + DrawRectangle((int)rec.x, (int)rec.y + borderWidth, borderWidth, (int)rec.height - 2*borderWidth, GuiFade(borderColor, guiAlpha)); + DrawRectangle((int)rec.x + (int)rec.width - borderWidth, (int)rec.y + borderWidth, borderWidth, (int)rec.height - 2*borderWidth, GuiFade(borderColor, guiAlpha)); + DrawRectangle((int)rec.x, (int)rec.y + (int)rec.height - borderWidth, (int)rec.width, borderWidth, GuiFade(borderColor, guiAlpha)); + } + +#if defined(RAYGUI_DEBUG_RECS_BOUNDS) + DrawRectangle((int)rec.x, (int)rec.y, (int)rec.width, (int)rec.height, Fade(RED, 0.4f)); +#endif +} + +// Draw tooltip using control bounds +static void GuiTooltip(Rectangle controlRec) +{ + if (!guiLocked && guiTooltip && (guiTooltipPtr != NULL) && !guiControlExclusiveMode) + { + Vector2 textSize = MeasureTextEx(GuiGetFont(), guiTooltipPtr, (float)GuiGetStyle(DEFAULT, TEXT_SIZE), (float)GuiGetStyle(DEFAULT, TEXT_SPACING)); + + if ((controlRec.x + textSize.x + 16) > GetScreenWidth()) controlRec.x -= (textSize.x + 16 - controlRec.width); + + int lineCount = 0; + GetTextLines(guiTooltipPtr, &lineCount); // Only using the line count + if ((controlRec.y + controlRec.height + textSize.y + 4 + 8*lineCount) > GetScreenHeight()) + controlRec.y -= (controlRec.height + textSize.y + 4 + 8*lineCount); + + // TODO: Probably TEXT_LINE_SPACING should be considered on panel size instead of hardcoding 8.0f + GuiPanel(RAYGUI_CLITERAL(Rectangle){ controlRec.x, controlRec.y + controlRec.height + 4, textSize.x + 16, textSize.y + 8.0f*lineCount }, NULL); + + int textPadding = GuiGetStyle(LABEL, TEXT_PADDING); + int textAlignment = GuiGetStyle(LABEL, TEXT_ALIGNMENT); + GuiSetStyle(LABEL, TEXT_PADDING, 0); + GuiSetStyle(LABEL, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER); + GuiLabel(RAYGUI_CLITERAL(Rectangle){ controlRec.x, controlRec.y + controlRec.height + 4, textSize.x + 16, textSize.y + 8.0f*lineCount }, guiTooltipPtr); + GuiSetStyle(LABEL, TEXT_ALIGNMENT, textAlignment); + GuiSetStyle(LABEL, TEXT_PADDING, textPadding); + } +} + +// Split controls text into multiple strings +// Also check for multiple columns (required by GuiToggleGroup()) +static char **GuiTextSplit(const char *text, char delimiter, int *count, int *textRow) +{ + // NOTE: Current implementation returns a copy of the provided string with '\0' (string end delimiter) + // inserted between strings defined by "delimiter" parameter. No memory is dynamically allocated, + // all used memory is static... it has some limitations: + // 1. Maximum number of possible split strings is set by RAYGUI_TEXTSPLIT_MAX_ITEMS + // 2. Maximum size of text to split is RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE + // NOTE: Those definitions could be externally provided if required + + // TODO: HACK: GuiTextSplit() - Review how textRows are returned to user + // textRow is an externally provided array of integers that stores row number for every splitted string + + #if !defined(RAYGUI_TEXTSPLIT_MAX_ITEMS) + #define RAYGUI_TEXTSPLIT_MAX_ITEMS 128 + #endif + #if !defined(RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE) + #define RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE 1024 + #endif + + static char *result[RAYGUI_TEXTSPLIT_MAX_ITEMS] = { NULL }; // String pointers array (points to buffer data) + static char buffer[RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE] = { 0 }; // Buffer data (text input copy with '\0' added) + memset(buffer, 0, RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE); + + result[0] = buffer; + int counter = 1; + + if (textRow != NULL) textRow[0] = 0; + + // Count how many substrings text contains and point to every one of them + for (int i = 0; i < RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE; i++) + { + buffer[i] = text[i]; + if (buffer[i] == '\0') break; + else if ((buffer[i] == delimiter) || (buffer[i] == '\n')) + { + result[counter] = buffer + i + 1; + + if (textRow != NULL) + { + if (buffer[i] == '\n') textRow[counter] = textRow[counter - 1] + 1; + else textRow[counter] = textRow[counter - 1]; + } + + buffer[i] = '\0'; // Set an end of string at this point + + counter++; + if (counter >= RAYGUI_TEXTSPLIT_MAX_ITEMS) break; + } + } + + *count = counter; + + return result; +} + +// Convert color data from RGB to HSV +// NOTE: Color data should be passed normalized +static Vector3 ConvertRGBtoHSV(Vector3 rgb) +{ + Vector3 hsv = { 0 }; + float min = 0.0f; + float max = 0.0f; + float delta = 0.0f; + + min = (rgb.x < rgb.y)? rgb.x : rgb.y; + min = (min < rgb.z)? min : rgb.z; + + max = (rgb.x > rgb.y)? rgb.x : rgb.y; + max = (max > rgb.z)? max : rgb.z; + + hsv.z = max; // Value + delta = max - min; + + if (delta < 0.00001f) + { + hsv.y = 0.0f; + hsv.x = 0.0f; // Undefined, maybe NAN? + return hsv; + } + + if (max > 0.0f) + { + // NOTE: If max is 0, this divide would cause a crash + hsv.y = (delta/max); // Saturation + } + else + { + // NOTE: If max is 0, then r = g = b = 0, s = 0, h is undefined + hsv.y = 0.0f; + hsv.x = 0.0f; // Undefined, maybe NAN? + return hsv; + } + + // NOTE: Comparing float values could not work properly + if (rgb.x >= max) hsv.x = (rgb.y - rgb.z)/delta; // Between yellow & magenta + else + { + if (rgb.y >= max) hsv.x = 2.0f + (rgb.z - rgb.x)/delta; // Between cyan & yellow + else hsv.x = 4.0f + (rgb.x - rgb.y)/delta; // Between magenta & cyan + } + + hsv.x *= 60.0f; // Convert to degrees + + if (hsv.x < 0.0f) hsv.x += 360.0f; + + return hsv; +} + +// Convert color data from HSV to RGB +// NOTE: Color data should be passed normalized +static Vector3 ConvertHSVtoRGB(Vector3 hsv) +{ + Vector3 rgb = { 0 }; + float hh = 0.0f, p = 0.0f, q = 0.0f, t = 0.0f, ff = 0.0f; + long i = 0; + + // NOTE: Comparing float values could not work properly + if (hsv.y <= 0.0f) + { + rgb.x = hsv.z; + rgb.y = hsv.z; + rgb.z = hsv.z; + return rgb; + } + + hh = hsv.x; + if (hh >= 360.0f) hh = 0.0f; + hh /= 60.0f; + + i = (long)hh; + ff = hh - i; + p = hsv.z*(1.0f - hsv.y); + q = hsv.z*(1.0f - (hsv.y*ff)); + t = hsv.z*(1.0f - (hsv.y*(1.0f - ff))); + + switch (i) + { + case 0: + { + rgb.x = hsv.z; + rgb.y = t; + rgb.z = p; + } break; + case 1: + { + rgb.x = q; + rgb.y = hsv.z; + rgb.z = p; + } break; + case 2: + { + rgb.x = p; + rgb.y = hsv.z; + rgb.z = t; + } break; + case 3: + { + rgb.x = p; + rgb.y = q; + rgb.z = hsv.z; + } break; + case 4: + { + rgb.x = t; + rgb.y = p; + rgb.z = hsv.z; + } break; + case 5: + default: + { + rgb.x = hsv.z; + rgb.y = p; + rgb.z = q; + } break; + } + + return rgb; +} + +// Scroll bar control (used by GuiScrollPanel()) +static int GuiScrollBar(Rectangle bounds, int value, int minValue, int maxValue) +{ + GuiState state = guiState; + + // Is the scrollbar horizontal or vertical? + bool isVertical = (bounds.width > bounds.height)? false : true; + + // The size (width or height depending on scrollbar type) of the spinner buttons + const int spinnerSize = GuiGetStyle(SCROLLBAR, ARROWS_VISIBLE)? + (isVertical? (int)bounds.width - 2*GuiGetStyle(SCROLLBAR, BORDER_WIDTH) : + (int)bounds.height - 2*GuiGetStyle(SCROLLBAR, BORDER_WIDTH)) : 0; + + // Arrow buttons [<] [>] [∧] [∨] + Rectangle arrowUpLeft = { 0 }; + Rectangle arrowDownRight = { 0 }; + + // Actual area of the scrollbar excluding the arrow buttons + Rectangle scrollbar = { 0 }; + + // Slider bar that moves --[///]----- + Rectangle slider = { 0 }; + + // Normalize value + if (value > maxValue) value = maxValue; + if (value < minValue) value = minValue; + + int valueRange = maxValue - minValue; + if (valueRange <= 0) valueRange = 1; + + int sliderSize = GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE); + if (sliderSize < 1) sliderSize = 1; // TODO: Consider a minimum slider size + + // Calculate rectangles for all of the components + arrowUpLeft = RAYGUI_CLITERAL(Rectangle){ + (float)bounds.x + GuiGetStyle(SCROLLBAR, BORDER_WIDTH), + (float)bounds.y + GuiGetStyle(SCROLLBAR, BORDER_WIDTH), + (float)spinnerSize, (float)spinnerSize }; + + if (isVertical) + { + arrowDownRight = RAYGUI_CLITERAL(Rectangle){ (float)bounds.x + GuiGetStyle(SCROLLBAR, BORDER_WIDTH), (float)bounds.y + bounds.height - spinnerSize - GuiGetStyle(SCROLLBAR, BORDER_WIDTH), (float)spinnerSize, (float)spinnerSize }; + scrollbar = RAYGUI_CLITERAL(Rectangle){ bounds.x + GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_PADDING), arrowUpLeft.y + arrowUpLeft.height, bounds.width - 2*(GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_PADDING)), bounds.height - arrowUpLeft.height - arrowDownRight.height - 2*GuiGetStyle(SCROLLBAR, BORDER_WIDTH) }; + + // Make sure the slider won't get outside of the scrollbar + sliderSize = (sliderSize >= scrollbar.height)? ((int)scrollbar.height - 2) : sliderSize; + slider = RAYGUI_CLITERAL(Rectangle){ + bounds.x + GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING), + scrollbar.y + (int)(((float)(value - minValue)/valueRange)*(scrollbar.height - sliderSize)), + bounds.width - 2*(GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING)), + (float)sliderSize }; + } + else // horizontal + { + arrowDownRight = RAYGUI_CLITERAL(Rectangle){ (float)bounds.x + bounds.width - spinnerSize - GuiGetStyle(SCROLLBAR, BORDER_WIDTH), (float)bounds.y + GuiGetStyle(SCROLLBAR, BORDER_WIDTH), (float)spinnerSize, (float)spinnerSize }; + scrollbar = RAYGUI_CLITERAL(Rectangle){ arrowUpLeft.x + arrowUpLeft.width, bounds.y + GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_PADDING), bounds.width - arrowUpLeft.width - arrowDownRight.width - 2*GuiGetStyle(SCROLLBAR, BORDER_WIDTH), bounds.height - 2*(GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_PADDING)) }; + + // Make sure the slider won't get outside of the scrollbar + sliderSize = (sliderSize >= scrollbar.width)? ((int)scrollbar.width - 2) : sliderSize; + slider = RAYGUI_CLITERAL(Rectangle){ + scrollbar.x + (int)(((float)(value - minValue)/valueRange)*(scrollbar.width - sliderSize)), + bounds.y + GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING), + (float)sliderSize, + bounds.height - 2*(GuiGetStyle(SCROLLBAR, BORDER_WIDTH) + GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING)) }; + } + + // Update control + //-------------------------------------------------------------------- + if ((state != STATE_DISABLED) && !guiLocked) + { + Vector2 mousePoint = GUI_POINTER_POSITION; + + if (guiControlExclusiveMode) // Allows to keep dragging outside of bounds + { + if (GUI_BUTTON_DOWN && + !CheckCollisionPointRec(mousePoint, arrowUpLeft) && + !CheckCollisionPointRec(mousePoint, arrowDownRight)) + { + if (CHECK_BOUNDS_ID(bounds, guiControlExclusiveRec)) + { + state = STATE_PRESSED; + + if (isVertical) value = (int)(((float)(mousePoint.y - scrollbar.y - slider.height/2)*valueRange)/(scrollbar.height - slider.height) + minValue); + else value = (int)(((float)(mousePoint.x - scrollbar.x - slider.width/2)*valueRange)/(scrollbar.width - slider.width) + minValue); + } + } + else + { + guiControlExclusiveMode = false; + guiControlExclusiveRec = RAYGUI_CLITERAL(Rectangle){ 0, 0, 0, 0 }; + } + } + else if (CheckCollisionPointRec(mousePoint, bounds)) + { + state = STATE_FOCUSED; + + // Handle mouse wheel + float scrollDelta = GUI_SCROLL_DELTA; + if (scrollDelta != 0) value += (int)scrollDelta; + + // Handle mouse button down + if (GUI_BUTTON_PRESSED) + { + guiControlExclusiveMode = true; + guiControlExclusiveRec = bounds; // Store bounds as an identifier when dragging starts + + // Check arrows click + if (CheckCollisionPointRec(mousePoint, arrowUpLeft)) value -= valueRange/GuiGetStyle(SCROLLBAR, SCROLL_SPEED); + else if (CheckCollisionPointRec(mousePoint, arrowDownRight)) value += valueRange/GuiGetStyle(SCROLLBAR, SCROLL_SPEED); + else if (!CheckCollisionPointRec(mousePoint, slider)) + { + // If click on scrollbar position but not on slider, place slider directly on that position + if (isVertical) value = (int)(((float)(mousePoint.y - scrollbar.y - slider.height/2)*valueRange)/(scrollbar.height - slider.height) + minValue); + else value = (int)(((float)(mousePoint.x - scrollbar.x - slider.width/2)*valueRange)/(scrollbar.width - slider.width) + minValue); + } + + state = STATE_PRESSED; + } + + // Keyboard control on mouse hover scrollbar + /* + if (isVertical) + { + if (GUI_KEY_DOWN(KEY_DOWN)) value += 5; + else if (GUI_KEY_DOWN(KEY_UP)) value -= 5; + } + else + { + if (GUI_KEY_DOWN(KEY_RIGHT)) value += 5; + else if (GUI_KEY_DOWN(KEY_LEFT)) value -= 5; + } + */ + } + + // Normalize value + if (value > maxValue) value = maxValue; + if (value < minValue) value = minValue; + } + //-------------------------------------------------------------------- + + // Draw control + //-------------------------------------------------------------------- + GuiDrawRectangle(bounds, GuiGetStyle(SCROLLBAR, BORDER_WIDTH), GetColor(GuiGetStyle(LISTVIEW, BORDER + state*3)), GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED))); // Draw the background + + GuiDrawRectangle(scrollbar, 0, BLANK, GetColor(GuiGetStyle(BUTTON, BASE_COLOR_NORMAL))); // Draw the scrollbar active area background + GuiDrawRectangle(slider, 0, BLANK, GetColor(GuiGetStyle(SLIDER, BORDER + state*3))); // Draw the slider bar + + // Draw arrows (using icon if available) + if (GuiGetStyle(SCROLLBAR, ARROWS_VISIBLE)) + { +#if defined(RAYGUI_NO_ICONS) + GuiDrawText(isVertical? "^" : "<", + RAYGUI_CLITERAL(Rectangle){ arrowUpLeft.x, arrowUpLeft.y, isVertical? bounds.width : bounds.height, isVertical? bounds.width : bounds.height }, + TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3)))); + GuiDrawText(isVertical? "v" : ">", + RAYGUI_CLITERAL(Rectangle){ arrowDownRight.x, arrowDownRight.y, isVertical? bounds.width : bounds.height, isVertical? bounds.width : bounds.height }, + TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(DROPDOWNBOX, TEXT + (state*3)))); +#else + GuiDrawText(isVertical? GuiIconText(ICON_ARROW_UP_FILL, NULL) : GuiIconText(ICON_ARROW_LEFT_FILL, NULL), + RAYGUI_CLITERAL(Rectangle){ arrowUpLeft.x, arrowUpLeft.y, isVertical? bounds.width : bounds.height, isVertical? bounds.width : bounds.height }, + TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(SCROLLBAR, TEXT + state*3))); // ICON_ARROW_UP_FILL / ICON_ARROW_LEFT_FILL + GuiDrawText(isVertical? GuiIconText(ICON_ARROW_DOWN_FILL, NULL) : GuiIconText(ICON_ARROW_RIGHT_FILL, NULL), + RAYGUI_CLITERAL(Rectangle){ arrowDownRight.x, arrowDownRight.y, isVertical? bounds.width : bounds.height, isVertical? bounds.width : bounds.height }, + TEXT_ALIGN_CENTER, GetColor(GuiGetStyle(SCROLLBAR, TEXT + state*3))); // ICON_ARROW_DOWN_FILL / ICON_ARROW_RIGHT_FILL +#endif + } + //-------------------------------------------------------------------- + + return value; +} + +// Color fade-in or fade-out, alpha goes from 0.0f to 1.0f +// WARNING: It multiplies current alpha by alpha scale factor +static Color GuiFade(Color color, float alpha) +{ + if (alpha < 0.0f) alpha = 0.0f; + else if (alpha > 1.0f) alpha = 1.0f; + + Color result = { color.r, color.g, color.b, (unsigned char)(color.a*alpha) }; + + return result; +} + +#if defined(RAYGUI_STANDALONE) +// Returns a Color struct from hexadecimal value +static Color GetColor(int hexValue) +{ + Color color; + + color.r = (unsigned char)(hexValue >> 24) & 0xff; + color.g = (unsigned char)(hexValue >> 16) & 0xff; + color.b = (unsigned char)(hexValue >> 8) & 0xff; + color.a = (unsigned char)hexValue & 0xff; + + return color; +} + +// Returns hexadecimal value for a Color +static int ColorToInt(Color color) +{ + return (((int)color.r << 24) | ((int)color.g << 16) | ((int)color.b << 8) | (int)color.a); +} + +// Check if point is inside rectangle +static bool CheckCollisionPointRec(Vector2 point, Rectangle rec) +{ + bool collision = false; + + if ((point.x >= rec.x) && (point.x <= (rec.x + rec.width)) && + (point.y >= rec.y) && (point.y <= (rec.y + rec.height))) collision = true; + + return collision; +} + +// Formatting of text with variables to 'embed' +static const char *TextFormat(const char *text, ...) +{ + #if !defined(RAYGUI_TEXTFORMAT_MAX_SIZE) + #define RAYGUI_TEXTFORMAT_MAX_SIZE 256 + #endif + + static char buffer[RAYGUI_TEXTFORMAT_MAX_SIZE]; + + va_list args; + va_start(args, text); + vsnprintf(buffer, RAYGUI_TEXTFORMAT_MAX_SIZE, text, args); + va_end(args); + + return buffer; +} + +// Draw rectangle with vertical gradient fill color +// NOTE: This function is only used by GuiColorPicker() +static void DrawRectangleGradientV(int posX, int posY, int width, int height, Color color1, Color color2) +{ + Rectangle bounds = { (float)posX, (float)posY, (float)width, (float)height }; + DrawRectangleGradientEx(bounds, color1, color2, color2, color1); +} + +// Split string into multiple strings +char **TextSplit(const char *text, char delimiter, int *count) +{ + // NOTE: Current implementation returns a copy of the provided string with '\0' (string end delimiter) + // inserted between strings defined by "delimiter" parameter. No memory is dynamically allocated, + // all used memory is static... it has some limitations: + // 1. Maximum number of possible split strings is set by RAYGUI_TEXTSPLIT_MAX_ITEMS + // 2. Maximum size of text to split is RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE + + #if !defined(RAYGUI_TEXTSPLIT_MAX_ITEMS) + #define RAYGUI_TEXTSPLIT_MAX_ITEMS 128 + #endif + #if !defined(RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE) + #define RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE 1024 + #endif + + static const char *result[RAYGUI_TEXTSPLIT_MAX_ITEMS] = { NULL }; + static char buffer[RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE] = { 0 }; + memset(buffer, 0, RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE); + + result[0] = buffer; + int counter = 0; + + if (text != NULL) + { + counter = 1; + + // Count how many substrings text contains and point to every one of them + for (int i = 0; i < RAYGUI_TEXTSPLIT_MAX_TEXT_SIZE; i++) + { + buffer[i] = text[i]; + if (buffer[i] == '\0') break; + else if (buffer[i] == delimiter) + { + buffer[i] = '\0'; // Set an end of string at this point + result[counter] = buffer + i + 1; + counter++; + + if (counter == RAYGUI_TEXTSPLIT_MAX_ITEMS) break; + } + } + } + + *count = counter; + return result; +} + +// Get integer value from text +// NOTE: This function replaces atoi() [stdlib.h] +static int TextToInteger(const char *text) +{ + int value = 0; + int sign = 1; + + if ((text[0] == '+') || (text[0] == '-')) + { + if (text[0] == '-') sign = -1; + text++; + } + + for (int i = 0; ((text[i] >= '0') && (text[i] <= '9')); i++) value = value*10 + (int)(text[i] - '0'); + + return value*sign; +} + +// Get float value from text +// NOTE: This function replaces atof() [stdlib.h] +// WARNING: Only '.' character is understood as decimal point +static float TextToFloat(const char *text) +{ + float value = 0.0f; + float sign = 1.0f; + + if ((text[0] == '+') || (text[0] == '-')) + { + if (text[0] == '-') sign = -1.0f; + text++; + } + + int i = 0; + for (; ((text[i] >= '0') && (text[i] <= '9')); i++) value = value*10.0f + (float)(text[i] - '0'); + + if (text[i++] != '.') value *= sign; + else + { + float divisor = 10.0f; + for (; ((text[i] >= '0') && (text[i] <= '9')); i++) + { + value += ((float)(text[i] - '0'))/divisor; + divisor = divisor*10.0f; + } + } + + return value; +} + +// Encode codepoint into UTF-8 text (char array size returned as parameter) +static const char *CodepointToUTF8(int codepoint, int *byteSize) +{ + static char utf8[6] = { 0 }; + int size = 0; + + if (codepoint <= 0x7f) + { + utf8[0] = (char)codepoint; + size = 1; + } + else if (codepoint <= 0x7ff) + { + utf8[0] = (char)(((codepoint >> 6) & 0x1f) | 0xc0); + utf8[1] = (char)((codepoint & 0x3f) | 0x80); + size = 2; + } + else if (codepoint <= 0xffff) + { + utf8[0] = (char)(((codepoint >> 12) & 0x0f) | 0xe0); + utf8[1] = (char)(((codepoint >> 6) & 0x3f) | 0x80); + utf8[2] = (char)((codepoint & 0x3f) | 0x80); + size = 3; + } + else if (codepoint <= 0x10ffff) + { + utf8[0] = (char)(((codepoint >> 18) & 0x07) | 0xf0); + utf8[1] = (char)(((codepoint >> 12) & 0x3f) | 0x80); + utf8[2] = (char)(((codepoint >> 6) & 0x3f) | 0x80); + utf8[3] = (char)((codepoint & 0x3f) | 0x80); + size = 4; + } + + *byteSize = size; + + return utf8; +} + +// Get next codepoint in a UTF-8 encoded text, scanning until '\0' is found +// When a invalid UTF-8 byte is encountered, exiting as soon as possible and returning a '?'(0x3f) codepoint +// Total number of bytes processed are returned as a parameter +// NOTE: The standard says U+FFFD should be returned in case of errors +// but that character is not supported by the default font in raylib +static int GetCodepointNext(const char *text, int *codepointSize) +{ + const char *ptr = text; + int codepoint = 0x3f; // Codepoint (defaults to '?') + *codepointSize = 1; + + // Get current codepoint and bytes processed + if (0xf0 == (0xf8 & ptr[0])) + { + // 4 byte UTF-8 codepoint + if (((ptr[1] & 0xC0) ^ 0x80) || ((ptr[2] & 0xC0) ^ 0x80) || ((ptr[3] & 0xC0) ^ 0x80)) { return codepoint; } //10xxxxxx checks + codepoint = ((0x07 & ptr[0]) << 18) | ((0x3f & ptr[1]) << 12) | ((0x3f & ptr[2]) << 6) | (0x3f & ptr[3]); + *codepointSize = 4; + } + else if (0xe0 == (0xf0 & ptr[0])) + { + // 3 byte UTF-8 codepoint + if (((ptr[1] & 0xC0) ^ 0x80) || ((ptr[2] & 0xC0) ^ 0x80)) { return codepoint; } //10xxxxxx checks + codepoint = ((0x0f & ptr[0]) << 12) | ((0x3f & ptr[1]) << 6) | (0x3f & ptr[2]); + *codepointSize = 3; + } + else if (0xc0 == (0xe0 & ptr[0])) + { + // 2 byte UTF-8 codepoint + if ((ptr[1] & 0xC0) ^ 0x80) { return codepoint; } //10xxxxxx checks + codepoint = ((0x1f & ptr[0]) << 6) | (0x3f & ptr[1]); + *codepointSize = 2; + } + else if (0x00 == (0x80 & ptr[0])) + { + // 1 byte UTF-8 codepoint + codepoint = ptr[0]; + *codepointSize = 1; + } + + return codepoint; +} +#endif // RAYGUI_STANDALONE + +#endif // RAYGUI_IMPLEMENTATION diff --git a/src/samples/viewer/.clang-format b/src/samples/viewer/.clang-format new file mode 100644 index 0000000..1c54f1d --- /dev/null +++ b/src/samples/viewer/.clang-format @@ -0,0 +1,24 @@ +# raylib code style for the viewer sample (per raylib's CONVENTIONS.md): 4-space +# indent, no tabs, Allman braces, spaces inside braced initializers. ColumnLimit +# 0 keeps the author's line breaks (only indent/braces/spacing are normalized, +# nothing is re-wrapped). Naming (TitleCase functions, camelCase variables) is a +# separate concern clang-format can't do. The rest of Purism Core keeps its own +# style; this config only applies to src/samples/viewer/. +Language: Cpp +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 120 +BreakBeforeBraces: Allman +AlwaysBreakAfterReturnType: None +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: true +SpaceBeforeParens: ControlStatements +PointerAlignment: Right +Cpp11BracedListStyle: false +IndentCaseLabels: false +KeepEmptyLinesAtTheStartOfBlocks: false diff --git a/src/samples/viewer/Info.plist.in b/src/samples/viewer/Info.plist.in new file mode 100644 index 0000000..1619aeb --- /dev/null +++ b/src/samples/viewer/Info.plist.in @@ -0,0 +1,17 @@ + + + + + CFBundleName Purism Viewer + CFBundleDisplayName Purism Viewer + CFBundleIdentifier org.sakura2d.purism-core-viewer + CFBundleVersion @PSM_VER_STRING@ + CFBundleShortVersionString@PSM_VER_STRING@ + CFBundleExecutable viewer + CFBundlePackageType APPL + CFBundleSignature ???? + LSMinimumSystemVersion 11.0 + NSHighResolutionCapable + NSPrincipalClass NSApplication + + diff --git a/src/samples/viewer/blend.c b/src/samples/viewer/blend.c new file mode 100644 index 0000000..03b14ad --- /dev/null +++ b/src/samples/viewer/blend.c @@ -0,0 +1,78 @@ +/* + * Purism Core: sample model viewer blend setup + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include "viewer.h" + +static void ApplyBlendMode(int mode) +{ + if (mode == PSM_BLEND_ADDITIVE) + { + rlSetBlendFactorsSeparate(RL_ONE, RL_ONE, RL_ZERO, RL_ONE, RL_FUNC_ADD, RL_FUNC_ADD); + } + else if (mode == PSM_BLEND_MULTIPLICATIVE) + { + rlSetBlendFactorsSeparate(RL_DST_COLOR, RL_ONE_MINUS_SRC_ALPHA, RL_ZERO, RL_ONE, RL_FUNC_ADD, RL_FUNC_ADD); + } + else + { /* normal (premultiplied over) */ + rlSetBlendFactorsSeparate(RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_ONE, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, + RL_FUNC_ADD); + } + rlSetBlendMode(RL_BLEND_CUSTOM_SEPARATE); +} + +void ApplyBlend(csmFlags flags) +{ + ApplyBlendMode((flags & csmBlendAdditive) ? PSM_BLEND_ADDITIVE + : (flags & csmBlendMultiplicative) ? PSM_BLEND_MULTIPLICATIVE + : PSM_BLEND_NORMAL); +} + +#if PSM_COMPAT_VERSION >= 0x06000000L +static bool BlendColorIsSimple(int color) +{ + return color == 0 || color == 1 || color == 2 || color == 3 || color == 4 || color == 6; +} + +bool BlendIsExotic(int extended) +{ + int color = extended & 0xFF; + int alpha = (extended >> 8) & 0xFF; + return !BlendColorIsSimple(color) || alpha >= 3; +} + +bool BlendAlphaNeedsCoverage(int extended) +{ + int color = extended & 0xFF; + int alpha = (extended >> 8) & 0xFF; + if (alpha == 0) return false; + return color == 0 || BlendIsExotic(extended); +} + +void ApplyExtendedBlend(int extended) +{ + int color = extended & 0xFF; + int alpha = (extended >> 8) & 0xFF; + + if (color == 1 || color == 3 || color == 4) + { /* additive color */ + rlSetBlendFactorsSeparate(RL_ONE, RL_ONE, RL_ZERO, RL_ONE, RL_FUNC_ADD, RL_FUNC_ADD); + } + else if (color == 2 || color == 6) + { /* multiplicative */ + rlSetBlendFactorsSeparate(RL_DST_COLOR, RL_ONE_MINUS_SRC_ALPHA, RL_ZERO, RL_ONE, RL_FUNC_ADD, RL_FUNC_ADD); + } + else + { /* normal color */ + int sf = (alpha == 1) ? RL_DST_ALPHA /* Atop */ + : (alpha == 2) ? RL_ZERO /* Out */ + : RL_ONE; /* Over / approx others */ + rlSetBlendFactorsSeparate(sf, RL_ONE_MINUS_SRC_ALPHA, sf, RL_ONE_MINUS_SRC_ALPHA, RL_FUNC_ADD, RL_FUNC_ADD); + } + rlSetBlendMode(RL_BLEND_CUSTOM_SEPARATE); +} +#endif diff --git a/src/samples/viewer/graphics.c b/src/samples/viewer/graphics.c new file mode 100644 index 0000000..01cdfe5 --- /dev/null +++ b/src/samples/viewer/graphics.c @@ -0,0 +1,412 @@ +/* + * Purism Core: sample model viewer graphics helpers + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include "viewer.h" + +#include "embed/draw.frag.h" +#include "embed/masked.frag.h" +#include "embed/maskwrite.frag.h" +#if PSM_COMPAT_VERSION >= 0x06000000L +#include "embed/blend.frag.h" +#include "embed/offscreen.frag.h" +#include "embed/offscreen_masked.frag.h" +#endif + +#if defined(__EMSCRIPTEN__) +#define GLSL_VERSION_HEADER "#version 300 es\nprecision highp float;\nprecision highp int;\n" +#else +#define GLSL_VERSION_HEADER "#version 330\n" +#endif + +static Shader LoadFs(const char *body) +{ + char *src = (char *)malloc(strlen(GLSL_VERSION_HEADER) + strlen(body) + 1); + if (!src) return LoadShaderFromMemory(NULL, body); /* OOM: try unversioned */ + strcpy(src, GLSL_VERSION_HEADER); + strcat(src, body); + Shader s = LoadShaderFromMemory(NULL, src); + free(src); + return s; +} + +void WorldToScreen(const View *v, float mx, float my, float *sx, float *sy) +{ + float wx = mx, wy = -my; /* Live2D y-up -> screen y-down */ + *sx = (wx - v->cx) * v->zoom + v->sw * 0.5f + v->panx; + *sy = (wy - v->cy) * v->zoom + v->sh * 0.5f + v->pany; +} + +/* Fit the camera to the model AABB at its current pose. */ +void FitView(View *v, csmModel *model) +{ + int dc = csmGetDrawableCount(model); + const int *vcount = csmGetDrawableVertexCounts(model); + const csmVector2 **pos = csmGetDrawableVertexPositions(model); + float minx = 1e30f, miny = 1e30f, maxx = -1e30f, maxy = -1e30f; + for (int d = 0; d < dc; d++) + { + for (int i = 0; i < vcount[d]; i++) + { + float x = pos[d][i].X, y = -pos[d][i].Y; + if (x < minx) minx = x; + if (x > maxx) maxx = x; + if (y < miny) miny = y; + if (y > maxy) maxy = y; + } + } + if (maxx <= minx || maxy <= miny) + { /* degenerate */ + v->cx = v->cy = 0.0f; + v->zoom = 100.0f; + } + else + { + v->cx = (minx + maxx) * 0.5f; + v->cy = (miny + maxy) * 0.5f; + float zx = (v->sw * 0.8f) / (maxx - minx); + float zy = (v->sh * 0.8f) / (maxy - miny); + v->zoom = zx < zy ? zx : zy; + } + v->panx = v->pany = 0.0f; +} + +void EmitDrawable(const View *v, int texId, const csmVector2 *pos, const csmVector2 *uv, const unsigned short *idx, + int idxCount) +{ + int i = 0; + while (i < idxCount) + { + int chunk = idxCount - i; + if (chunk > EMIT_CHUNK) chunk = EMIT_CHUNK; /* EMIT_CHUNK is a multiple of 3 (whole triangles) */ + rlCheckRenderBatchLimit(chunk); + rlSetTexture((unsigned)texId); + rlBegin(RL_TRIANGLES); + rlColor4ub(255, 255, 255, 255); + for (int k = 0; k < chunk; k++) + { + unsigned short j = idx[i + k]; + float sx, sy; + WorldToScreen(v, pos[j].X, pos[j].Y, &sx, &sy); + /* Cubism UVs are bottom-left origin; raylib textures are top-left. */ + rlTexCoord2f(uv[j].X, 1.0f - uv[j].Y); + rlVertex2f(sx, sy); + } + rlEnd(); + i += chunk; + } +} + +/* A unit vector selecting RGBA channel c. */ +void ChannelVec(int c, float v[4]) +{ + v[0] = v[1] = v[2] = v[3] = 0.0f; + v[c] = 1.0f; +} + +/* Two mask lists describe the same clip if they are equal as sets. */ +static bool SameMaskSet(const int *a, int na, const int *b, int nb) +{ + if (na != nb) return false; + for (int i = 0; i < na; i++) + { + bool found = false; + for (int j = 0; j < nb; j++) + if (a[i] == b[j]) + { + found = true; + break; + } + if (!found) return false; + } + return true; +} + +/* Group masked drawables by their mask-set. The grouping is static model + * data (mask membership never changes), so it is built once at load. */ +static bool BuildClipGroups(csmModel *model, ClipGroups *cg) +{ + int dc = csmGetDrawableCount(model); + const int *mcount = csmGetDrawableMaskCounts(model); + const int **masks = csmGetDrawableMasks(model); + + memset(cg, 0, sizeof(*cg)); + cg->of = (int *)malloc(sizeof(int) * (dc > 0 ? dc : 1)); + if (!cg->of) return false; + + for (int d = 0; d < dc; d++) + { + if (mcount[d] <= 0) + { + cg->of[d] = -1; + continue; + } + int g = -1; + for (int k = 0; k < cg->count; k++) + if (SameMaskSet(masks[d], mcount[d], cg->masks[k], cg->maskCount[k])) + { + g = k; + break; + } + if (g < 0) + { + if (cg->count >= MAX_CLIP_GROUPS) + { + fprintf(stderr, "warning: >%d clip groups; drawable %d unmasked\n", MAX_CLIP_GROUPS, d); + cg->of[d] = -1; + continue; + } + g = cg->count++; + cg->masks[g] = masks[d]; + cg->maskCount[g] = mcount[d]; + } + cg->of[d] = g; + } + cg->bufCount = (cg->count + GROUPS_PER_BUF - 1) / GROUPS_PER_BUF; + return true; +} + +static void ClipGroupsAllocBuffers(ClipGroups *cg, int w, int h) +{ + for (int b = 0; b < cg->bufCount; b++) + cg->buf[b] = LoadRenderTexture(w, h); +} + +static void ClipGroupsFreeBuffers(ClipGroups *cg) +{ + for (int b = 0; b < cg->bufCount; b++) + UnloadRenderTexture(cg->buf[b]); +} + +#if PSM_COMPAT_VERSION >= 0x06000000L +/* Walk a part ancestry chain: is `ancestor` part an ancestor of (or equal to) + * `part`? -1 means "no part". */ +static bool PartIsAncestor(const int *partParent, int ancestor, int part) +{ + if (ancestor < 0) return false; + while (part >= 0) + { + if (part == ancestor) return true; + part = partParent[part]; + } + return false; +} + +/* Does drawable d fall within offscreen o's scope? True iff o's owner part is + * an ancestor (inclusive) of d's parent part. */ +bool DrawableInOffscreen(const Offscreens *os, const int *partParent, const int *drawPart, int d, int o) +{ + if (o < 0) return true; /* the root screen contains everything */ + return PartIsAncestor(partParent, os->owner[o], drawPart[d]); +} + +/* Precompute each offscreen's parent offscreen (mirrors SetupParentOffscreens): + * walk up from the owner part's parent until a part that owns some offscreen. */ +static void OffscreensLinkParents(Offscreens *os, const int *partParent) +{ + for (int o = 0; o < os->count; o++) + { + os->parent[o] = -1; + int p = os->owner[o] >= 0 ? partParent[os->owner[o]] : -1; + while (p >= 0) + { + int found = -1; + for (int k = 0; k < os->count; k++) + if (os->owner[k] == p) + { + found = k; + break; + } + if (found >= 0) + { + os->parent[o] = found; + break; + } + p = partParent[p]; + } + } +} + +/* Build the per-offscreen mask groups (channel-packed clip buffers), reusing + * the same set-dedup the drawable masking uses. */ +static bool OffscreensBuildMasks(Offscreens *os, csmModel *model) +{ + const int *mcount = csmGetOffscreenMaskCounts(model); + const int **masks = csmGetOffscreenMasks(model); + ClipGroups *cg = &os->mcg; + + memset(cg, 0, sizeof(*cg)); + cg->of = NULL; /* unused for offscreens; mgroup[] is the index */ + + for (int o = 0; o < os->count; o++) + { + if (mcount[o] <= 0) + { + os->mgroup[o] = -1; + continue; + } + int g = -1; + for (int k = 0; k < cg->count; k++) + if (SameMaskSet(masks[o], mcount[o], cg->masks[k], cg->maskCount[k])) + { + g = k; + break; + } + if (g < 0) + { + if (cg->count >= MAX_CLIP_GROUPS) + { + os->mgroup[o] = -1; + continue; + } + g = cg->count++; + cg->masks[g] = masks[o]; + cg->maskCount[g] = mcount[o]; + } + os->mgroup[o] = g; + } + cg->bufCount = (cg->count + GROUPS_PER_BUF - 1) / GROUPS_PER_BUF; + return true; +} + +static bool OffscreensInit(Offscreens *os, csmModel *model, int w, int h) +{ + memset(os, 0, sizeof(*os)); + os->count = csmGetOffscreenCount(model); + if (os->count <= 0) return true; /* dormant: the flat path runs, byte-identical to before */ + if (os->count > MAX_OFFSCREENS) + { + fprintf(stderr, "warning: %d offscreens > %d; compositing disabled\n", os->count, MAX_OFFSCREENS); + os->count = 0; + return true; + } + os->owner = csmGetOffscreenOwnerIndices(model); + OffscreensLinkParents(os, csmGetPartParentPartIndices(model)); + for (int o = 0; o < os->count; o++) + os->rt[o] = LoadRenderTexture(w, h); + OffscreensBuildMasks(os, model); + ClipGroupsAllocBuffers(&os->mcg, (int)(w * DEFAULT_MASK_SCALE), (int)(h * DEFAULT_MASK_SCALE)); + return true; +} + +static void OffscreensResize(Offscreens *os, int w, int h, float maskScale) +{ + for (int o = 0; o < os->count; o++) + { + UnloadRenderTexture(os->rt[o]); + os->rt[o] = LoadRenderTexture(w, h); + } + ClipGroupsFreeBuffers(&os->mcg); + ClipGroupsAllocBuffers(&os->mcg, (int)(w * maskScale), (int)(h * maskScale)); +} + +static void OffscreensFree(Offscreens *os) +{ + for (int o = 0; o < os->count; o++) + UnloadRenderTexture(os->rt[o]); + ClipGroupsFreeBuffers(&os->mcg); +} +#endif /* offscreens (v6) */ + +bool RendererInit(Renderer *r, const Model3 *m3, const char *dir, csmModel *model, int w, int h, float maskScale) +{ + memset(r, 0, sizeof(*r)); + r->maskScale = maskScale; + r->texCount = m3->texCount; + for (int i = 0; i < m3->texCount; i++) + { + char tp[1024]; + snprintf(tp, sizeof(tp), "%s%s", dir, m3->tex[i]); + r->textures[i] = LoadTexture(tp); + if (r->textures[i].id) SetTextureFilter(r->textures[i], TEXTURE_FILTER_BILINEAR); + } + + r->sdraw = LoadFs(draw_frag); + r->smask = LoadFs(masked_frag); + r->smaskw = LoadFs(maskwrite_frag); + r->locBase = GetShaderLocation(r->sdraw, "baseColor"); + r->locMul = GetShaderLocation(r->sdraw, "multiplyColor"); + r->locScr = GetShaderLocation(r->sdraw, "screenColor"); + r->mbase = GetShaderLocation(r->smask, "baseColor"); + r->mmul = GetShaderLocation(r->smask, "multiplyColor"); + r->mscr = GetShaderLocation(r->smask, "screenColor"); + r->mmask = GetShaderLocation(r->smask, "maskTexture"); + r->mres = GetShaderLocation(r->smask, "resolution"); + r->minv = GetShaderLocation(r->smask, "maskInvert"); + r->msel = GetShaderLocation(r->smask, "channelSelector"); + r->wmask = GetShaderLocation(r->smaskw, "channelMask"); + +#if PSM_COMPAT_VERSION >= 0x06000000L + r->soff = LoadFs(offscreen_frag); + r->soffm = LoadFs(offscreen_masked_frag); + r->obBase = GetShaderLocation(r->soff, "baseColor"); + r->obMul = GetShaderLocation(r->soff, "multiplyColor"); + r->obScr = GetShaderLocation(r->soff, "screenColor"); + r->omBase = GetShaderLocation(r->soffm, "baseColor"); + r->omMul = GetShaderLocation(r->soffm, "multiplyColor"); + r->omScr = GetShaderLocation(r->soffm, "screenColor"); + r->omMask = GetShaderLocation(r->soffm, "maskTexture"); + r->omRes = GetShaderLocation(r->soffm, "resolution"); + r->omInv = GetShaderLocation(r->soffm, "maskInvert"); + r->omSel = GetShaderLocation(r->soffm, "channelSelector"); + + r->sblend = LoadFs(blend_frag); + r->sbBlend = GetShaderLocation(r->sblend, "blendTexture"); + r->sbMask = GetShaderLocation(r->sblend, "maskTexture"); + r->sbRes = GetShaderLocation(r->sblend, "resolution"); + r->sbCmode = GetShaderLocation(r->sblend, "colorMode"); + r->sbAmode = GetShaderLocation(r->sblend, "alphaMode"); + r->sbUsemask = GetShaderLocation(r->sblend, "useMask"); + r->sbInv = GetShaderLocation(r->sblend, "maskInvert"); + r->sbSel = GetShaderLocation(r->sblend, "channelSelector"); + r->sbBase = GetShaderLocation(r->sblend, "baseColor"); + r->sbMul = GetShaderLocation(r->sblend, "multiplyColor"); + r->sbScr = GetShaderLocation(r->sblend, "screenColor"); + r->backdrop = (Texture2D){ rlLoadTexture(NULL, w, h, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, 1), w, h, 1, + PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 }; + r->scratch = LoadRenderTexture(w, h); +#endif + + if (!BuildClipGroups(model, &r->cg)) return false; + ClipGroupsAllocBuffers(&r->cg, (int)(w * r->maskScale), (int)(h * r->maskScale)); +#if PSM_COMPAT_VERSION >= 0x06000000L + if (!OffscreensInit(&r->os, model, w, h)) return false; +#endif + return true; +} + +void RendererResize(Renderer *r, int w, int h) +{ + ClipGroupsFreeBuffers(&r->cg); + ClipGroupsAllocBuffers(&r->cg, (int)(w * r->maskScale), (int)(h * r->maskScale)); +#if PSM_COMPAT_VERSION >= 0x06000000L + OffscreensResize(&r->os, w, h, r->maskScale); + rlUnloadTexture(r->backdrop.id); + r->backdrop = (Texture2D){ rlLoadTexture(NULL, w, h, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, 1), w, h, 1, + PIXELFORMAT_UNCOMPRESSED_R8G8B8A8 }; + UnloadRenderTexture(r->scratch); + r->scratch = LoadRenderTexture(w, h); +#endif +} + +void RendererFree(Renderer *r) +{ + for (int i = 0; i < r->texCount; i++) + if (r->textures[i].id) UnloadTexture(r->textures[i]); + UnloadShader(r->sdraw); + UnloadShader(r->smask); + UnloadShader(r->smaskw); + ClipGroupsFreeBuffers(&r->cg); + free(r->cg.of); +#if PSM_COMPAT_VERSION >= 0x06000000L + UnloadShader(r->soff); + UnloadShader(r->soffm); + UnloadShader(r->sblend); + rlUnloadTexture(r->backdrop.id); + UnloadRenderTexture(r->scratch); + OffscreensFree(&r->os); +#endif +} diff --git a/src/samples/viewer/io.c b/src/samples/viewer/io.c new file mode 100644 index 0000000..12c348a --- /dev/null +++ b/src/samples/viewer/io.c @@ -0,0 +1,129 @@ +/* + * Purism Core: sample model viewer I/O + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include "viewer.h" + +static void *AlignedBlob(size_t align, size_t size, void **outBase) +{ + void *base = malloc(size + align); + if (!base) + { + *outBase = NULL; + return NULL; + } + *outBase = base; + return (void *)(((size_t)base + align - 1) & ~(align - 1)); +} + +static bool JsonStringValue(const char *buf, const char *key, char *dst, int dstsz) +{ + int ki = TextFindIndex(buf, TextFormat("\"%s\"", key)); + if (ki < 0) return false; + const char *p = buf + ki; + int colon = TextFindIndex(p, ":"); + if (colon < 0) return false; + p += colon; + int q1 = TextFindIndex(p, "\""); /* opening quote of the value */ + if (q1 < 0) return false; + p += q1 + 1; + int q2 = TextFindIndex(p, "\""); /* closing quote -> value length */ + if (q2 < 0) return false; + if (q2 > dstsz - 1) q2 = dstsz - 1; + TextCopy(dst, TextSubtext(p, 0, q2)); + return true; +} + +bool ParseModel3(const char *path, Model3 *m3) +{ + char *buf = LoadFileText(path); /* raylib: NUL-terminated text */ + if (!buf) return false; + + memset(m3, 0, sizeof(*m3)); + bool ok = JsonStringValue(buf, "Moc", m3->moc, sizeof(m3->moc)); + + /* Textures: find the [ ... ] array, then pull each "quoted" path until ']'. + * Scanned in place with TextFindIndex (no fixed-size split buffer), so a + * long texture list is not truncated; each value is bounded on copy. */ + int ti = TextFindIndex(buf, "\"Textures\""); + if (ti >= 0) + { + const char *p = buf + ti; + int lb = TextFindIndex(p, "["); + int rb = TextFindIndex(p, "]"); + if (lb >= 0 && rb > lb) + { + const char *end = p + rb; + const char *q = p + lb + 1; + while (m3->texCount < MAX_TEXTURES) + { + int o = TextFindIndex(q, "\""); /* opening quote */ + if (o < 0 || q + o >= end) break; + q += o + 1; + int c = TextFindIndex(q, "\""); /* closing quote */ + if (c < 0 || q + c > end) break; + int len = c < (int)sizeof(m3->tex[0]) ? c : (int)sizeof(m3->tex[0]) - 1; + TextCopy(m3->tex[m3->texCount++], TextSubtext(q, 0, len)); + q += c + 1; + } + if (m3->texCount >= MAX_TEXTURES && TextFindIndex(q, "\"") >= 0 && q + TextFindIndex(q, "\"") < end) + TraceLog(LOG_WARNING, "more than %d textures; extras ignored", MAX_TEXTURES); + } + } + UnloadFileText(buf); + return ok; +} + +bool LoadCore(const char *mocPath, Core *c) +{ + memset(c, 0, sizeof(*c)); + int sz; + unsigned char *file = LoadFileData(mocPath, &sz); /* raylib */ + if (!file) + { + fprintf(stderr, "cannot read moc: %s\n", mocPath); + return false; + } + void *mocMem = AlignedBlob(csmAlignofMoc, (size_t)sz, &c->mocBase); + if (!mocMem) + { + fprintf(stderr, "out of memory\n"); + UnloadFileData(file); + return false; + } + memcpy(mocMem, file, (size_t)sz); + UnloadFileData(file); + + c->moc = csmReviveMocInPlace(mocMem, (unsigned)sz); + if (!c->moc) + { + fprintf(stderr, "revive failed: %s\n", csmGetErrorString(csmGetMocError((const csmMoc *)mocMem))); + FreeCore(c); /* free what we allocated (matters for repeated drops) */ + return false; + } + unsigned modelSz = csmGetSizeofModel(c->moc); + void *modelMem = AlignedBlob(csmAlignofModel, modelSz, &c->modelBase); + if (!modelMem) + { + fprintf(stderr, "out of memory\n"); + FreeCore(c); + return false; + } + c->model = csmInitializeModelInPlace(c->moc, modelMem, modelSz); + if (!c->model) + { + fprintf(stderr, "init failed: %s\n", csmGetErrorString(csmGetMocError(c->moc))); + FreeCore(c); + return false; + } + return true; +} + +void FreeCore(Core *c) +{ + free(c->mocBase); + free(c->modelBase); +} diff --git a/src/samples/viewer/panel.c b/src/samples/viewer/panel.c new file mode 100644 index 0000000..e9d4d65 --- /dev/null +++ b/src/samples/viewer/panel.c @@ -0,0 +1,261 @@ +/* + * Purism Core: sample model viewer control panel + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include "viewer.h" +/* UI font baked in by bin2h (see the Makefile): cascadia_fnt (the .fnt text) + + * cascadia_0_png (the grayscale atlas). */ +#include "embed/cascadia.fnt.h" +#include "embed/cascadia_0.png.h" + +#define RAYGUI_IMPLEMENTATION +#include "../vendor/raygui.h" + +/* Copy the next '\n'-terminated line of *pp into buf (bounded), advancing *pp + * past the newline. Returns 0 at end of text. */ +static int FontNextLine(const char **pp, char *buf, int bufsz) +{ + const char *p = *pp; + if (!*p) return 0; + int n = 0; + while (*p && *p != '\n') + { + if (n < bufsz - 1) buf[n++] = *p; + p++; + } + buf[n] = '\0'; + if (*p == '\n') p++; + *pp = p; + return 1; +} + +Font LoadUiFont(void) +{ + Font font = { 0 }; + + Image img = LoadImageFromMemory(".png", (const unsigned char *)cascadia_0_png, (int)cascadia_0_png_size); + if (img.data == NULL) + { + Font f = GetFontDefault(); + GuiSetFont(f); + return f; + } + /* The atlas is 8-bit grayscale where the gray value is the glyph mask; raylib + * turns that into GRAY+ALPHA (gray=0xff, alpha=mask) so it composites right. */ + if (img.format == PIXELFORMAT_UNCOMPRESSED_GRAYSCALE) + { + int count = img.width * img.height; + unsigned char *src = (unsigned char *)img.data; + unsigned char *ga = (unsigned char *)RL_MALLOC((size_t)count * 2); + for (int i = 0; i < count; i++) + { + ga[i * 2] = 0xff; + ga[i * 2 + 1] = src[i]; + } + UnloadImage(img); + img.data = ga; + img.format = PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA; + img.mipmaps = 1; + } + font.texture = LoadTextureFromImage(img); + UnloadImage(img); + + /* Parse the .fnt: baseSize from `common lineHeight`, then the `char` lines. + * (Single page; mirrors raylib's LoadBMFont field order.) */ + const char *p = (const char *)cascadia_fnt; + char line[256]; + int fontSize = 0, base = 0, sw = 0, sh = 0, glyphCount = 0; + FontNextLine(&p, line, sizeof line); /* info ... (skip) */ + FontNextLine(&p, line, sizeof line); /* common lineHeight=.. */ + const char *s = strstr(line, "lineHeight"); + if (s) sscanf(s, "lineHeight=%i base=%i scaleW=%i scaleH=%i", &fontSize, &base, &sw, &sh); + FontNextLine(&p, line, sizeof line); /* page file=".." (skip) */ + FontNextLine(&p, line, sizeof line); /* chars count=.. */ + s = strstr(line, "count"); + if (s) sscanf(s, "count=%i", &glyphCount); + (void)base; + (void)sw; + (void)sh; + + font.baseSize = fontSize; + font.glyphCount = glyphCount; + font.glyphPadding = 0; + font.glyphs = (GlyphInfo *)RL_MALLOC((size_t)glyphCount * sizeof(GlyphInfo)); + font.recs = (Rectangle *)RL_MALLOC((size_t)glyphCount * sizeof(Rectangle)); + + for (int i = 0; i < glyphCount; i++) + { + int id = 0, x = 0, y = 0, w = 0, h = 0, ox = 0, oy = 0, ax = 0, pg = 0; + FontNextLine(&p, line, sizeof line); + sscanf(line, + "char id=%i x=%i y=%i width=%i height=%i xoffset=%i yoffset=%i" + " xadvance=%i page=%i", + &id, &x, &y, &w, &h, &ox, &oy, &ax, &pg); + font.recs[i] = (Rectangle){ (float)x, (float)y, (float)w, (float)h }; + font.glyphs[i].value = id; + font.glyphs[i].offsetX = ox; + font.glyphs[i].offsetY = oy; + font.glyphs[i].advanceX = ax; + font.glyphs[i].image = (Image){ 0 }; /* unused for drawing; freed safely */ + } + + if (font.texture.id == 0) + { + UnloadFont(font); + Font f = GetFontDefault(); + GuiSetFont(f); + return f; + } + SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR); + GuiSetFont(font); + return font; +} + +/* case-insensitive substring test ("" matches everything) */ +static bool CiContains(const char *hay, const char *needle) +{ + if (!needle[0]) return true; + for (const char *h = hay; *h; h++) + { + const char *a = h, *b = needle; + while (*a && *b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) + { + a++; + b++; + } + if (!*b) return true; + } + return false; +} + +/* One row: id label, a slider, and a value box you can click and type into. + * `id` identifies the row for edit-focus tracking (unique within a tab). */ +static void ValueRow(Rectangle r, const char *label, float *value, float mn, float mx, int id, PanelState *ps) +{ + float lh = r.height * 0.5f; + float vbw = lh * 3.2f; + GuiLabel((Rectangle){ r.x, r.y, r.width, lh }, label); + GuiSliderBar((Rectangle){ r.x, r.y + lh, r.width - vbw - 4, lh }, NULL, NULL, value, mn, mx); + + Rectangle vb = { r.x + r.width - vbw, r.y + lh, vbw, lh }; + bool edit = (ps->editId == id); + char tmp[32], *txt; + if (edit) + { + txt = ps->editBuf; /* persistent: holds the in-progress text */ + } + else + { + snprintf(tmp, sizeof(tmp), "%.3f", *value); + txt = tmp; /* transient: display only */ + } + if (GuiValueBoxFloat(vb, NULL, txt, value, edit)) + { + if (edit) + { + ps->editId = -1; /* Enter / click-away -> commit */ + } + else + { + ps->editId = id; /* click -> start editing this box */ + snprintf(ps->editBuf, sizeof(ps->editBuf), "%.3f", *value); + } + } +} + +/* A filtered, scrollable list of value rows. mins/maxs may be NULL (range + * defaults to [0,1], used for part opacities). */ +static void ValueList(Rectangle area, PanelState *ps, float *scroll, int n, const char **ids, float *vals, + const float *mins, const float *maxs) +{ + float s = ps->uiScale; + float rowH = 44 * s; + + int shown = 0; + for (int i = 0; i < n; i++) + if (CiContains(ids[i], ps->filter)) shown++; + + Rectangle content = { 0, 0, area.width - 16, shown * rowH + 8 }; + Vector2 sc = { 0, *scroll }; + Rectangle view; + GuiScrollPanel(area, NULL, content, &sc, &view); + *scroll = sc.y; + + BeginScissorMode((int)view.x, (int)view.y, (int)view.width, (int)view.height); + float x = area.x + 8, w = area.width - 24; + float y = area.y + 4 + *scroll; + for (int i = 0; i < n; i++) + { + if (!CiContains(ids[i], ps->filter)) continue; + /* skip rows scrolled out of view (cheap; keeps 100+ params fast) */ + if (y + rowH >= view.y && y <= view.y + view.height) + { + float mn = mins ? mins[i] : 0.0f; + float mx = maxs ? maxs[i] : 1.0f; + ValueRow((Rectangle){ x, y, w, rowH - 8 * s }, ids[i], &vals[i], mn, mx, i, ps); + } + y += rowH; + } + EndScissorMode(); +} + +void DrawPanel(csmModel *m, Rectangle area, PanelState *ps) +{ + float s = ps->uiScale; + GuiSetStyle(DEFAULT, TEXT_SIZE, (int)(16 * s)); + GuiPanel(area, NULL); + + float pad = 8 * s, lh = 24 * s, gap = 4 * s, btn = lh; + float x = area.x + pad, w = area.width - 2 * pad; + float y = area.y + pad; + + /* search filter + UI-scale buttons */ + if (GuiTextBox((Rectangle){ x, y, w - 2 * btn - 2 * gap, lh }, ps->filter, sizeof(ps->filter), ps->filterEdit)) + ps->filterEdit = !ps->filterEdit; + if (GuiButton((Rectangle){ x + w - 2 * btn - gap, y, btn, lh }, "-")) ps->uiScale = s > 0.75f ? s - 0.1f : 0.7f; + if (GuiButton((Rectangle){ x + w - btn, y, btn, lh }, "+")) ps->uiScale = s < 2.15f ? s + 0.1f : 2.2f; + y += lh + gap; + + /* tab switch (toggle group, not a tab bar -- no spurious close buttons) */ + int prev = ps->tab; + GuiToggleGroup((Rectangle){ x, y, (w - gap) / 2, lh }, "Parameters;Parts", &ps->tab); + if (ps->tab != prev) ps->editId = -1; /* drop edit focus when switching tabs */ + y += lh + gap; + + /* reset button (context-sensitive) */ + if (GuiButton((Rectangle){ x, y, w, lh }, ps->tab == 0 ? "Reset parameters to defaults" : "Reset part opacities")) + { + if (ps->tab == 0) + { + int pc = csmGetParameterCount(m); + const float *def = csmGetParameterDefaultValues(m); + float *v = csmGetParameterValues(m); + for (int i = 0; i < pc; i++) + v[i] = def[i]; + } + else + { + int pc = csmGetPartCount(m); + float *o = csmGetPartOpacities(m); + for (int i = 0; i < pc; i++) + o[i] = 1.0f; + } + ps->editId = -1; + } + y += lh + gap; + + Rectangle content = { area.x, y, area.width, area.y + area.height - y }; + if (ps->tab == 0) + { + ValueList(content, ps, &ps->scrollParam, csmGetParameterCount(m), csmGetParameterIds(m), + csmGetParameterValues(m), csmGetParameterMinimumValues(m), csmGetParameterMaximumValues(m)); + } + else + { + ValueList(content, ps, &ps->scrollPart, csmGetPartCount(m), csmGetPartIds(m), csmGetPartOpacities(m), NULL, + NULL); + } +} diff --git a/src/samples/viewer/render.c b/src/samples/viewer/render.c new file mode 100644 index 0000000..4fda6f9 --- /dev/null +++ b/src/samples/viewer/render.c @@ -0,0 +1,712 @@ +/* + * Purism Core: sample model viewer rendering + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include "viewer.h" + +static void RenderMasks(csmModel *model, Renderer *r, const View *view) +{ + int dc = csmGetDrawableCount(model); + const csmVector2 **pos = csmGetDrawableVertexPositions(model); + const csmVector2 **uv = csmGetDrawableVertexUvs(model); + const int *icount = csmGetDrawableIndexCounts(model); + const unsigned short **idx = csmGetDrawableIndices(model); + const int *texidx = csmGetDrawableTextureIndices(model); + + /* The mask buffers are MASK_SCALE of screen size. Render the masks with a + * matchingly-scaled view so their content lands at the same normalized + * screen position the masked drawables sample at (gl_FragCoord/resolution). */ + float s = r->maskScale; + View mv = *view; + mv.zoom *= s; + mv.panx *= s; + mv.pany *= s; + mv.sw = (int)(view->sw * s); + mv.sh = (int)(view->sh * s); + + BeginShaderMode(r->smaskw); + for (int b = 0; b < r->cg.bufCount; b++) + { + BeginTextureMode(r->cg.buf[b]); + ClearBackground(BLANK); + /* Pure additive (src*1 + dst*1) so each group's coverage lands in its + * own channel without disturbing the others. */ + rlSetBlendFactorsSeparate(RL_ONE, RL_ONE, RL_ONE, RL_ONE, RL_FUNC_ADD, RL_FUNC_ADD); + rlSetBlendMode(RL_BLEND_CUSTOM_SEPARATE); + + int g0 = b * GROUPS_PER_BUF; + int g1 = g0 + GROUPS_PER_BUF; + if (g1 > r->cg.count) g1 = r->cg.count; + for (int g = g0; g < g1; g++) + { + float cmask[4]; + ChannelVec(g % GROUPS_PER_BUF, cmask); + SetShaderValue(r->smaskw, r->wmask, cmask, SHADER_UNIFORM_VEC4); + for (int mi = 0; mi < r->cg.maskCount[g]; mi++) + { + int md = r->cg.masks[g][mi]; + if (md < 0 || md >= dc) continue; + int t = texidx[md] >= 0 ? r->textures[texidx[md]].id : 0; + EmitDrawable(&mv, t, pos[md], uv[md], idx[md], icount[md]); + } + rlDrawRenderBatchActive(); /* flush so this group draws with its channel */ + } + EndTextureMode(); + } + EndShaderMode(); + rlSetBlendMode(RL_BLEND_ALPHA); /* restore default for the main pass */ +} + +static void DrawOneDrawable(csmModel *model, Renderer *r, const View *view, bool maskingOn, int d, const float resv[2], + int blendOverride) +{ + const int *texidx = csmGetDrawableTextureIndices(model); + const csmVector2 **pos = csmGetDrawableVertexPositions(model); + const csmVector2 **uv = csmGetDrawableVertexUvs(model); + const int *icount = csmGetDrawableIndexCounts(model); + const unsigned short **idx = csmGetDrawableIndices(model); + const float *opacity = csmGetDrawableOpacities(model); + const csmVector4 *mul = csmGetDrawableMultiplyColors(model); + const csmVector4 *scr = csmGetDrawableScreenColors(model); + const csmFlags *cflags = csmGetDrawableConstantFlags(model); + + int g = maskingOn ? r->cg.of[d] : -1; + csmVector4 mc = mul[d], sc = scr[d]; + float base[4] = { 1, 1, 1, opacity[d] }; + float mulv[4] = { mc.X, mc.Y, mc.Z, 1 }; + float scrv[4] = { sc.X, sc.Y, sc.Z, 0 }; + + Shader sh = (g >= 0) ? r->smask : r->sdraw; + BeginShaderMode(sh); + /* Set the blend mode BEFORE binding the mask sampler: rlSetBlendMode + * flushes the batch when the mode changes, and the flush clears the + * extra-texture registration (activeTextureId). Registering the mask + * first, then flushing, would unbind it and the mask sampler would read + * a stale unit (~1 coverage = no clipping). */ +#if PSM_COMPAT_VERSION >= 0x06000000L + /* v6 drawables carry an extended color+alpha blend (e.g. the hologram is + * HardLight+Atop): honor it so Atop/Out clip to the destination. */ + ApplyExtendedBlend(blendOverride >= 0 ? blendOverride : csmGetDrawableBlendModes(model)[d]); +#else + ApplyBlend(cflags[d]); +#endif + if (g >= 0) + { + SetShaderValue(sh, r->mbase, base, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->mmul, mulv, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->mscr, scrv, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->mres, resv, SHADER_UNIFORM_VEC2); + float invf = (cflags[d] & csmIsInvertedMask) ? 1.0f : 0.0f; + SetShaderValue(sh, r->minv, &invf, SHADER_UNIFORM_FLOAT); + float csel[4]; + ChannelVec(g % GROUPS_PER_BUF, csel); + SetShaderValue(sh, r->msel, csel, SHADER_UNIFORM_VEC4); + SetShaderValueTexture(sh, r->mmask, r->cg.buf[g / GROUPS_PER_BUF].texture); + } + else + { + SetShaderValue(sh, r->locBase, base, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->locMul, mulv, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->locScr, scrv, SHADER_UNIFORM_VEC4); + } + + int t = texidx[d] >= 0 ? r->textures[texidx[d]].id : 0; + EmitDrawable(view, t, pos[d], uv[d], idx[d], icount[d]); + EndShaderMode(); +} + +/* True if drawable d passes the debug filters (--only/--hide/--maxorder). */ +static bool DrawablePassesFilters(int d, int ord, const Options *opt) +{ + if (ord > opt->maxOrder) return false; + if (opt->onlyDrawable >= 0 && d != opt->onlyDrawable) return false; + for (int h = 0; h < opt->hideCount; h++) + if (opt->hideIdx[h] == d) return false; + return true; +} + +static void RenderModelFlat(csmModel *model, Renderer *r, const View *view, bool maskingOn, const Options *opt, + int *sorted) +{ + int dc = csmGetDrawableCount(model); + const int *order = VIEWER_GET_RENDER_ORDERS(model); + const float *opacity = csmGetDrawableOpacities(model); + const csmFlags *dflags = csmGetDrawableDynamicFlags(model); + + /* sort drawable indices by render order (ascending = back to front). + * Insertion sort: near-O(n) since order is stable frame to frame. */ + for (int i = 0; i < dc; i++) + sorted[i] = i; + for (int i = 1; i < dc; i++) + { + int key = sorted[i], j = i - 1; + while (j >= 0 && order[sorted[j]] > order[key]) + { + sorted[j + 1] = sorted[j]; + j--; + } + sorted[j + 1] = key; + } + + if (maskingOn) RenderMasks(model, r, view); + + float resv[2] = { (float)view->sw, (float)view->sh }; + rlDisableBackfaceCulling(); + + for (int s = 0; s < dc; s++) + { + int d = sorted[s]; + if (!(dflags[d] & csmIsVisible) || opacity[d] <= 0.0f) continue; + if (!DrawablePassesFilters(d, order[d], opt)) continue; + DrawOneDrawable(model, r, view, maskingOn, d, resv, -1); + } + + rlSetBlendMode(RL_BLEND_ALPHA); /* restore default for UI */ +} + +#if PSM_COMPAT_VERSION >= 0x06000000L +static void BindTarget(unsigned fbo, int w, int h) +{ + rlDrawRenderBatchActive(); /* flush whatever was queued for old target */ + rlEnableFramebuffer(fbo); + rlViewport(0, 0, w, h); + rlSetFramebufferWidth(w); + rlSetFramebufferHeight(h); + rlMatrixMode(RL_PROJECTION); + rlLoadIdentity(); + rlOrtho(0, w, h, 0, -1.0, 1.0); /* y-down, raylib screen convention */ + rlMatrixMode(RL_MODELVIEW); + rlLoadIdentity(); +} + +static void ScissorToOffscreen(const Offscreens *os, int o, int sh) +{ + if (o < 0) + { + rlDisableScissorTest(); + return; + } + const int *b = os->bbox[o]; + int x = b[0], y = b[1], w = b[2] - b[0], h = b[3] - b[1]; + if (w < 0) w = 0; + if (h < 0) h = 0; + rlEnableScissorTest(); + rlScissor(x, sh - (y + h), w, h); +} + +static void OffscreensComputeBboxes(Offscreens *os, csmModel *model, const View *view, bool maskingOn) +{ + int dc = csmGetDrawableCount(model); + const csmVector2 **pos = csmGetDrawableVertexPositions(model); + const int *vcount = csmGetDrawableVertexCounts(model); + const csmFlags *dflags = csmGetDrawableDynamicFlags(model); + const float *opacity = csmGetDrawableOpacities(model); + const int *partParent = csmGetPartParentPartIndices(model); + const int *drawPart = csmGetDrawableParentPartIndices(model); + const int *bm = csmGetOffscreenBlendModes(model); + const float *oop = csmGetOffscreenOpacities(model); + const csmVector4 *mul = csmGetOffscreenMultiplyColors(model); + const csmVector4 *scr = csmGetOffscreenScreenColors(model); + + for (int o = 0; o < os->count; o++) + { + os->bbox[o][0] = os->bbox[o][1] = 1 << 29; /* empty: x0,y0 = +inf */ + os->bbox[o][2] = os->bbox[o][3] = -(1 << 29); + /* A group needs its own RT only if it changes pixels as a unit. */ + os->effectful[o] = bm[o] != 0 || oop[o] < 0.999f || (maskingOn && os->mgroup[o] >= 0) || mul[o].X != 1.0f || + mul[o].Y != 1.0f || mul[o].Z != 1.0f || scr[o].X != 0.0f || scr[o].Y != 0.0f || + scr[o].Z != 0.0f; + } + const int *dbm = csmGetDrawableBlendModes(model); + for (int d = 0; d < dc; d++) + { + if (!(dflags[d] & csmIsVisible) || opacity[d] <= 0.0f) continue; + float minx = 1e30f, miny = 1e30f, maxx = -1e30f, maxy = -1e30f; + for (int i = 0; i < vcount[d]; i++) + { + float sx, sy; + WorldToScreen(view, pos[d][i].X, pos[d][i].Y, &sx, &sy); + if (sx < minx) minx = sx; + if (sx > maxx) maxx = sx; + if (sy < miny) miny = sy; + if (sy > maxy) maxy = sy; + } + /* clamp + pad by 1px (bilinear sampling) and to the screen */ + int x0 = (int)minx - 1, y0 = (int)miny - 1; + int x1 = (int)maxx + 2, y1 = (int)maxy + 2; + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > view->sw) x1 = view->sw; + if (y1 > view->sh) y1 = view->sh; + if (x1 <= x0 || y1 <= y0) continue; + /* A drawable with an Atop/Out alpha op blends against its target's alpha, + * so the group it draws into (its deepest owning group) must be a real RT, + * not a pass-through to the opaque screen. Mark that group effectful. */ + bool needCov = BlendAlphaNeedsCoverage(dbm[d]); + int deepest = -1; + /* add to every offscreen whose owner part is an ancestor of this part */ + for (int p = drawPart[d]; p >= 0; p = partParent[p]) + { + for (int o = 0; o < os->count; o++) + { + if (os->owner[o] != p) continue; + if (deepest < 0) deepest = o; /* first match = deepest group */ + if (x0 < os->bbox[o][0]) os->bbox[o][0] = x0; + if (y0 < os->bbox[o][1]) os->bbox[o][1] = y0; + if (x1 > os->bbox[o][2]) os->bbox[o][2] = x1; + if (y1 > os->bbox[o][3]) os->bbox[o][3] = y1; + } + } + if (needCov && deepest >= 0) os->effectful[deepest] = true; + } +} + +/* Screen-space AABB (top-left px, clamped + 1px pad) of one drawable. */ +static void DrawableScreenBbox(const View *view, csmModel *model, int d, int out[4]) +{ + const csmVector2 **pos = csmGetDrawableVertexPositions(model); + const int *vcount = csmGetDrawableVertexCounts(model); + float minx = 1e30f, miny = 1e30f, maxx = -1e30f, maxy = -1e30f; + for (int i = 0; i < vcount[d]; i++) + { + float sx, sy; + WorldToScreen(view, pos[d][i].X, pos[d][i].Y, &sx, &sy); + if (sx < minx) minx = sx; + if (sx > maxx) maxx = sx; + if (sy < miny) miny = sy; + if (sy > maxy) maxy = sy; + } + int x0 = (int)minx - 1, y0 = (int)miny - 1; + int x1 = (int)maxx + 2, y1 = (int)maxy + 2; + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > view->sw) x1 = view->sw; + if (y1 > view->sh) y1 = view->sh; + out[0] = x0; + out[1] = y0; + out[2] = x1; + out[3] = y1; +} + +#if defined(__EMSCRIPTEN__) +#include +#elif !defined(_WIN32) +#include +#endif + +typedef void (*PsmGlActiveTexture)(unsigned int); +typedef void (*PsmGlBindTexture)(unsigned int, unsigned int); +typedef void (*PsmGlCopyTexSubImage2D)(unsigned int, int, int, int, int, int, int, int); +static PsmGlActiveTexture psm_glActiveTexture; +static PsmGlBindTexture psm_glBindTexture; +static PsmGlCopyTexSubImage2D psm_glCopyTexSubImage2D; + +#if !defined(__EMSCRIPTEN__) && defined(_WIN32) +extern void *__stdcall wglGetProcAddress(const char *name); +extern void *__stdcall GetModuleHandleA(const char *name); +extern void *__stdcall GetProcAddress(void *module, const char *name); +static void *GlProc(const char *name) +{ + void *p = wglGetProcAddress(name); /* GL >1.1 */ + if (!p) p = GetProcAddress(GetModuleHandleA("opengl32.dll"), name); /* GL 1.1 */ + return p; +} +#endif + +static void LoadGLProcs(void) +{ + if (psm_glActiveTexture) return; +#if defined(__EMSCRIPTEN__) + psm_glActiveTexture = (PsmGlActiveTexture)glActiveTexture; + psm_glBindTexture = (PsmGlBindTexture)glBindTexture; + psm_glCopyTexSubImage2D = (PsmGlCopyTexSubImage2D)glCopyTexSubImage2D; +#elif defined(_WIN32) + psm_glActiveTexture = (PsmGlActiveTexture)GlProc("glActiveTexture"); + psm_glBindTexture = (PsmGlBindTexture)GlProc("glBindTexture"); + psm_glCopyTexSubImage2D = (PsmGlCopyTexSubImage2D)GlProc("glCopyTexSubImage2D"); +#else + psm_glActiveTexture = (PsmGlActiveTexture)dlsym(RTLD_DEFAULT, "glActiveTexture"); + psm_glBindTexture = (PsmGlBindTexture)dlsym(RTLD_DEFAULT, "glBindTexture"); + psm_glCopyTexSubImage2D = (PsmGlCopyTexSubImage2D)dlsym(RTLD_DEFAULT, "glCopyTexSubImage2D"); +#endif +} + +static void CompositeBackdrop(Renderer *r, const View *view, unsigned destFbo, const int bbox[4], int colorMode, + int alphaMode, const float base[4], const float mulv[4], const float scrv[4], int mg, + float maskInvert, unsigned sourceTex) +{ + int x = bbox[0], y = bbox[1], w = bbox[2] - bbox[0], h = bbox[3] - bbox[1]; + if (w <= 0 || h <= 0) return; + int gy = view->sh - (y + h); /* GL bottom-left origin */ + + BindTarget(destFbo, view->sw, view->sh); /* flush + bind dest */ + rlDisableBackfaceCulling(); + rlEnableScissorTest(); + rlScissor(x, gy, w, h); + + /* Copy the dest's bbox into the backdrop texture (same pixel coords). Done on + * texture unit 0 while no batch is pending; raylib re-binds its own textures + * at draw time, so this transient binding does not disturb it. */ + LoadGLProcs(); + psm_glActiveTexture(PSM_GL_TEXTURE0); + psm_glBindTexture(PSM_GL_TEXTURE_2D, r->backdrop.id); + psm_glCopyTexSubImage2D(PSM_GL_TEXTURE_2D, 0, x, gy, x, gy, w, h); + psm_glBindTexture(PSM_GL_TEXTURE_2D, 0); + + float resv[2] = { (float)view->sw, (float)view->sh }; + Shader sh = r->sblend; + BeginShaderMode(sh); + rlSetBlendFactorsSeparate(RL_ONE, RL_ZERO, RL_ONE, RL_ZERO, RL_FUNC_ADD, + RL_FUNC_ADD); /* REPLACE: shader is the result */ + rlSetBlendMode(RL_BLEND_CUSTOM_SEPARATE); + SetShaderValue(sh, r->sbRes, resv, SHADER_UNIFORM_VEC2); + SetShaderValue(sh, r->sbCmode, &colorMode, SHADER_UNIFORM_INT); + SetShaderValue(sh, r->sbAmode, &alphaMode, SHADER_UNIFORM_INT); + SetShaderValue(sh, r->sbBase, base, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->sbMul, mulv, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->sbScr, scrv, SHADER_UNIFORM_VEC4); + float usemask = (mg >= 0) ? 1.0f : 0.0f; + SetShaderValue(sh, r->sbUsemask, &usemask, SHADER_UNIFORM_FLOAT); + SetShaderValue(sh, r->sbInv, &maskInvert, SHADER_UNIFORM_FLOAT); + if (mg >= 0) + { + float csel[4]; + ChannelVec(mg % GROUPS_PER_BUF, csel); + SetShaderValue(sh, r->sbSel, csel, SHADER_UNIFORM_VEC4); + SetShaderValueTexture(sh, r->sbMask, r->os.mcg.buf[mg / GROUPS_PER_BUF].texture); + } + SetShaderValueTexture(sh, r->sbBlend, r->backdrop); + + float W = (float)view->sw, H = (float)view->sh; + rlCheckRenderBatchLimit(6); + rlSetTexture(sourceTex); + rlBegin(RL_TRIANGLES); + rlColor4ub(255, 255, 255, 255); + rlTexCoord2f(0, 1); + rlVertex2f(0, 0); + rlTexCoord2f(0, 0); + rlVertex2f(0, H); + rlTexCoord2f(1, 0); + rlVertex2f(W, H); + rlTexCoord2f(0, 1); + rlVertex2f(0, 0); + rlTexCoord2f(1, 0); + rlVertex2f(W, H); + rlTexCoord2f(1, 1); + rlVertex2f(W, 0); + rlEnd(); + EndShaderMode(); +} + +/* Composite the offscreen `o` (already fully rendered) onto `destFbo`, + * applying group opacity / blend / multiply+screen color / clipping mask. */ +static void CompositeOffscreen(csmModel *model, Renderer *r, const View *view, bool maskingOn, int o, unsigned destFbo) +{ + const float *opa = csmGetOffscreenOpacities(model); + const int *bm = csmGetOffscreenBlendModes(model); + const csmVector4 *mul = csmGetOffscreenMultiplyColors(model); + const csmVector4 *scr = csmGetOffscreenScreenColors(model); + const csmFlags *cf = csmGetOffscreenConstantFlags(model); + Offscreens *os = &r->os; + + int mg = maskingOn ? os->mgroup[o] : -1; + csmVector4 mc = mul[o], sc = scr[o]; + float base[4] = { 1, 1, 1, opa[o] }; + float mulv[4] = { mc.X, mc.Y, mc.Z, 1 }; + float scrv[4] = { sc.X, sc.Y, sc.Z, 0 }; + float resv[2] = { (float)view->sw, (float)view->sh }; + + /* Exotic color/alpha: route through the exact backdrop-sampling composite. */ + if (BlendIsExotic(bm[o])) + { + float invf = (cf[o] & csmIsInvertedMask) ? 1.0f : 0.0f; + CompositeBackdrop(r, view, destFbo, os->bbox[o], bm[o] & 0xFF, (bm[o] >> 8) & 0xFF, base, mulv, scrv, mg, invf, + os->rt[o].texture.id); + return; + } + + /* Switch back to the destination target. */ + BindTarget(destFbo, view->sw, view->sh); + rlDisableBackfaceCulling(); + /* The composite quad spans the screen but the group's content lives only in + * its box, so clip the fill to it (BindTarget just flushed). */ + ScissorToOffscreen(os, o, view->sh); + + Shader sh = (mg >= 0) ? r->soffm : r->soff; + BeginShaderMode(sh); + ApplyExtendedBlend(bm[o]); + if (mg >= 0) + { + SetShaderValue(sh, r->omBase, base, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->omMul, mulv, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->omScr, scrv, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->omRes, resv, SHADER_UNIFORM_VEC2); + float invf = (cf[o] & csmIsInvertedMask) ? 1.0f : 0.0f; + SetShaderValue(sh, r->omInv, &invf, SHADER_UNIFORM_FLOAT); + float csel[4]; + ChannelVec(mg % GROUPS_PER_BUF, csel); + SetShaderValue(sh, r->omSel, csel, SHADER_UNIFORM_VEC4); + SetShaderValueTexture(sh, r->omMask, os->mcg.buf[mg / GROUPS_PER_BUF].texture); + } + else + { + SetShaderValue(sh, r->obBase, base, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->obMul, mulv, SHADER_UNIFORM_VEC4); + SetShaderValue(sh, r->obScr, scrv, SHADER_UNIFORM_VEC4); + } + + /* Full-screen quad sampling the offscreen RT. The RT is stored bottom-up, + * so flip V. The destination target's own projection handles orientation. */ + Texture2D rt = os->rt[o].texture; + float W = (float)view->sw, H = (float)view->sh; + rlCheckRenderBatchLimit(6); + rlSetTexture(rt.id); + rlBegin(RL_TRIANGLES); + rlColor4ub(255, 255, 255, 255); + /* two triangles covering [0,W]x[0,H], V flipped */ + rlTexCoord2f(0, 1); + rlVertex2f(0, 0); + rlTexCoord2f(0, 0); + rlVertex2f(0, H); + rlTexCoord2f(1, 0); + rlVertex2f(W, H); + rlTexCoord2f(0, 1); + rlVertex2f(0, 0); + rlTexCoord2f(1, 0); + rlVertex2f(W, H); + rlTexCoord2f(1, 1); + rlVertex2f(W, 0); + rlEnd(); + EndShaderMode(); +} + +static void DrawDrawableExotic(csmModel *model, Renderer *r, const View *view, bool maskingOn, int d, unsigned destFbo) +{ + int bb[4]; + DrawableScreenBbox(view, model, d, bb); + if (bb[2] <= bb[0] || bb[3] <= bb[1]) return; + + /* Lay the drawable down in isolation (normal+over) into the scratch RT. */ + BindTarget(r->scratch.id, view->sw, view->sh); + int x = bb[0], y = bb[1], w = bb[2] - bb[0], h = bb[3] - bb[1]; + rlEnableScissorTest(); + rlScissor(x, view->sh - (y + h), w, h); + rlClearColor(0, 0, 0, 0); + rlClearScreenBuffers(); + rlDisableBackfaceCulling(); + float resv[2] = { (float)view->sw, (float)view->sh }; + DrawOneDrawable(model, r, view, maskingOn, d, resv, 0 /* normal */); + + /* Composite the isolated drawable with its real blend (colors/opacity/mask + * already baked into the scratch RT, so use identity here). */ + int ext = csmGetDrawableBlendModes(model)[d]; + float base[4] = { 1, 1, 1, 1 }, mul[4] = { 1, 1, 1, 1 }, scr[4] = { 0, 0, 0, 0 }; + CompositeBackdrop(r, view, destFbo, bb, ext & 0xFF, (ext >> 8) & 0xFF, base, mul, scr, -1, 0.0f, + r->scratch.texture.id); +} + +static void RenderOffscreenMasks(csmModel *model, Renderer *r, const View *view) +{ + Offscreens *os = &r->os; + if (os->mcg.count == 0) return; + int dc = csmGetDrawableCount(model); + const csmVector2 **pos = csmGetDrawableVertexPositions(model); + const csmVector2 **uv = csmGetDrawableVertexUvs(model); + const int *icount = csmGetDrawableIndexCounts(model); + const unsigned short **idx = csmGetDrawableIndices(model); + const int *texidx = csmGetDrawableTextureIndices(model); + + float s = DEFAULT_MASK_SCALE; + View mv = *view; + mv.zoom *= s; + mv.panx *= s; + mv.pany *= s; + mv.sw = (int)(view->sw * s); + mv.sh = (int)(view->sh * s); + + BeginShaderMode(r->smaskw); + for (int b = 0; b < os->mcg.bufCount; b++) + { + BeginTextureMode(os->mcg.buf[b]); + ClearBackground(BLANK); + rlSetBlendFactorsSeparate(RL_ONE, RL_ONE, RL_ONE, RL_ONE, RL_FUNC_ADD, RL_FUNC_ADD); + rlSetBlendMode(RL_BLEND_CUSTOM_SEPARATE); + int g0 = b * GROUPS_PER_BUF, g1 = g0 + GROUPS_PER_BUF; + if (g1 > os->mcg.count) g1 = os->mcg.count; + for (int g = g0; g < g1; g++) + { + float cmask[4]; + ChannelVec(g % GROUPS_PER_BUF, cmask); + SetShaderValue(r->smaskw, r->wmask, cmask, SHADER_UNIFORM_VEC4); + for (int mi = 0; mi < os->mcg.maskCount[g]; mi++) + { + int md = os->mcg.masks[g][mi]; + if (md < 0 || md >= dc) continue; + int t = texidx[md] >= 0 ? r->textures[texidx[md]].id : 0; + EmitDrawable(&mv, t, pos[md], uv[md], idx[md], icount[md]); + } + rlDrawRenderBatchActive(); + } + EndTextureMode(); + } + EndShaderMode(); + rlSetBlendMode(RL_BLEND_ALPHA); +} + +static void RenderModelOffscreen(csmModel *model, Renderer *r, const View *view, bool maskingOn, const Options *opt, + int *sorted) +{ + Offscreens *os = &r->os; + int dc = csmGetDrawableCount(model); + int oc = os->count; + int total = dc + oc; + const int *order = VIEWER_GET_RENDER_ORDERS(model); + const float *opacity = csmGetDrawableOpacities(model); + const csmFlags *dflags = csmGetDrawableDynamicFlags(model); + const int *partParent = csmGetPartParentPartIndices(model); + const int *drawPart = csmGetDrawableParentPartIndices(model); + + for (int i = 0; i < total; i++) + { + int ord = order[i]; + if (ord < 0 || ord >= total) continue; + sorted[ord] = (i < dc) ? i : (dc + (i - dc)); /* same encoding below */ + } + + /* Mask pre-passes (drawable + offscreen) up front, before any RT is bound + * as a sampler. */ + if (maskingOn) + { + RenderMasks(model, r, view); + RenderOffscreenMasks(model, r, view); + } + + /* Per-group: screen-space content box (for scissoring the clear/composite) + * and whether the group is effectful (needs its own RT) or a pass-through + * (draws straight into the parent target). */ + OffscreensComputeBboxes(os, model, view, maskingOn); + + unsigned rootFbo = (unsigned)rlGetActiveFramebuffer(); + int current = -1; /* top of group stack (scope), or -1 */ + int activeScissor = -1; /* group whose box the scissor matches */ + unsigned curTarget = rootFbo; /* FBO drawables currently render into */ + unsigned destFbo[MAX_OFFSCREENS]; + float resv[2] = { (float)view->sw, (float)view->sh }; + + BindTarget(rootFbo, view->sw, view->sh); + rlDisableBackfaceCulling(); + ScissorToOffscreen(os, -1, view->sh); /* full screen */ + + /* Pop the current group: composite it (if effectful) onto its destination, + * leaving curTarget = that destination. */ +#define POP_CURRENT() \ + do \ + { \ + if (os->effectful[current]) \ + { \ + CompositeOffscreen(model, r, view, maskingOn, current, destFbo[current]); \ + activeScissor = current; /* composite left the scissor at this box */ \ + } \ + curTarget = destFbo[current]; \ + current = os->parent[current]; \ + } while (0) + + for (int p = 0; p < total; p++) + { + int obj = sorted[p]; + bool isOffscreen = obj >= dc; + int oi = isOffscreen ? obj - dc : -1; + int di = isOffscreen ? -1 : obj; + + if (!isOffscreen) + { + /* Drawable. Skip invisibles first (matches reference DrawDrawable). */ + if (!(dflags[di] & csmIsVisible) || opacity[di] <= 0.0f) continue; + /* Submit-to-parent: pop groups that no longer contain this drawable. */ + while (current >= 0 && !DrawableInOffscreen(os, partParent, drawPart, di, current)) + POP_CURRENT(); + if (!DrawablePassesFilters(di, order[di], opt)) continue; + /* Exotic-blend drawables (e.g. the HardLight hologram) need the exact + * backdrop composite: render in isolation, then blend onto curTarget. */ + if (BlendIsExotic(csmGetDrawableBlendModes(model)[di])) + { + DrawDrawableExotic(model, r, view, maskingOn, di, curTarget); + activeScissor = -2; /* composite left the scissor at the bbox */ + continue; + } + /* Match the scissor to the active group's box before drawing. */ + if (activeScissor != current) + { + rlDrawRenderBatchActive(); + ScissorToOffscreen(os, current, view->sh); + activeScissor = current; + } + /* Draw into the active target (an effectful ancestor's RT, or screen). */ + DrawOneDrawable(model, r, view, maskingOn, di, resv, -1); + } + else + { + /* Group begin. Pop current groups until this one nests in it. */ + while (current >= 0 && os->parent[oi] != current && current != oi) + { + bool nested = false; + int a = os->parent[oi]; + while (a >= 0) + { + if (a == current) + { + nested = true; + break; + } + a = os->parent[a]; + } + if (nested) break; + POP_CURRENT(); + } + destFbo[oi] = curTarget; /* where this group composites onto */ + if (os->effectful[oi]) + { + /* Bind a fresh RT and clear it (scissored to the group box; the rest + * of the RT is never written or sampled). */ + BindTarget(os->rt[oi].id, view->sw, view->sh); + ScissorToOffscreen(os, oi, view->sh); + activeScissor = oi; + rlClearColor(0, 0, 0, 0); + rlClearScreenBuffers(); + rlDisableBackfaceCulling(); + curTarget = os->rt[oi].id; + } + /* Pass-through groups keep curTarget: their drawables draw straight + * into the parent, with no RT, clear or composite. */ + current = oi; + } + } + + /* Composite any groups left on the stack up to the root. */ + while (current >= 0) + POP_CURRENT(); +#undef POP_CURRENT + + /* End on the root framebuffer with the screen projection so the UI / overlay + * draw correctly (and lift the scissor). */ + BindTarget(rootFbo, view->sw, view->sh); + rlDisableScissorTest(); + rlSetBlendMode(RL_BLEND_ALPHA); +} +#endif /* offscreens (v6) */ + +/* Draw the model in render order. `sorted` is scratch of length >= dc+oc. */ +void RenderModel(csmModel *model, Renderer *r, const View *view, bool maskingOn, const Options *opt, int *sorted) +{ +#if PSM_COMPAT_VERSION >= 0x06000000L + if (r->os.count > 0 && !opt->forceFlat) + { + RenderModelOffscreen(model, r, view, maskingOn, opt, sorted); + return; + } +#endif + RenderModelFlat(model, r, view, maskingOn, opt, sorted); +} diff --git a/src/samples/viewer/shaders/blend.frag b/src/samples/viewer/shaders/blend.frag new file mode 100644 index 0000000..e949ac0 --- /dev/null +++ b/src/samples/viewer/shaders/blend.frag @@ -0,0 +1,133 @@ +// Purism Core viewer: compositing shader for "unusual" (Photoshop-style) blend modes +// +// Copyright (c) 2026 Sakura Motion Project +// SPDX-License-Identifier: MIT + +in vec2 fragTexCoord; +out vec4 finalColor; + +uniform sampler2D texture0; // source +uniform sampler2D blendTexture; // backdrop +uniform sampler2D maskTexture; +uniform vec2 resolution; + +uniform int colorMode; // must be a valid csmColorBlendType value +uniform int alphaMode; // must be a valid csmAlphaBlendType value + +uniform float useMask; +uniform float maskInvert; +uniform vec4 channelSelector; + +uniform vec4 baseColor; +uniform vec4 multiplyColor; +uniform vec4 screenColor; + +vec4 straight(vec4 c) { + return c.a < 1e-5 ? vec4(0.0) : vec4(c.rgb / c.a, c.a); +} + +float burn(float s, float d) { + if (d >= 0.999999) return 1.0; + if (s < 1e-6) return 0.0; + return 1.0 - min(1.0, (1.0 - d) / s); +} + +float dodge(float s, float d) { + if (d <= 0.0) return 0.0; + if (s >= 1.0) return 1.0; + return min(1.0, d / (1.0 - s)); +} + +float overlay(float s, float d) { + return d < 0.5 ? 2.0 * s * d : 1.0 - 2.0 * (1.0 - s) * (1.0 - d); +} + +float hardl(float s, float d) { + return s < 0.5 ? 2.0 * s * d : 1.0 - 2.0 * (1.0 - s) * (1.0 - d); +} + +float softl(float s, float d) { + float a = d - (1.0 - 2.0 * s) * d * (1.0 - d); + float b = d + (2.0 * s - 1.0) * d * ((16.0 * d - 12.0) * d + 3.0); + float c = d + (2.0 * s - 1.0) * (sqrt(d) - d); + if (s <= 0.5) return a; + return d <= 0.25 ? b : c; +} + +float linl(float s, float d) { + float bn = max(0.0, 2.0 * s + d - 1.0); + float dg = min(1.0, 2.0 * (s - 0.5) + d); + return s < 0.5 ? bn : dg; +} + +float luma(vec3 c) { + return 0.30 * c.r + 0.59 * c.g + 0.11 * c.b; +} + +vec3 clipcolor(vec3 c) { + float l = luma(c); + float mn = min(c.r, min(c.g, c.b)); + float mx = max(c.r, max(c.g, c.b)); + if (mn < 0.0) c = l + (c - l) * l / (l - mn); + if (mx > 1.0) c = l + (c - l) * (1.0 - l) / (mx - l); + return c; +} + +vec3 setluma(vec3 c, float l) { return clipcolor(c + (l - luma(c))); } + +vec3 setsat(vec3 c, float s) { + float mx = max(c.r, max(c.g, c.b)); + float mn = min(c.r, min(c.g, c.b)); + float md = c.r + c.g + c.b - mx - mn; + float oMax = mn < mx ? s : 0.0; + float oMed = mn < mx ? (md - mn) * s / (mx - mn) : 0.0; + if (c.r == mx) return c.b < c.g ? vec3(oMax, oMed, 0.0) : vec3(oMax, 0.0, oMed); + else if (c.g == mx) return c.r < c.b ? vec3(0.0, oMax, oMed) : vec3(oMed, oMax, 0.0); + return c.g < c.r ? vec3(oMed, 0.0, oMax) : vec3(0.0, oMed, oMax); +} + +vec3 colorBlend(vec3 s, vec3 d) { + if (colorMode == 1 || colorMode == 3) return min(s + d, 1.0); // Add / AddCompatible + if (colorMode == 4) return s + d; // AddGlow + if (colorMode == 5) return min(s, d); // Darken + if (colorMode == 2 || colorMode == 6) return s * d; // Multiply / MultiplyCompatible + if (colorMode == 7) return vec3(burn(s.r, d.r), burn(s.g, d.g), burn(s.b, d.b)); + if (colorMode == 8) return max(vec3(0.0), s + d - 1.0); // LinearBurn + if (colorMode == 9) return max(s, d); // Lighten + if (colorMode == 10) return s + d - s * d; + if (colorMode == 11) return vec3(dodge(s.r, d.r), dodge(s.g, d.g), dodge(s.b, d.b)); + if (colorMode == 12) return vec3(overlay(s.r, d.r), overlay(s.g, d.g), overlay(s.b, d.b)); + if (colorMode == 13) return vec3(softl(s.r, d.r), softl(s.g, d.g), softl(s.b, d.b)); + if (colorMode == 14) return vec3(hardl(s.r, d.r), hardl(s.g, d.g), hardl(s.b, d.b)); + if (colorMode == 15) return vec3(linl(s.r, d.r), linl(s.g, d.g), linl(s.b, d.b)); + if (colorMode == 16) return setluma(setsat(s, max(d.r, max(d.g, d.b)) - min(d.r, min(d.g, d.b))), luma(d)); + if (colorMode == 17) return setluma(s, luma(d)); + return s; // Normal +} + +// Porter-Duff coverage weights (sa,da = source/dest alpha) for the result +// result = col*w.x + s*w.y + d*w.z, alpha = w.x + w.y + w.z. +vec3 alphaBlend(float sa, float da) { + if (alphaMode == 1) return vec3(sa * da, 0.0, da * (1.0 - sa)); // Atop + if (alphaMode == 2) return vec3(0.0, 0.0, da * (1.0 - sa)); // Out + if (alphaMode == 3) return vec3(min(sa, da), max(sa - da, 0.0), max(da - sa, 0.0)); // ConjointOver + if (alphaMode == 4) return vec3(max(sa + da - 1.0, 0.0), min(sa, 1.0 - da), min(da, 1.0 - sa)); // DisjointOver + return vec3(sa * da, sa * (1.0 - da), da * (1.0 - sa)); // Over +} + +void main() { + vec4 t = texture(texture0, fragTexCoord); + t.rgb = t.rgb * multiplyColor.rgb; + t.rgb = t.rgb + screenColor.rgb * t.a - t.rgb * screenColor.rgb; + if (useMask > 0.5) { + float m = dot(texture(maskTexture, gl_FragCoord.xy / resolution), channelSelector); + if (maskInvert > 0.5) m = 1.0 - m; + t *= m; + } + vec4 s = straight(t * baseColor); + vec4 d = straight(texture(blendTexture, gl_FragCoord.xy / resolution)); + + vec3 col = colorBlend(s.rgb, d.rgb); + vec3 p = alphaBlend(s.a, d.a); + finalColor = vec4(col * p.x + s.rgb * p.y + d.rgb * p.z, p.x + p.y + p.z); +} diff --git a/src/samples/viewer/shaders/draw.frag b/src/samples/viewer/shaders/draw.frag new file mode 100644 index 0000000..3da13f9 --- /dev/null +++ b/src/samples/viewer/shaders/draw.frag @@ -0,0 +1,19 @@ +// Purism Core viewer: drawable shader +// +// Copyright (c) 2026 Sakura Motion Project +// SPDX-License-Identifier MIT + +in vec2 fragTexCoord; +out vec4 finalColor; +uniform sampler2D texture0; +uniform vec4 baseColor; // (1, 1, 1, opacity) +uniform vec4 multiplyColor; +uniform vec4 screenColor; + +void main() { + vec4 t = texture(texture0, fragTexCoord); + t.rgb *= multiplyColor.rgb; + t.rgb = t.rgb + screenColor.rgb - t.rgb * screenColor.rgb; + vec4 c = t * baseColor; + finalColor = vec4(c.rgb * c.a, c.a); // premultiply +} diff --git a/src/samples/viewer/shaders/masked.frag b/src/samples/viewer/shaders/masked.frag new file mode 100644 index 0000000..665f63e --- /dev/null +++ b/src/samples/viewer/shaders/masked.frag @@ -0,0 +1,29 @@ +// Purism Core viewer: drawable shader (with a clipping mask) +// +// Copyright (c) 2026 Sakura Motion Project +// SPDX-License-Identifier: MIT + +// Note: clip groups are channel-packed (4 per RGBA buffer). channelSelector determines +// the group. + +in vec2 fragTexCoord; +out vec4 finalColor; +uniform sampler2D texture0; +uniform sampler2D maskTexture; +uniform vec2 resolution; +uniform float maskInvert; +uniform vec4 channelSelector; +uniform vec4 baseColor; +uniform vec4 multiplyColor; +uniform vec4 screenColor; + +void main() { + vec4 t = texture(texture0, fragTexCoord); + t.rgb *= multiplyColor.rgb; + t.rgb = t.rgb + screenColor.rgb - t.rgb * screenColor.rgb; + vec4 c = t * baseColor; + float m = dot(texture(maskTexture, gl_FragCoord.xy / resolution), channelSelector); + if (maskInvert > 0.5) m = 1.0 - m; + c.a *= m; + finalColor = vec4(c.rgb * c.a, c.a); +} diff --git a/src/samples/viewer/shaders/maskwrite.frag b/src/samples/viewer/shaders/maskwrite.frag new file mode 100644 index 0000000..0e0dea5 --- /dev/null +++ b/src/samples/viewer/shaders/maskwrite.frag @@ -0,0 +1,13 @@ +// Purism Core viewer: write alpha coverage of a drawable (mask) to an RGBA channel +// +// Copyright (c) 2026 Sakura Motion Project +// SPDX-License-Identifier: MIT + +in vec2 fragTexCoord; +out vec4 finalColor; +uniform sampler2D texture0; +uniform vec4 channelMask; +void main() { + float a = texture(texture0, fragTexCoord).a; + finalColor = a * channelMask; +} diff --git a/src/samples/viewer/shaders/offscreen.frag b/src/samples/viewer/shaders/offscreen.frag new file mode 100644 index 0000000..1fe7bd9 --- /dev/null +++ b/src/samples/viewer/shaders/offscreen.frag @@ -0,0 +1,18 @@ +// Purism Core viewer: offscreen group compositing +// +// Copyright (c) 2026 Sakura Motion Project +// SPDX-License-Identifier: MIT + +in vec2 fragTexCoord; +out vec4 finalColor; +uniform sampler2D texture0; // premultiplied +uniform vec4 baseColor; // (1, 1, 1, opacity) +uniform vec4 multiplyColor; +uniform vec4 screenColor; + +void main() { + vec4 t = texture(texture0, fragTexCoord); + vec3 c = t.rgb * multiplyColor.rgb; + c = c + screenColor.rgb * t.a - c * screenColor.rgb; + finalColor = vec4(c, t.a) * baseColor.a; // scale premult by opacity +} diff --git a/src/samples/viewer/shaders/offscreen_masked.frag b/src/samples/viewer/shaders/offscreen_masked.frag new file mode 100644 index 0000000..b17225f --- /dev/null +++ b/src/samples/viewer/shaders/offscreen_masked.frag @@ -0,0 +1,25 @@ +// Purism Core viewer: offscreen group compositing with masking +// +// Copyright (c) 2026 Sakura Motion Project +// SPDX-License-Identifier: MIT + +in vec2 fragTexCoord; +out vec4 finalColor; +uniform sampler2D texture0; +uniform sampler2D maskTexture; +uniform vec2 resolution; +uniform float maskInvert; +uniform vec4 channelSelector; +uniform vec4 baseColor; +uniform vec4 multiplyColor; +uniform vec4 screenColor; + +void main() { + vec4 t = texture(texture0, fragTexCoord); + vec3 c = t.rgb * multiplyColor.rgb; + c = c + screenColor.rgb * t.a - c * screenColor.rgb; + vec4 o = vec4(c, t.a) * baseColor.a; + float m = dot(texture(maskTexture, gl_FragCoord.xy / resolution), channelSelector); + if (maskInvert > 0.5) m = 1.0 - m; + finalColor = o * m; +} diff --git a/src/samples/viewer/shell.html b/src/samples/viewer/shell.html new file mode 100644 index 0000000..4238dc9 --- /dev/null +++ b/src/samples/viewer/shell.html @@ -0,0 +1,172 @@ + + + + + + +Purism Core viewer + + + +
+ Purism Core viewer + + a .model3.json + its textures + .moc3, or a .zip, + or you can drop files on the canvas + +
+ + + + +{{{ SCRIPT }}} + + diff --git a/src/samples/viewer/viewer.c b/src/samples/viewer/viewer.c new file mode 100644 index 0000000..4b69758 --- /dev/null +++ b/src/samples/viewer/viewer.c @@ -0,0 +1,410 @@ +/* + * Purism Core: sample model viewer entrypoint + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include "viewer.h" + +#if defined(__EMSCRIPTEN__) +#include +#endif + +static void usage(void) +{ + fprintf(stderr, "usage: viewer [options] \n" + " --shot render a few frames, save PNG, exit\n" + " --zoom (with --shot) zoom multiplier, recenter on face\n" + " --nomask start with masking disabled\n" + " --flat debug: bypass offscreen compositing (flat path)\n" + " --novsync uncap frame rate (measure true ceiling)\n" + " --maskscale mask buffer resolution fraction (default 0.5)\n" + " --setparam NAME VAL force a parameter each frame (repeatable)\n" + " --hide N suppress drawable N (repeatable)\n" + " --only N draw only drawable N\n" + " --maxorder N draw only drawables with renderOrder <= N\n" + "interactive: Tab panel M masking R reset view wheel zoom LMB pan\n" + " drag a .model3.json or .moc3 onto the window to load it\n"); +} + +static bool ParseArgs(int argc, char **argv, Options *o) +{ + memset(o, 0, sizeof(*o)); + o->shotZoom = 1.0f; + o->maskScale = DEFAULT_MASK_SCALE; + o->maxOrder = 1 << 30; + o->onlyDrawable = -1; + for (int i = 1; i < argc; i++) + { + const char *a = argv[i]; + if (!strcmp(a, "--shot") && i + 1 < argc) + { + o->shotPath = argv[++i]; + } + else if (!strcmp(a, "--zoom") && i + 1 < argc) + { + o->shotZoom = (float)atof(argv[++i]); + } + else if (!strcmp(a, "--nomask")) + { + o->startNomask = true; + } + else if (!strcmp(a, "--flat")) + { + o->forceFlat = true; + } + else if (!strcmp(a, "--bench") && i + 1 < argc) + { + o->bench = atoi(argv[++i]); + o->noVsync = true; + } + else if (!strcmp(a, "--novsync")) + { + o->noVsync = true; + } + else if (!strcmp(a, "--maskscale") && i + 1 < argc) + { + o->maskScale = (float)atof(argv[++i]); + } + else if (!strcmp(a, "--maxorder") && i + 1 < argc) + { + o->maxOrder = atoi(argv[++i]); + } + else if (!strcmp(a, "--only") && i + 1 < argc) + { + o->onlyDrawable = atoi(argv[++i]); + } + else if (!strcmp(a, "--setparam") && i + 2 < argc && o->setParamCount < 32) + { + o->setParamId[o->setParamCount] = argv[++i]; + o->setParamVal[o->setParamCount] = (float)atof(argv[++i]); + o->setParamCount++; + } + else if (!strcmp(a, "--hide") && i + 1 < argc && o->hideCount < 64) + { + o->hideIdx[o->hideCount++] = atoi(argv[++i]); + } + else if (a[0] == '-') + { + fprintf(stderr, "unknown option: %s\n", a); + return false; + } + else + { + o->path = a; + } + } + return o->path != NULL; +} + +static int CountMasked(csmModel *m) +{ + int dc = csmGetDrawableCount(m); + const int *mc = csmGetDrawableMaskCounts(m); + int n = 0; + for (int d = 0; d < dc; d++) + if (mc[d] > 0) n++; + return n; +} + +static bool LoadModel3(const char *path, Model3 *m3, char *dir, size_t dirsz, Core *core) +{ + /* GetDirectoryPath omits the trailing slash for absolute paths but keeps + * "./" for a bare filename; add a slash only when one isn't already there. */ + const char *gd = GetDirectoryPath(path); + int gn = TextLength(gd); + bool hasSep = gn > 0 && (gd[gn - 1] == '/' || gd[gn - 1] == '\\'); + snprintf(dir, dirsz, hasSep ? "%s" : "%s/", gd); + + char mocPath[768 + 512]; /* dir + moc field, both bounded */ + if (IsFileExtension(path, ".json")) + { + if (!ParseModel3(path, m3)) + { + fprintf(stderr, "could not parse %s\n", path); + return false; + } + snprintf(mocPath, sizeof(mocPath), "%s%s", dir, m3->moc); + } + else + { + memset(m3, 0, sizeof(*m3)); + snprintf(mocPath, sizeof(mocPath), "%s", path); + } + return LoadCore(mocPath, core); +} + +typedef struct +{ + Options opt; + Core core; + Model3 m3; + char dir[768]; + csmModel *model; /* == core.model, cached for the loop */ + Renderer rend; + int *sorted; /* render-order scratch */ + View view; + Font uiFont; + PanelState ps; + bool showPanel; + bool maskingOn; + int frame; + double benchT0; + bool quit; /* frame() sets this to stop the loop */ +} App; + +static bool ViewerLoad(App *a, const char *path) +{ + Model3 nm3; + Core ncore; + char ndir[sizeof(a->dir)]; + if (!LoadModel3(path, &nm3, ndir, sizeof(ndir), &ncore)) return false; + + Renderer nrend; + if (!RendererInit(&nrend, &nm3, ndir, ncore.model, a->view.sw, a->view.sh, a->opt.maskScale)) + { + fprintf(stderr, "out of memory\n"); + FreeCore(&ncore); + return false; + } + int n = csmGetDrawableCount(ncore.model); +#if PSM_COMPAT_VERSION >= 0x06000000L + n += csmGetOffscreenCount(ncore.model); +#endif + int *nsorted = (int *)malloc(sizeof(int) * (n > 0 ? n : 1)); + if (!nsorted) + { + fprintf(stderr, "out of memory\n"); + RendererFree(&nrend); + FreeCore(&ncore); + return false; + } + + /* commit: the new model is ready, so release the previous one (if any). */ + if (a->model) + { + RendererFree(&a->rend); + FreeCore(&a->core); + free(a->sorted); + } + a->m3 = nm3; + a->core = ncore; + a->model = ncore.model; + memcpy(a->dir, ndir, sizeof(a->dir)); + a->rend = nrend; + a->sorted = nsorted; + FitView(&a->view, a->model); + /* reset the model-specific panel state (param/part lists changed) */ + a->ps.editId = -1; + a->ps.filterEdit = false; + a->ps.filter[0] = '\0'; + a->ps.scrollParam = a->ps.scrollPart = 0.0f; + SetWindowTitle(TextFormat("Purism Core viewer - %s", GetFileName(path))); + return true; +} + +#if defined(__EMSCRIPTEN__) +static char gPendingLoad[1024]; + +EMSCRIPTEN_KEEPALIVE void ViewerRequestLoad(const char *path) +{ + snprintf(gPendingLoad, sizeof gPendingLoad, "%s", path ? path : ""); +} +#endif + +/* One frame: resize, drag-and-drop, input, parameter push, render, overlay. + * Self-contained (state via `a`) so it can be a browser main-loop callback. */ +static void frame(App *a) +{ + a->frame++; + + /* resize: keep view + clip-group buffers in sync with the window */ + int nsw = GetScreenWidth(), nsh = GetScreenHeight(); + if (nsw != a->view.sw || nsh != a->view.sh) + { + a->view.sw = nsw; + a->view.sh = nsh; + if (a->model) RendererResize(&a->rend, nsw, nsh); + } + +#if defined(__EMSCRIPTEN__) + /* file-picker / zip bridge (shell.html): a model was written to MEMFS and its + * path handed to ViewerRequestLoad; load it here, on the loop thread. */ + if (gPendingLoad[0]) + { + char p[sizeof gPendingLoad]; + memcpy(p, gPendingLoad, sizeof p); + gPendingLoad[0] = '\0'; + if (!ViewerLoad(a, p)) TraceLog(LOG_WARNING, "load: could not load %s", p); + } +#endif + + /* drag-and-drop: load a dropped .model3.json / .moc3. The current model is + * kept if the dropped file fails to load (e.g. a stray image). */ + if (IsFileDropped()) + { + FilePathList drop = LoadDroppedFiles(); + if (drop.count > 0 && !ViewerLoad(a, drop.paths[0])) + TraceLog(LOG_WARNING, "drop: could not load %s", drop.paths[0]); + UnloadDroppedFiles(drop); + } + + /* Nothing loaded yet (the web build starts empty): prompt and wait. */ + if (!a->model) + { + BeginDrawing(); + ClearBackground((Color){ 30, 30, 36, 255 }); + DrawTextEx(a->uiFont, "Purism Core viewer", (Vector2){ 40, a->view.sh / 2.0f - 40 }, 30, 1, RAYWHITE); + DrawTextEx(a->uiFont, + "Load a model with the button above, or drop a " + ".moc3 / .model3.json / .zip here.", + (Vector2){ 40, a->view.sh / 2.0f + 4 }, 18, 1, (Color){ 180, 180, 190, 255 }); + EndDrawing(); + return; + } + + /* input */ + float pw = PANEL_W * a->ps.uiScale; + if (pw > a->view.sw * 0.5f) pw = a->view.sw * 0.5f; + Rectangle panel = { a->view.sw - pw, 0, pw, (float)a->view.sh }; + bool overPanel = a->showPanel && CheckCollisionPointRec(GetMousePosition(), panel); + /* don't let model hotkeys fire while typing in a panel text/value box */ + bool typing = a->showPanel && (a->ps.filterEdit || a->ps.editId >= 0); + if (!typing) + { + if (IsKeyPressed(KEY_TAB)) a->showPanel = !a->showPanel; + if (IsKeyPressed(KEY_M)) a->maskingOn = !a->maskingOn; + if (IsKeyPressed(KEY_R)) FitView(&a->view, a->model); + } + if (!overPanel && !typing) + { + float wheel = GetMouseWheelMove(); + if (wheel != 0.0f) a->view.zoom *= (wheel > 0 ? 1.1f : 1.0f / 1.1f); + if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) + { + Vector2 md = GetMouseDelta(); + a->view.panx += md.x; + a->view.pany += md.y; + } + } + + /* forced parameters (--setparam) */ + if (a->opt.setParamCount > 0) + { + int ppc = csmGetParameterCount(a->model); + const char **pids = csmGetParameterIds(a->model); + float *pvals = csmGetParameterValues(a->model); + for (int s = 0; s < a->opt.setParamCount; s++) + for (int p = 0; p < ppc; p++) + if (strcmp(pids[p], a->opt.setParamId[s]) == 0) + { + pvals[p] = a->opt.setParamVal[s]; + break; + } + } + + csmUpdateModel(a->model); + + BeginDrawing(); + ClearBackground((Color){ 30, 30, 36, 255 }); + RenderModel(a->model, &a->rend, &a->view, a->maskingOn, &a->opt, a->sorted); + + if (a->showPanel) DrawPanel(a->model, panel, &a->ps); + DrawTextEx(a->uiFont, + "Tab: panel M: masking R: reset view " + "wheel: zoom LMB drag: pan drop a model to load it", + (Vector2){ 10, a->view.sh - 26 }, 18, 1, RAYWHITE); + char fps[32]; + snprintf(fps, sizeof(fps), "%d FPS", GetFPS()); + DrawTextEx(a->uiFont, fps, (Vector2){ 10, 8 }, 20, 1, (Color){ 0, 228, 48, 255 }); + EndDrawing(); + + /* --bench: warm up a few frames, then time a fixed count and exit. */ + if (a->opt.bench > 0) + { + if (a->frame == 4) a->benchT0 = GetTime(); + if (a->frame >= 4 + a->opt.bench) + { + double ms = (GetTime() - a->benchT0) * 1000.0 / a->opt.bench; + printf("bench: %.3f ms/frame (%.1f fps) over %d frames\n", ms, 1000.0 / ms, a->opt.bench); + a->quit = true; + return; + } + } + + /* --shot: let a couple of frames settle, capture, then exit. */ + if (a->opt.shotPath && a->frame >= 3) + { + TakeScreenshot(a->opt.shotPath); + a->quit = true; + return; + } +} + +int main(int argc, char **argv) +{ + static App app; + +#if defined(__EMSCRIPTEN__) + (void)argc; + (void)argv; + app.opt.maskScale = DEFAULT_MASK_SCALE; + app.opt.shotZoom = 1.0f; + app.opt.maxOrder = 1 << 30; +#else + if (!ParseArgs(argc, argv, &app.opt)) + { + usage(); + return 2; + } +#endif + + csmSetLogFunction(NULL); + SetTraceLogLevel(LOG_WARNING); /* quiet raylib's per-file INFO chatter */ + + SetConfigFlags(FLAG_WINDOW_RESIZABLE | (app.opt.noVsync ? 0 : FLAG_VSYNC_HINT)); + InitWindow(1280, 720, "Purism Core viewer"); + if (!app.opt.noVsync) SetTargetFPS(60); + + app.uiFont = LoadUiFont(); + bool uiFontCustom = (app.uiFont.texture.id != GetFontDefault().texture.id); + + app.view.sw = GetScreenWidth(); + app.view.sh = GetScreenHeight(); + app.showPanel = true; + app.maskingOn = !app.opt.startNomask; + app.ps.editId = -1; + app.ps.uiScale = 1.0f; + +#if !defined(__EMSCRIPTEN__) + if (!ViewerLoad(&app, app.opt.path)) + { + if (uiFontCustom) UnloadFont(app.uiFont); + CloseWindow(); + return 1; + } + if (app.opt.shotZoom != 1.0f) + { + /* Zoom toward the face: lift the center to the upper part of the model. */ + app.view.zoom *= app.opt.shotZoom; + app.view.pany = app.view.sh * 0.33f * app.opt.shotZoom; + } +#endif + +#if defined(__EMSCRIPTEN__) + (void)uiFontCustom; + emscripten_set_main_loop_arg((em_arg_callback_func)frame, &app, 0, 1); + return 0; +#else + while (!WindowShouldClose() && !app.quit) + frame(&app); + + free(app.sorted); + RendererFree(&app.rend); + if (uiFontCustom) UnloadFont(app.uiFont); + CloseWindow(); + FreeCore(&app.core); + return 0; +#endif +} diff --git a/src/samples/viewer/viewer.h b/src/samples/viewer/viewer.h new file mode 100644 index 0000000..06bdd35 --- /dev/null +++ b/src/samples/viewer/viewer.h @@ -0,0 +1,192 @@ +/* + * Purism Core: sample model viewer declarations + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#ifndef PURISM_VIEWER_H +#define PURISM_VIEWER_H + +#include "raylib.h" +#include "rlgl.h" + +#include "PurismCore.h" + +#include +#include +#include +#include +#include + +/* General limits. */ +#define MAX_TEXTURES 64 +#define MAX_CLIP_GROUPS 64 +#define EMIT_CHUNK 6000 +#define PANEL_W 340 + +#define PSM_GL_TEXTURE_2D 0x0DE1 +#define PSM_GL_TEXTURE0 0x84C0 + +/* Subsampling for clipping masks. */ +#define DEFAULT_MASK_SCALE 0.5f + +/* Clip groups are channel packed. */ +#define GROUPS_PER_BUF 4 +#define MAX_MASK_BUFS ((MAX_CLIP_GROUPS + GROUPS_PER_BUF - 1) / GROUPS_PER_BUF) + +#if PSM_COMPAT_VERSION >= 0x06000000L +#define MAX_OFFSCREENS 256 +#endif + +#if PSM_COMPAT_VERSION >= 0x06000000L +#define VIEWER_GET_RENDER_ORDERS csmGetRenderOrders +#else +#define VIEWER_GET_RENDER_ORDERS csmGetDrawableRenderOrders +#endif + +/* Blend selector independent of the drawable constant-flag bits, so the same + * GL factor setup can be shared by drawables (flags) and offscreens (which + * carry an extended blend-mode integer instead). */ +enum +{ + PSM_BLEND_NORMAL = 0, + PSM_BLEND_ADDITIVE = 1, + PSM_BLEND_MULTIPLICATIVE = 2 +}; + +typedef struct +{ + char moc[512]; + char tex[MAX_TEXTURES][512]; + int texCount; +} Model3; + +typedef struct +{ + void *mocBase, *modelBase; + csmMoc *moc; + csmModel *model; +} Core; + +typedef struct +{ + float cx, cy; /* world-space center (y already flipped to screen-down) */ + float zoom; /* pixels per world unit */ + float panx, pany; + int sw, sh; /* screen size */ +} View; + +typedef struct +{ + int count; /* number of unique groups */ + const int *masks[MAX_CLIP_GROUPS]; /* mask indices (borrowed) */ + int maskCount[MAX_CLIP_GROUPS]; + int bufCount; /* ceil(count / 4) */ + RenderTexture2D buf[MAX_MASK_BUFS]; + int *of; /* [dc] group index, or -1 */ +} ClipGroups; + +#if PSM_COMPAT_VERSION >= 0x06000000L +typedef struct +{ + int count; /* offscreen count for this model */ + const int *owner; /* [count] owner PART index per offscreen */ + int parent[MAX_OFFSCREENS]; /* parent offscreen index, or -1 */ + RenderTexture2D rt[MAX_OFFSCREENS]; /* screen-sized target per offscreen */ + ClipGroups mcg; /* mask groups built from offscreen masks */ + int mgroup[MAX_OFFSCREENS]; /* mask group index per offscreen, or -1 */ + int bbox[MAX_OFFSCREENS][4]; + bool effectful[MAX_OFFSCREENS]; +} Offscreens; +#endif /* offscreens (v6) */ + +typedef struct +{ + Texture2D textures[MAX_TEXTURES]; + int texCount; + Shader sdraw, smask, smaskw; + int locBase, locMul, locScr; /* sdraw */ + int mbase, mmul, mscr, mmask, mres, minv, msel; /* smask */ + int wmask; /* smaskw */ + float maskScale; + ClipGroups cg; +#if PSM_COMPAT_VERSION >= 0x06000000L + Offscreens os; + Shader soff, soffm; /* offscreen composite shaders */ + int obBase, obMul, obScr; /* soff */ + int omBase, omMul, omScr, omMask, omRes, omInv, omSel; /* soffm */ + /* Exact backdrop-sampling blend (exotic color/alpha): shader + scratch RTs. */ + Shader sblend; + int sbBlend, sbMask, sbRes, sbCmode, sbAmode, sbUsemask, sbInv, sbSel, sbBase, sbMul, sbScr; + Texture2D backdrop; /* copy of the destination for sampling */ + RenderTexture2D scratch; /* an exotic drawable rendered in isolation */ +#endif +} Renderer; + +typedef struct +{ + const char *path; + const char *shotPath; /* render a few frames, save, exit */ + float shotZoom; /* zoom multiplier, recenter on face */ + bool startNomask; + bool forceFlat; /* debug: bypass offscreen compositing */ + bool noVsync; /* uncap frame rate (for measuring) */ + int bench; /* render N frames, print avg ms, exit */ + float maskScale; /* mask buffer resolution fraction */ + /* debug filters */ + int maxOrder; /* draw only renderOrder <= this */ + int onlyDrawable; /* draw only this drawable, or -1 */ + int hideIdx[64]; /* suppress these drawables */ + int hideCount; + const char *setParamId[32]; /* force these parameters each frame */ + float setParamVal[32]; + int setParamCount; +} Options; + +typedef struct +{ + int tab; /* 0 = Parameters, 1 = Parts */ + float scrollParam; + float scrollPart; + char filter[48]; /* search filter (substring, case-insensitive) */ + bool filterEdit; + int editId; /* row whose value box is being typed in, or -1 */ + char editBuf[32]; + float uiScale; /* UI zoom */ +} PanelState; + +/* io.c */ +bool ParseModel3(const char *path, Model3 *m3); +bool LoadCore(const char *mocPath, Core *c); +void FreeCore(Core *c); + +/* blend.c */ +void ApplyBlend(csmFlags flags); +#if PSM_COMPAT_VERSION >= 0x06000000L +bool BlendIsExotic(int extended); +bool BlendAlphaNeedsCoverage(int extended); +void ApplyExtendedBlend(int extended); +#endif + +/* graphics.c */ +void WorldToScreen(const View *v, float mx, float my, float *sx, float *sy); +void FitView(View *v, csmModel *model); +void EmitDrawable(const View *v, int texId, const csmVector2 *pos, const csmVector2 *uv, const unsigned short *idx, + int idxCount); +void ChannelVec(int c, float v[4]); +#if PSM_COMPAT_VERSION >= 0x06000000L +bool DrawableInOffscreen(const Offscreens *os, const int *partParent, const int *drawPart, int d, int o); +#endif +bool RendererInit(Renderer *r, const Model3 *m3, const char *dir, csmModel *model, int w, int h, float maskScale); +void RendererResize(Renderer *r, int w, int h); +void RendererFree(Renderer *r); + +/* render.c */ +void RenderModel(csmModel *model, Renderer *r, const View *view, bool maskingOn, const Options *opt, int *sorted); + +/* panel.c */ +Font LoadUiFont(void); +void DrawPanel(csmModel *m, Rectangle area, PanelState *ps); + +#endif /* PURISM_VIEWER_H */ diff --git a/src/tests/.clang-format b/src/tests/.clang-format new file mode 100644 index 0000000..62275fd --- /dev/null +++ b/src/tests/.clang-format @@ -0,0 +1,5 @@ +# src/tests is excluded from the library house style: +# - src/tests: includes vendored (partcl.h) and generated (refdata.h) headers +# - src/samples: standalone example code (the viewer keeps its own .clang-format) +DisableFormat: true +SortIncludes: Never diff --git a/src/tests/extract.c b/src/tests/extract.c index 8a6bd91..ab70485 100644 --- a/src/tests/extract.c +++ b/src/tests/extract.c @@ -186,7 +186,7 @@ extract_one(const char *path, const char *pfx) float ti[] = {0, 0, 1, 0, 0, 1, -0.5f, 0.5f}; float to[8]; - psm__rot_transform(m, di, ti, to, 4); + psm__rotation_transform(m, di, ti, to, 4); print_arr(pfx, "rot_in", ti, 8); print_arr(pfx, "rot_out", to, 8); } diff --git a/src/tests/fuzzer.c b/src/tests/fuzzer.c index fcdd0d1..04d7a75 100644 --- a/src/tests/fuzzer.c +++ b/src/tests/fuzzer.c @@ -10,6 +10,10 @@ #include "PurismCore.h" #include "../samples/common.h" +#ifdef PSM_DEBUG_MALLOC +void psm__dbg_free_all(void); +#endif + static int g_initialized = 0; static void @@ -21,6 +25,10 @@ null_log(const char *msg) int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { +#ifdef PSM_DEBUG_MALLOC + psm__dbg_free_all(); +#endif + if (!g_initialized) { csmSetLogFunction(null_log); g_initialized = 1; @@ -64,14 +72,23 @@ LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) float *values = csmGetParameterValues(model); const float *mins = csmGetParameterMinimumValues(model); const float *maxs = csmGetParameterMaximumValues(model); - for (int i = 0; i < param_count && i < (int)size / 4; i++) { - uint32_t r; - memcpy(&r, data + i * 4 % size, sizeof(r)); - float t = (float)(r % 1000) / 999.0f; - values[i] = mins[i] + t * (maxs[i] - mins[i]); + for (int pass = 0; pass < 6; pass++) { + for (int i = 0; i < param_count; i++) { + uint32_t r; + /* read 4 bytes from a varying in-bounds offset (size >= 4 here) */ + size_t off = ((size_t)i * 4u + (size_t)pass * 2654435761u) + % (size - 3); + memcpy(&r, data + off, sizeof(r)); + r ^= (uint32_t)pass * 0x9e3779b9u; + + /* sweep extremes/out-of-range on some passes to exercise clamping */ + float t = (pass & 1) ? (float)(int32_t)r / 64.0f + : (float)(r % 1000) / 999.0f; + values[i] = mins[i] + t * (maxs[i] - mins[i]); + } + csmResetDrawableDynamicFlags(model); + csmUpdateModel(model); } - csmResetDrawableDynamicFlags(model); - csmUpdateModel(model); } psm_aligned_free(model_buf); diff --git a/src/tests/negctl_triidx.c b/src/tests/negctl_triidx.c new file mode 100644 index 0000000..1117ed4 --- /dev/null +++ b/src/tests/negctl_triidx.c @@ -0,0 +1,112 @@ +/* + * Negative control for the triangle-index-value check in verify_idx: + * load a model, confirm it validates, corrupt one triangle index to an + * out-of-range value, and confirm validation now REJECTS it. + * Single-TU build (like unit.c / test_endian.c) for access to internals. + */ +#include +#include +#include +#include +#include +#include + +#define PURISM_CORE_STATIC +#include "../private.h" +#include "../error.h" +#include "../debug.h" +#include "../arena.h" +#include "../array.h" +#include "../math2.h" +#include "../moc3.h" +#include "../model.h" +#include "../gather.h" +#include "../interpolate.h" +#include "../artmesh.h" +#include "../blendshape.h" +#include "../deformer.h" +#include "../glue.h" +#include "../offscreen.h" +#include "../param.h" +#include "../part.h" +#include "../render.h" +#include "../update.h" +#include "../core.c" +#include "../debug.c" +#include "../arena.c" +#include "../math2.c" +#include "../verify.c" +#include "../moc3.c" +#include "../model.c" +#include "../update.c" +#include "../param.c" +#include "../part.c" +#include "../deformer.c" +#include "../artmesh.c" +#include "../glue.c" +#include "../offscreen.c" +#include "../blendshape.c" +#include "../interpolate.c" +#include "../render.c" +#include "../samples/common.h" + +static int g_tested, g_rejected, g_no_indices; + +static int ends_moc3(const char *s){ size_t l=strlen(s); return l>5 && !strcmp(s+l-5,".moc3"); } + +static void +run_one(const char *path) +{ + size_t n = 0; + void *raw = psm_read_file(path, &n, csmAlignofMoc, 0); + if (!raw) return; + csmMoc *moc = csmReviveMocInPlace(raw, (unsigned)n); + if (!moc) { psm_aligned_free(raw); return; } + struct psm__moc3_data *d = psm__moc_to_data(moc); + struct psm__sections *ms = d->sections; + struct psm__count_info *cnt = ms->count_info; + psm__u8 ver = d->header->version; + + /* baseline: must validate clean */ + if (psm__verify_idx(ver, ms) != PSM__OK) { psm_aligned_free(raw); return; } + + if (!ms->idx_src.idx) { g_no_indices++; psm_aligned_free(raw); return; } + + /* find an art mesh with at least one index and corrupt its first index */ + for (psm__i32 i = 0; i < cnt->art_meshes; i++) { + psm__i32 off = ms->art_mesh_src.idx_off[i]; + psm__i32 len = ms->art_mesh_src.idx_len[i]; + psm__i32 vc = ms->art_mesh_src.vertex_count[i]; + if (len <= 0) continue; + psm__u16 saved = ms->idx_src.idx[off]; + ms->idx_src.idx[off] = (psm__u16)(vc); /* index == vc -> out of range */ + g_tested++; + if (psm__verify_idx(ver, ms) != PSM__OK) + g_rejected++; + else + fprintf(stderr, " NOT REJECTED: %s art_mesh[%d] idx=vc=%d\n", + path, i, vc); + ms->idx_src.idx[off] = saved; + break; + } + psm_aligned_free(raw); +} + +int +main(int argc, char **argv) +{ + csmSetLogFunction(NULL); + for (int a = 1; a < argc; a++) { + DIR *dir = opendir(argv[a]); if (!dir) continue; + struct dirent *e; char p[4096]; + while ((e = readdir(dir))) { + if (!ends_moc3(e->d_name)) continue; + snprintf(p, sizeof p, "%s/%s", argv[a], e->d_name); + run_one(p); + } + closedir(dir); + } + fprintf(stderr, "corrupted+tested: %d, rejected: %d, (no-index models: %d)\n", + g_tested, g_rejected, g_no_indices); + return (g_tested > 0 && g_rejected == g_tested) ? 0 : 1; +} diff --git a/src/tests/test_arena.c b/src/tests/test_arena.c index 6b1b41b..decc3ab 100644 --- a/src/tests/test_arena.c +++ b/src/tests/test_arena.c @@ -1,9 +1,14 @@ /* Arena allocator tests */ +/* The arena assumes its backing memory is already suitably aligned (the + * public API requires an aligned `address`); a bare stack array is only + * 16-aligned by -O2 luck. Force it with a union carrying a max-align member. */ +#define ARENA_BUF(name, n) union { psm__u8 b[n]; max_align_t _a; } name + TEST(arena_basic) { - psm__u8 buf[4096]; - struct psm__arena a = PSM__ARENA_INIT(buf, sizeof(buf)); + ARENA_BUF(buf, 4096); + struct psm__arena a = PSM__ARENA_INIT(buf.b, sizeof(buf.b)); void *p1 = psm__arena_alloc(&a, 64); CHECK(p1 != NULL); @@ -18,8 +23,8 @@ TEST(arena_basic) TEST(arena_overflow) { - psm__u8 buf[64]; - struct psm__arena a = PSM__ARENA_INIT(buf, sizeof(buf)); + ARENA_BUF(buf, 64); + struct psm__arena a = PSM__ARENA_INIT(buf.b, sizeof(buf.b)); psm__arena_alloc(&a, 1024); CHECK(!psm__arena_ok(&a)); @@ -38,8 +43,8 @@ TEST(arena_dry_run) TEST(arena_alignment) { - psm__u8 buf[4096]; - struct psm__arena a = PSM__ARENA_INIT(buf, sizeof(buf)); + ARENA_BUF(buf, 4096); + struct psm__arena a = PSM__ARENA_INIT(buf.b, sizeof(buf.b)); psm__arena_alloc(&a, 1); void *p = psm__arena_alloc(&a, 4); @@ -48,8 +53,8 @@ TEST(arena_alignment) TEST(arena_zero) { - psm__u8 buf[256]; - struct psm__arena a = PSM__ARENA_INIT(buf, sizeof(buf)); + ARENA_BUF(buf, 256); + struct psm__arena a = PSM__ARENA_INIT(buf.b, sizeof(buf.b)); /* Zero-size alloc: implementation may return non-NULL */ psm__arena_alloc(&a, 0); @@ -58,8 +63,8 @@ TEST(arena_zero) TEST(arena_safe_mul) { - psm__u8 buf[64]; - struct psm__arena a = PSM__ARENA_INIT(buf, sizeof(buf)); + ARENA_BUF(buf, 64); + struct psm__arena a = PSM__ARENA_INIT(buf.b, sizeof(buf.b)); psm__u32 r = psm__arena_safe_mul(&a, 4, 8); CHECK(r == 32); diff --git a/src/tests/test_endian.c b/src/tests/test_endian.c new file mode 100644 index 0000000..ce80c19 --- /dev/null +++ b/src/tests/test_endian.c @@ -0,0 +1,409 @@ +/* + * Purism Core: big-endian round-trip differential test + * + * The entire test corpus is little-endian (endian_flag == 0), so the byte-swap + * load path has no correctness coverage. This test synthesizes a big-endian + * copy of each model using an INDEPENDENT swapper (its own swap primitives, its + * own count_info/canvas/offset-table sizing, and a uniform sizeof() predicate + * over the section table) -- it never calls the library's psm__bswap_* code. + * It then loads the synthesized BE file through the public API and asserts the + * model output is bit-identical to the little-endian load after an identical + * parameter sweep. + * + * Shared-and-correct logic agrees (pass); any divergence between the library's + * swap and this independent reference -- a regression, a count_info version + * sizing bug, a missed canvas field, a bad offset-table count -- corrupts the + * BE file the library reconstructs and shows up as an output mismatch. + * + * Single-TU build (like unit.c) for access to internal structs and the + * PSM__SECTIONS_* table. + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include + +#define PURISM_CORE_STATIC +#include "../private.h" +#include "../error.h" +#include "../debug.h" +#include "../arena.h" +#include "../array.h" +#include "../math2.h" +#include "../moc3.h" +#include "../model.h" +#include "../gather.h" +#include "../interpolate.h" +#include "../artmesh.h" +#include "../blendshape.h" +#include "../deformer.h" +#include "../glue.h" +#include "../offscreen.h" +#include "../param.h" +#include "../part.h" +#include "../render.h" +#include "../update.h" + +#include "../core.c" +#include "../debug.c" +#include "../arena.c" +#include "../math2.c" +#include "../verify.c" +#include "../moc3.c" +#include "../model.c" +#include "../update.c" +#include "../param.c" +#include "../part.c" +#include "../deformer.c" +#include "../artmesh.c" +#include "../glue.c" +#include "../offscreen.c" +#include "../blendshape.c" +#include "../interpolate.c" +#include "../render.c" + +#include "../samples/common.h" + +/* ---- independent byte-swap primitives (not the library's) ---- */ + +static void +ind_swap32(void *p, size_t n) +{ + uint8_t *b = (uint8_t *)p; + for (size_t i = 0; i < n; i++, b += 4) { + uint8_t t; + t = b[0]; b[0] = b[3]; b[3] = t; + t = b[1]; b[1] = b[2]; b[2] = t; + } +} + +static void +ind_swap16(void *p, size_t n) +{ + uint8_t *b = (uint8_t *)p; + for (size_t i = 0; i < n; i++, b += 2) { + uint8_t t = b[0]; b[0] = b[1]; b[1] = t; + } +} + +/* + * Swap one section field in the synth (pristine) buffer. The field pointer + * comes from a throwaway little-endian parse (map buffer); we translate it to + * the same offset in the pristine buffer. Runtime/arena pointers (8-byte) and + * byte/string fields fall outside {2,4} or outside the file and are skipped -- + * exactly the fields a big-endian file must NOT swap. + */ +static void +ind_swap_field(size_t width, const void *fieldptr, size_t count, + const uint8_t *mapbase, uint8_t *synthbase, size_t size) +{ + if (count == 0 || fieldptr == NULL) + return; + if (width != 4 && width != 2) + return; + const uint8_t *p = (const uint8_t *)fieldptr; + if (p < mapbase || p >= mapbase + size) + return; /* runtime/arena field, not in file */ + size_t off = (size_t)(p - mapbase); + if (off + count * width > size) + return; /* defensive; shouldn't happen */ + if (width == 4) + ind_swap32(synthbase + off, count); + else + ind_swap16(synthbase + off, count); +} + +/* + * Produce a big-endian copy of `raw` (n bytes). Returns a freshly aligned + * buffer the caller frees with psm_aligned_free, or NULL if the model won't + * parse as little-endian (not a valid model -> nothing to test). + */ +static void * +build_be(const uint8_t *raw, size_t n) +{ + /* throwaway LE parse to locate every in-file field */ + void *mapbuf = psm_aligned_alloc(csmAlignofMoc, n); + if (!mapbuf) + return NULL; + memcpy(mapbuf, raw, n); + csmMoc *moc = csmReviveMocInPlace(mapbuf, (unsigned int)n); + if (!moc) { + psm_aligned_free(mapbuf); + return NULL; + } + struct psm__moc3_data *data = psm__moc_to_data(moc); + struct psm__sections *ms = data->sections; + struct psm__count_info *cnt = ms->count_info; + psm__u8 ver = data->header->version; + + void *synthv = psm_aligned_alloc(csmAlignofMoc, n); + if (!synthv) { + psm_aligned_free(mapbuf); + return NULL; + } + memcpy(synthv, raw, n); + uint8_t *synth = (uint8_t *)synthv; + const uint8_t *mapb = (const uint8_t *)mapbuf; + + /* offset table: fixed-size region right after the 64-byte header */ + size_t sec_count = (ver >= csmMocVersion_53) ? 480 : 160; + if (64 + sec_count * 4 <= n) + ind_swap32(synth + 64, sec_count); + + /* count_info: 32 ints (=v5.0) -- independent sizing */ + { + size_t ci_off = (size_t)((const uint8_t *)cnt - mapb); + size_t ci_ints = (ver >= csmMocVersion_50) ? 64 : 32; + if (ci_off + ci_ints * 4 <= n) + ind_swap32(synth + ci_off, ci_ints); + } + + /* canvas_info: 5 leading f32 fields (the trailing u8 flag stays put) */ + { + size_t cv_off = (size_t)((const uint8_t *)ms->canvas_info - mapb); + if (cv_off + 5 * 4 <= n) + ind_swap32(synth + cv_off, 5); + } + + /* every dynamic section array, swapped iff its element is 2 or 4 bytes */ +#define SW_S(T, M, C) /* count_info / canvas_info handled above */ +#define SW_D(T, M, C) \ + ind_swap_field(sizeof(T), (const void *)ms->M, (size_t)cnt->C, mapb, synth, n); + PSM__SECTIONS_V30(SW_S, SW_D) + if (ver >= csmMocVersion_33) { PSM__SECTIONS_V33(SW_S, SW_D) } + if (ver >= csmMocVersion_42) { PSM__SECTIONS_V42(SW_S, SW_D) } + if (ver >= csmMocVersion_50) { PSM__SECTIONS_V50(SW_S, SW_D) } + if (ver >= csmMocVersion_53) { PSM__SECTIONS_V53(SW_S, SW_D) } +#undef SW_S +#undef SW_D + + /* mark the file big-endian */ + synth[offsetof(struct psm__moc3_header, endian_flag)] = 1; + + psm_aligned_free(mapbuf); + return synth; +} + +/* ---- output snapshot ---- */ + +struct buf { + uint8_t *data; + size_t len, cap; +}; + +static void +buf_put(struct buf *b, const void *p, size_t n) +{ + if (b->len + n > b->cap) { + size_t nc = b->cap ? b->cap * 2 : 4096; + while (nc < b->len + n) + nc *= 2; + b->data = (uint8_t *)realloc(b->data, nc); + b->cap = nc; + } + memcpy(b->data + b->len, p, n); + b->len += n; +} + +/* identical parameter sweep applied to both models before snapshotting */ +static void +sweep(csmModel *m) +{ + int pc = csmGetParameterCount(m); + float *v = csmGetParameterValues(m); + const float *mn = csmGetParameterMinimumValues(m); + const float *mx = csmGetParameterMaximumValues(m); + for (int pass = 0; pass < 3; pass++) { + for (int i = 0; i < pc; i++) { + unsigned k = (unsigned)i * 2654435761u + (unsigned)pass * 40503u; + float t = (float)(k % 1000) / 999.0f; + v[i] = mn[i] + t * (mx[i] - mn[i]); + } + csmResetDrawableDynamicFlags(m); + csmUpdateModel(m); + } +} + +static void +snapshot(csmModel *m, struct buf *b) +{ + int pc = csmGetParameterCount(m); + buf_put(b, csmGetParameterValues(m), (size_t)pc * sizeof(float)); + + int part_n = csmGetPartCount(m); + buf_put(b, csmGetPartOpacities(m), (size_t)part_n * sizeof(float)); + + int dc = csmGetDrawableCount(m); + buf_put(b, csmGetDrawableOpacities(m), (size_t)dc * sizeof(float)); + buf_put(b, csmGetDrawableDrawOrders(m), (size_t)dc * sizeof(int)); +#if PSM_COMPAT_VERSION >= 0x06000000L + buf_put(b, csmGetRenderOrders(m), (size_t)dc * sizeof(int)); +#else + buf_put(b, csmGetDrawableRenderOrders(m), (size_t)dc * sizeof(int)); +#endif + buf_put(b, csmGetDrawableMultiplyColors(m), (size_t)dc * 4 * sizeof(float)); + buf_put(b, csmGetDrawableScreenColors(m), (size_t)dc * 4 * sizeof(float)); + + const int *vc = csmGetDrawableVertexCounts(m); + const csmVector2 **pos = csmGetDrawableVertexPositions(m); + for (int i = 0; i < dc; i++) + if (pos[i] && vc[i] > 0) + buf_put(b, pos[i], (size_t)vc[i] * sizeof(csmVector2)); + + float ppu, ow, oh, cw, ch; + csmVector2 origin, sizepix; + csmReadCanvasInfo(m, &sizepix, &origin, &ppu); + (void)ow; (void)oh; (void)cw; (void)ch; + buf_put(b, &sizepix, sizeof(sizepix)); + buf_put(b, &origin, sizeof(origin)); + buf_put(b, &ppu, sizeof(ppu)); +} + +/* load a model (revive in place + init + sweep) and snapshot it. takes + * ownership of mocbuf (must be a freshly aligned buffer of n bytes). returns + * 0 ok, -1 if revive/init fails. */ +static int +load_and_snapshot(void *mocbuf, size_t n, struct buf *out) +{ + csmMoc *moc = csmReviveMocInPlace(mocbuf, (unsigned int)n); + if (!moc) { + psm_aligned_free(mocbuf); + return -1; + } + unsigned int msz = csmGetSizeofModel(moc); + if (msz == 0 || msz > 256u * 1024 * 1024) { + psm_aligned_free(mocbuf); + return -1; + } + void *modelbuf = psm_aligned_alloc(csmAlignofModel, msz); + if (!modelbuf) { + psm_aligned_free(mocbuf); + return -1; + } + csmModel *model = csmInitializeModelInPlace(moc, modelbuf, msz); + if (!model) { + psm_aligned_free(modelbuf); + psm_aligned_free(mocbuf); + return -1; + } + csmResetDrawableDynamicFlags(model); + csmUpdateModel(model); + sweep(model); + snapshot(model, out); + psm_aligned_free(modelbuf); + psm_aligned_free(mocbuf); + return 0; +} + +/* returns 1 pass, 0 fail, -1 skip (not a loadable LE model) */ +static int +run_one(const char *path, const char **why) +{ + size_t n = 0; + void *raw = psm_read_file(path, &n, csmAlignofMoc, 0); + if (!raw) { *why = "read failed"; return -1; } + + /* little-endian reference */ + void *refbuf = psm_aligned_alloc(csmAlignofMoc, n); + memcpy(refbuf, raw, n); + struct buf snap_le = {0}; + if (load_and_snapshot(refbuf, n, &snap_le) != 0) { + free(snap_le.data); + psm_aligned_free(raw); + *why = "LE load failed"; + return -1; /* not a valid model; nothing to test */ + } + + /* independently synthesized big-endian copy */ + void *be = build_be((const uint8_t *)raw, n); + psm_aligned_free(raw); + if (!be) { free(snap_le.data); *why = "BE synth failed"; return -1; } + + /* consistency check must accept the valid BE file (double-swaps internally) */ + void *becopy = psm_aligned_alloc(csmAlignofMoc, n); + memcpy(becopy, be, n); + int consistent = csmHasMocConsistency(becopy, (unsigned int)n); + psm_aligned_free(becopy); + if (!consistent) { + free(snap_le.data); psm_aligned_free(be); + *why = "csmHasMocConsistency rejected BE file"; + return 0; + } + + struct buf snap_be = {0}; + if (load_and_snapshot(be, n, &snap_be) != 0) { + free(snap_le.data); free(snap_be.data); + *why = "BE load failed"; + return 0; + } + + int ok = (snap_le.len == snap_be.len) && + memcmp(snap_le.data, snap_be.data, snap_le.len) == 0; + if (!ok) + *why = (snap_le.len != snap_be.len) ? "snapshot length mismatch" + : "output bytes differ LE vs BE"; + free(snap_le.data); + free(snap_be.data); + return ok ? 1 : 0; +} + +static int g_pass, g_fail, g_skip; + +static int +ends_moc3(const char *s) +{ + size_t l = strlen(s); + return l > 5 && strcmp(s + l - 5, ".moc3") == 0; +} + +static void +run_dir(const char *dir) +{ + DIR *d = opendir(dir); + if (!d) { + fprintf(stderr, " (cannot open dir: %s)\n", dir); + return; + } + struct dirent *e; + char path[4096]; + while ((e = readdir(d)) != NULL) { + if (!ends_moc3(e->d_name)) + continue; + snprintf(path, sizeof(path), "%s/%s", dir, e->d_name); + const char *why = ""; + int r = run_one(path, &why); + if (r == 1) { + g_pass++; + } else if (r == 0) { + g_fail++; + fprintf(stderr, " FAIL %s: %s\n", e->d_name, why); + } else { + g_skip++; + } + } + closedir(d); +} + +int +main(int argc, char **argv) +{ + csmSetLogFunction(NULL); + fprintf(stderr, "big-endian round-trip differential test:\n"); + + if (argc > 1) { + for (int i = 1; i < argc; i++) + run_dir(argv[i]); + } else { + run_dir("testdata/moc3"); + } + + fprintf(stderr, "\n%d passed, %d failed, %d skipped\n", + g_pass, g_fail, g_skip); + return g_fail ? 1 : 0; +} diff --git a/src/tests/test_misc.c b/src/tests/test_misc.c index 6ee5835..6d3b4a7 100644 --- a/src/tests/test_misc.c +++ b/src/tests/test_misc.c @@ -100,3 +100,88 @@ TEST(version_api) csmMocVersion mv = csmGetLatestMocVersion(); CHECK(mv == csmMocVersion_53); } + +/* psm__resolve_params flags non-repeat inputs outside [min,max] and clamps. */ +TEST(resolve_params_range) +{ + struct psm__param items[2]; + psm__f32 input[2]; + struct psm__params params; + + memset(items, 0, sizeof(items)); + /* param 0: non-repeat, range [-1, 1] */ + items[0].repeat = 0; + items[0].range[0] = -1.0f; + items[0].range[1] = 1.0f; + items[0].range_length = 2.0f; + /* param 1: repeat, range [0, 1] (wraps, never a range error) */ + items[1].repeat = 1; + items[1].range[0] = 0.0f; + items[1].range[1] = 1.0f; + items[1].range_length = 1.0f; + + params.count = 2; + params.items = items; + params.input_value = input; + params.type = NULL; + + /* all in range: no error, repeat param wraps without flagging */ + input[0] = 0.5f; + input[1] = 3.25f; + CHECK(psm__resolve_params(¶ms) == false); + CHECK_FLOAT(items[0].value, 0.5f, 0.0001f); + CHECK_FLOAT(items[1].value, 0.25f, 0.0001f); + + /* non-repeat above max: error, value clamped, input rewritten to clamp */ + input[0] = 5.0f; + CHECK(psm__resolve_params(¶ms) == true); + CHECK_FLOAT(items[0].value, 1.0f, 0.0001f); + CHECK_FLOAT(input[0], 1.0f, 0.0001f); + + /* non-repeat below min: error, value clamped */ + input[0] = -9.0f; + CHECK(psm__resolve_params(¶ms) == true); + CHECK_FLOAT(items[0].value, -1.0f, 0.0001f); + + /* empty parameter set: no error */ + params.count = 0; + CHECK(psm__resolve_params(¶ms) == false); +} + +/* csmGetLastError on NULL is benign; csmGetErrorString covers every code. */ +TEST(error_api) +{ + CHECK(csmGetLastError(NULL) == csmError_NoError); + + CHECK(strcmp(csmGetErrorString(csmError_NoError), "no error") == 0); + CHECK(strcmp(csmGetErrorString(csmError_ParameterRange), + "parameter out of range") == 0); + CHECK(strcmp(csmGetErrorString(csmError_FileUnrecognized), + "unrecognized MOC3 file") == 0); + CHECK(strcmp(csmGetErrorString(csmError_FileCorrupt), + "corrupt MOC3 file") == 0); + /* out-of-enum code maps to the catch-all string, never NULL */ + CHECK(strcmp(csmGetErrorString((csmError)999), "unknown error") == 0); +} + +/* csmReviveMocInPlace stashes its failure reason in the moc header. */ +TEST(moc_error_api) +{ + CHECK(csmGetMocError(NULL) == csmError_NoError); + + /* Both cases are rejected before any aligned layout cast, so a plain + * (4-byte-aligned) buffer is enough to exercise the error stash. */ + _Alignas(8) psm__u8 buf[256]; + memset(buf, 0, sizeof(buf)); + + /* not a MOC3 at all -> unrecognized */ + memcpy(buf, "XXXX", 4); + CHECK(csmReviveMocInPlace(buf, sizeof(buf)) == NULL); + CHECK(csmGetMocError((const csmMoc *)buf) == csmError_FileUnrecognized); + + /* MOC3 magic but an impossible version -> corrupt */ + memcpy(buf, "MOC3", 4); + buf[4] = 200; + CHECK(csmReviveMocInPlace(buf, sizeof(buf)) == NULL); + CHECK(csmGetMocError((const csmMoc *)buf) == csmError_FileCorrupt); +} diff --git a/src/tests/test_refdata.c b/src/tests/test_refdata.c index ee88856..8b815c5 100644 --- a/src/tests/test_refdata.c +++ b/src/tests/test_refdata.c @@ -43,7 +43,7 @@ TEST(P##_rot) \ &a, &s, &ox, &oy, &rfx, &rfy); \ rc.base_angle = P##_ROT_BASE; \ psm__f32 out[8]; \ - psm__rot_transform(&m, 0, P##_rot_in, out, 4); \ + psm__rotation_transform(&m, 0, P##_rot_in, out, 4); \ for (int i = 0; i < 8; i++) \ CHECK_FLOAT(out[i], P##_rot_out[i], 0.0001f); \ } diff --git a/src/tests/test_transform.c b/src/tests/test_transform.c index a882c87..16a2f95 100644 --- a/src/tests/test_transform.c +++ b/src/tests/test_transform.c @@ -2,7 +2,7 @@ * Warp and rotation deformer transform tests. * * Sets up minimal model state on the stack to exercise - * psm__warp_transform and psm__rot_transform directly. + * psm__warp_transform and psm__rotation_transform directly. */ static void @@ -199,7 +199,7 @@ TEST(rotation_identity) &a, &s, &ox, &oy, &rfx, &rfy); psm__f32 inp[] = {1.0f, 0.0f}, out[2]; - psm__rot_transform(&m, 0, inp, out, 1); + psm__rotation_transform(&m, 0, inp, out, 1); CHECK_FLOAT(out[0], 1.0f, 0.001f); CHECK_FLOAT(out[1], 0.0f, 0.001f); } @@ -215,7 +215,7 @@ TEST(rotation_90_degrees) &a, &s, &ox, &oy, &rfx, &rfy); psm__f32 inp[] = {1.0f, 0.0f}, out[2]; - psm__rot_transform(&m, 0, inp, out, 1); + psm__rotation_transform(&m, 0, inp, out, 1); CHECK_FLOAT(out[0], 0.0f, 0.01f); CHECK_FLOAT(out[1], 1.0f, 0.01f); } @@ -235,7 +235,7 @@ TEST(rotation_with_origin) /* M(180°) = (-1, 0; 0, -1) */ /* result = (-1,0;0,-1) * (1,0) + (5,5) = (4, 5) */ psm__f32 inp[] = {1.0f, 0.0f}, out[2]; - psm__rot_transform(&m, 0, inp, out, 1); + psm__rotation_transform(&m, 0, inp, out, 1); CHECK_FLOAT(out[0], 4.0f, 0.01f); CHECK_FLOAT(out[1], 5.0f, 0.01f); } @@ -251,7 +251,7 @@ TEST(rotation_with_scale) &a, &s, &ox, &oy, &rfx, &rfy); psm__f32 inp[] = {3.0f, 4.0f}, out[2]; - psm__rot_transform(&m, 0, inp, out, 1); + psm__rotation_transform(&m, 0, inp, out, 1); CHECK_FLOAT(out[0], 6.0f, 0.01f); CHECK_FLOAT(out[1], 8.0f, 0.01f); } @@ -267,7 +267,7 @@ TEST(rotation_reflect_x) &a, &s, &ox, &oy, &rfx, &rfy); psm__f32 inp[] = {3.0f, 4.0f}, out[2]; - psm__rot_transform(&m, 0, inp, out, 1); + psm__rotation_transform(&m, 0, inp, out, 1); CHECK_FLOAT(out[0], -3.0f, 0.01f); CHECK_FLOAT(out[1], 4.0f, 0.01f); } diff --git a/src/tests/unit.c b/src/tests/unit.c index 328d388..d51ea9c 100644 --- a/src/tests/unit.c +++ b/src/tests/unit.c @@ -39,6 +39,7 @@ #include "../debug.c" #include "../arena.c" #include "../math2.c" +#include "../verify.c" #include "../moc3.c" #include "../model.c" #include "../update.c" @@ -127,6 +128,9 @@ main(void) RUN(glob_question); RUN(glob_case_insensitive); RUN(version_api); + RUN(resolve_params_range); + RUN(error_api); + RUN(moc_error_api); SUITE("reference data (extracted from real models)"); #ifdef REF0_WARP_ROW diff --git a/src/update.c b/src/update.c index 1413a6e..4ab247d 100644 --- a/src/update.c +++ b/src/update.c @@ -7,6 +7,7 @@ #include #include "private.h" +#include "error.h" #include "debug.h" #include "math2.h" #include "moc3.h" @@ -33,7 +34,8 @@ psm__reverse_y(struct psm__model *m) return; struct psm__art_mesh *meshes = m->art_meshes.meshes; - bool *en = m->art_meshes.enable; + + bool *en = m->art_meshes.enable; psm__f32 **pos = m->art_meshes.pos; for (psm__i32 i = 0; i < count; i++) { @@ -56,7 +58,7 @@ psm__save_flags(struct psm__model *m) return; psm__u8 ver = m->source->header->version; - psm__i32 n = am->count; + psm__i32 n = am->count; memcpy(am->last_render_order, m->render_order, sizeof(psm__i32) * n); memcpy(am->last_draw_order, am->draw_order, sizeof(psm__i32) * n); @@ -73,8 +75,9 @@ static void psm__update_flags(struct psm__model *m) { struct psm__art_meshes *am = &m->art_meshes; + psm__u8 ver = m->source->header->version; - psm__i32 n = am->count; + psm__i32 n = am->count; /* Force update: mark everything dirty */ if (m->force_update) { @@ -82,9 +85,9 @@ psm__update_flags(struct psm__model *m) if (n <= 0) return; - bool *en = am->enable; + bool *en = am->enable; psm__f32 *opacity = am->opacity; - psm__u8 *df = am->change_flags; + psm__u8 *df = am->change_flags; for (psm__i32 i = 0; i < n; i++) { if (!en[i] || opacity[i] == 0.0f) @@ -101,28 +104,28 @@ psm__update_flags(struct psm__model *m) if (n <= 0) return; - bool *en = am->enable; + bool *en = am->enable; psm__f32 *opacity = am->opacity; - psm__u8 *df = am->change_flags; - psm__i32 *ro = m->render_order; + psm__u8 *df = am->change_flags; + psm__i32 *ro = m->render_order; psm__i32 *last_ro = am->last_render_order; - psm__i32 *cdo = am->draw_order; + psm__i32 *cdo = am->draw_order; psm__i32 *last_do = am->last_draw_order; psm__f32 *last_op = am->last_opacity; psm__f32 *mc = NULL, *lmc = NULL; psm__f32 *sc = NULL, *lsc = NULL; if (ver >= csmMocVersion_42) { - mc = am->mul_color; + mc = am->mul_color; lmc = am->last_mul_color; - sc = am->scr_color; + sc = am->scr_color; lsc = am->last_scr_color; } for (psm__i32 i = 0; i < n; i++) { psm__i32 visible = en[i] && opacity[i] != 0.0f; psm__i32 was = (df[i] & PSM__FLAG_IS_VISIBLE) != 0; - psm__u8 flags = visible; + psm__u8 flags = visible; if (visible != was) flags |= PSM__FLAG_VISIBILITY_CHANGED; @@ -135,8 +138,8 @@ psm__update_flags(struct psm__model *m) if (en[i]) flags |= PSM__FLAG_VERTEX_CHANGED; - if (mc && (memcmp(&mc[i * 4], &lmc[i * 4], 16) != 0 - || memcmp(&sc[i * 4], &lsc[i * 4], 16) != 0)) + if (mc && (memcmp(&mc[i * 4], &lmc[i * 4], 16) != 0 || + memcmp(&sc[i * 4], &lsc[i * 4], 16) != 0)) flags |= PSM__FLAG_BLEND_COLOR_CHANGED; df[i] = flags; @@ -148,9 +151,9 @@ psm__update_flags(struct psm__model *m) if (n <= 0) return; - bool *en = am->enable; + bool *en = am->enable; psm__f32 *opacity = am->opacity; - psm__u8 *df = am->change_flags; + psm__u8 *df = am->change_flags; for (psm__i32 i = 0; i < n; i++) { if (!en[i] || opacity[i] == 0.0f) @@ -163,9 +166,11 @@ psm__update_flags(struct psm__model *m) PSM__DEF void psm__update_model(struct psm__model *m) { + m->last_error = PSM__OK; psm__save_flags(m); - psm__resolve_params(&m->params); + if (psm__resolve_params(&m->params)) + m->last_error = PSM__ERR_PARAMETER_RANGE_ERROR; psm__resolve_key_tables(m); psm__resolve_blend_key_tables(m); psm__resolve_bindings(m); @@ -219,8 +224,8 @@ psm__update_model(struct psm__model *m) /* v6+: zero opacity for disabled offscreen surfaces */ if (m->source->header->version >= csmMocVersion_53) { - psm__i32 oc = m->offscreens.count; - bool *en = m->offscreens.enable; + psm__i32 oc = m->offscreens.count; + bool *en = m->offscreens.enable; psm__f32 *opa = m->offscreens.opacity; if (oc > 0 && en && opa) { for (psm__i32 i = 0; i < oc; i++) { @@ -243,8 +248,8 @@ PSMDEF void csmResetDrawableDynamicFlags(csmModel *model) { struct psm__model *m = (struct psm__model *)model; - psm__i32 count = m->art_meshes.count; - psm__u8 *flags = m->art_meshes.change_flags; + psm__i32 count = m->art_meshes.count; + psm__u8 *flags = m->art_meshes.change_flags; for (psm__i32 i = 0; i < count; i++) flags[i] &= PSM__FLAG_IS_VISIBLE; m->art_meshes.state_changed = 1; diff --git a/src/verify.c b/src/verify.c new file mode 100644 index 0000000..6d2ba85 --- /dev/null +++ b/src/verify.c @@ -0,0 +1,801 @@ +/* + * Purism Core: MOC3 load-time validation + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#include "private.h" +#include "array.h" +#include "debug.h" +#include "deformer.h" +#include "error.h" +#include "moc3.h" +#include "verify.h" + +PSM__DEF int +psm__verify_count_info(psm__u8 ver, const struct psm__count_info *cnt) +{ + const psm__i32 *field = (const psm__i32 *)cnt; + + psm__i32 n = PSM__COUNT_INFO_INTS(ver); + for (psm__i32 i = 0; i < n; i++) + PSM__FAIL(field[i] < 0, PSM__ERR_FILE_CORRUPT, + "count field[%d] = %d is negative", i, field[i]); + + PSM__FAILM(((psm__u32)cnt->warps + (psm__u32)cnt->rotations) != + (psm__u32)cnt->deformers, + PSM__ERR_FILE_CORRUPT, "deformer count mismatch"); + return PSM__OK; +} + +/* Section bounds: 8-byte aligned, count sane, + * [offset, offset+size) in [0,n). */ +// clang-format off +#define psm__bounds_check_static(TYPE, COUNT, offsets, i, n) \ + if ((offsets[i] & 7) != 0) { \ + PSM__LOGF("section %d misaligned: offset=%u", i, (unsigned)offsets[i]); \ + return PSM__ERR_FILE_CORRUPT; \ + } \ + if ((COUNT) > (psm_size)-1 / sizeof(TYPE)) { \ + PSM__LOGF("section %d count overflow: count=%u", i, (unsigned)(COUNT)); \ + return PSM__ERR_FILE_CORRUPT; \ + } \ + psm_size _sz = sizeof(TYPE) * (COUNT); \ + if (offsets[i] > n || n - offsets[i] < _sz) { \ + PSM__LOGF("section %d out of bounds: offset=%u size=%u n=%u", i, (unsigned)offsets[i], (unsigned)_sz, (unsigned)n); \ + return PSM__ERR_FILE_CORRUPT; \ + } +#define psm__bounds_check_dynamic(TYPE, COUNT_MEMBER, offsets, i, n, cnt) \ + if ((offsets[i] & 7) != 0) { \ + PSM__LOGF("section %d misaligned: offset=%u", i, (unsigned)offsets[i]); \ + return PSM__ERR_FILE_CORRUPT; \ + } \ + if (cnt->COUNT_MEMBER < 0 || (psm_size)cnt->COUNT_MEMBER > (psm_size)-1 / sizeof(TYPE)) { \ + PSM__LOGF("section %d count invalid: count=%d", i, (int)cnt->COUNT_MEMBER); \ + return PSM__ERR_FILE_CORRUPT; \ + } \ + psm_size _sz = sizeof(TYPE) * (psm_size)cnt->COUNT_MEMBER; \ + if (offsets[i] > n || n - offsets[i] < _sz) { \ + PSM__LOGF("section %d out of bounds: offset=%u size=%u n=%u", i, (unsigned)offsets[i], (unsigned)_sz, (unsigned)n); \ + return PSM__ERR_FILE_CORRUPT; \ + } +// clang-format on + +PSM__DEF int +psm__verify_sections(struct psm__sections *ms, psm__u8 *p, + psm__u32 *offsets, psm_size n, psm_size off, psm__u8 ver, bool bounds) +{ + psm_size prev_end = off; + psm_size ci_bytes = (psm_size)PSM__COUNT_INFO_INTS(ver) * sizeof(psm__i32); + + int i = 0; + + /* + * count_info (section 0) is padded in-struct to 256 bytes but only ci_bytes + * are on disk; use ci_bytes for the monotonic cursor. Dynamic sections that + * are empty get a NULL pointer (the runtime relies on that). + */ +// clang-format off +#define psm__predicate_static(TYPE, MEMBER, COUNT) { \ + if (bounds) { \ + psm__bounds_check_static(TYPE, COUNT, offsets, i, n) \ + psm_size _ssz = (i == 0) ? ci_bytes : sizeof(TYPE) * (COUNT); \ + psm_size _static_end = offsets[i] + _ssz; \ + if (_static_end > prev_end) prev_end = _static_end; \ + } \ + ms->MEMBER = (TYPE *)(p + offsets[i]); \ + i++; \ + } +#define psm__predicate_dynamic(TYPE, MEMBER, COUNT_MEMBER) { \ + if (bounds) { \ + psm__bounds_check_dynamic(TYPE, COUNT_MEMBER, offsets, i, n, ms->count_info) \ + if (offsets[i] < prev_end) { \ + PSM__LOGF("section[%d] not monotonic: off=%u prev=%u sz=%u", \ + i, (unsigned)offsets[i], (unsigned)prev_end, (unsigned)_sz); \ + return PSM__ERR_FILE_CORRUPT; \ + } \ + prev_end = offsets[i] + _sz; \ + ms->MEMBER = _sz ? (TYPE *)(p + offsets[i]) : NULL; \ + } else { \ + ms->MEMBER = (TYPE *)(p + offsets[i]); \ + } \ + i++; \ + } +// clang-format on + + PSM__SECTIONS_V30(psm__predicate_static, psm__predicate_dynamic) + if (ver < csmMocVersion_33) goto done; + PSM__SECTIONS_V33(psm__predicate_static, psm__predicate_dynamic) + if (ver < csmMocVersion_42) goto done; + PSM__SECTIONS_V42(psm__predicate_static, psm__predicate_dynamic) + if (ver < csmMocVersion_50) goto done; + PSM__SECTIONS_V50(psm__predicate_static, psm__predicate_dynamic) + if (ver < csmMocVersion_53) goto done; + PSM__SECTIONS_V53(psm__predicate_static, psm__predicate_dynamic) + +done: +#undef psm__predicate_static +#undef psm__predicate_dynamic + return PSM__OK; +} + +static psm__u32 +psm__binding_keyform_count(const struct psm__sections *src, + const struct psm__count_info *cnt, psm__i32 bi) +{ + const struct psm__binding_src *bs = &src->binding_src; + + if (bi < 0 || bi >= cnt->bindings || !bs->key_table_idx_off || + !bs->key_table_idx_len || !src->key_table_idx_src.idx || + !src->key_table_src.keys_len) + return 1; + + psm__i32 off = bs->key_table_idx_off[bi]; + psm__i32 len = bs->key_table_idx_len[bi]; + if (len <= 0 || !psm__valid_range(off, len, cnt->key_table_idx)) + return 1; + + psm__u32 prod = 1; + for (psm__i32 k = 0; k < len; k++) { + psm__i32 kt = src->key_table_idx_src.idx[off + k]; + if (!psm__valid_idx(kt, cnt->key_tables)) + continue; + psm__i32 kc = src->key_table_src.keys_len[kt]; + if (kc <= 1) + continue; /* 0/1-key tables do not extend the keyform grid */ + if (prod > 0x7FFFFFFFu / (psm__u32)kc) + return 0x7FFFFFFF; /* saturate: exceeds any valid key_len */ + prod *= (psm__u32)kc; + } + return prod; +} + +static int +psm__verify_bs_windows(const struct psm__sections *src, + const struct psm__blend_src *bs, psm__i32 shape_count, + psm__i32 target_keyforms, + const psm__i32 *key_pos_off, const psm__i32 *vertex_count, + psm__i32 target_count, psm__i32 max_pos, + const psm__i32 *key_mul_off, psm__i32 max_mul_colors, + const psm__i32 *key_scr_off, psm__i32 max_scr_colors) +{ + const struct psm__count_info *cnt = src->count_info; + + const psm__i32 *kt_idx = src->blend_binding_src.key_table_idx; + const psm__i32 *ks_off = src->blend_binding_src.key_bs_off; + const psm__i32 *kc_len = src->blend_key_table_src.keys_len; + + if (!bs->target_idx || !bs->bs_binding_off || !bs->bs_binding_len || + !kt_idx || !ks_off || !kc_len) + return PSM__OK; + + for (psm__i32 i = 0; i < shape_count; i++) { + psm__i32 bo = bs->bs_binding_off[i]; + psm__i32 bn = bs->bs_binding_len[i]; + if (bn <= 0 || !psm__valid_range(bo, bn, cnt->blend_bindings)) + continue; + + /* pos is gated on the target existing and having vertices */ + psm__i32 vc = 0; + bool do_pos = false; + if (key_pos_off && vertex_count) { + psm__i32 ti = bs->target_idx[i]; + if (psm__valid_idx(ti, target_count) && vertex_count[ti] > 0) { + vc = vertex_count[ti]; + do_pos = true; + } + } + + for (psm__i32 j = 0; j < bn; j++) { + psm__i32 bb = bo + j; + psm__i32 kti = kt_idx[bb]; + if (!psm__valid_idx(kti, cnt->blend_key_tables)) + continue; + psm__i32 kc = kc_len[kti]; + if (kc < 1) kc = 1; /* a 0/1-key binding still reads keyform index 0 */ + psm__i32 so = ks_off[bb]; + + /* F4: the whole keyform window must fit */ + PSM__FAIL(!psm__valid_range(so, kc, target_keyforms), + PSM__ERR_FILE_CORRUPT, + "bs binding[%d] keyform window off=%d kc=%d > max=%d", + bb, so, kc, target_keyforms); + + if (!do_pos && !key_mul_off && !key_scr_off) + continue; /* keyform-only target */ + + for (psm__i32 k = 0; k < kc; k++) { + psm__i32 ki = so + k; + if (!psm__valid_idx(ki, target_keyforms)) + continue; + if (do_pos) { + psm__i32 po = key_pos_off[ki]; + PSM__FAIL(po < 0 || (psm__u32)po + 2u * (psm__u32)vc > + (psm__u32)max_pos, + PSM__ERR_FILE_CORRUPT, + "bs pos window ki=%d po=%d vc=%d max=%d", ki, po, vc, max_pos); + } + if (key_mul_off) { + psm__i32 ci = key_mul_off[ki]; + PSM__FAIL(ci >= max_mul_colors, PSM__ERR_FILE_CORRUPT, + "bs mul color window ki=%d ci=%d max=%d", ki, ci, max_mul_colors); + } + if (key_scr_off) { + psm__i32 ci = key_scr_off[ki]; + PSM__FAIL(ci >= max_scr_colors, PSM__ERR_FILE_CORRUPT, + "bs scr color window ki=%d ci=%d max=%d", ki, ci, max_scr_colors); + } + } + } + } + return PSM__OK; +} + +static int +psm__verify_offscreen_window(const struct psm__sections *src, + psm__i32 offscreen_count, psm__i32 offscreen_keyforms) +{ + const struct psm__count_info *cnt = src->count_info; + + const psm__i32 *owner = src->offscreen_src.owner_idx; + const psm__i32 *binding_idx = src->part_src.binding_idx; + const psm__i32 *keyform_off = src->part_src.keyform_off; + const psm__i32 *key_idx = src->part_key_src.key_idx; + const psm__i32 *mul_off = src->offscreen_key_src.key_mul_color_off; + + if (!owner || !binding_idx || !keyform_off || !key_idx) + return PSM__OK; + + for (psm__i32 i = 0; i < offscreen_count; i++) { + psm__i32 oi = owner[i]; + if (!psm__valid_idx(oi, cnt->parts)) + continue; + psm__i32 kbi = keyform_off[oi]; + if (!psm__valid_idx(kbi, cnt->part_keyforms)) + continue; + psm__i32 ki = key_idx[kbi]; + if (ki < 0) + continue; /* no offscreen keyforms for this surface */ + + psm__i32 prod = (psm__i32)psm__binding_keyform_count(src, cnt, + binding_idx[oi]); + + PSM__FAIL(!psm__valid_range(ki, prod, offscreen_keyforms), + PSM__ERR_FILE_CORRUPT, + "offscreen[%d] keyform window ki=%d prod=%d > max=%d", + i, ki, prod, offscreen_keyforms); + + if (mul_off) { + psm__i32 cb = mul_off[ki]; + /* + * gather_offscreens indexes BOTH the mul and scr color pools with the + * mul offset (it never reads key_scr_color_off), so cb+prod must fit + * in both pools. cb < 0 means "no color" and is skipped at runtime. + */ + PSM__FAIL(cb >= 0 && + (!psm__valid_range(cb, prod, cnt->keyform_mul_colors) || + !psm__valid_range(cb, prod, cnt->keyform_scr_colors)), + PSM__ERR_FILE_CORRUPT, + "offscreen[%d] color window cb=%d prod=%d > max(%d,%d)", + i, cb, prod, cnt->keyform_mul_colors, cnt->keyform_scr_colors); + } + } + return PSM__OK; +} + +PSM__DEF int +psm__verify_idx(psm__u8 ver, const struct psm__sections *src) +{ + const struct psm__count_info *cnt = src->count_info; + +// clang-format off +#define psm__check_nonnull(TYPE, MEMBER, COUNT_MEMBER) \ + if (cnt->COUNT_MEMBER > 0 && !src->MEMBER) { \ + PSM__LOGF("missing: %s (count=%d)", \ + #MEMBER, cnt->COUNT_MEMBER); \ + return PSM__ERR_FILE_CORRUPT; \ + } + + PSM__SECTIONS_V30(psm__nop_predicate, psm__check_nonnull) + if (ver < csmMocVersion_33) goto done_nonnull; + PSM__SECTIONS_V33(psm__nop_predicate, psm__check_nonnull) + if (ver < csmMocVersion_42) goto done_nonnull; + PSM__SECTIONS_V42(psm__nop_predicate, psm__check_nonnull) + if (ver < csmMocVersion_50) goto done_nonnull; + PSM__SECTIONS_V50(psm__nop_predicate, psm__check_nonnull) + if (ver < csmMocVersion_53) goto done_nonnull; + PSM__SECTIONS_V53(psm__nop_predicate, psm__check_nonnull) +done_nonnull: +#undef psm__check_nonnull + +#define psm__model_check_index(arr, i, max) \ + PSM__FAIL((arr)[i] < 0 || (arr)[i] >= (max), \ + PSM__ERR_FILE_CORRUPT, \ + "invalid index: %s[%d]=%d (max=%d)", \ + #arr, i, (arr)[i], (max)) + +#define psm__model_check_index_or_neg1(arr, i, max) \ + PSM__FAIL((arr)[i] < -1 || (arr)[i] >= (max), \ + PSM__ERR_FILE_CORRUPT, \ + "invalid index: %s[%d]=%d (max=%d)", \ + #arr, i, (arr)[i], (max)) + +#define psm__model_check_range(begin_arr, count_arr, i, max) \ + PSM__FAIL((count_arr)[i] < 0 || \ + ((count_arr)[i] > 0 && ((begin_arr)[i] < 0 || \ + (psm__u32)(begin_arr)[i] + \ + (psm__u32)(count_arr)[i] > (psm__u32)(max))), \ + PSM__ERR_FILE_CORRUPT, \ + "invalid range: %s[%d] begin=%d count=%d (max=%d)", \ + #begin_arr, i, (begin_arr)[i], (count_arr)[i], (max)) + + /* + * Validate the keyform grid covers the full combo span. The combo + * builder reaches keyform index product(key_counts)-1, so key_len must be + * at least that product. With the per-object keyform_off+key_len<=*_keyforms + * range check this bounds every reachable keyform index inside the object's + * declared keyforms. + */ +#define psm__check_key_combo(obj_src, keyform_total, obj_len) \ + for (psm__i32 _i = 0; _i < (obj_len); _i++) { \ + psm__i32 _bi = (obj_src).binding_idx[_i]; \ + if (_bi < 0 || _bi >= cnt->bindings) continue; \ + psm__u32 _prod = psm__binding_keyform_count(src, cnt, _bi); \ + psm__i32 _kl = (obj_src).key_len[_i]; \ + PSM__FAIL(_kl < 0 || _prod > (psm__u32)_kl, \ + PSM__ERR_FILE_CORRUPT, \ + "%s[%d] combo span %u > key_len %d", \ + #obj_src, _i, _prod, _kl); \ + psm__i32 _mc = 1 << psm__clamp_i32( \ + src->binding_src.key_table_idx_len[_bi], 0, PSM__MAX_KEY_TABLES); \ + PSM__FAIL(!psm__valid_range((obj_src).keyform_off[_i], _mc, (keyform_total)), \ + PSM__ERR_FILE_CORRUPT, \ + "%s[%d] keyform_off=%d + max_blend=%d > total %d", #obj_src, _i, \ + (obj_src).keyform_off[_i], _mc, (keyform_total)); \ + } +// clang-format on + + /* Part sources */ + for (psm__i32 i = 0; i < cnt->parts; i++) { + psm__model_check_index(src->part_src.binding_idx, i, cnt->bindings); + psm__model_check_range(src->part_src.keyform_off, + src->part_src.key_len, i, cnt->part_keyforms); + psm__model_check_index_or_neg1( + src->part_src.parent_part_idx, i, cnt->parts); + } + psm__check_key_combo(src->part_src, cnt->part_keyforms, cnt->parts); + + /* Deformer sources */ + for (psm__i32 i = 0; i < cnt->deformers; i++) { + psm__model_check_index(src->deformer_src.binding_idx, i, cnt->bindings); + psm__model_check_index_or_neg1( + src->deformer_src.parent_part_idx, i, cnt->parts); + psm__model_check_index_or_neg1(src->deformer_src.parent_deformer_idx, + i, cnt->deformers); + + psm__i32 dtype = src->deformer_src.type[i]; + psm__i32 sidx = src->deformer_src.local_idx[i]; + switch (dtype) { + case PSM__DEFORMER_TYPE_WARP: + if (sidx < 0 || sidx >= cnt->warps) { + PSM__LOGF("deformer[%d].specific=%d (warp max=%d)", + i, sidx, cnt->warps); + return PSM__ERR_FILE_CORRUPT; + } + break; + case PSM__DEFORMER_TYPE_ROTATION: + if (sidx < 0 || sidx >= cnt->rotations) { + PSM__LOGF("deformer[%d].specific=%d (rot max=%d)", + i, sidx, cnt->rotations); + return PSM__ERR_FILE_CORRUPT; + } + break; + default: + PSM__LOGF("deformer[%d].type=%d invalid", i, dtype); + return PSM__ERR_FILE_CORRUPT; + } + } + + /* Warp deformer sources */ + for (psm__i32 i = 0; i < cnt->warps; i++) { + psm__model_check_index(src->warp_src.binding_idx, + i, cnt->bindings); + psm__model_check_range(src->warp_src.keyform_off, + src->warp_src.key_len, i, cnt->warp_keyforms); + psm__i32 row = src->warp_src.row[i]; + psm__i32 col = src->warp_src.col[i]; + psm__i32 vc = src->warp_src.vertex_count[i]; + PSM__FAIL(row <= 0 || col <= 0, PSM__ERR_FILE_CORRUPT, + "warp[%d] grid row=%d col=%d", i, row, col); + /* row/col are only bounded > 0, so compute in unsigned (well-defined + * wraparound); row+1 in int overflows when row == INT_MAX. */ + psm__u32 expect = ((psm__u32)row + 1u) * ((psm__u32)col + 1u); + PSM__FAIL((psm__u32)vc != expect, PSM__ERR_FILE_CORRUPT, + "warp[%d] vert_count=%d expected=%u", i, vc, (unsigned)expect); + } + + psm__check_key_combo(src->warp_src, cnt->warp_keyforms, cnt->warps); + + /* Warp deformer keyform positions */ + for (psm__i32 i = 0; i < cnt->warps; i++) { + psm__i32 off = src->warp_src.keyform_off[i]; + psm__i32 count = src->warp_src.key_len[i]; + psm__i32 vc = src->warp_src.vertex_count[i]; + for (psm__i32 j = 0; j < count; j++) { + psm__i32 po = src->warp_key_src.key_pos_off[off + j]; + PSM__FAIL(po < 0 || (psm__u32)po + 2u * (psm__u32)vc > + (psm__u32)cnt->keyform_pos, + PSM__ERR_FILE_CORRUPT, + "warp[%d] kf[%d] pos_off=%d vc=%d max=%d", + i, j, po, vc, cnt->keyform_pos); + } + } + + /* Rotation deformer sources */ + for (psm__i32 i = 0; i < cnt->rotations; i++) { + psm__model_check_index(src->rotation_src.binding_idx, i, cnt->bindings); + psm__model_check_range(src->rotation_src.keyform_off, + src->rotation_src.key_len, i, cnt->rotation_keyforms); + } + + psm__check_key_combo(src->rotation_src, cnt->rotation_keyforms, + cnt->rotations); + + /* Art mesh sources */ + for (psm__i32 i = 0; i < cnt->art_meshes; i++) { + psm__model_check_index(src->art_mesh_src.binding_idx, i, cnt->bindings); + psm__model_check_range(src->art_mesh_src.keyform_off, + src->art_mesh_src.key_len, i, cnt->art_mesh_keyforms); + psm__model_check_index_or_neg1( + src->art_mesh_src.parent_part_idx, i, cnt->parts); + psm__model_check_index_or_neg1(src->art_mesh_src.parent_deformer_idx, + i, cnt->deformers); + PSM__FAIL(src->art_mesh_src.vertex_count[i] < 0 || + src->art_mesh_src.uv_off[i] < 0 || + (psm__u32)src->art_mesh_src.uv_off[i] + + 2u * (psm__u32)src->art_mesh_src.vertex_count[i] > + (psm__u32)cnt->uvs, + PSM__ERR_FILE_CORRUPT, + "art_mesh[%d]: UV [%d, +%d*2) oob (max %d)", + i, src->art_mesh_src.uv_off[i], + src->art_mesh_src.vertex_count[i], cnt->uvs); + psm__model_check_range(src->art_mesh_src.idx_off, + src->art_mesh_src.idx_len, i, cnt->idx); + psm__model_check_range(src->art_mesh_src.mask_off, + src->art_mesh_src.mask_len, i, cnt->masks); + } + + psm__check_key_combo(src->art_mesh_src, cnt->art_mesh_keyforms, + cnt->art_meshes); + + /* + * Art mesh keyform position indices. Each keyform stores vc vertices + * (2 floats each), so the readable span is [po, po + 2*vc); validate + * the full span, not just the start index. + */ + for (psm__i32 i = 0; i < cnt->art_meshes; i++) { + psm__i32 off = src->art_mesh_src.keyform_off[i]; + psm__i32 count = src->art_mesh_src.key_len[i]; + psm__i32 vc = src->art_mesh_src.vertex_count[i]; + for (psm__i32 j = 0; j < count; j++) { + psm__i32 po = src->art_mesh_key_src.key_pos_off[off + j]; + PSM__FAIL(po < 0 || (psm__u32)po + 2u * (psm__u32)vc > + (psm__u32)cnt->keyform_pos, + PSM__ERR_FILE_CORRUPT, + "art_mesh[%d] kf[%d] pos_off=%d vc=%d max=%d", + i, j, po, vc, cnt->keyform_pos); + } + } + + /* + * Triangle index VALUES. Each art mesh's index slice + * [idx_off, idx_off+idx_len) contains + * holds vertex indices into that mesh's own vertex array, so every index + * must be < vertex_count. The library never dereferences these, but callers + * receive them directly from csmGetDrawableIndices, so a malformed index + * could cause an OOB read in a renderer. + * + * We reject at load time. + */ + if (src->idx_src.idx) { + for (psm__i32 i = 0; i < cnt->art_meshes; i++) { + psm__i32 off = src->art_mesh_src.idx_off[i]; + psm__i32 len = src->art_mesh_src.idx_len[i]; + psm__i32 vc = src->art_mesh_src.vertex_count[i]; + for (psm__i32 j = 0; j < len; j++) { + psm__i32 vi = (psm__i32)src->idx_src.idx[off + j]; + PSM__FAIL(vi >= vc, PSM__ERR_FILE_CORRUPT, + "art_mesh[%d] index[%d]=%d >= vertex_count %d", i, j, vi, vc); + } + } + } + + /* Parameter sources */ + for (psm__i32 i = 0; i < cnt->parameters; i++) { + psm__model_check_range(src->param_src.key_table_off, + src->param_src.key_table_len, i, cnt->key_tables); + } + + /* Keyform binding sources */ + for (psm__i32 i = 0; i < cnt->bindings; i++) { + psm__model_check_range(src->binding_src.key_table_idx_off, + src->binding_src.key_table_idx_len, i, cnt->key_table_idx); + psm__i32 pc = src->binding_src.key_table_idx_len[i]; + PSM__FAIL(pc < 0 || pc > PSM__MAX_KEY_TABLES, PSM__ERR_FILE_CORRUPT, + "binding[%d] param_count=%d oob", i, pc); + } + + /* Parameter binding index sources */ + for (psm__i32 i = 0; i < cnt->key_table_idx; i++) { + psm__model_check_index(src->key_table_idx_src.idx, i, cnt->key_tables); + } + + /* Parameter binding sources */ + for (psm__i32 i = 0; i < cnt->key_tables; i++) { + psm__model_check_range(src->key_table_src.keys_off, + src->key_table_src.keys_len, i, cnt->keys); + } + + /* Drawable mask sources */ + for (psm__i32 i = 0; i < cnt->masks; i++) { + psm__model_check_index_or_neg1( + src->mask_src.art_mesh_idx, i, cnt->art_meshes); + } + + psm__check_key_combo(src->glue_src, cnt->glue_keyforms, cnt->glues); + + /* Glue sources */ + for (psm__i32 i = 0; i < cnt->glues; i++) { + psm__model_check_index(src->glue_src.binding_idx, i, cnt->bindings); + psm__model_check_range(src->glue_src.keyform_off, + src->glue_src.key_len, i, cnt->glue_keyforms); + psm__model_check_index(src->glue_src.art_mesh_idx_a, i, cnt->art_meshes); + psm__model_check_index(src->glue_src.art_mesh_idx_b, i, cnt->art_meshes); + psm__model_check_range(src->glue_src.info_off, + src->glue_src.info_len, i, cnt->glue_info); + /* F5: glue info is consumed in (mesh0, mesh1) pairs -> must be even. */ + PSM__FAIL((src->glue_src.info_len[i] & 1) != 0, PSM__ERR_FILE_CORRUPT, + "glue[%d]: odd info_len %d", i, src->glue_src.info_len[i]); + } + + /* Glue position indices */ + if (src->glue_src.info_off && src->glue_src.info_len && + src->glue_src.art_mesh_idx_a && src->glue_src.art_mesh_idx_b && + src->glue_info_src.pos_idx && src->art_mesh_src.vertex_count) { + for (psm__i32 i = 0; i < cnt->glues; i++) { + psm__i32 m0 = src->glue_src.art_mesh_idx_a[i]; + psm__i32 m1 = src->glue_src.art_mesh_idx_b[i]; + if (m0 < 0 || m0 >= cnt->art_meshes || m1 < 0 || m1 >= cnt->art_meshes) + continue; + psm__i32 vc0 = src->art_mesh_src.vertex_count[m0]; + psm__i32 vc1 = src->art_mesh_src.vertex_count[m1]; + psm__i32 ib = src->glue_src.info_off[i]; + psm__i32 ic = src->glue_src.info_len[i]; + if (ib < 0 || ic <= 0 || ib + ic > cnt->glue_info) + continue; + for (psm__i32 j = 0; j < ic; j += 2) { + psm__u16 p0 = src->glue_info_src.pos_idx[ib + j]; + PSM__FAIL(p0 >= (psm__u16)vc0, PSM__ERR_FILE_CORRUPT, + "glue[%d] pos_idx[%d]=%u OOB (vc=%d)", i, j, p0, vc0); + if (j + 1 < ic) { + psm__u16 p1 = src->glue_info_src.pos_idx[ib + j + 1]; + PSM__FAIL(p1 >= (psm__u16)vc1, PSM__ERR_FILE_CORRUPT, + "glue[%d] pos_idx[%d]=%u OOB (vc=%d)", i, j + 1, p1, vc1); + } + } + } + } + + /* Draw order group sources */ + for (psm__i32 i = 0; i < cnt->draw_groups; i++) { + psm__model_check_range(src->draw_group_src.obj_off, + src->draw_group_src.obj_len, i, cnt->draw_items); + } + + /* Draw order group object sources */ + if (src->draw_group_obj_src.type && src->draw_group_obj_src.idx) { + for (psm__i32 i = 0; i < cnt->draw_items; i++) { + psm__model_check_index_or_neg1(src->draw_group_obj_src.self_group_idx, + i, cnt->draw_groups); + psm__i32 t = src->draw_group_obj_src.type[i]; + psm__i32 oi = src->draw_group_obj_src.idx[i]; + PSM__FAIL(t != 0 && t != 1, PSM__ERR_FILE_CORRUPT, + "draw_item[%d]: bad type %d", i, t); + psm__i32 max = t ? cnt->parts : cnt->art_meshes; + PSM__FAIL(oi < 0 || oi >= max, PSM__ERR_FILE_CORRUPT, + "draw_item[%d]: index %d OOB (type=%d max=%d)", i, oi, t, max); + /* + * only part items (type 1) recurse into a child group via + * self_group_idx, and at runtime that index must be valid. -1 is + * allowed for art-mesh items (never used) but not for parts. + */ + PSM__FAIL(t == 1 && src->draw_group_obj_src.self_group_idx[i] < 0, + PSM__ERR_FILE_CORRUPT, + "draw_item[%d]: part item has no group", i); + } + } + + if (ver < csmMocVersion_42) + goto done_ver; + + /* Warp deformer color indices */ + for (psm__i32 i = 0; i < cnt->warps; i++) { + psm__model_check_range(src->warp_src.key_color_off, + src->warp_src.key_len, i, cnt->keyform_mul_colors); + } + + /* Rotation deformer color indices */ + for (psm__i32 i = 0; i < cnt->rotations; i++) { + psm__model_check_range(src->rotation_src.key_color_off, + src->rotation_src.key_len, i, cnt->keyform_mul_colors); + } + + /* Art mesh color indices */ + for (psm__i32 i = 0; i < cnt->art_meshes; i++) { + psm__model_check_range(src->art_mesh_src.key_color_off, + src->art_mesh_src.key_len, i, cnt->keyform_mul_colors); + } + + /* Parameter extension sources */ + for (psm__i32 i = 0; i < cnt->parameters; i++) { + psm__model_check_range(src->param_keys_src.keys_off, + src->param_keys_src.keys_len, i, cnt->keys); + } + + /* Blend shape parameter binding sources */ + for (psm__i32 i = 0; + i < cnt->blend_key_tables; i++) { + psm__model_check_range(src->blend_key_table_src.keys_off, + src->blend_key_table_src.keys_len, i, cnt->keys); + } + + /* Parameter blend shape binding indices */ + for (psm__i32 i = 0; i < cnt->parameters; i++) { + psm__model_check_range(src->param_src.blend_key_table_off, + src->param_src.blend_key_table_len, i, cnt->blend_key_tables); + } + + /* Blend shape keyform binding sources */ + for (psm__i32 i = 0; i < cnt->blend_bindings; i++) { + psm__model_check_index(src->blend_binding_src.key_table_idx, + i, cnt->blend_key_tables); + psm__model_check_range(src->blend_binding_src.bs_constraint_idx_off, + src->blend_binding_src.bs_constraint_idx_len, + i, cnt->bs_constraint_idx); + } + + /* Blend shape warp deformer sources */ + for (psm__i32 i = 0; + i < cnt->bs_warps; i++) { + psm__model_check_index(src->bs_warp_src.target_idx, i, cnt->warps); + psm__model_check_range(src->bs_warp_src.bs_binding_off, + src->bs_warp_src.bs_binding_len, i, cnt->blend_bindings); + } + + /* Blend shape art mesh sources */ + for (psm__i32 i = 0; + i < cnt->bs_art_meshes; i++) { + psm__model_check_index(src->bs_art_mesh_src.target_idx, + i, cnt->art_meshes); + psm__model_check_range(src->bs_art_mesh_src.bs_binding_off, + src->bs_art_mesh_src.bs_binding_len, i, cnt->blend_bindings); + } + + /* Blend shape constraint index sources */ + for (psm__i32 i = 0; + i < cnt->bs_constraint_idx; i++) { + psm__model_check_index(src->blend_constraint_idx_src.constraint_idx, + i, cnt->bs_constraints); + } + + /* Blend shape constraint sources */ + for (psm__i32 i = 0; + i < cnt->bs_constraints; i++) { + psm__model_check_index(src->blend_constraint_src.parameter_idx, + i, cnt->parameters); + psm__model_check_range(src->blend_constraint_src.value_off, + src->blend_constraint_src.value_len, i, cnt->bs_constraint_vals); + } + + /* Blend shape keyform-window bounds (v4.2 targets) */ +// clang-format off +#define PSM__VERIFY(call) \ + { if ((call) != PSM__OK) return PSM__ERR_FILE_CORRUPT; } +// clang-format on + + PSM__VERIFY(psm__verify_bs_windows(src, &src->bs_warp_src, cnt->bs_warps, + cnt->warp_keyforms, + src->warp_key_src.key_pos_off, src->warp_src.vertex_count, + cnt->warps, cnt->keyform_pos, + src->warp_key_src.key_mul_color_off, cnt->keyform_mul_colors, + src->warp_key_src.key_scr_color_off, cnt->keyform_scr_colors)); + + PSM__VERIFY(psm__verify_bs_windows(src, &src->bs_art_mesh_src, + cnt->bs_art_meshes, cnt->art_mesh_keyforms, + src->art_mesh_key_src.key_pos_off, src->art_mesh_src.vertex_count, + cnt->art_meshes, cnt->keyform_pos, + src->art_mesh_key_src.key_mul_color_off, cnt->keyform_mul_colors, + src->art_mesh_key_src.key_scr_color_off, cnt->keyform_scr_colors)); + + if (ver < csmMocVersion_50) + goto done_ver; + + /* Blend shape part sources */ + for (psm__i32 i = 0; i < cnt->bs_parts; i++) { + psm__model_check_index(src->bs_part_src.target_idx, i, cnt->parts); + psm__model_check_range(src->bs_part_src.bs_binding_off, + src->bs_part_src.bs_binding_len, i, cnt->blend_bindings); + } + + /* Blend shape rotation deformer sources */ + for (psm__i32 i = 0; + i < cnt->bs_rotations; i++) { + psm__model_check_index(src->bs_rotation_src.target_idx, i, cnt->rotations); + psm__model_check_range(src->bs_rotation_src.bs_binding_off, + src->bs_rotation_src.bs_binding_len, i, cnt->blend_bindings); + } + + /* Blend shape glue sources */ + for (psm__i32 i = 0; i < cnt->bs_glues; i++) { + psm__model_check_index(src->bs_glue_src.target_idx, i, cnt->glues); + psm__model_check_range(src->bs_glue_src.bs_binding_off, + src->bs_glue_src.bs_binding_len, i, cnt->blend_bindings); + } + + /* Blend shape keyform-window bounds (v5.0 targets) */ + PSM__VERIFY(psm__verify_bs_windows(src, &src->bs_part_src, cnt->bs_parts, + cnt->part_keyforms, NULL, NULL, 0, 0, NULL, 0, NULL, 0)); + PSM__VERIFY(psm__verify_bs_windows(src, &src->bs_glue_src, cnt->bs_glues, + cnt->glue_keyforms, NULL, NULL, 0, 0, NULL, 0, NULL, 0)); + + PSM__VERIFY(psm__verify_bs_windows(src, &src->bs_rotation_src, + cnt->bs_rotations, cnt->rotation_keyforms, + NULL, NULL, 0, 0, + src->rotation_key_src.key_mul_color_off, cnt->keyform_mul_colors, + src->rotation_key_src.key_scr_color_off, cnt->keyform_scr_colors)); + + if (ver < csmMocVersion_53) + goto done_ver; + + /* Part offscreen rendering index */ + for (psm__i32 i = 0; i < cnt->parts; i++) { + psm__model_check_index_or_neg1(src->part_src.offscreen_idx, + i, cnt->offscreens); + } + + /* Offscreen rendering sources */ + for (psm__i32 i = 0; i < cnt->offscreens; i++) { + psm__model_check_index(src->offscreen_src.owner_idx, i, cnt->parts); + psm__model_check_range(src->offscreen_src.mask_off, + src->offscreen_src.mask_len, i, cnt->masks); + } + + /* Blend shape offscreen rendering sources */ + for (psm__i32 i = 0; + i < cnt->bs_offscreens; i++) { + psm__model_check_index(src->bs_offscreen_src.target_idx, + i, cnt->offscreens); + psm__model_check_range(src->bs_offscreen_src.bs_binding_off, + src->bs_offscreen_src.bs_binding_len, i, cnt->blend_bindings); + } + + /* Blend shape keyform-window bounds (v5.3 targets) */ + PSM__VERIFY(psm__verify_bs_windows(src, &src->bs_offscreen_src, + cnt->bs_offscreens, cnt->offscreen_keyforms, + NULL, NULL, 0, 0, + src->offscreen_key_src.key_mul_color_off, cnt->keyform_mul_colors, + src->offscreen_key_src.key_scr_color_off, cnt->keyform_scr_colors)); + + PSM__VERIFY(psm__verify_offscreen_window(src, cnt->offscreens, + cnt->offscreen_keyforms)); + +#undef PSM__VERIFY + +done_ver: +#undef psm__model_check_index +#undef psm__model_check_index_or_neg1 +#undef psm__model_check_range + + return PSM__OK; +} diff --git a/src/verify.h b/src/verify.h new file mode 100644 index 0000000..558ee0b --- /dev/null +++ b/src/verify.h @@ -0,0 +1,22 @@ +/* + * Purism Core: MOC3 load-time validation + * + * Copyright (c) 2026 Sakura Motion Project + * SPDX-License-Identifier: MIT + */ + +#ifndef PSM__VERIFY_H +#define PSM__VERIFY_H + +#include "private.h" +#include "moc3.h" + +PSM__DEF int psm__verify_count_info(psm__u8 ver, + const struct psm__count_info *cnt); + +PSM__DEF int psm__verify_sections(struct psm__sections *ms, psm__u8 *p, + psm__u32 *offsets, psm_size n, psm_size off, psm__u8 ver, bool bounds); + +PSM__DEF int psm__verify_idx(psm__u8 ver, const struct psm__sections *src); + +#endif /* PSM__VERIFY_H */ From 92fec630d635e2f824dac1c87a9c389896c76caa Mon Sep 17 00:00:00 2001 From: ronsor Date: Wed, 19 Aug 2026 23:49:47 -0700 Subject: [PATCH 02/32] Tests: remove `opendir` dependency; move variable declarations before functions --- Makefile | 4 +- src/tests/negctl_triidx.c | 19 ++++----- src/tests/test_endian.c | 85 +++++--------------------------------- src/tests/test_transform.c | 2 +- 4 files changed, 20 insertions(+), 90 deletions(-) diff --git a/Makefile b/Makefile index 259e7e5..c51c703 100644 --- a/Makefile +++ b/Makefile @@ -187,12 +187,12 @@ build/unit: src/tests/unit.c $(SRC) $(HDR) $(COMMON_H) | build $(CC) $(CFLAGS) -Wno-unused-function $< -o $@ -lm endian-test: build/endian-test - ./build/endian-test testdata/moc3 $(TEST_DATA) + ./build/endian-test $(shell find "$(TEST_DATA)" -iname '*.moc3') build/endian-test: src/tests/test_endian.c $(SRC) $(HDR) $(COMMON_H) | build $(CC) $(CFLAGS) -Wno-unused-function $< -o $@ -lm verify-negctl: build/negctl-triidx - ./build/negctl-triidx testdata/moc3 $(TEST_DATA) + ./build/negctl-triidx $(shell find "$(TEST_DATA)" -iname '*.moc3') build/negctl-triidx: src/tests/negctl_triidx.c $(SRC) $(HDR) $(COMMON_H) | build $(CC) $(CFLAGS) -Wno-unused-function $< -o $@ -lm diff --git a/src/tests/negctl_triidx.c b/src/tests/negctl_triidx.c index 1117ed4..742a531 100644 --- a/src/tests/negctl_triidx.c +++ b/src/tests/negctl_triidx.c @@ -1,8 +1,5 @@ /* * Negative control for the triangle-index-value check in verify_idx: - * load a model, confirm it validates, corrupt one triangle index to an - * out-of-range value, and confirm validation now REJECTS it. - * Single-TU build (like unit.c / test_endian.c) for access to internals. */ #include #include @@ -52,7 +49,12 @@ static int g_tested, g_rejected, g_no_indices; -static int ends_moc3(const char *s){ size_t l=strlen(s); return l>5 && !strcmp(s+l-5,".moc3"); } +static int +ends_moc3(const char *s) +{ + size_t l = strlen(s); + return l > 5 && !strcmp(s+l-5, ".moc3"); +} static void run_one(const char *path) @@ -97,14 +99,7 @@ main(int argc, char **argv) { csmSetLogFunction(NULL); for (int a = 1; a < argc; a++) { - DIR *dir = opendir(argv[a]); if (!dir) continue; - struct dirent *e; char p[4096]; - while ((e = readdir(dir))) { - if (!ends_moc3(e->d_name)) continue; - snprintf(p, sizeof p, "%s/%s", argv[a], e->d_name); - run_one(p); - } - closedir(dir); + run_one(argv[a]); } fprintf(stderr, "corrupted+tested: %d, rejected: %d, (no-index models: %d)\n", g_tested, g_rejected, g_no_indices); diff --git a/src/tests/test_endian.c b/src/tests/test_endian.c index ce80c19..07535d4 100644 --- a/src/tests/test_endian.c +++ b/src/tests/test_endian.c @@ -1,23 +1,6 @@ /* * Purism Core: big-endian round-trip differential test * - * The entire test corpus is little-endian (endian_flag == 0), so the byte-swap - * load path has no correctness coverage. This test synthesizes a big-endian - * copy of each model using an INDEPENDENT swapper (its own swap primitives, its - * own count_info/canvas/offset-table sizing, and a uniform sizeof() predicate - * over the section table) -- it never calls the library's psm__bswap_* code. - * It then loads the synthesized BE file through the public API and asserts the - * model output is bit-identical to the little-endian load after an identical - * parameter sweep. - * - * Shared-and-correct logic agrees (pass); any divergence between the library's - * swap and this independent reference -- a regression, a count_info version - * sizing bug, a missed canvas field, a bad offset-table count -- corrupts the - * BE file the library reconstructs and shows up as an output mismatch. - * - * Single-TU build (like unit.c) for access to internal structs and the - * PSM__SECTIONS_* table. - * * Copyright (c) 2026 Sakura Motion Project * SPDX-License-Identifier: MIT */ @@ -70,7 +53,7 @@ #include "../samples/common.h" -/* ---- independent byte-swap primitives (not the library's) ---- */ +static int g_pass, g_fail, g_skip; static void ind_swap32(void *p, size_t n) @@ -92,13 +75,6 @@ ind_swap16(void *p, size_t n) } } -/* - * Swap one section field in the synth (pristine) buffer. The field pointer - * comes from a throwaway little-endian parse (map buffer); we translate it to - * the same offset in the pristine buffer. Runtime/arena pointers (8-byte) and - * byte/string fields fall outside {2,4} or outside the file and are skipped -- - * exactly the fields a big-endian file must NOT swap. - */ static void ind_swap_field(size_t width, const void *fieldptr, size_t count, const uint8_t *mapbase, uint8_t *synthbase, size_t size) @@ -112,18 +88,13 @@ ind_swap_field(size_t width, const void *fieldptr, size_t count, return; /* runtime/arena field, not in file */ size_t off = (size_t)(p - mapbase); if (off + count * width > size) - return; /* defensive; shouldn't happen */ + return; if (width == 4) ind_swap32(synthbase + off, count); else ind_swap16(synthbase + off, count); } -/* - * Produce a big-endian copy of `raw` (n bytes). Returns a freshly aligned - * buffer the caller frees with psm_aligned_free, or NULL if the model won't - * parse as little-endian (not a valid model -> nothing to test). - */ static void * build_be(const uint8_t *raw, size_t n) { @@ -190,8 +161,6 @@ build_be(const uint8_t *raw, size_t n) return synth; } -/* ---- output snapshot ---- */ - struct buf { uint8_t *data; size_t len, cap; @@ -265,9 +234,6 @@ snapshot(csmModel *m, struct buf *b) buf_put(b, &ppu, sizeof(ppu)); } -/* load a model (revive in place + init + sweep) and snapshot it. takes - * ownership of mocbuf (must be a freshly aligned buffer of n bytes). returns - * 0 ok, -1 if revive/init fails. */ static int load_and_snapshot(void *mocbuf, size_t n, struct buf *out) { @@ -317,7 +283,7 @@ run_one(const char *path, const char **why) free(snap_le.data); psm_aligned_free(raw); *why = "LE load failed"; - return -1; /* not a valid model; nothing to test */ + return -1; } /* independently synthesized big-endian copy */ @@ -353,55 +319,24 @@ run_one(const char *path, const char **why) return ok ? 1 : 0; } -static int g_pass, g_fail, g_skip; - -static int -ends_moc3(const char *s) +int +main(int argc, char **argv) { - size_t l = strlen(s); - return l > 5 && strcmp(s + l - 5, ".moc3") == 0; -} + csmSetLogFunction(NULL); + fprintf(stderr, "big-endian round-trip differential test:\n"); -static void -run_dir(const char *dir) -{ - DIR *d = opendir(dir); - if (!d) { - fprintf(stderr, " (cannot open dir: %s)\n", dir); - return; - } - struct dirent *e; - char path[4096]; - while ((e = readdir(d)) != NULL) { - if (!ends_moc3(e->d_name)) - continue; - snprintf(path, sizeof(path), "%s/%s", dir, e->d_name); + for (int i = 1; i < argc; i++) { const char *why = ""; - int r = run_one(path, &why); + int r = run_one(argv[i], &why); if (r == 1) { g_pass++; } else if (r == 0) { g_fail++; - fprintf(stderr, " FAIL %s: %s\n", e->d_name, why); + fprintf(stderr, " FAIL %s: %s\n", argv[i], why); } else { g_skip++; } } - closedir(d); -} - -int -main(int argc, char **argv) -{ - csmSetLogFunction(NULL); - fprintf(stderr, "big-endian round-trip differential test:\n"); - - if (argc > 1) { - for (int i = 1; i < argc; i++) - run_dir(argv[i]); - } else { - run_dir("testdata/moc3"); - } fprintf(stderr, "\n%d passed, %d failed, %d skipped\n", g_pass, g_fail, g_skip); diff --git a/src/tests/test_transform.c b/src/tests/test_transform.c index 16a2f95..6fe54f8 100644 --- a/src/tests/test_transform.c +++ b/src/tests/test_transform.c @@ -232,7 +232,7 @@ TEST(rotation_with_origin) make_rot_model(&m, &dn, &rc, &a, &s, &ox, &oy, &rfx, &rfy); - /* M(180°) = (-1, 0; 0, -1) */ + /* M(180) = (-1, 0; 0, -1) */ /* result = (-1,0;0,-1) * (1,0) + (5,5) = (4, 5) */ psm__f32 inp[] = {1.0f, 0.0f}, out[2]; psm__rotation_transform(&m, 0, inp, out, 1); From b8b6122241b0b2b21a4b4445286373fe8dce85f2 Mon Sep 17 00:00:00 2001 From: ronsor Date: Thu, 20 Aug 2026 01:00:11 -0700 Subject: [PATCH 03/32] Viewer: DRY for initializing default options --- src/samples/viewer/viewer.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/samples/viewer/viewer.c b/src/samples/viewer/viewer.c index 4b69758..e996d0e 100644 --- a/src/samples/viewer/viewer.c +++ b/src/samples/viewer/viewer.c @@ -28,13 +28,18 @@ static void usage(void) " drag a .model3.json or .moc3 onto the window to load it\n"); } -static bool ParseArgs(int argc, char **argv, Options *o) +static void ResetOptions(Options *o) { memset(o, 0, sizeof(*o)); o->shotZoom = 1.0f; o->maskScale = DEFAULT_MASK_SCALE; o->maxOrder = 1 << 30; o->onlyDrawable = -1; +} + +static bool ParseArgs(int argc, char **argv, Options *o) +{ + ResetOptions(o); for (int i = 1; i < argc; i++) { const char *a = argv[i]; @@ -349,9 +354,7 @@ int main(int argc, char **argv) #if defined(__EMSCRIPTEN__) (void)argc; (void)argv; - app.opt.maskScale = DEFAULT_MASK_SCALE; - app.opt.shotZoom = 1.0f; - app.opt.maxOrder = 1 << 30; + ResetOptions(&app.opt); #else if (!ParseArgs(argc, argv, &app.opt)) { From b1a7144acd443af75b3b549e9893a2088ee11220 Mon Sep 17 00:00:00 2001 From: ronsor Date: Thu, 20 Aug 2026 13:22:36 -0700 Subject: [PATCH 04/32] Build: set up distribution workflow for web builds; add warning that WebGL 2 is required --- .github/workflows/ci.yml | 42 ++++++++++++++++++++---------- .gitignore | 3 +++ Makefile | 19 +++++++------- cmake/zig-toolchain.cmake | 2 +- docs/BUILDING.md | 6 +++++ docs/SDKINFO-WEB.txt | 11 ++++++++ docs/SDKINFO.txt | 2 +- scripts/build-dist-web.sh | 49 +++++++++++++++++++++++++++++++++++ scripts/build-dist.sh | 7 ++--- src/samples/viewer/shell.html | 6 ++--- 10 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 docs/SDKINFO-WEB.txt create mode 100755 scripts/build-dist-web.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e80ecb7..5fdd02b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,31 +114,27 @@ jobs: steps: - uses: actions/checkout@v4 - # Pinned to the emsdk version raylib 6.0's own web build used, so the - # archived libraylib.web.a stays ABI-compatible with this emcc. - uses: emscripten-core/setup-emsdk@v15 with: version: 5.0.3 - - name: Fetch raylib v6.0 (webassembly) + - name: Fetch and build raylib v6.0 (webassembly) run: | - mkdir -p build/raylib - curl -sL https://github.com/raysan5/raylib/releases/download/6.0/raylib-6.0_webassembly.zip \ - -o build/raylib/raylib-6.0_webassembly.zip - ( cd build/raylib && unzip -q raylib-6.0_webassembly.zip ) - echo "RAYLIB_WEB_DIR=$PWD/build/raylib/raylib-6.0_webassembly/include" >> "$GITHUB_ENV" - echo "RAYLIB_WEB_LIB=$PWD/build/raylib/raylib-6.0_webassembly/lib/libraylib.web.a" >> "$GITHUB_ENV" + mkdir -p build/raylib-web/ + curl -sL https://github.com/raysan5/raylib/archive/refs/tags/6.0.zip \ + -o build/raylib-6.0.zip + ( cd build; unzip raylib-6.0.zip; mv raylib-6.0 raylib-web ) + make -C build/raylib-web/src PLATFORM=PLATFORM_WEB GRAPHICS=GRAPHICS_API_OPENGL_ES3 RAYLIB_BUILD_MODE=RELEASE + echo "RAYLIB_WEB_DIR=$PWD/build/raylib-web/src" >> "$GITHUB_ENV" - - name: Build web viewer (Emscripten) - run: make viewer-web + - name: Build for web + run: ./scripts/build-dist-web.sh - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: wasm - path: | - dist/Live2DCubismCore*.js - dist/viewer.* + path: dist/sdk-web/ release: if: startsWith(github.ref, 'refs/tags/v') @@ -172,6 +168,10 @@ jobs: with: version: 0.16.0 + - uses: emscripten-core/setup-emsdk@v15 + with: + version: 5.0.3 + - name: Install lipo (Linux) run: | curl -sL https://github.com/konoui/lipo/releases/download/v0.9.4/lipo_linux_amd64 \ @@ -202,9 +202,21 @@ jobs: fetch raylib-6.0_win64_mingw-w64.zip raylib-6.0_win64_mingw-w64 zip RAYLIB_DIR_WINDOWS_AMD64 fetch raylib-6.0_win32_mingw-w64.zip raylib-6.0_win32_mingw-w64 zip RAYLIB_DIR_WINDOWS_X86 + - name: Fetch and build raylib v6.0 (webassembly) + run: | + mkdir -p build/raylib-web/ + curl -sL https://github.com/raysan5/raylib/archive/refs/tags/6.0.zip \ + -o build/raylib-6.0.zip + ( cd build; unzip raylib-6.0.zip; mv raylib-6.0 raylib-web ) + make -C build/raylib-web/src PLATFORM=PLATFORM_WEB GRAPHICS=GRAPHICS_API_OPENGL_ES3 RAYLIB_BUILD_MODE=RELEASE + echo "RAYLIB_WEB_DIR=$PWD/build/raylib-web/src" >> "$GITHUB_ENV" + - name: Build distribution run: ./scripts/build-dist.sh + - name: Build distribution for web + run: ./scripts/build-dist-web.sh + - name: Generate bundle run: make bundle @@ -214,6 +226,8 @@ jobs: mv dist/sdk "dist/PurismCore-${tag}" cp dist/PurismCoreBundle.h "dist/PurismCoreBundle-${tag}.h" cd dist && zip -r "PurismCore-${tag}.zip" "PurismCore-${tag}" + mv dist/sdk-web "dist/PurismCore-${tag}-Web" + cd dist && zip -r "PurismCore-${tag}-Web.zip" "PurismCore-${tag}-Web" - name: Create release uses: softprops/action-gh-release@v2 diff --git a/.gitignore b/.gitignore index b247bee..ad6e167 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ testdata/* .DS_Store Thumbs.db /shot.png + +# Non-vendored dependencies +third_party/local/ diff --git a/Makefile b/Makefile index c51c703..3f6737d 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ LIB_A = build/libPurismCore$(ABI_SUFFIX)$(LIBSUF) LIB_SHARED = build/$(DLLPRE)PurismCore$(ABI_SUFFIX)$(DLLSUF) ifeq ($(OS),wasm) -all: dist/Live2DCubismCore$(ABI_SUFFIX).js +all: web-lib else all: static-lib shared-lib endif @@ -121,8 +121,8 @@ else endif VIEWER_SRC = src/samples/viewer/io.c src/samples/viewer/blend.c \ - src/samples/viewer/graphics.c src/samples/viewer/render.c \ - src/samples/viewer/panel.c src/samples/viewer/viewer.c + src/samples/viewer/graphics.c src/samples/viewer/render.c \ + src/samples/viewer/panel.c src/samples/viewer/viewer.c viewer: build/viewer$(ABI_SUFFIX)$(EXESUF) build/viewer$(ABI_SUFFIX)$(EXESUF): $(VIEWER_SRC) src/samples/viewer/viewer.h \ @@ -132,13 +132,13 @@ build/viewer$(ABI_SUFFIX)$(EXESUF): $(VIEWER_SRC) src/samples/viewer/viewer.h \ $(VIEWER_SRC) -o $@ $(LIB_A) $(RAYLIB_LIBS) EMCC ?= emcc -RAYLIB_WEB_DIR ?= $(HOME)/raylib/src +RAYLIB_WEB_DIR ?= third_party/local/raylib/src RAYLIB_WEB_LIB ?= $(RAYLIB_WEB_DIR)/libraylib.web.a -viewer-web: dist/viewer.html -dist/viewer.html: $(VIEWER_SRC) src/samples/viewer/viewer.h \ +viewer-web: build/viewer.html +build/viewer.html: $(VIEWER_SRC) src/samples/viewer/viewer.h \ src/samples/viewer/shell.html $(VIEWER_EMBEDS) \ - src/samples/vendor/raygui.h $(SRC) $(HDR) | dist + src/samples/vendor/raygui.h $(SRC) $(HDR) | build $(EMCC) -O2 -std=gnu11 $(ABI_CPPFLAGS) \ -I./include -I./src -I./src/samples/viewer -Ibuild -I$(RAYLIB_WEB_DIR) \ -Wall -Wno-unused-function \ @@ -153,8 +153,9 @@ WASM_SRC = $(SRC) src/core_js.c WASM_RT_METHODS := ccall,cwrap,addFunction,removeFunction,UTF8ToString WASM_RT_METHODS := $(WASM_RT_METHODS),HEAP8,HEAPU8,HEAPU16,HEAP32,HEAPU32,HEAPF32 -dist/Live2DCubismCore$(ABI_SUFFIX).js: $(WASM_SRC) src/core_js.js src/core_js_tail.js \ - scripts/assemble-core-js.sh $(HDR) | dist build/wasm +web-lib: build/purismcore$(ABI_SUFFIX).js +build/purismcore$(ABI_SUFFIX).js: $(WASM_SRC) src/core_js.js src/core_js_tail.js \ + scripts/assemble-core-js.sh $(HDR) | build build/wasm $(EMCC) $(ABI_CPPFLAGS) -O3 -DPSM_GIT_HASH='"$(GIT_HASH)"' \ -I./include -I./src $(WASM_SRC) \ -o build/wasm/em-module$(ABI_SUFFIX).js \ diff --git a/cmake/zig-toolchain.cmake b/cmake/zig-toolchain.cmake index 6f48f10..f84c4c8 100644 --- a/cmake/zig-toolchain.cmake +++ b/cmake/zig-toolchain.cmake @@ -33,7 +33,7 @@ execute_process(COMMAND chmod +x set(CMAKE_C_COMPILER "${_zig_tool_dir}/zig-cc") set(CMAKE_CXX_COMPILER "${_zig_tool_dir}/zig-cxx") set(CMAKE_AR "${_zig_tool_dir}/zig-ar") -set(CMAKE_RANLIB "${_zig_tool_dir}/zig-ranlib") +set(CMAKE_RANLIB "${_zig_tool_dir}/zig-ranlib") # Windows resource compiler. zig ships `zig rc` (LLVM-rc). file(WRITE "${_zig_tool_dir}/zig-rc" diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 7c4f1a7..ee73a2e 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -181,6 +181,12 @@ The raylib model viewer (`src/samples/viewer/`) builds three ways: - **Web:** `make viewer-web` (Emscripten; needs a WebGL2/GLES3 raylib build). The shell page accepts `?model=`. +> [!IMPORTANT] +> The model viewer requires WebGL 2, and the prebuilt Raylib 6.0 for WASM +> only supports WebGL 1. You'll need to build specifically for WebGL 2/GLES3: +> +> `cd raylib && make PLATFORM=PLATFORM_WEB GRAPHICS=GRAPHICS_API_OPENGL_ES3 RAYLIB_BUILD_MODE=RELEASE` + ## Web (Emscripten/WASM) The Emscripten build is the most compatible way of using Purism Core in diff --git a/docs/SDKINFO-WEB.txt b/docs/SDKINFO-WEB.txt new file mode 100644 index 0000000..f0d0dcd --- /dev/null +++ b/docs/SDKINFO-WEB.txt @@ -0,0 +1,11 @@ +From scripts/build-dist-web.sh: + +# Output layout: +# +# dist/sdk-web/ +# Core/purismcore.js +# Samples/Viewer/viewer.html + viewer.js + viewer.wasm +# +# v5 compat builds go under Core-v5/ (no viewer). + +Documentation is included as well (*.md and *.txt files). diff --git a/docs/SDKINFO.txt b/docs/SDKINFO.txt index fc44e64..4ce0eb0 100644 --- a/docs/SDKINFO.txt +++ b/docs/SDKINFO.txt @@ -25,7 +25,7 @@ From scripts/build-dist.sh: # bin/macos/x86_64/viewer + libraylib.dylib (rpath @loader_path) # bin/macos/arm64/viewer + libraylib.dylib # bin/macos/universal/Viewer.app/ (lipo'd; Contents/{Info.plist,MacOS/}) -# bin/windows/x86_64/viewer.exe (static raylib -- self-contained) +# bin/windows/x86_64/viewer.exe (static raylib; self-contained) # bin/windows/x86/viewer.exe # bundle/PurismCoreBundle.h # diff --git a/scripts/build-dist-web.sh b/scripts/build-dist-web.sh new file mode 100755 index 0000000..cf5d52c --- /dev/null +++ b/scripts/build-dist-web.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Cross-compile Purism Core SDK distribution using Emscripten +# +# Copyright (c) 2026 Sakura Motion Project +# SPDX-License-Identifier: MIT + +# Output layout: +# +# dist/sdk-web/ +# Core/purismcore.js +# Samples/Viewer/viewer.html + viewer.js + viewer.wasm +# +# v5 compat builds go under Core-v5/ (no viewer). +# +# Usage: +# ./scripts/build-dist-web.sh +# +# Viewer (bin/): opt-in via RAYLIB_WEB_DIR env var. + +set -e +cd "$(dirname "$0")/.." + +DIST="${DISTDIR:-dist/sdk-web}" + +rm -rf "$DIST" +mkdir -p "$DIST" + +mkdir -p "$DIST/Core" "$DIST/Core-v5" "$DIST/Samples/Viewer" + +make wasm-all +cp build/purismcore.js "$DIST/Core/" +cp build/purismcore-v5.js "$DIST/Core-v5/" + +if [ ! -z "$RAYLIB_WEB_DIR" ]; then + make viewer-web + cp build/viewer.html build/viewer.js build/viewer.wasm "$DIST/Samples/Viewer/" +fi + +# Documentation +echo "=== Docs ===" +for f in LICENSE README.md docs/*.md docs/SDKINFO-WEB.txt; do + [ -f "$f" ] && cp "$f" "$DIST/" +done + +rm -rf "$TMP" + +echo "" +echo "=== Done ===" +find "$DIST" -type f -not -path '*/obj/*' | sort | sed 's|^| |' diff --git a/scripts/build-dist.sh b/scripts/build-dist.sh index 64f0125..792f667 100755 --- a/scripts/build-dist.sh +++ b/scripts/build-dist.sh @@ -29,7 +29,7 @@ # bin/macos/x86_64/viewer + libraylib.dylib (rpath @loader_path) # bin/macos/arm64/viewer + libraylib.dylib # bin/macos/universal/Viewer.app/ (lipo'd; Contents/{Info.plist,MacOS/}) -# bin/windows/x86_64/viewer.exe (static raylib -- self-contained) +# bin/windows/x86_64/viewer.exe (static raylib; self-contained) # bin/windows/x86/viewer.exe # bundle/PurismCoreBundle.h # @@ -49,7 +49,6 @@ # RAYLIB_DIR_MACOS -> zig-cross-macos-x86_64 AND zig-cross-macos-arm64 (universal raylib) # RAYLIB_DIR_WINDOWS_AMD64 -> zig-cross-windows-x86_64 # RAYLIB_DIR_WINDOWS_X86 -> zig-cross-windows-x86 -# (zig-cross-windows-arm64 viewer: no raylib v6.0 mingw archive ships for it -- skip.) set -e cd "$(dirname "$0")/.." @@ -317,7 +316,7 @@ mkdir -p "$DIST/bundle" # Documentation echo "=== Docs ===" -for f in LICENSE README.md docs/*.md docs/*.txt; do +for f in LICENSE README.md docs/*.md docs/SDKINFO.txt; do [ -f "$f" ] && cp "$f" "$DIST/" done @@ -326,3 +325,5 @@ rm -rf "$TMP" echo "" echo "=== Done ===" find "$DIST" -type f -not -path '*/obj/*' | sort | sed 's|^| |' +SDKINFO + diff --git a/src/samples/viewer/shell.html b/src/samples/viewer/shell.html index 4238dc9..40594c0 100644 --- a/src/samples/viewer/shell.html +++ b/src/samples/viewer/shell.html @@ -1,6 +1,6 @@