diff --git a/FABRIC.md b/FABRIC.md index 7ac4539..8152693 100644 --- a/FABRIC.md +++ b/FABRIC.md @@ -3757,11 +3757,46 @@ document and committing that amendment as its own item.* > to the same `-27` as `sin(π)`, confirming range reduction across multiple turns. Both > hosted and kernel (amd64) builds clean. -- [ ] **4.3.3b — Geometry drawing primitive wordset.** `LINE`, `CIRCLE`, `ARC`, `ELLIPSE` in +- [x] **4.3.3b — Geometry drawing primitive wordset.** `LINE`, `CIRCLE`, `ARC`, `ELLIPSE` in `capsules/fabric.4th`, built on 4.3.3's `PLOT`/`CART-PLOT` and 4.3.3a's `Q.SIN`/`Q.COS`. Raised 2026-08-07. Q48.16 throughout; resolution-agnostic (48 integer bits comfortably covers 1080p and well beyond — no hardcoded viewport assumptions). *Refs:* §27.3. + > **Done, 2026-08-07.** Blocks 4903–4912. `TO-RASTER` factored out of `CART-PLOT` (same + > behavior, not a change) so `LINE` can project both endpoints once and Bresenham the + > straight line between them in raster space — valid because the cavalier projection is + > linear, so projecting endpoints and interpolating is equivalent to projecting every point + > along the line. `LINE` itself split across three helper words (`LINE-SETUP`, + > `LINE-DONE?`/`LINE-STUCK?`, `LINE-STEP`) — discovered mid-implementation that colon + > definitions **cannot span block boundaries** in this capsule loader (verified with a + > throwaway test capsule: the continuation lands in a `[CAPSULE][DEFER]` path that never + > resolves and errors out), so anything too long for one 16-line/64-char block has to be + > factored into separate, block-local word definitions instead. `CIRCLE`/`ELLIPSE` are + > 36-segment polygon approximations (`LINE` calls between consecutive `Q.SIN`/`Q.COS` + > points); `ARC` is the same at 18 segments over a caller-supplied `[a0, a1]` radian range. + > + > **A fourth real bug, this one serious — found, fixed, verified with the recommended fix + > applied both times.** `CIRCLE`'s first live test rendered only its first quadrant, then + > a follow-up test call hung the VM for several minutes before being killed. Root cause: + > `q48_to_u64()` (`include/q48_16.h` and `include/starkernel/q48_16.h`, backing + > `Q.TO-INT`) did `q >> 16` as an **unsigned logical shift**. For any negative `q48_16_t` — + > inevitable once `Q.SIN`/`Q.COS` leave the first quadrant — this produces a huge garbage + > integer instead of sign-extending. That garbage became a bogus `LINE` target, and + > `LINE-STEP`'s Bresenham loop had no bound, so it churned for a very long time trying to + > converge on a point that was effectively unreachable. Fixed by shifting through a signed + > `int64_t` intermediate (bit-identical output for the non-negative case, which is all the + > inference engine's own caller ever produces). Independently, added `LINE-STUCK?` + > (`LSTEPS` counter vs. `FB-WIDTH + FB-HEIGHT`, the true worst case for any on-screen line) + > as a defense-in-depth cap, so a future bad target degrades to "stops drawing" rather than + > hanging the VM again. + > + > **Verified live on amd64**, fresh boot after both fixes: `-65536 Q.TO-INT .` now prints + > `-1`. `LINE`, `CIRCLE`, `ARC` (semicircle, 0 to π), and `ELLIPSE` all completed without + > hanging or erroring, and a combined screendump shows all four rendering correctly and + > distinctly — full circle, correct upper-half arc, properly proportioned ellipse (wider + > than tall, matching unequal radii), and the earlier diagonal `LINE` test. + > `fb/amd64/geom-test-circle-fixed.png`, `fb/amd64/geom-test-full-wordset.png`. + - [ ] **4.3.4 — Checkpoint: draw a cube.** First real exercise of the 4.3.3/4.3.3a/4.3.3b coordinate/projection/geometry machinery — cube edges use `LINE`. Stop and review here before scoping the next 4.3.x item — not expected to be fast. *Refs:* §27.4. diff --git a/capsules/BLOCK_MAP.md b/capsules/BLOCK_MAP.md index b17dbed..d85a40e 100644 --- a/capsules/BLOCK_MAP.md +++ b/capsules/BLOCK_MAP.md @@ -1,5 +1,5 @@ # Capsule Block Manifest — Auto-generated - + @@ -13,7 +13,7 @@ | `common:msg.4th` | 4055 | `0xa99c5bcd3877f80e` | | `doe-campaign.4th` | 4060, 4061, 4062, 4063, 4064, 4065 | `0x3d4549142d91ec20` | | `doe.4th` | 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107 | `0xb6ecf5374e8ee77c` | -| `fabric.4th` | 4900, 4901, 4902 | `0xb7db7380ec4a42a0` | +| `fabric.4th` | 4900, 4901, 4902, 4903, 4904, 4905, 4906, 4907, 4908, 4909, 4910, 4911, 4912 | `0x355ccc154aac15b3` | | `hermes:init.4th` | 4100, 4101, 4102, 4103, 4104, 4105, 4106, 4107, 4108, 4109, 4114, 4115, 4116, 4117, 4118, 4119, 4120, 4121, 4142, 4143, 4144, 4145, 4146, 4147, 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158, 4159, 4175, 4176 | `0x85c7b311d1e5bf97` | | `init-0.4th` | 2200, 2201 | `0xd0a9550baf786bb3` | | `init-1.4th` | 4406, 4415, 4425, 4435 | `0x63e251adb0a03613` | @@ -225,9 +225,19 @@ | 4842 | `init-l8-transition.4th` | `0xbcc1a81976f0a4c9` | ok | | 4851 | `artemis:init.4th` | `0xc9e92cd18f4c7b49` | ok | | 4852 | `artemis:init.4th` | `0xc9e92cd18f4c7b49` | ok | -| 4900 | `fabric.4th` | `0xb7db7380ec4a42a0` | ok | -| 4901 | `fabric.4th` | `0xb7db7380ec4a42a0` | ok | -| 4902 | `fabric.4th` | `0xb7db7380ec4a42a0` | ok | +| 4900 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4901 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4902 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4903 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4904 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4905 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4906 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4907 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4908 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4909 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4910 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4911 | `fabric.4th` | `0x355ccc154aac15b3` | ok | +| 4912 | `fabric.4th` | `0x355ccc154aac15b3` | ok | ## Conflicts diff --git a/capsules/fabric.4th b/capsules/fabric.4th index 8616b80..e6ae8c7 100644 --- a/capsules/fabric.4th +++ b/capsules/fabric.4th @@ -30,3 +30,123 @@ Block 4902 CART-Y R> PLOT ; + +Block 4903 +( TO-RASTER: 3D Cartesian point -> raster (rx ry). Factored ) +( out of CART-PLOT so LINE can reuse it for both endpoints ) +( -- the projection is linear, so projecting endpoints then ) +( drawing 2D is equivalent to projecting every line point. ) +( Item 4.3.3b. CART-PLOT redefined in terms of it (same ) +( behavior, not a change). ) +: TO-RASTER ( x y z -- rx ry ) + PROJECT CART-Y ; +: CART-PLOT ( x y z color -- ) + >R TO-RASTER R> PLOT ; + +Block 4904 +( LINE: 3D Cartesian line segment, Bresenham in raster ) +( space (state in VARIABLEs; LINE-SETUP below). ) +VARIABLE LX VARIABLE LY VARIABLE LX2 VARIABLE LY2 +VARIABLE LDX VARIABLE LDY VARIABLE LSX VARIABLE LSY +VARIABLE LERR VARIABLE LCOLOR VARIABLE LSTEPS +: LINE-SETUP ( x1 y1 z1 x2 y2 z2 color -- ) + LCOLOR ! + TO-RASTER LY2 ! LX2 ! + TO-RASTER LY ! LX ! + LX2 @ LX @ - ABS LDX ! + LY2 @ LY @ - ABS NEGATE LDY ! + LX @ LX2 @ < IF 1 ELSE -1 THEN LSX ! + LY @ LY2 @ < IF 1 ELSE -1 THEN LSY ! + LDX @ LDY @ + LERR ! 0 LSTEPS ! ; + +Block 4905 +( LINE-DONE?: true once current point reached target. ) +: LINE-DONE? ( -- flag ) + LX @ LX2 @ = LY @ LY2 @ = AND ; +( LINE-STUCK?: safety cap, width+height worst case. ) +: LINE-STUCK? ( -- flag ) + LSTEPS @ FB-WIDTH FB-HEIGHT + > ; +( LINE-STEP: one Bresenham step (no plot). ) +: LINE-STEP ( -- ) + LERR @ 2* + DUP LDY @ >= IF LERR @ LDY @ + LERR ! LX @ LSX @ + LX ! THEN + DUP LDX @ <= IF LERR @ LDX @ + LERR ! LY @ LSY @ + LY ! THEN + LSTEPS @ 1+ LSTEPS ! DROP ; + +Block 4906 +( LINE: draws the segment using the helpers above. ) +: LINE ( x1 y1 z1 x2 y2 z2 color -- ) + LINE-SETUP + BEGIN + LX @ LY @ LCOLOR @ PLOT + LINE-DONE? 0= LINE-STUCK? 0= AND + WHILE + LINE-STEP + REPEAT ; + +Block 4907 +( Shared state for CIRCLE/ARC/ELLIPSE. CIRC-PT: point on a ) +( circle of radius CRAD centered (CX,CY) at given angle. ) +VARIABLE CX VARIABLE CY VARIABLE CZ VARIABLE CRAD +VARIABLE CCOLOR VARIABLE PX VARIABLE PY +: CIRC-PT ( ang -- x y ) + DUP Q.SIN CRAD @ Q.FROM-INT Q.* Q.TO-INT CY @ + + SWAP Q.COS CRAD @ Q.FROM-INT Q.* Q.TO-INT CX @ + + SWAP ; + +Block 4908 +( CIRCLE: 36-segment polygon approximation, flat at z=cz. ) +: CIRCLE ( cx cy cz r color -- ) + CCOLOR ! CRAD ! CZ ! CY ! CX ! + 0 CIRC-PT PY ! PX ! + 36 0 DO + PX @ PY @ CZ @ + I 1+ 11438 * CIRC-PT + 2DUP PY ! PX ! + CZ @ CCOLOR @ + LINE + LOOP ; + +Block 4909 +( ARC: partial circle, start/end angles in Q48.16 radians, ) +( 18 segments. Reuses CIRC-PT/CX/CY/CZ/CRAD/CCOLOR/PX/PY. ) +VARIABLE ASTART VARIABLE AEND VARIABLE ASTEP +: ARC-SETUP ( cx cy cz r a0 a1 color -- ) + CCOLOR ! AEND ! ASTART ! + CRAD ! CZ ! CY ! CX ! + AEND @ ASTART @ - 18 / ASTEP ! + ASTART @ CIRC-PT PY ! PX ! ; + +Block 4910 +( ARC: draws the arc using ARC-SETUP above. ) +: ARC ( cx cy cz r a0 a1 color -- ) + ARC-SETUP + 18 0 DO + PX @ PY @ CZ @ + ASTART @ I 1+ ASTEP @ * + + CIRC-PT + 2DUP PY ! PX ! + CZ @ CCOLOR @ + LINE + LOOP ; + +Block 4911 +( ELLIPSE-PT: independent x/y radii, reuses CX/CY/CZ. ) +VARIABLE ERX VARIABLE ERY +: ELLIPSE-PT ( ang -- x y ) + DUP Q.SIN ERY @ Q.FROM-INT Q.* Q.TO-INT CY @ + + SWAP Q.COS ERX @ Q.FROM-INT Q.* Q.TO-INT CX @ + + SWAP ; + +Block 4912 +( ELLIPSE: 36-segment polygon, flat at z=cz. ) +: ELLIPSE ( cx cy cz rx ry color -- ) + CCOLOR ! ERY ! ERX ! CZ ! CY ! CX ! + 0 ELLIPSE-PT PY ! PX ! + 36 0 DO + PX @ PY @ CZ @ + I 1+ 11438 * ELLIPSE-PT + 2DUP PY ! PX ! + CZ @ CCOLOR @ + LINE + LOOP ; diff --git a/fb/amd64/geom-test-circle-fixed.png b/fb/amd64/geom-test-circle-fixed.png new file mode 100644 index 0000000..eaf00f6 Binary files /dev/null and b/fb/amd64/geom-test-circle-fixed.png differ diff --git a/fb/amd64/geom-test-full-wordset.png b/fb/amd64/geom-test-full-wordset.png new file mode 100644 index 0000000..245ab75 Binary files /dev/null and b/fb/amd64/geom-test-full-wordset.png differ diff --git a/include/q48_16.h b/include/q48_16.h index cf33dce..f2e46a8 100644 --- a/include/q48_16.h +++ b/include/q48_16.h @@ -157,16 +157,23 @@ static inline q48_16_t q48_from_u64(uint64_t u) { } /** - * @brief Convert Q48.16 to unsigned 64-bit integer (truncate fractional) + * @brief Convert Q48.16 to a 64-bit integer (truncate fractional) * - * Math: q / 2^16 (shift right by 16 bits) - * Example: q48_to_u64(0x10000) = 1 + * Math: q / 2^16 (arithmetic shift right by 16 bits) + * Example: q48_to_u64(0x10000) = 1, q48_to_u64(-0x10000) = -1 + * + * q48_16_t values are two's-complement signed under the hood (q48_neg/ + * q48_abs already treat them that way); a plain unsigned (logical) shift + * would corrupt negative inputs instead of sign-extending them, so this + * shifts via a signed intermediate. Bit-identical to the old behavior for + * non-negative q (the only case this function's other caller, the + * inference engine's sum-of-squares accumulation, ever produces). * * @param q Q48.16 value - * @return q >> 16 as unsigned 64-bit integer + * @return q >> 16, sign-extended, reinterpreted as uint64_t */ static inline uint64_t q48_to_u64(q48_16_t q) { - return q >> 16; + return (uint64_t)(((int64_t)q) >> 16); } #endif /* STARKERNEL_Q48_16_H */ diff --git a/include/starkernel/q48_16.h b/include/starkernel/q48_16.h index 237ea4c..5d1b263 100644 --- a/include/starkernel/q48_16.h +++ b/include/starkernel/q48_16.h @@ -114,10 +114,13 @@ static inline q48_16_t q48_from_u64(uint64_t u) { } /** - * Convert Q48.16 to unsigned 64-bit integer (truncate fractional): q >> 16 + * Convert Q48.16 to a 64-bit integer (truncate fractional): q >> 16, + * arithmetic (signed) shift so negative q sign-extends correctly instead + * of producing garbage from an unsigned logical shift. Bit-identical to + * the old behavior for non-negative q. */ static inline uint64_t q48_to_u64(q48_16_t q) { - return q >> 16; + return (uint64_t)(((int64_t)q) >> 16); } /* ============================================================================ diff --git a/lfs/amd64/starforth b/lfs/amd64/starforth index 5079f34..8367b06 100755 Binary files a/lfs/amd64/starforth and b/lfs/amd64/starforth differ