Skip to content

Refactor SIMD into Facade/Backend layers and consolidate CRC engine#100

Merged
Xor-el merged 10 commits into
masterfrom
refactor/simd-facade-backend
Jul 13, 2026
Merged

Refactor SIMD into Facade/Backend layers and consolidate CRC engine#100
Xor-el merged 10 commits into
masterfrom
refactor/simd-facade-backend

Conversation

@Xor-el

@Xor-el Xor-el commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces the per-algorithm *Dispatch.pas units with a Facade/Backend SIMD architecture under HashLib/src/Simd/, consolidates CRC into a single generic engine with fold-by-8 carry-less kernels, expands x86 SIMD tier coverage (especially i386), and fixes several XXH3/CRC correctness issues found by new tests.

Core hash units now call Hlp<Hash>Simd.Select(...) once at init and hold a function pointer — no TCpuFeatures or HASHLIB_*_ASM knowledge in the cores, and zero per-block dispatch cost.

SIMD architecture

Introduces HashLib/src/Simd/:

Layer Role
Facade/Hlp<Hash>Simd.pas Compile-time arch selection; delegates to the active backend
Backend/Hlp<Hash>X86Backend.pas x86 tiers (SSE2, SSSE3, AVX/AVX2, SHA-NI, PCLMUL/VPCLMUL) + Select
Backend/Hlp<Hash>ArmBackend.pas AArch64 tiers (NEON, Crypto Extensions, PMULL) + Select

Applies to all 12 SIMD primitives: SHA-1, SHA-256, SHA-512, SHA-3, Blake2B, Blake2S, Blake3, XXH3, Adler32, Argon2, Scrypt, and CRC fold.

Removed: Hlp*Dispatch.pas for every primitive above.

CRC consolidation

Engine reduced from 6 units to 3 (~1,800 lines removed):

  • Deleted: HlpCRCStandard, HlpCRC32Fast, HlpCRCFoldConstants, HlpCRCDispatch
  • Unified: single generic HlpCRC engine + HlpCRCCore (GF(2) fold math merged in)
  • 108 CRC standards in one const table (params + aliases); factory named methods route through the generic engine
  • Widths 3–64 on one path; bit-serial engine removed; MSB-first widths < 8 run left-aligned at width 8

Bug fixes:

  • TransformFinal no longer truncates widths 17–23 to 16 bits (affected CRC-17/CAN-FD)
  • Table cache guarded by critical section (was unsynchronized TDictionary)
  • Fold state handoff uses TBinaryPrimitives.LoadUInt64/StoreUInt64

fold-by-8 kernels (128 bytes/iteration for inputs ≥ 128 B):

  • x86_64 PCLMUL/VPCLMUL reflected + forward: 8-accumulator upper path
  • aarch64 PMULL: same structure, opcodes verified against aarch64-linux-gnu-as
  • New i386 PCLMUL/VPCLMUL carry-less multiply kernels

Tests: ChunkedData lengthened to 299 bytes; new fold-boundary sweep (22 lengths × 108 standards) against byte-table reference.

x86 SIMD expansion

  • New SSSE3 i386 kernels: Blake2B, Blake2S, Blake3 (compress + hash4), SHA-1, SHA-256, SHA-512, Argon2
  • New AVX/AVX2 i386 kernels: Adler32, Argon2, Blake2B, Blake2S, Blake3, Scrypt, XXH3 (accumulate/scramble/initSecret)
  • Expanded x86_64 tier coverage across existing backends

Improvements

  • Kernel naming: bodies named for their true minimum ISA (Gpr, Avx, not dispatch-slot labels); Keccak Sse2Gpr on x86_64 where appropriate
  • AArch64 GPR fallbacks: SHA-1/256/512 compress and Keccak-f[1600] permute/absorb gain baseline GPR kernels when Crypto Extensions are absent (was scalar Pascal)
  • NEON rotate optimization: 2-op insert forms across Blake2/Blake3/Argon2 kernels (e.g. Blake3 hash4 −17% instruction count)
  • XXH3 bug fixes exposed by new long-input known-answer tests:
    • aarch64 NEON Acc512/Scramble: stray bit in vector encoders (wrong source registers)
    • i386 InitSecret: dword-swapped seed materialization; seed now passed by pointer (ASeedPtr)
    • Big-endian custom secret: scalar init now uses WriteUInt64LittleEndian

