diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4a36a5..a72aa75 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,8 +30,8 @@ jobs: - name: Run all tests run: make test - - name: Run all tests (small CRC table) - run: make clean && make test UR_CRC32_SLICE_BY_8=0 + - name: Run all tests (slice-by-8 CRC) + run: make clean && make test UR_CRC32_SLICE_BY_8=1 esp-idf: name: ESP-IDF ${{ matrix.idf }} build (${{ matrix.target }}) diff --git a/CMakeLists.txt b/CMakeLists.txt index 82a5867..7b3ec62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,7 +61,7 @@ if(ESP_PLATFORM) else() # Plain host build (e.g. added via add_subdirectory from a simulator or # test harness). Uses the bundled SHA-256 so it has no dependencies. - option(UR_CRC32_SLICE_BY_8 "CRC32: use slice-by-8 (faster, +8 KB flash)" ON) + option(UR_CRC32_SLICE_BY_8 "CRC32: use slice-by-8 (faster, +8 KB flash)" OFF) add_library(ur STATIC ${UR_SRCS} "src/sha256/sha256.c") set_target_properties(ur PROPERTIES POSITION_INDEPENDENT_CODE ON) target_include_directories(ur PUBLIC "src") diff --git a/Kconfig b/Kconfig index f405980..cf6f639 100644 --- a/Kconfig +++ b/Kconfig @@ -2,23 +2,27 @@ menu "cUR (Uniform Resources)" config UR_CRC32_SLICE_BY_8 bool "CRC32: use slice-by-8 (faster, +8 KB flash)" - default y + default n help - Use the slice-by-8 CRC32 implementation instead of the small - nibble table. About 2.7x faster on ESP32-P4, at the cost of 8 KB - of flash for a const lookup table. + Use the slice-by-8 CRC32 implementation instead of the default + 64-byte nibble table. About 2.7x faster on ESP32-P4, at the cost + of 8 KB of flash for a const lookup table. UR CRCs run over + individual fragments (typically a few hundred bytes per QR + frame), so the small table is rarely a bottleneck in practice. config UR_XOR_ESP32P4_SIMD bool "Fountain XOR: use ESP32-P4 PIE 128-bit SIMD" depends on IDF_TARGET_ESP32P4 - default n + default y help Use the ESP32-P4 PIE (xesppie) 128-bit vector unit for the - fountain-code XOR reduction. About 16x faster than a byte loop - (and ~4x faster than the portable word-wise path) on aligned - runs; unaligned data transparently falls back to the word-wise - path. When enabled, cUR 16-byte-aligns its buffer allocations - so the vector path engages on them. ESP32-P4 only. + fountain-code XOR reduction. About 4x faster than the portable + word-wise path on aligned runs; unaligned data transparently + falls back to the word-wise path. When enabled, cUR + 16-byte-aligns its buffer allocations so the vector path + engages on them. Only visible on ESP32-P4; enabled by default + there since it costs no RAM and little flash — disable it to + keep the library free of target-specific code paths. config UR_ALLOC_PSRAM bool "Allocate working buffers in PSRAM" diff --git a/Makefile b/Makefile index 0d1d496..5baff27 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ LDFLAGS = INCLUDES = -Isrc SRCDIR = src OBJDIR = src/obj -UR_CRC32_SLICE_BY_8 ?= 1 +UR_CRC32_SLICE_BY_8 ?= 0 # DEBUG=1 switches to -O0 with AddressSanitizer + UndefinedBehaviorSanitizer. # Requires a full rebuild when toggling (sanitized and non-sanitized objects @@ -22,7 +22,7 @@ else CFLAGS += -O2 endif -# Defaults to the fast CRC path; set UR_CRC32_SLICE_BY_8=0 for the small table. +# Defaults to the small CRC table; set UR_CRC32_SLICE_BY_8=1 for slice-by-8. ifeq ($(UR_CRC32_SLICE_BY_8),1) CFLAGS += -DUR_CRC32_SLICE_BY_8 endif diff --git a/README.md b/README.md index 34fa6d4..dfd2653 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,13 @@ A lightweight C implementation of the [Blockchain Commons Uniform Resources (UR) specification — a method for encoding structured binary data optimized for transport via QR codes. -Targets PC, ESP-IDF (e.g. ESP32, ESP32-P4), and the Kendryte K210 -(Krux / MaixPy) from a single source tree. SHA-256 backend is selected -at compile time to use the platform's hardware when available. +Targets PC and ESP-IDF (e.g. ESP32, ESP32-P4) from a single source +tree. SHA-256 backend is selected at compile time to use the +platform's hardware when available. + +The implementation favors being small and easy to review. Optional +performance paths exist, but every default is the simplest, smallest +variant; see [Build options](#build-options). ## Supported UR types @@ -107,6 +111,21 @@ expected during scanning). Type-specific helpers (`bytes_from_cbor`, `psbt_from_cbor`, `output_from_descriptor_string`, etc.) live in `src/types/*.h`. +## Build options + +All options default to the smallest, simplest implementation; the +performance variants are opt-in (or, for the P4 vector path, gated by +the CPU with an opt-out). Each option is a single flag with the same +name everywhere it appears — Makefile variable, CMake option, and +ESP-IDF Kconfig. + +| Option | Default | Effect | +|--------|---------|--------| +| `UR_CRC32_SLICE_BY_8` | off | Slice-by-8 CRC32: ~2.7x faster, +8 KB flash for a const table. The default 64-byte nibble table is rarely a bottleneck (CRCs run per fragment, a few hundred bytes each). | +| `UR_XOR_ESP32P4_SIMD` | on (ESP32-P4 only) | PIE 128-bit vector XOR for fountain-code mixing, with transparent word-wise fallback on unaligned data. Only exists on ESP32-P4; Kconfig opt-out. | +| `UR_ALLOC_PSRAM` | on (ESP targets) | Route the library's buffers to PSRAM with internal-RAM fallback, keeping fountain-decoder churn out of scarce internal heap. Kconfig opt-out; no-op elsewhere. | +| `UR_ENVELOPE_ONLY` | off | CMake: build only the UR transport layer (bytewords, fountain, multi-part assembly), excluding the `src/types/` payload codecs, for integrators that do their own CBOR. | + ## Platform integration The SHA-256 dependency is the only platform-sensitive piece. A @@ -116,7 +135,7 @@ compile-time flag picks the backend; see `src/sha256/sha256_compat.h`. |------------------|-------------------------|-------------------------------| | PC (default) | *(none)* | bundled `src/sha256/sha256.c` | | ESP-IDF | `UR_USE_MBEDTLS_SHA256` | mbedTLS PSA (HW-accelerated) | -| Kendryte K210 | `UR_USE_K210_SHA256` | `sha256_hard_calculate` (HW) | +| K210 (legacy) | `UR_USE_K210_SHA256` | `sha256_hard_calculate` (HW) | ### ESP-IDF component @@ -132,32 +151,19 @@ idf_component_register(SRCS "app_main.c" INCLUDE_DIRS "." REQUIRES cUR) Then include headers normally: `#include "ur_encoder.h"`. -### Kendryte K210 (Krux / MaixPy) - -MaixPy's port does **not** wire components via USERMOD/`py.mk`; it -uses a hardcoded `register_component()` + `append_srcs_dir()` block in -`components/micropython/CMakeLists.txt`. The `-DUR_USE_K210_SHA256` -flag must be added inside that block, or the link fails with -undefined references to `ur_bundled_sha256_*`: - -```cmake -# Inside the CONFIG_MAIXPY_BC_UR_ENABLE block: -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUR_USE_K210_SHA256") -``` +### Legacy K210 port -This mirrors how `CONFIG_MAIXPY_SECP256K1_ENABLE` sets its own flags. -Without this flag the bundled SHA implementation is selected, but its -symbol names now live under the `ur_bundled_sha256_*` namespace -precisely to surface the missing flag as a link error rather than a -silent runtime hardfault. +Maintained for compatibility only. The build wiring lives in the +consuming project, which must pass `-DUR_USE_K210_SHA256`; without it +the link fails with undefined references to `ur_bundled_sha256_*` +(deliberate — a link error instead of a silent runtime hardfault). ### MicroPython wrapper (`uUR.c`) `uUR.c` at the repo root wraps the C library as a MicroPython module -exposing `URDecoder`, `UREncoder`, `UR`, and `FountainEncoder`. Krux -consumes this file as-is (byte-identical). For vanilla MicroPython -USERMOD builds the repo-root `micropython.mk` provides the wiring; -MaixPy K210 ignores it and uses the CMakeLists route above. +exposing `URDecoder`, `UREncoder`, `UR`, and `FountainEncoder`. For +vanilla MicroPython USERMOD builds the repo-root `micropython.mk` +provides the wiring; legacy ports wire it in their own build instead. ## Repository layout diff --git a/src/utils.c b/src/utils.c index f4f0eb4..c67af15 100644 --- a/src/utils.c +++ b/src/utils.c @@ -16,11 +16,56 @@ #include #include +// Word type for the portable XOR loops: pointer-width chunks, so 32-bit +// targets don't emulate 64-bit arithmetic with register pairs. +#if UINTPTR_MAX > 0xFFFFFFFFu +typedef uint64_t ur_xor_word_t; +#else +typedef uint32_t ur_xor_word_t; +#endif + +// Portable word-wise XOR loops (byte tail). memcpy is the aliasing- and +// alignment-safe word access and compiles to plain loads/stores wherever the +// target allows them; pointer casts here would be undefined behavior under +// strict aliasing and fault on strict-alignment CPUs. +static void ur_xor_portable_inplace(uint8_t *restrict dst, + const uint8_t *restrict src, size_t n) { + size_t i = 0; + for (; i + sizeof(ur_xor_word_t) <= n; i += sizeof(ur_xor_word_t)) { + ur_xor_word_t a, b; + memcpy(&a, dst + i, sizeof(a)); + memcpy(&b, src + i, sizeof(b)); + a ^= b; + memcpy(dst + i, &a, sizeof(a)); + } + for (; i < n; i++) + dst[i] ^= src[i]; +} + +static void ur_xor_portable(uint8_t *restrict out, const uint8_t *restrict a, + const uint8_t *restrict b, size_t n) { + size_t i = 0; + for (; i + sizeof(ur_xor_word_t) <= n; i += sizeof(ur_xor_word_t)) { + ur_xor_word_t x, y; + memcpy(&x, a + i, sizeof(x)); + memcpy(&y, b + i, sizeof(y)); + x ^= y; + memcpy(out + i, &x, sizeof(x)); + } + for (; i < n; i++) + out[i] = a[i] ^ b[i]; +} + #if defined(UR_XOR_ESP32P4_SIMD) #if !defined(CONFIG_IDF_TARGET_ESP32P4) #error "UR_XOR_ESP32P4_SIMD requires ESP32-P4" #endif +// ESP32-P4 PIE (xesppie) implementations: scalar head bytes until the +// destination is 16-byte aligned, 128-bit vector body when the operands are +// co-aligned, portable word-wise loop for the tail (and as the fallback when +// co-alignment is impossible). + // Pointers are pinned because esp.vld/esp.vst require low address registers. static inline void ur_xor128_inplace(uint8_t *dst, const uint8_t *src, size_t chunks) { @@ -53,15 +98,10 @@ static inline void ur_xor128_out(uint8_t *out, const uint8_t *a, : : "memory"); } -#endif -// In-place XOR: dst[i] ^= src[i] for n bytes. Word-wise (8 bytes/iter) with a -// scalar tail -- roughly 4x faster than a byte loop. On ESP32-P4 with -// UR_XOR_ESP32P4_SIMD, 16-byte-aligned runs use the PIE 128-bit path (~16x). -// Buffers must not overlap. +// In-place XOR: dst[i] ^= src[i] for n bytes. Buffers must not overlap. void ur_xor_inplace(uint8_t *restrict dst, const uint8_t *restrict src, size_t n) { -#if defined(UR_XOR_ESP32P4_SIMD) size_t head = (size_t)((0u - (uintptr_t)dst) & 15u); if (head > n) head = n; @@ -78,23 +118,12 @@ void ur_xor_inplace(uint8_t *restrict dst, const uint8_t *restrict src, src += done; n -= done; } -#endif - size_t i = 0; - for (; i + 8 <= n; i += 8) { - uint64_t a, b; - memcpy(&a, dst + i, 8); - memcpy(&b, src + i, 8); - a ^= b; - memcpy(dst + i, &a, 8); - } - for (; i < n; i++) - dst[i] ^= src[i]; + ur_xor_portable_inplace(dst, src, n); } // Out-of-place XOR: out[i] = a[i] ^ b[i] for n bytes. Buffers must not overlap. void ur_xor(uint8_t *restrict out, const uint8_t *restrict a, const uint8_t *restrict b, size_t n) { -#if defined(UR_XOR_ESP32P4_SIMD) size_t head = (size_t)((0u - (uintptr_t)out) & 15u); if (head > n) head = n; @@ -113,19 +142,25 @@ void ur_xor(uint8_t *restrict out, const uint8_t *restrict a, b += done; n -= done; } -#endif - size_t i = 0; - for (; i + 8 <= n; i += 8) { - uint64_t x, y; - memcpy(&x, a + i, 8); - memcpy(&y, b + i, 8); - x ^= y; - memcpy(out + i, &x, 8); - } - for (; i < n; i++) - out[i] = a[i] ^ b[i]; + ur_xor_portable(out, a, b, n); +} + +#else // portable builds: the word-wise loops are the whole implementation + +// In-place XOR: dst[i] ^= src[i] for n bytes. Buffers must not overlap. +void ur_xor_inplace(uint8_t *restrict dst, const uint8_t *restrict src, + size_t n) { + ur_xor_portable_inplace(dst, src, n); } +// Out-of-place XOR: out[i] = a[i] ^ b[i] for n bytes. Buffers must not overlap. +void ur_xor(uint8_t *restrict out, const uint8_t *restrict a, + const uint8_t *restrict b, size_t n) { + ur_xor_portable(out, a, b, n); +} + +#endif + /* On ESP32 targets, route allocations to PSRAM (falling back to internal RAM * when absent). ESP-IDF's malloc() keeps small allocations in internal RAM, * where the fountain decoder's churn of variably-sized part buffers fragments