Tooling

  • Maintenance scripts moved to scripts/maintenance/
  • New check-file-format.ps1 (CRLF, strict UTF-8, no BOM); chained in pre-commit
  • All 107 SIMD kernel .inc headers normalized to a canonical shape

Breaking changes

  • Internal units removed: all Hlp*Dispatch.pas, HlpCRC32Fast, HlpCRCStandard, HlpCRCFoldConstants — package/project references updated
  • Public API: factory CRC entry points preserved; behavior fixes for widths 17–23 and big-endian XXH3 seeded hashing

Xor-el added 10 commits July 11, 2026 23:22
Introduce src/Simd/ with a Facade/ subfolder (arch-agnostic selection) and a
Backend/ subfolder (per-hash, per-arch SIMD kernels + runtime tier selection).
Each hash's core unit keeps the scalar implementation and calls the SIMD path
through its facade, so the cores carry no TCpuFeatures / HASHLIB_*_ASM
knowledge.

For all 12 primitives (SHA-1, SHA-256, SHA-512, SHA-3, Blake2B, Blake2S,
Blake3, XXH3, Adler32, Argon2, Scrypt, CRC):

  - Hlp<Hash>Simd (Facade)      picks the backend at compile time
  - Hlp<Hash>X86Backend         SSE2/SSSE3/AVX2/SHA-NI/PCLMUL kernels + Select
  - Hlp<Hash>ArmBackend         NEON/SHA/PMULL kernels + Select

Init-time slot selection is preserved: each core's initialization sets its
function pointer once via the facade's Select, so TransformBlock pays zero
per-call dispatch cost.
Engine consolidation (6 units -> 3, ~1,800 lines removed):
- Delete HlpCRCStandard (wrapper TCRC16/32/64 + dead duplicate PKZIP/
  Castagnoli classes), HlpCRC32Fast (whole UInt32 fast-path ecosystem
  incl. both Reflected32 SSE2 kernels), and HlpCRCFoldConstants (GF(2)
  math merged into HlpCRCCore). Factory named methods now route through
  the generic engine; on carry-less-multiply CPUs they already used the
  same kernel.
- All 108 standards live in a single const table (params + aliases);
  replaces the 460-line case and per-class parameter duplicates.
- Unified width 3..64 path: bit-serial engine deleted; MSB-first CRCs
  narrower than 8 bits run left-aligned at width 8 and un-shift at
  finalization; reflected CRCs are width-agnostic as-is.

Fixes:
- TransformFinal truncated widths 17-23 to 16 bits (live for
  CRC-17/CAN-FD); output is now sized by the constructor's HashSize
  ranges and the width-21 special case is gone.
- Table cache was a class-level TDictionary mutated without a lock;
  now guarded by a critical section.
- Fold state handoff uses TBinaryPrimitives.LoadUInt64/StoreUInt64 and
  a single UInt64 (kernels never touched the second qword).
- Forward SSE2 kernels (x86_64 + i386) hoist width/mask/byte-count out
  of the per-16-byte-block loop.

fold-by-8 kernels (128 bytes/iter for inputs >= 128B):
- PCLMUL x86_64 reflected + forward gain an 8-accumulator upper path
  (xmm0-3 + xmm8-11) using the runtime-generated Fold_8x128 stride-1024
  constants, reducing 8->4->1 into the untouched Barrett tail. New
  REX-prefixed pclmulqdq/pshufb encodings verified against GNU as.
- PMULL aarch64 kernels regenerated with the same structure (v19-v22);
  all 32 new opcodes cross-checked against aarch64-linux-gnu-as.

Tests: ChunkedData lengthened to 299 bytes so chunked tests cross-
validate table/fold4/fold8; new fold-boundary sweep (22 lengths x 108
standards) against the byte-table reference.
* Rename kernels to their true minimum ISA and add aarch64 pure-GPR fallbacks

Naming pass: a kernel's name now states the ISA its body actually
requires, not the dispatch slot it serves - pure-GPR bodies are named
Gpr, VEX-128-only bodies Avx.

- KeccakF1600Sse2(+Absorb)_x86_64 -> KeccakF1600Gpr(+Absorb)_x86_64
- CRCFoldForwardSse2_{i386,x86_64} -> CRCFoldForwardGpr_*
- SHA1/SHA256CompressAvx2_i386 -> *CompressAvx_i386
- Blake2SCompressAvx2_{i386,x86_64} -> Blake2SCompressAvx_*
- Blake3CompressAvx2_x86_64 -> Blake3CompressAvx_x86_64
- ScryptSalsa8Avx2_{i386,x86_64} -> ScryptSalsa8Avx_*
- backend procs/wraps follow; banner comments list the dispatch tiers,
  kernel headers no longer mention them
- Reflected-CRC keeps its Sse2 name (SSE2 genuinely required for the
  128-bit loads and the CRC state shuttle) - headers now say so

New aarch64 fallbacks for the Crypto-Extensions-only hashes: SHA-1/
SHA-256/SHA-512 compress and Keccak-f[1600] permute + multi-block absorb
gain baseline-AArch64 GPR kernels dispatched at the NEON slot when
FEAT_SHA1/SHA256/SHA512/SHA3 are absent - previously scalar Pascal.
Bodies are the OpenSSL CRYPTOGAMS plain paths, .long-encoded and
verified byte-exact against the GNU-as reference objects.

- SHA-256/SHA-512 use sentinel-terminated K tables (K256_Gpr/K512_Gpr
  with a zero terminator - the schedule loop ends on a zero K word);
  SHA-1 needs no table (movz/movk round constants)
- Keccak's original round loop ends on a 256-aligned iotas table, which
  a Pascal const cannot guarantee; the termination is patched to an
  end-pointer compare so both kernels reuse the plain RC table

* force neon mode

* Cut NEON rotates to 2-op insert forms across the Blake/Argon2 kernels

Every 32/64-bit lane rotate in the aarch64 NEON kernels was 3-op
(ushr+shl+orr; Blake3's rot12/rot7 were even 4-op through a copy).
The 2-op insert forms (ushr+sli, shl+sri, add+sri for ror63) land the
result in a temp, so the generators now thread a register map: the
rotated row renames into the temp instead of copying back, and rows
normalize to their home registers only before writebacks and loop
back-edges. Blake3 hash4's G is restructured 26 -> 21 ops with zero
move instructions (b ping-pongs v_b -> t13 -> v_b; d lands in the
register whose message word is dead).

Instruction counts: Blake2S 632->573, Blake2B 1377->1233, Blake3
compress 714->645, Blake3 hash4 1653->1373 (-17%, the multi-chunk
throughput path), Argon2 -12 ops per round slice (x16 executed per
FillBlock). tbl-rot8 deliberately not used: official BLAKE3 NEON uses
shl+sri and there is no mask setup or extra register cost.

* Fix XXH3 SIMD kernel bugs exposed by new long-input known-answer tests

The XXH3 test vectors all stayed under 240 bytes, so the SIMD
accumulate/scramble/initSecret paths were only ever checked for
self-consistency - a wrong kernel agreed with itself and passed. Four
known-answer tests (values cross-checked against the official xxHash
implementation) now cover accumulate512 (299 B), scrambleAcc (2 KiB)
and the seeded long path (299 B with MaxUInt64 and with an asymmetric
key) on every target.

They surfaced three shipped bugs:

- aarch64 NEON Acc512/Scramble: five vector encoders carried a stray
  bit in the source-register field, so every emitted uzp1/uzp2/umlal/
  umlal2/mul executed with source register 2|m. Acc512 mixed the raw
  secret into accumulator lanes 2-3/6-7; Scramble multiplied by
  uninitialized registers. The mnemonic comments were correct - the
  encoded words were not.

- i386 InitSecret (SSE2 + AVX2): the 64-bit seed materialization
  pushed lo before hi, leaving the high dword at [esp], so movq loaded
  a dword-swapped qword (-seed became 2^32 instead of 1).

- InitSecret's seed parameter itself: a by-value UInt64 third
  parameter never rides in ecx under the i386 register convention, so
  the shared Proc3 prologue handed the kernels an undefined register -
  one compiler worked only by caller-codegen accident. The seed now
  travels by pointer (ASeedPtr: PUInt64), the same pattern as the
  Blake2 counter/flags pointers, and every kernel dereferences it on
  entry ([edi] on i386, mov r8,[r8] on x86_64, ldr x2,[x2] on NEON).

* Fix big-endian custom secret in scalar XXH3 and make the acc domain explicit

XXH3_InitSecret_Scalar wrote the derived secret with native PUInt64
stores while every consumer reads the secret as little-endian wire
bytes - on big-endian targets the custom secret came out byte-swapped,
so seeded hashing of inputs over 240 bytes was wrong (caught by the
seeded long-input known-answer tests on the big-endian CI; the
unseeded ones passed since that path copies the default secret
byte-for-byte). The custom-secret stores now go through
WriteUInt64LittleEndian.

XXH3_Accumulate512_Scalar / XXH3_ScrambleAcc_Scalar were already
endian-correct (the accumulator is native value data, read and written
natively everywhere including mergeAccs), but their raw PUInt64
pointer derefs looked identical to the buggy pattern. They now use
TBinaryPrimitives.LoadUInt64/StoreUInt64 (native-order, alignment-safe)
so wire accesses (Read/WriteUInt64LittleEndian) and value accesses
(Load/StoreUInt64) are distinguishable at a glance.

* Restructure X86 backends to single-wrap kernel declarations

Each SIMD kernel wrapper was declared twice - once per architecture
section - so signature changes had to be made in two places and could
drift. Following the CryptoLib4Pascal backend style, every kernel now
has ONE declaration with per-arch {$IFDEF} selecting the prologue and
body includes (x86_64 first), and pure-Pascal wraps that were
identical across architectures are shared.

Genuinely per-arch code keeps grouped arch sections: SHA3 (different
kernel sets per arch), the SHA-1/SHA-256 ShaNi pairs (i386 is a
4-param proc with the doubled-K table, x86_64 a 5-param proc with the
byte-swap mask) and the i386 Avx / x86_64 Avx2 wrap pairs.
* enforce file format at commit time

A new scripts/maintenance/check-file-format.ps1 verifies CRLF, strict
UTF-8 and no BOM across the source/text tree.

* Normalize source file format and unify the SIMD kernel headers

Phase A - format: 74 files converted to CRLF (10 x86_64 kernel
includes, 29 library units, one test unit, the scripts tree). The repo
encoding rule is valid UTF-8 without BOM; a new
scripts/maintenance/check-file-format.ps1 enforces CRLF + strict UTF-8
+ no BOM and the pre-commit hook chains it after the duplicate-GUID
check.

Phase B - headers: all 107 kernel .inc headers now follow one
canonical shape: title, strategy notes, "ABI (after
HlpSimdProcNBegin_<arch>.inc): ..." with layout sub-lines, register
map, frame/saves, the encoding one-liner, and "Reference:" always
last. Typographic punctuation in the Simd tree normalized to ASCII
(BOM-less sources render non-ASCII as mojibake in the IDEs). The
Common/CpuFeatures prologue includes keep their structure - per-
compiler adaptation is their purpose - with punctuation normalized.

Every aarch64 header was changed through its generator constant, so
regeneration stays byte-idempotent (asm-check passes for all 15
generated kernels). Two stale headers were corrected: Blake2B NEON now
documents its d8-d15 save/restore via the shared include, and Blake3
hash4 NEON's duplicated G-temps map lines are merged.

* Silence FPC's below-esp warning in the SHA-256 i386 kernels

FPC flags "lea esp, [esp - $60]" with "Use of -offset(%esp), access
may cause a crash or value may be lost" in the SSE2/SSSE3/AVX SHA-256
i386 kernels. The warning is a false positive - lea is address
arithmetic, not a memory access; the line is the CRYPTOGAMS
flag-neutral spelling of a stack allocation and nothing ever
dereferences below esp. Spelled it "sub esp, $60" instead: no flag
consumer lives between the allocation and the next flag-setting
instruction at any of the three sites, so the forms are equivalent -
and the recurring build noise is gone.
Blake2B_Compress_Scalar / Blake2S_Compress_Scalar accessed the state,
IV, counter-flags and message words through raw PUInt64/PUInt32
pointer derefs. All four buffers are native value data (the caller
converts the message block to native words before the call), so the
accesses now go through TBinaryPrimitives.LoadUInt64/StoreUInt64 and
LoadUInt32/StoreUInt32 - behavior-identical, alignment-safe, and the
wire-vs-native domain is visible in the API used, consistent with the
XXH3 scalar cleanup.
@Xor-el Xor-el merged commit 3071678 into master Jul 13, 2026
24 checks passed
@Xor-el Xor-el deleted the refactor/simd-facade-backend branch July 13, 2026 12:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant