up to settings

This commit is contained in:
water111
2026-08-30 21:43:53 -07:00
parent f2c5c7a120
commit cd889a5f18
32 changed files with 2269 additions and 1742 deletions
+3 -3
View File
@@ -21912,7 +21912,7 @@ a positive implicit w, and convert 300 Hz angular velocity to a per-display-fram
(define-extern sp-rotate-system
"Rotate a particle's launch offset, velocity, and local acceleration by the owning system's
xyz-only quaternion."
(function sparticle-launchinfo sparticle-cpuinfo transformq none))
(function sparticle-launchinfo sparticle-cpuinfo sprite-vec-data-3d none))
(define-extern sp-launch-particles-death
"Launch the enemy-death particle as a single 2D sprite, initialize its sprite and CPU fields,
apply current time-of-day color, copy the shared death shader, and mark it just launched."
@@ -22261,10 +22261,10 @@ record as user-modified."
:flag-assert #xc00000010
:pack-me
(:methods
(ambient-control-method-9
(reset-ambient-cooldown!
"Restart the ambient-chatter cooldown at the current gameplay frame."
(_type_) none) ;; 9
(ambient-control-method-10
(ambient-chatter-ready?
"Fill out-vector with the displacement from the listener to speaker and return it only when
cooldown ticks have elapsed and the speaker is within max-distance. Return #f otherwise."
(_type_ vector time-frame float process-drawable) vector) ;; 10
+5 -3
View File
@@ -1115,9 +1115,11 @@
;; at some point, this could be more optimized.
;; MIPS has an explicit abs.s instruction, but x86-64 doesn't.
;; modern clang on O3 does a comiss/branch and this is probably pretty close.
`(if (< (the float ,x) 0)
(- (the float ,x))
(the float ,x))
(with-gensyms (value)
`(let ((,value (the float ,x)))
(if (< ,value 0)
(- ,value)
,value)))
)
(defmacro sqrtf (x)
@@ -723,7 +723,7 @@
(set! (-> this been-kicked) #f)
(set! (-> this neck-joint-index) neck-joint)
(set! (-> this cur-trans-hook) nothing)
(ambient-control-method-9 (-> this ambient))
(reset-ambient-cooldown! (-> this ambient))
(set! (-> this event-hook) (-> (method-of-object this idle) event))
(set! (-> this draw shadow-ctrl) (new 'process 'shadow-control 0.0 0.0 614400.0 (the-as float 60) 245760.0))
(setup-shadow-settings! this)
@@ -743,13 +743,13 @@
"Try to play character-specific ambient speech. The base implementation does nothing."
(the-as symbol 0))
(defmethod ambient-control-method-9 ((this ambient-control))
(defmethod reset-ambient-cooldown! ((this ambient-control))
"Restart the ambient-chatter cooldown at the current gameplay frame."
(set! (-> this last-ambient-time) (-> *display* game-frame-counter))
0
(none))
(defmethod ambient-control-method-10 ((this ambient-control) (out-vector vector) (cooldown time-frame) (max-distance float) (speaker process-drawable))
(defmethod ambient-chatter-ready? ((this ambient-control) (out-vector vector) (cooldown time-frame) (max-distance float) (speaker process-drawable))
"Fill out-vector with the displacement from the listener to speaker and return it only when
cooldown ticks have elapsed and the speaker is within max-distance. Return #f otherwise."
(when (< (- (-> *display* game-frame-counter) (-> this last-ambient-time)) cooldown)
+1 -2
View File
@@ -12,8 +12,7 @@
;; :heap-size the required size for the process heap
;; This table was much larger in the demo and was reduced for the final game. The actor birth path
;; uses heap-size to size each process heap; the other fields retain the package, art, and pool
;; requirements that go with the process type.
;; uses heap-size to size each process heap; the other fields seem unused.
(define *entity-info*
(new 'static
+12 -12
View File
@@ -29,9 +29,7 @@
(defmethod debug-draw! ((this border-plane))
"Draw this plane's name, origin, and normal, using green for a load action and red otherwise."
(let* ((action (-> this action))
;; pick color based on action
(color (if (= action 'load) (new 'static 'rgba :g #xff :a #x80) (new 'static 'rgba :r #xff :a #x80))))
;; add text and vector
(color (if (= action 'load) (static-rgba 0 #xff 0 #x80) (static-rgba #xff 0 0 #x80))))
(add-debug-text-sphere #t (bucket-id debug-no-zbuf) (-> this trans) 819.2 (symbol->string (-> this name)) color)
(add-debug-vector #t (bucket-id debug-no-zbuf) (-> this trans) (-> this normal) (meters 2) color))
0
@@ -131,10 +129,10 @@
(-> this task-perm-list data task))
(defmethod initialize! ((this game-info) (cause symbol) (save-to-load game-save) (continue-point-override string))
"Reset or reload game state for cause. dead records death statistics and becomes life or try
in play mode; game clears all persistent progress and selects a starting checkpoint; try and
game restore lives. In play mode, restart actors asynchronously at the selected checkpoint and
optionally load save-to-load. continue-point-override applies only to a game reset."
"Reset or reload game state for cause:
- dead : count a death. If in 'play mode, then continue with 'life/'try logic.
- game : reset all persistent information, restart at continue-point-override
- life : out of lives (unused feature), reset life counter."
(local-vars (death-count int))
(case cause
(('dead)
@@ -199,6 +197,7 @@
(set! (-> this in-level-time i) 0)
(set! (-> this level-opened i) (the-as uint 0))
(nop!))))
;; reset lives
(case cause
(('game 'try)
;; full restart, or ran out of lives
@@ -276,7 +275,8 @@
;; increment our total money in the game (out of the 2000 max orbs)
(+! (-> this money-total) amount)
;; if we have all the money in our level, display the all orbs graphic
(if (= (-> this money-per-level level-idx) (-> (get-game-count level-idx) money-count)) (activate-orb-all level-idx)))))))
(if (= (-> this money-per-level level-idx) (-> (get-game-count level-idx) money-count))
(activate-orb-all level-idx)))))))
;; increment our current money count
(+! (-> this money) amount))
(('fuel-cell)
@@ -544,14 +544,14 @@
(else ((method-of-type fact-info pickup-collectable!) this kind amount source-handle))))
(defmethod lookup-entity-perm-by-aid ((this game-info) (aid actor-id))
"Return the saved entity permission record with actor ID aid, or #f when absent."
"Return the saved entity permanent records with actor ID aid, or #f when absent."
(let ((perms (-> this perm-list)))
(countdown (i (-> perms length))
(if (= aid (-> perms data i aid)) (return (-> perms data i)))))
(the-as entity-perm #f))
(defmethod copy-perms-from-level! ((this game-info) (lev level))
"Copy task-bearing entity permission records from lev into persistent game state, replacing
"Copy entity permanent records from lev into persistent game state, replacing
records with matching actor IDs and appending new records while capacity remains."
(let ((perms (-> this perm-list)) ;; our perms
(lev-entities (-> lev bsp level entity)) ;; entities in the level
@@ -576,7 +576,7 @@
(none))
(defmethod copy-perms-to-level! ((this game-info) (lev level))
"Copy matching persistent entity permission records into lev, then clear transient try-reset
"Copy matching persistent entity permanent records into lev, then clear transient try-reset
status bits before the entities are used."
(let ((lev-entities (-> lev bsp level entity)))
(dotimes (lev-entity-idx (-> lev-entities length))
@@ -659,7 +659,7 @@
(enum->string game-task task))
(defmethod debug-print ((this game-info) (category symbol))
"Inspect game state and optionally print completed game tasks or saved entity permissions.
"Inspect game state and optionally print completed game tasks or saved entity permanents.
Pass #f for both categories."
(inspect this)
(when (or (not category) (= category 'game-task))
@@ -8,12 +8,13 @@
(define-extern othercam-init-by-other (function process-taskable symbol symbol symbol none :behavior othercam))
;; Each task-control holds an ordered list of progression stages. A game task may have several
;; stages, each identified by a monotonically increasing status. current-stage selects the first
;; available stage or is -1 when none is active. The complete game-task list lives in game-info-h.
;; Each task-control holds an ordered list of progression stages (cstages).
;; A game task may have several stages, each identified by a monotonically increasing status.
;; current-stage selects the first available stage or is -1 when none is active.
;; The complete game-task list lives in game-info-h.
;; Most tasks award a power cell, while special tasks cover interactions such as the levitator.
;; Stage conditions run as process-taskable behaviors so they can use the current character context
;; while receiving the task-control whose sibling stages and reminders they inspect.
;; Stage conditions run as process-taskable behaviors.
(defenum task-status
:type uint64
(invalid 0)
@@ -37,35 +38,36 @@
;; DECOMP BEGINS
;; A stage that a task can be at
(deftype task-cstage (structure)
((game-task game-task)
(status task-status)
(flags task-flags)
(condition (function task-control symbol :behavior process-taskable)))
(:methods
(get-task "Return this stage's game task." (_type_) game-task)
(get-status "Return this stage's progress status." (_type_) task-status)
(task-available? "Return whether this stage may become current, closing it when persistent
progress has already passed it." (_type_ task-control) symbol)
(closed? "Return whether this stage is currently closed." (_type_) symbol)
(closed-by-default? "Return whether non-game resets leave this stage closed." (_type_) symbol)
(close-task! "Close this stage and advance its associated persistent task status when present." (_type_) int)
(open-task! "Clear this stage's transient closed flag." (_type_) int)))
(get-task (_type_) game-task)
(get-status (_type_) task-status)
(task-available? (_type_ task-control) symbol)
(closed? (_type_) symbol)
(closed-by-default? (_type_) symbol)
(close-task! (_type_) int)
(open-task! (_type_) int)))
;; The array of stages for a single task.
(deftype task-control (basic)
((current-stage int16)
(stage (array task-cstage)))
(:methods
(current-task "Return the current task or game-task.none." (_type_) game-task)
(current-status "Return the current status or task-status.invalid." (_type_) task-status)
(close-current! "Close the current stage and select the first available task." (_type_) game-task)
(close-status! "Close a status for the current task and select the first available task." (_type_ task-status) game-task)
(first-any "Select and return the first available task in stage-array order." (_type_ symbol) game-task)
(reset! "Reset stage closure according to reset mode and closed-by-default flags." (_type_ symbol symbol) int)
(closed? "Return whether the matching task and status stage is closed." (_type_ game-task task-status) symbol)
(get-reminder "Read an indexed reminder byte from the task's persistent record." (_type_ int) int)
(save-reminder "Write an indexed reminder byte to the task's persistent record." (_type_ int int) int)
(exists? "Return whether a stage with the given task and status exists." (_type_ game-task task-status) symbol)))
(current-task (_type_) game-task)
(current-status (_type_) task-status)
(close-current! (_type_) game-task)
(close-status! (_type_ task-status) game-task)
(first-any (_type_ symbol) game-task)
(reset! (_type_ symbol symbol) int)
(closed? (_type_ game-task task-status) symbol)
(get-reminder (_type_ int) int)
(save-reminder (_type_ int int) int)
(exists? (_type_ game-task task-status) symbol)))
;; Tracks cooldown and repetition for positional NPC chatter.
(deftype ambient-control (structure)
@@ -74,11 +76,9 @@
(last-ambient-id sound-id))
:pack-me
(:methods
(ambient-control-method-9 "Restart the ambient-chatter cooldown." (_type_) none)
(ambient-control-method-10 "Return the listener-to-speaker displacement after the cooldown when
the speaker is within the supplied distance." (_type_ vector time-frame float process-drawable) vector)
(play-ambient "Play a nonrepeating positional ambient line when hint and level conditions permit
it; force bypasses the hint-availability test." (_type_ string symbol vector) symbol)))
(reset-ambient-cooldown! (_type_) none)
(ambient-chatter-ready? (_type_ vector time-frame float process-drawable) vector)
(play-ambient (_type_ string symbol vector) symbol)))
;; Shared interaction, animation, camera, audio, and shadow state for task-giving characters.
(deftype process-taskable (process-drawable)
@@ -121,47 +121,32 @@
(be-clone handle)
idle)
(:methods
(get-art-elem "Return the art element on the active root animation channel." (_type_) art-element)
(play-anim! "Select the current task animation; commit? lets subtype implementations apply
progression changes when playback begins." (_type_ symbol) basic)
(prefetch-play-anim! "Select the current task animation without committing progression and
request it for streaming." (_type_) none)
(get-accept-anim "Return the task-accept animation, optionally committing the accepted choice." (_type_ symbol) spool-anim)
(push-accept-anim "Select the task-accept animation without committing and request it for
streaming." (_type_) none)
(get-reject-anim "Return the task-reject animation, optionally committing the rejected choice." (_type_ symbol) spool-anim)
(push-reject-anim "Select the task-reject animation without committing and request it for
streaming." (_type_) none)
(goto-post-anim-state! "Enter give-cell when a reward is owed, otherwise enter release." (_type_) none)
(should-display? "Return whether this taskable character should be visible." (_type_) symbol)
(init-as-taskable! "Initialize a taskable character's collision, skeleton, interaction, audio,
and shadow state." (_type_ object skeleton-group int int vector int) none)
(initialize-collision "Build and install this character's solid collision sphere at the supplied
center joint." (_type_ int vector) none)
(goto-initial-state! "Enter this character's initial hidden, reward, or idle state." (_type_) none)
(try-play-ambient-chatter "Try to play character-specific ambient speech; the base method does
nothing." (_type_) symbol)
(play-reminder "Try to play a reminder for the current task stage." (_type_) symbol)
(get-art-elem (_type_) art-element)
(play-anim! (_type_ symbol) basic)
(prefetch-play-anim! (_type_) none)
(get-accept-anim (_type_ symbol) spool-anim)
(push-accept-anim (_type_) none)
(get-reject-anim (_type_ symbol) spool-anim)
(push-reject-anim (_type_) none)
(goto-post-anim-state! (_type_) none)
(should-display? (_type_) symbol)
(init-as-taskable! (_type_ object skeleton-group int int vector int) none)
(initialize-collision (_type_ int vector) none)
(goto-initial-state! (_type_) none)
(try-play-ambient-chatter (_type_) symbol)
(play-reminder (_type_) symbol)
(process-taskable-method-45 (_type_) symbol)
(update-music-and-flava! "Update proximity-controlled music and sound flavor settings, weighting
vertical separation fourfold." (_type_) none)
(target-above-threshold? "Return whether the target is above the interaction threshold." (_type_) symbol)
(draw-npc-shadow "Update the NPC shadow when its highest-detail geometry was drawn, otherwise
disable it." (_type_) none)
(update-music-and-flava! (_type_) none)
(target-above-threshold? (_type_) symbol)
(draw-npc-shadow (_type_) none)
(hidden-other () _type_ :state)
(target-far-away? "Return whether the target, or the camera when no target exists, is far enough
away to leave hidden-other." (_type_) symbol)
(close-anim-file! "Return the streaming state of the current play animation." (_type_) symbol)
(setup-shadow-settings! "Set the character's default shadow clipping planes." (_type_) none)))
(target-far-away? (_type_) symbol)
(close-anim-file! (_type_) symbol)
(setup-shadow-settings! (_type_) none)))
(defun-extern task-known? game-task symbol)
(defun-extern task-control-reset symbol none)
(define-extern task-closed? (function game-task task-status symbol))
(define-extern get-task-status (function game-task task-status))
(define-extern get-task-control (function game-task task-control))
(define-extern close-specific-task! (function game-task task-status game-task))
@@ -0,0 +1,690 @@
(#unless PC_PORT
(defun sp-get-particle ((sys sparticle-system) (group int) (binding sparticle-launch-state))
"Allocate a free particle in group, beginning at binding's rotating block index when supplied.
Clear its bitmap bit, mark its CPU record valid, and return it; return false when full."
(let ((block-offset 0)
(num-blocks (-> sys blocks 0))
(block-index 0))
(when (= group 1)
(set! block-offset num-blocks)
(set! num-blocks (-> sys blocks 1)))
;; Rotate the first block examined so repeated launchers spread their allocations.
(when binding
(set! block-index (the int (-> binding randomize)))
(+! (-> binding randomize) 1)
(when (= (-> binding randomize) num-blocks)
(set! (-> binding randomize) (the uint 0))))
(dotimes (i num-blocks)
(let ((free-bits (-> sys alloc-table (+ block-offset block-index))))
(when (nonzero? free-bits)
;; Find the least-significant set bit in six branchless halves. Shifting a candidate
;; half to the sign end lets mov.nz keep it when that half contains a free slot;
;; mov.z advances the bit index only when the candidate half is empty.
(let ((free-bit (rlet ((bits :reg t1)
(shifted :reg t2)
(candidate-index :reg t3)
(bit-index :reg a2))
(m! bits free-bits)
(m! bit-index 0)
(sll32 shifted bits)
(add.i candidate-index bit-index 32)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 16)
(add.i candidate-index bit-index 16)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 8)
(add.i candidate-index bit-index 8)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 4)
(add.i candidate-index bit-index 4)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 2)
(add.i candidate-index bit-index 2)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 1)
(add.i candidate-index bit-index 1)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
bit-index)))
(let ((slot (+ free-bit (* (+ block-offset block-index) 64))))
(logxor! (-> sys alloc-table (+ block-offset block-index)) (ash 1 free-bit))
(+! (-> sys num-alloc group) 1)
(let ((cpuinfo (-> sys cpuinfo-table slot))) (set! (-> cpuinfo valid) #t) (return cpuinfo))))))
(+! block-index 1)
(if (= block-index num-blocks) (set! block-index 0))))
(the-as sparticle-cpuinfo #f)))
(#unless PC_PORT
(defun sp-process-block-2d ((sys sparticle-system)
(scratchpad-cpuinfo int)
(scratchpad-sprite-data int)
(start-slot int)
(count int)
(paused? symbol))
"Update count 2D particle records in scratchpad: age, integrate, fade, invoke callbacks, handle
death or relaunch, and write their billboard sprite data."
(declare (asm-func none))
;; Each iteration walks one 144-byte CPU record beside one 48-byte sprite record. The sprite
;; record is three quadwords:
;;
;; +0 position.xyz, x-scale
;; +16 sprite flags/matrix mode, rotation, y-scale
;; +32 color.rgba
;;
;; The matching CPU record keeps velocity.xyz/x-scale velocity at +16, rotation/y-scale
;; velocity at +32, color fade at +48, acceleration at +64, friction at +96, timer at +100,
;; flags at +104, the per-particle update function at +112, relaunch state at +116/+120, cached
;; launch alpha at +124, and validity at +128. Keeping these records separate lets the sprite
;; record remain ready for the renderer while simulation-only state stays in the CPU record.
;;
;; *sp-frame-time*.x carries the integer 300 Hz tick count in its low byte. Its y lane scales
;; position, rotation, scale, and color increments, while z and w express the same interval in
;; 60 Hz frames for acceleration and friction. Invalid slots are skipped. Pausing freezes a
;; particle unless run-while-paused is set. A timer of -1 never expires; zero frees the particle.
;; On the first visible update, just-launched restores the intended alpha from cache-alpha.
(rlet ((sys :reg gp)
(cpuinfo :reg s5 :type sparticle-cpuinfo)
(sprite-data :reg s4 :type sprite-vec-data-2d)
(slot :reg s1)
(remaining :reg s3)
(paused? :reg s2)
(ticks :reg s0)
(frame-time :reg vf9)
(sprite-pos-scale :reg vf1)
(sprite-rot-scale :reg vf2)
(sprite-color :reg vf3)
(velocity-scale-rate :reg vf4)
(rotation-scale-rate :reg vf5)
(color-fade-rate :reg vf6)
(acceleration :reg vf7)
(friction-factor :reg vf8)
(position-step :reg vf10)
(rotation-scale-step :reg vf11)
(color-step :reg vf12))
;; Establish the scratchpad cursors and recover this frame's integer tick count.
(asm-block setup-2d
(label particle-update-2d-entry)
(add.i sp sp -128)
(s.d ra sp)
(s.q s0 sp 16)
(s.q s1 sp 32)
(s.q s2 sp 48)
(s.q s3 sp 64)
(s.q s4 sp 80)
(s.q s5 sp 96)
(s.q gp sp 112)
(m sys a0)
(m cpuinfo a1)
(m sprite-data a2)
(m slot a3)
(m remaining t0)
(m paused? t1)
(m! v1 *sp-frame-time*)
(l.vf frame-time v1)
(m v1 frame-time)
(and.i ticks v1 255))
;; Reject unused slots, then decide whether a paused particle is allowed to simulate.
(asm-block particle-lifetime-2d
(label particle-loop)
(l.w.field v1 cpuinfo (valid))
(b.eq v1 s7 next-particle :delay (nop!))
(b.eq paused? s7 simulate-particle :delay (l.w.field v1 cpuinfo (flags)))
(and.i v1 v1 #x2000)
(b.nz v1 simulate-particle :delay (nop!))
(l.w.field v1 cpuinfo (timer))
(add.i a0 r0 -1)
(b.eq v1 a0 reveal-new-particle :delay (nop!))
(b.z v1 free-particle :delay (nop!))
;; A newly launched particle is hidden with alpha zero. Reveal it without advancing time when
;; the rest of its simulation is frozen.
(label reveal-new-particle)
(l.w.field a0 cpuinfo (flags))
(and.i v1 a0 64)
(xor a0 a0 v1)
(b.z v1 next-particle :delay (s.w.field a0 cpuinfo (flags)))
(l.w.field v1 cpuinfo (cache-alpha))
(b next-particle :delay (s.w.field v1 sprite-data (a)))
;; Age finite particles in 300 Hz ticks. The packed maximum clamps an overshoot to zero so the
;; next iteration follows the ordinary zero-timer death path.
(label simulate-particle)
(l.w.field v1 cpuinfo (timer))
(add.i a0 r0 -1)
(b.eq v1 a0 run-callback :delay (sub a0 v1 ticks))
(b.z v1 free-particle :delay (max.w v1 a0 r0))
(s.w.field v1 cpuinfo (timer))
;; Restore the launch alpha before callbacks so custom particle logic sees the visible state.
(label run-callback)
(l.w.field a0 cpuinfo (flags))
(and.i v1 a0 64)
(xor a0 a0 v1)
(b.z v1 call-update-function :delay (s.w.field a0 cpuinfo (flags)))
(l.w.field v1 cpuinfo (cache-alpha))
(s.w.field v1 sprite-data (a)))
;; The callback may edit either the CPU record or its renderer-facing sprite record.
(asm-block callback-and-relaunch-2d
(label call-update-function)
(l.w.field t9 cpuinfo (func))
(b.z t9 check-relaunch :delay (nop!))
(add.i sp sp -80)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(m a0 sys)
(m a1 cpuinfo)
(m a2 sprite-data)
(jalr ra t9 :delay (s.q remaining sp 64))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(add.i sp sp 80)
;; A nonfalse next-launcher owns a countdown independent of the particle lifetime. Relaunch as
;; soon as subtracting this frame's ticks carries it below zero.
(label check-relaunch)
(l.w.field a1 cpuinfo (next-launcher))
(l.w.field v1 cpuinfo (next-time))
(b.z a1 integrate :delay (sub a0 v1 ticks))
(add.i v1 a0 -1)
(b.ge v1 r0 integrate :delay (s.w.field a0 cpuinfo (next-time)))
(add.i sp sp -96)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(s.q remaining sp 64)
(s.q paused? sp 80)
(m a0 sys)
(m a3 sprite-data)
(m a2 cpuinfo)
(m! t9 sp-relaunch-particle-2d)
(jalr ra t9 :delay (sll v0 ra 0))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(l.q paused? sp 80)
(add.i sp sp 96))
;; Integrate the four adjacent CPU vectors directly into the three sprite quadwords.
(asm-block integrate-motion-2d
(label integrate)
(l.vf.field sprite-pos-scale sprite-data (x-y-z-sx))
(l.vf.field sprite-rot-scale sprite-data (flag-rot-sy))
(l.vf.field sprite-color sprite-data (r-g-b-a))
(l.vf.field velocity-scale-rate cpuinfo (vel-sxvel))
(l.vf.field rotation-scale-rate cpuinfo (rot-syvel))
(l.vf.field color-fade-rate cpuinfo (fade))
(l.vf.field acceleration cpuinfo (acc))
(l.s.field f0 cpuinfo (friction))
(m v1 f0)
;; Acceleration uses 60 Hz frame units. Nonzero friction is converted to a retained-velocity
;; factor for this interval, then applied to xyz without disturbing x-scale velocity in w.
(mul.z.vf acceleration acceleration frame-time)
(b.z v1 no-friction :delay (add.vf.xyz velocity-scale-rate velocity-scale-rate acceleration))
(m friction-factor v1)
(sub.x.vf.w friction-factor vf0 friction-factor)
(mul.w.vf friction-factor friction-factor frame-time)
(sub.w.vf.w friction-factor vf0 friction-factor)
(mul.w.vf.xyz velocity-scale-rate velocity-scale-rate friction-factor)
(label no-friction)
;; Apply velocity, rotation/scale velocity, and fade with the full 300 Hz tick count. Clamp
;; color to zero so the packed death tests below can compare its float bits as integers.
(mul.y.vf position-step velocity-scale-rate frame-time)
(mul.y.vf rotation-scale-step rotation-scale-rate frame-time)
(mul.y.vf color-step color-fade-rate frame-time)
(add.vf sprite-pos-scale sprite-pos-scale position-step)
(add.vf.zw sprite-rot-scale sprite-rot-scale rotation-scale-step)
(add.vf sprite-color sprite-color color-step)
(max.x.vf sprite-color sprite-color vf0)
(s.vf.field velocity-scale-rate cpuinfo (vel-sxvel))
(s.vf.field sprite-pos-scale sprite-data (x-y-z-sx))
(s.vf.field sprite-rot-scale sprite-data (flag-rot-sy))
(s.vf.field sprite-color sprite-data (r-g-b-a)))
;; 2D rotation is kept as an integer-valued float, matching the sprite packet format.
(asm-block billboard-post-update
(l.s.field f0 sprite-data (rot))
(cvt.w.s f0 f0)
(m v1 f0)
(sll32 v1 v1 16)
(sra32 v1 v1 16)
(m f0 v1)
(cvt.s.w f0 f0)
(s.s.field f0 sprite-data (rot))
;; Orbiting is a post-integration position adjustment around the particle's stored center.
(l.w.field v1 cpuinfo (flags))
(and.i v1 v1 128)
(b.z v1 test-death-flags :delay (m! t9 sp-orbiter))
(add.i sp sp -96)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(s.q remaining sp 64)
(m a0 sys)
(m a1 cpuinfo)
(m a2 sprite-data)
(jalr ra t9 :delay (s.q paused? sp 80))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(l.q paused? sp 80)
(add.i sp sp 96))
;; Packed extracts test RGB together, then alpha, then x- and y-scale. These correspond to
;; die-when-color-0, die-when-faded, and die-when-scale-0.
(asm-block death-tests-2d
(label test-death-flags)
(l.q.field v1 sprite-data (r-g-b-a))
(l.w.field a0 cpuinfo (flags))
(and.i a1 a0 2)
(b.z a1 test-fade-death :delay (and.i a1 a0 4))
(b.nz v1 test-fade-death :delay (pextuw t4 v1 r0))
(b.z t4 free-particle :delay (nop!))
(label test-fade-death)
(b.z a1 test-scale-death :delay (and.i a0 a0 1))
(pcpyud t4 v1 r0)
(pexew t4 t4)
(b.le t4 r0 free-particle :delay (nop!))
(label test-scale-death)
(b.z a0 next-particle :delay (nop!))
(m v1 sprite-pos-scale)
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.lt v1 r0 free-particle :delay (m v1 sprite-rot-scale))
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.ge v1 r0 next-particle :delay (nop!))
;; Return dead particles to the bitmap-managed pool.
(label free-particle)
(m! t9 sp-free-particle)
(m a0 sys)
(m a1 slot)
(m a2 cpuinfo)
(m a3 sprite-data)
(jalr ra t9 :delay (sll v0 ra 0)))
;; Advance every parallel cursor even when this slot was invalid.
(asm-block advance-and-return-2d
(label next-particle)
(add.i remaining remaining -1)
(add.i cpuinfo cpuinfo 144)
(add.i sprite-data sprite-data 48)
(b.nz remaining particle-loop :delay (add.i slot slot 1))
;; Return the first slot after the processed block.
(m v0 slot)
(l.d ra sp)
(l.q sys sp 112)
(l.q cpuinfo sp 96)
(l.q sprite-data sp 80)
(l.q remaining sp 64)
(l.q paused? sp 48)
(l.q slot sp 32)
(l.q ticks sp 16)
(jr ra :delay (add.i sp sp 128))
(nop! :count 3)))))
(#unless PC_PORT
(defun sp-process-block-3d ((sys sparticle-system)
(scratchpad-cpuinfo int)
(scratchpad-sprite-data int)
(start-slot int)
(count int)
(paused? symbol))
"Update count 3D particle records in scratchpad: age, integrate, rotate, fade, invoke callbacks,
handle death or relaunch, and write their oriented sprite data."
(declare (asm-func none))
;; The 3D path shares the 144-byte CPU record and 48-byte renderer record with the 2D path. The
;; renderer record still stores position.xyz/x-scale at +0 and color.rgba at +32, but its middle
;; quadword holds quaternion.xyz/y-scale rather than a billboard rotation. The complete
;; rotational velocity quaternion is kept in the CPU record at +80.
;;
;; Quaternion w is omitted from the renderer record because a unit quaternion has only three
;; independent components. Rebuild |w| from xyz, compose the rotational step, then choose the
;; sign of the stored xyz so the omitted w can remain nonnegative. q and -q represent the same
;; rotation, so the renderer can reconstruct an equivalent orientation without spending a sign
;; bit.
;;
;; Lifetime, pause, callback, relaunch, integration, and packed death tests otherwise match the
;; 2D update. A long frame of at least ten 300 Hz ticks composes the rotation step twice.
(rlet ((sys :reg gp)
(cpuinfo :reg s5 :type sparticle-cpuinfo)
(sprite-data :reg s4 :type sprite-vec-data-3d)
(slot :reg s0)
(remaining :reg s3)
(paused? :reg s2)
(rotation :reg s1)
(frame-time :reg vf16)
(sprite-pos-scale :reg vf8)
(sprite-quat-scale :reg vf9)
(sprite-color :reg vf10)
(velocity-scale-rate :reg vf11)
(rotation-scale-rate :reg vf12)
(color-fade-rate :reg vf13)
(acceleration :reg vf14)
(friction-factor :reg vf15)
(position-step :reg vf17)
(rotation-scale-step :reg vf18)
(color-step :reg vf19)
(stored-quat-scale :reg vf1)
(full-quaternion :reg vf2))
;; Establish the scratchpad cursors, reserve a complete quaternion, and cache the tick count.
(asm-block setup-3d
(label particle-update-3d-entry)
(add.i sp sp -160)
(s.d ra sp)
(s.d fp sp 8)
(m fp t9)
(s.q s0 sp 48)
(s.q s1 sp 64)
(s.q s2 sp 80)
(s.q s3 sp 96)
(s.q s4 sp 112)
(s.q s5 sp 128)
(s.q gp sp 144)
(m sys a0)
(m cpuinfo a1)
(m sprite-data a2)
(m slot a3)
(m remaining t0)
(m paused? t1)
(add.i rotation sp 16)
(s.q r0 rotation)
(m! v1 *sp-frame-time*)
(l.vf frame-time v1)
(m v1 frame-time)
(and.i v1 v1 255)
(s.q v1 sp 32)
(nop!))
;; Reject unused slots, then decide whether a paused particle is allowed to simulate.
(asm-block particle-lifetime-3d
(label particle-loop-3d)
(l.w.field v1 cpuinfo (valid))
(b.eq v1 s7 next-particle-3d :delay (nop!))
(b.eq paused? s7 simulate-particle-3d :delay (l.w.field v1 cpuinfo (flags)))
(and.i v1 v1 #x2000)
(b.nz v1 simulate-particle-3d :delay (nop!))
(l.w.field v1 cpuinfo (timer))
(add.i a0 r0 -1)
(b.eq v1 a0 reveal-new-particle-3d :delay (nop!))
(b.z v1 free-particle-3d :delay (nop!))
;; Reveal a newly launched particle without advancing it when simulation is frozen.
(label reveal-new-particle-3d)
(l.w.field a0 cpuinfo (flags))
(and.i v1 a0 64)
(xor a0 a0 v1)
(b.z v1 next-particle-3d :delay (s.w.field a0 cpuinfo (flags)))
(l.w.field v1 cpuinfo (cache-alpha))
(b next-particle-3d :delay (s.w.field v1 sprite-data (a)))
;; Age finite particles in 300 Hz ticks and clamp an overshoot to zero.
(label simulate-particle-3d)
(l.w.field v1 cpuinfo (timer))
(add.i a0 r0 -1)
(b.eq v1 a0 run-callback-3d :delay (l.q a0 sp 32))
(sub a0 v1 a0)
(b.z v1 free-particle-3d :delay (max.w v1 a0 r0))
(s.w.field v1 cpuinfo (timer))
;; Restore the launch alpha before custom particle logic runs.
(label run-callback-3d)
(l.w.field a0 cpuinfo (flags))
(and.i v1 a0 64)
(xor a0 a0 v1)
(b.z v1 call-update-function-3d :delay (s.w.field a0 cpuinfo (flags)))
(l.w.field v1 cpuinfo (cache-alpha))
(s.w.field v1 sprite-data (a)))
;; The callback may edit either the CPU record or its renderer-facing sprite record.
(asm-block callback-3d
(label call-update-function-3d)
(l.w.field t9 cpuinfo (func))
(b.z t9 check-relaunch-3d :delay (nop!))
(add.i sp sp -96)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(s.q remaining sp 64)
(m a0 sys)
(m a1 cpuinfo)
(m a2 sprite-data)
(jalr ra t9 :delay (s.q paused? sp 80))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(l.q paused? sp 80)
(add.i sp sp 96))
;; Relaunch on the first frame that carries the independent next-time countdown below zero.
(asm-block relaunch-3d
(label check-relaunch-3d)
(l.w.field a1 cpuinfo (next-launcher))
(l.w.field v1 cpuinfo (next-time))
(b.z a1 integrate-3d :delay (l.q a0 sp 32))
(sub v1 v1 a0)
(b.ge v1 r0 integrate-3d :delay (s.w.field v1 cpuinfo (next-time)))
(add.i sp sp -96)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(s.q remaining sp 64)
(s.q paused? sp 80)
(m a0 sys)
(m a3 sprite-data)
(m a2 cpuinfo)
(m! t9 sp-relaunch-particle-3d)
(jalr ra t9 :delay (sll v0 ra 0))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(l.q paused? sp 80)
(add.i sp sp 96))
;; Integrate the four adjacent CPU vectors directly into the three sprite quadwords.
(asm-block integrate-motion-3d
(label integrate-3d)
(l.vf.field sprite-pos-scale sprite-data (x-y-z-sx))
(l.vf.field sprite-quat-scale sprite-data (qx-qy-qz-sy))
(l.vf.field sprite-color sprite-data (r-g-b-a))
(l.vf.field velocity-scale-rate cpuinfo (vel-sxvel))
(l.vf.field rotation-scale-rate cpuinfo (rot-syvel))
(l.vf.field color-fade-rate cpuinfo (fade))
(l.vf.field acceleration cpuinfo (acc))
(l.s.field f0 cpuinfo (friction))
(m v1 f0)
;; Acceleration uses 60 Hz frame units. Nonzero friction becomes a retained-velocity factor
;; for this interval and applies to xyz without disturbing x-scale velocity in w.
(mul.z.vf acceleration acceleration frame-time)
(b.z v1 no-friction-3d :delay (add.vf.xyz velocity-scale-rate velocity-scale-rate acceleration))
(m friction-factor v1)
(sub.x.vf.w friction-factor vf0 friction-factor)
(mul.w.vf friction-factor friction-factor frame-time)
(sub.w.vf.w friction-factor vf0 friction-factor)
(mul.w.vf.xyz velocity-scale-rate velocity-scale-rate friction-factor)
(label no-friction-3d)
;; Apply velocity, y-scale velocity, and fade with the full tick count. The xyz lanes of
;; rotation-scale-rate are not used here; orientation advances from rotvel3d below.
(mul.y.vf position-step velocity-scale-rate frame-time)
(mul.y.vf rotation-scale-step rotation-scale-rate frame-time)
(mul.y.vf color-step color-fade-rate frame-time)
(add.vf sprite-pos-scale sprite-pos-scale position-step)
(add.w.vf.w sprite-quat-scale sprite-quat-scale rotation-scale-step)
(add.vf sprite-color sprite-color color-step)
(max.x.vf sprite-color sprite-color vf0)
(s.vf.field velocity-scale-rate cpuinfo (vel-sxvel))
(s.vf.field sprite-pos-scale sprite-data (x-y-z-sx))
(s.vf.field sprite-quat-scale sprite-data (qx-qy-qz-sy))
(s.vf.field sprite-color sprite-data (r-g-b-a)))
;; Rebuild w = sqrt(1-x^2-y^2-z^2) in the stack quaternion. The constant is loaded relative to
;; this function because the hand-written function keeps 1.0 in its local literal pool.
(asm-block update-quaternion
(m v1 rotation)
(m a0 sprite-data)
(l.s f0 a0 16)
(l.s f1 a0 20)
(l.s f3 a0 24)
(s.s f0 v1)
(s.s f1 v1 4)
(s.s f3 v1 8)
(l.s f2 fp L157)
(mul.s f3 f3 f3)
(sub.s f2 f2 f3)
(mul.s f1 f1 f1)
(sub.s f1 f2 f1)
(mul.s f0 f0 f0)
(sub.s f0 f1 f0)
(sqrt.s f0 f0)
(s.s f0 v1 12)
(m a0 f0)
;; Compose the particle's rotational velocity once, or twice when a long frame crosses the
;; ten-tick threshold.
(m! v1 *sp-frame-time*)
(l.s f0 v1)
(m v1 f0)
(and.i v1 v1 255)
(add.i v1 v1 -10)
(b.lt v1 r0 apply-rotation-step :delay (nop!))
(m! t9 quaternion*!)
(m a0 rotation)
(m a1 rotation)
(lea.field a2 cpuinfo (rotvel3d))
(jalr ra t9 :delay (sll v0 ra 0))
(label apply-rotation-step)
(m! t9 quaternion*!)
(m a0 rotation)
(m a1 rotation)
(lea.field a2 cpuinfo (rotvel3d))
(jalr ra t9 :delay (sll v0 ra 0)))
;; q and -q are equivalent. Store the sign whose omitted w is nonnegative so the renderer can
;; reconstruct the same convention from xyz alone; preserve y-scale in the untouched w lane.
(asm-block store-compact-quaternion
(m a0 sprite-data)
(m v1 rotation)
(l.s f0 v1 12)
(m f1 r0)
(c.lt.s f0 f1)
(b.fpf store-positive-quaternion :delay (nop!))
(l.vf stored-quat-scale a0 16)
(l.vf full-quaternion v1)
(sub.vf.xyz stored-quat-scale vf0 full-quaternion)
(s.vf stored-quat-scale a0 16)
(m a0 stored-quat-scale)
(b test-death-flags-3d :delay (nop!))
(label store-positive-quaternion)
(l.vf stored-quat-scale a0 16)
(l.vf full-quaternion v1)
(add.vf.xyz stored-quat-scale vf0 full-quaternion)
(s.vf stored-quat-scale a0 16)
(m a0 stored-quat-scale))
;; Packed extracts test RGB together, then alpha, then x- and y-scale. These correspond to
;; die-when-color-0, die-when-faded, and die-when-scale-0.
(asm-block death-tests-3d
(label test-death-flags-3d)
(m v1 sprite-color)
(l.w.field a0 cpuinfo (flags))
(and.i a1 a0 2)
(b.z a1 test-fade-death-3d :delay (and.i a1 a0 4))
(b.nz v1 test-fade-death-3d :delay (pextuw a2 v1 r0))
(b.z a2 free-particle-3d :delay (nop!))
(label test-fade-death-3d)
(b.z a1 test-scale-death-3d :delay (and.i a0 a0 1))
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.le v1 r0 free-particle-3d :delay (nop!))
(label test-scale-death-3d)
(b.z a0 next-particle-3d :delay (nop!))
(m v1 sprite-pos-scale)
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.lt v1 r0 free-particle-3d :delay (m v1 sprite-quat-scale))
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.ge v1 r0 next-particle-3d :delay (nop!))
;; Return dead particles to the bitmap-managed pool.
(label free-particle-3d)
(m! t9 sp-free-particle)
(m a0 sys)
(m a1 slot)
(m a2 cpuinfo)
(m a3 sprite-data)
(jalr ra t9 :delay (sll v0 ra 0)))
;; Advance every parallel cursor even when this slot was invalid.
(asm-block advance-and-return-3d
(label next-particle-3d)
(add.i remaining remaining -1)
(add.i cpuinfo cpuinfo 144)
(add.i sprite-data sprite-data 48)
(b.nz remaining particle-loop-3d :delay (add.i slot slot 1))
;; Return the first slot after the processed block.
(m v0 slot)
(l.d ra sp)
(l.d fp sp 8)
(l.q sys sp 144)
(l.q cpuinfo sp 128)
(l.q sprite-data sp 112)
(l.q remaining sp 96)
(l.q paused? sp 80)
(l.q rotation sp 64)
(l.q slot sp 48)
(jr ra :delay (add.i sp sp 160))
(nop! :count 3)))))
(#unless PC_PORT
(defun memcpy ((dst pointer) (src pointer) (size int))
"Copy size rounded-up bytes from src to dst in 128-byte bursts followed by 16-byte quadwords."
(declare (asm-func none))
;; Particle scratchpad records are all quadword-padded, so this copier deliberately rounds size
;; upward. Large transfers use eight quadword loads followed by eight stores per iteration.
(rlet ((dst :reg a0)
(src :reg a1)
(size :reg a2)
(quadwords :reg v1)
(data0 :reg a2 :type i128)
(data1 :reg a3 :type i128)
(data2 :reg t0 :type i128)
(data3 :reg t1 :type i128)
(data4 :reg t2 :type i128)
(data5 :reg t3 :type i128)
(data6 :reg t4 :type i128)
(data7 :reg t5 :type i128))
(add.i quadwords size 15)
(sra quadwords quadwords 4)
(add.i quadwords quadwords -8)
(b.lt quadwords r0 finish-bursts :delay (at-label copy-burst (l.q data0 src)))
(l.q data1 src 16)
(l.q data2 src 32)
(l.q data3 src 48)
(l.q data4 src 64)
(l.q data5 src 80)
(l.q data6 src 96)
(l.q data7 src 112)
(s.q data0 dst)
(s.q data1 dst 16)
(s.q data2 dst 32)
(s.q data3 dst 48)
(s.q data4 dst 64)
(s.q data5 dst 80)
(s.q data6 dst 96)
(s.q data7 dst 112)
(add.i src src 128)
(add.i quadwords quadwords -8)
(b.ge quadwords r0 copy-burst :delay (add.i dst dst 128))
(label finish-bursts)
(add.i quadwords quadwords 8)
(b.z quadwords done-copying :delay (at-label copy-quadword (l.q data0 src)))
(add.i src src 16)
(s.q data0 dst)
(add.i quadwords quadwords -1)
(nop!)
(b.nz quadwords copy-quadword :delay (add.i dst dst 16))
(label done-copying)
(m v0 r0)
(jr ra :delay (add sp sp r0))
(nop! :count 3))))
@@ -0,0 +1,899 @@
(#unless PC_PORT
;; Each init spec is one 16-byte record:
;;
;; +0 uint16 field ID +2 uint16 sp-flag
;; +4 initial value +8 random range
;; +12 random multiplier
;;
;; The last three words are interpreted as floats, integers, symbols, or pointers according to
;; the flag. Specs are sorted by field ID, so adjacent field ranges can share one forward cursor.
;; Float and integer-random forms advance the VU random register; copy and reference forms avoid
;; that arithmetic and resolve their value directly.
(defun sp-init-fields! ((dest (pointer float))
(specs (inline-array sp-field-init-spec))
(field-id sp-field-id)
(end-field-id sp-field-id)
(write-missing-fields symbol))
"Initialize consecutive particle fields from the sorted init-spec stream, returning its next
unconsumed entry. Missing fields may be cleared for a new particle or retained for a relaunch."
(declare (asm-func (inline-array sp-field-init-spec)))
(rlet ((dest-reg :reg a0)
(field-reg :reg a2)
(end-field-reg :reg a3)
(write-missing-reg :reg t0)
(spec-reg :reg v0))
(label entry)
(m v1 dest-reg)
(m v1 field-reg)
(m v1 end-field-reg)
(m v1 write-missing-reg)
(nop!)
(add.i field-reg field-reg 1)
(m spec-reg a1)
;; Init specs are sorted by field ID. Skip any earlier range that a previous call consumed.
(label scan-specs)
(l.h a1 spec-reg)
(nop!)
(sub a1 a1 field-reg)
(nop! :count 3)
(b.ltl a1 r0 scan-specs :delay (add.i spec-reg spec-reg 16))
(sub a1 field-reg end-field-reg)
(nop!)
(b.ge a1 r0 done :delay (nop!))
;; vf2 receives one random value before dispatch. The selected field kind decides whether to
;; use it as a float, quantize it, ignore it, or replace it with a pointer-like value.
(label field-loop)
(l.h a1 spec-reg)
(nop!)
(b.ne a1 field-reg missing-field :delay (vrget.xyzw vf1))
(sqrt.x Q vf1)
(l.h a1 spec-reg 2)
(vaddq.x vf2 vf0 Q)
(l.w t2 spec-reg 8)
(add.i v1 r0 7)
(b.eq field-reg v1 store-sound :delay (add.i t1 r0 1))
(b.eq a1 t1 store-float :delay (add.i t1 r0 2))
(b.eq a1 t1 store-quantized-float :delay (add.i t1 r0 3))
(b.eq a1 t1 copy-field :delay (add.i t1 r0 5))
(b.eq a1 t1 load-symbol :delay (add.i t1 r0 6))
(b.eq a1 t1 lookup-launcher :delay (add.i t1 r0 4))
(b.eq a1 t1 store-object :delay (nop!))
(b.z t2 store-integer :delay (nop!))
;; Mix the square root of the current VU random value back into R, then use the next value in
;; [0, 1). The common integer path truncates the scaled random term before adding the initial
;; value.
(vrxorw vf2)
(l.w t1 spec-reg 12)
(vrnext.xyzw vf1)
(l.w t3 spec-reg 4)
(sub.w.vf vf1 vf1 vf0)
(nop!)
(m vf2 t2)
(nop!)
(itof.vf vf2 vf2)
(nop!)
(mul.vf vf1 vf1 vf2)
(nop!)
(ftoi.vf vf1 vf1)
(nop!)
(m t2 vf1)
(nop!)
(mult3 t2 t2 t1)
(nop!)
(add t2 t2 t3)
(add.i field-reg field-reg 1)
(s.w t2 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
;; Integer fields use initial + trunc(random * range) * multiplier.
(label store-integer)
(l.w t3 spec-reg 4)
(add.i field-reg field-reg 1)
(s.w t3 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
(label store-sound)
(l.w t3 spec-reg 4)
(add.i field-reg field-reg 1)
(s.w t3 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
;; Floating fields use initial + random * range * multiplier.
(label store-float)
(b.z t2 store-plain-float :delay (vrxorw vf2))
(vrnext.xyzw vf1)
(l.w t1 spec-reg 12)
(sub.w.vf vf1 vf1 vf0)
(l.w t3 spec-reg 4)
(m vf2 t2)
(nop!)
(mul.vf vf1 vf1 vf2)
(nop!)
(m vf2 t1)
(nop!)
(mul.vf vf1 vf1 vf2)
(nop!)
(m vf2 t3)
(nop!)
(add.vf vf1 vf1 vf2)
(nop!)
(m t2 vf1)
(add.i field-reg field-reg 1)
(s.w t2 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
(label store-plain-float)
(l.w t3 spec-reg 4)
(add.i field-reg field-reg 1)
(s.w t3 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
;; This form chooses one of range+1 integer steps, then stores the scaled result as a float.
(label store-quantized-float)
(b.z t2 store-plain-float :delay (vrxorw vf2))
(vrnext.xyzw vf1)
(l.w t1 spec-reg 12)
(sub.w.vf vf1 vf1 vf0)
(add.i t2 t2 1)
(m vf2 t2)
(l.w t3 spec-reg 4)
(itof.vf vf2 vf2)
(nop!)
(mul.vf vf1 vf1 vf2)
(nop!)
(ftoi.vf vf1 vf1)
(nop!)
(itof.vf vf1 vf1)
(nop!)
(m vf2 t1)
(nop!)
(mul.vf vf1 vf1 vf2)
(nop!)
(m vf2 t3)
(nop!)
(add.vf vf1 vf1 vf2)
(nop!)
(m t2 vf1)
(add.i field-reg field-reg 1)
(s.w t2 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
;; initial-value is a negative field offset, relative to the destination currently being written.
(label copy-field)
(l.w t1 spec-reg 4)
(nop!)
(sll t1 t1 2)
(nop!)
(add t1 t1 dest-reg)
(nop!)
(l.w t3 t1)
(add.i field-reg field-reg 1)
(s.w t3 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
(label load-symbol)
(l.w t1 spec-reg 4)
(nop!)
(l.w t3 t1)
(add.i field-reg field-reg 1)
(s.w t3 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
(label lookup-launcher)
(m! t1 *part-id-table*)
(nop!)
(l.w t3 spec-reg 4)
(nop!)
(sll t3 t3 2)
(add.i t1 t1 12)
(add t3 t3 t1)
(nop!)
(l.w t2 t3)
(add.i field-reg field-reg 1)
(s.w t2 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
(label store-object)
(l.w t3 spec-reg 4)
(add.i field-reg field-reg 1)
(s.w t3 dest-reg)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (add.i spec-reg spec-reg 16))
(jr ra :delay (nop!))
;; Missing fields are either cleared or left intact. The branch-likely keeps the store in the
;; taken path, which is needed by relaunches that preserve unspecified fields.
(label missing-field)
(b.nel write-missing-reg s7 advance-field :delay (s.w r0 dest-reg))
(label advance-field)
(add.i field-reg field-reg 1)
(add.i dest-reg dest-reg 4)
(b.ne field-reg end-field-reg field-loop :delay (nop!))
(label done)
(jr ra :delay (nop!))
(jr ra :delay (add sp sp r0))
(nop! :count 3))))
(#unless PC_PORT
;; Cache layout:
;;
;; +0 number of populated entries
;; +4 hash of the last texture
;; +8 shader for the last texture
;; +12 eighty uint16 texture hashes
;; +172 eighty 80-byte adgif shaders
;;
;; The last pair handles consecutive uses without a scan. On a general hit, the hash and shader
;; cursors advance together. A miss fills the next pair of slots; after all eighty slots are used,
;; the shader is built directly in the caller's output instead of evicting an entry.
(defun particle-adgif ((shader-out adgif-shader) (tex-id texture-id))
"Copy the particle shader for tex-id to shader-out, filling a free cache entry when possible."
(declare (asm-func none))
(rlet ((shader-out :reg a0)
(cache :reg t1)
(remaining :reg t2)
(hash-cursor :reg t3)
(shader-cursor :reg t4)
(texture-hash :reg a3)
(shader-qw0 :reg vf16)
(shader-qw1 :reg vf17)
(shader-qw2 :reg vf18)
(shader-qw3 :reg vf19)
(shader-qw4 :reg vf20))
(label entry)
(m v1 shader-out)
(m v1 a1)
(nop!)
;; Fold the texture ID's upper fields into a compact key. The cache keeps the full expanded
;; shader, so a hit needs only five quadword loads and stores.
(sra texture-hash a1 20)
(m! cache *particle-adgif-cache*)
(sra t0 a1 8)
(l.w remaining cache)
(xor texture-hash texture-hash t0)
(l.hu v1 cache 4)
(and.i texture-hash texture-hash #xffff)
(l.w shader-cursor cache 8)
(b.eq v1 texture-hash cache-hit :delay (add.i hash-cursor cache 12))
(b.z remaining cache-miss :delay (add.i shader-cursor cache 172))
;; tidhash and spadgif advance in parallel. The folded 16-bit key avoids comparing or
;; dereferencing texture objects during the common lookup path.
(label scan-cache)
(l.hu v1 hash-cursor)
(add.i hash-cursor hash-cursor 2)
(nop! :count 2)
(b.eq v1 texture-hash cache-hit :delay (add.i remaining remaining -1))
(b.nz remaining scan-cache :delay (add.i shader-cursor shader-cursor 80))
;; Fill the next free slot until all eighty entries are used. Once full, initialize the
;; caller's shader directly without changing the cache.
(label cache-miss)
(add.i sp sp -16)
(l.w v1 cache)
(add.i v1 v1 -80)
(s.w shader-out sp)
(b.z v1 uncached-miss :delay (add.i v1 v1 81))
(s.h texture-hash hash-cursor)
(nop!)
(s.w shader-cursor sp 4)
(m shader-out shader-cursor)
(m! t9 particle-setup-adgif)
(nop!)
(s.w ra sp 8)
(nop!)
(jalr ra t9 :delay (s.w v1 cache))
(l.w shader-out sp)
(nop!)
(l.w shader-cursor sp 4)
(nop!)
(l.w ra sp 8)
(add.i sp sp 16)
;; A shader is five quadwords; copy it directly and make this entry the next fast-path hit.
(label cache-hit)
(l.vf shader-qw0 shader-cursor)
(l.vf shader-qw1 shader-cursor 16)
(l.vf shader-qw2 shader-cursor 32)
(l.vf shader-qw3 shader-cursor 48)
(l.vf shader-qw4 shader-cursor 64)
(s.vf shader-qw0 shader-out)
(s.vf shader-qw1 shader-out 16)
(s.vf shader-qw2 shader-out 32)
(s.vf shader-qw3 shader-out 48)
(s.vf shader-qw4 shader-out 64)
(s.w shader-cursor cache 8)
(s.h texture-hash cache 4)
(jr ra :delay (nop!))
;; A full cache still uses particle-setup-adgif; only the destination changes.
(label uncached-miss)
(s.w shader-cursor sp 4)
(nop!)
(m! t9 particle-setup-adgif)
(nop!)
(s.w ra sp 8)
(nop!)
(jalr ra t9 :delay (nop!))
(l.w shader-cursor sp 4)
(nop!)
(l.w ra sp 8)
(add.i sp sp 16)
(jr ra :delay (nop!))
(jr ra :delay (add sp sp r0))
(nop! :count 2))))
(#unless PC_PORT
;; The launcher decodes its definition into two stack records before entering the particle loop:
;;
;; +96 sparticle-birthinfo
;; texture, animation, birth callback, count, and sound shared by this launch
;;
;; +128 three sprite quadwords
;; position.xyz / scale-x
;; 2D: flags / matrix mode / angle / scale-y
;; 3D: quaternion.xyz / scale-y
;; color.rgba
;;
;; The first two sprite quadwords also have the sparticle-launchinfo layout while launch rotation
;; is being applied: position is the launch offset and the second quadword holds its Euler angles.
;; A 3D particle converts those angles to the renderer's xyz-only quaternion before publication.
;; Keeping the packet in scratch until the end lets bindings, callbacks, level ownership, and
;; time-of-day color finish without exposing a half-initialized sprite to the update pass.
;;
;; Birth counts use a floating accumulator, either on launch-state for an instanced group item or
;; on the launcher itself. Each completed particle consumes one whole unit, leaving the fraction
;; for the next call.
(defun sp-launch-particles-var ((sys sparticle-system)
(launcher sparticle-launcher)
(pos vector)
(launch-state sparticle-launch-state)
(launch-control sparticle-launch-control)
(rate float))
"Birth rate-scaled particles, initialize their renderer and simulation state, and attach them to
the optional launch control."
(declare (asm-func none) (allow-saved-regs))
(rlet ((system-reg :reg s3)
(launcher-reg :reg s4)
(launch-state-reg :reg s0)
(launch-control-reg :reg s1)
(cpuinfo-reg :reg s2)
(particle-flags :reg s5)
(is-3d :reg s6)
(origin-vf :reg vf30)
(max-color-vf :reg vf31))
(label entry)
(m v1 a0)
(m v1 a1)
(m v1 a2)
(m v1 a3)
(m v1 t0)
(m v1 t1)
(nop!)
;; Save the persistent arguments and check whether this call can launch immediately.
(add.i sp sp -256)
(nop!)
(m! v1 *sp-launcher-enable*)
(s.w ra sp)
(b.eq v1 s7 finish-disabled :delay (s.q launch-state-reg sp 16))
(m! v1 *sp-launcher-lock*)
(s.q launch-control-reg sp 32)
(b.eq v1 s7 begin-launch :delay (s.q cpuinfo-reg sp 48))
(b.ne a3 s7 bad-nested-launch :delay (lui v1 #x3f80))
(b.ne t0 s7 bad-nested-launch :delay (nop!))
(b.ne t1 v1 bad-nested-launch :delay (nop!))
(m! t9 sp-queue-launch)
(nop!)
(jalr ra t9 :delay (nop!))
(l.w ra sp)
(nop!)
(label finish-disabled)
(jr ra :delay (add.i sp sp 256))
;; Reentrant launches may only use the default state, control, and rate because the deferred
;; queue stores only system, launcher, and position. Trap instead of silently discarding state.
(label bad-nested-launch)
(s.d r0 r0 2)
(nop!)
;; Decode the misc range once. birthinfo holds the texture, animation, callback, count, and sound
;; shared by all particles born in this call.
(label begin-launch)
(s.q system-reg sp 176)
(nop!)
(s.q launcher-reg sp 192)
(nop!)
(s.q particle-flags sp 208)
(nop!)
(s.q is-3d sp 224)
(nop!)
(l.vf origin-vf a2)
(m f1 t1)
(s.vf origin-vf sp 64)
(lui v1 #x437f)
(m system-reg a0)
(m max-color-vf v1)
(m launcher-reg a1)
(l.w is-3d system-reg 24) ;; sparticle-system.is-3d
(m launch-state-reg a3)
(m launch-control-reg t0)
(l.w a1 a1 8)
(add.i a0 sp 96)
(add.i a2 r0 (sp-field-id misc-fields-start))
(add.i a3 r0 (sp-field-id misc-fields-end))
(m! t9 sp-init-fields!)
(add.i t0 s7 8)
(jalr ra t9 :delay (nop!))
(s.w v0 sp 12)
(nop!)
(l.s f2 sp 116) ;; sparticle-birthinfo.num-to-birth
(nop!)
;; Scale the launcher's requested count by rate, then add it to the selected accumulator.
(b.eq launch-state-reg s7 use-launcher-accum :delay (mul.s f1 f2 f1))
(l.s f2 launch-state-reg 24)
(nop!)
(add.s f2 f2 f1)
(nop!)
(s.s f2 launch-state-reg 24)
(nop!)
(cvt.w.s f2 f2)
(nop!)
(m v1 f2)
(nop!)
(b.z v1 finish :delay (nop!))
(b particle-loop :delay (nop!))
(label use-launcher-accum)
(nop!)
(l.s f2 launcher-reg)
(nop!)
(add.s f2 f2 f1)
(nop!)
(s.s f2 launcher-reg)
(nop!)
(cvt.w.s f2 f2)
(nop!)
(m v1 f2)
(nop!)
(b.z v1 finish :delay (nop!))
;; The integer part of the accumulator is the number to birth now. Each successful iteration
;; subtracts one, leaving the fractional remainder for the next call.
(label particle-loop)
(add.i a1 r0 0)
(m a2 s7)
(b.eq launch-control-reg s7 allocate-particle :delay (l.w v1 launch-control-reg 12))
(b.eq v1 s7 allocate-particle :delay (l.h v1 v1 6))
(and.i a0 v1 (sp-group-flag screen-space))
(add.i a3 r0 1)
(movn a1 a3 a0)
;; Group flag bit 3 passes the item's launch-state to the allocator.
(and.i a0 v1 8)
(movn a2 launch-state-reg a0)
(nop!)
;; Screen-space groups allocate from the alternate particle group. A binding can also supply the
;; launch-state pointer used by sp-get-particle.
(label allocate-particle)
(m! t9 sp-get-particle)
(m a0 system-reg)
(jalr ra t9 :delay (nop!))
(b.eq v0 s7 finish :delay (m cpuinfo-reg v0))
(add.i a0 sp 128)
(l.w a1 sp 12)
(add.i a2 r0 (sp-field-id sprite-fields-start))
(add.i a3 r0 (sp-field-id sprite-fields-end))
(m! t9 sp-init-fields!)
(add.i t0 s7 8)
(jalr ra t9 :delay (nop!))
(add.i a0 cpuinfo-reg 12)
(m a1 v0)
(add.i a2 r0 (sp-field-id cpu-fields-start))
(add.i a3 r0 (sp-field-id cpu-fields-end))
(jalr ra t9 :delay (add.i t0 s7 8))
(s.w v0 sp 80)
(nop!)
(l.w particle-flags cpuinfo-reg 104) ;; sparticle-cpuinfo.flags
(nop!)
;; The second sprite quadword has different meanings for 2D and 3D particles. Normalize the
;; matrix selector and signed 16-bit angle only for the 2D layout.
(b.ne is-3d s7 clamp-color :delay (nop!))
(b.eq launch-control-reg s7 default-matrix-mode :delay (l.w v1 launch-control-reg 12))
(b.eq v1 s7 default-matrix-mode :delay (l.h v1 v1 6))
(and.i v1 v1 (sp-group-flag screen-space))
(m a0 particle-flags)
(b.z v1 default-matrix-mode :delay (l.w v1 launch-control-reg 28))
(and.i a0 a0 (sp-cpuinfo-flag aux-list))
(add.i a1 r0 1)
(movn v1 a1 a0)
(nop!)
(b normalize-matrix-mode :delay (s.w v1 sp 148))
(label default-matrix-mode)
(and.i v1 particle-flags (sp-cpuinfo-flag no-fog))
(nop!)
(sra v1 v1 14)
(nop!)
(s.w v1 sp 148)
(nop!)
;; Screen-space controls supply their selected matrix mode, with auxiliary sprites forced to
;; screen mode. Otherwise the no-fog convention selects screen instead of camera space.
;; Truncate the packed rotation field to the signed 16-bit angle used by the renderer.
(label normalize-matrix-mode)
(l.s f2 sp 152)
(nop!)
(cvt.w.s f2 f2)
(nop!)
(m v1 f2)
(nop!)
(dsll32 v1 v1 16)
(nop!)
(sra32 v1 v1 16)
(nop!)
(m f2 v1)
(nop!)
(cvt.s.w f2 f2)
(nop!)
(s.s f2 sp 152)
(nop!)
(label clamp-color)
(and.i v1 particle-flags (sp-cpuinfo-flag aux-list))
(rlet ((particle-color :reg vf4))
(l.vf particle-color sp 160)
(b.nz v1 make-axis-rotation :delay (vminix.xyz particle-color particle-color max-color-vf))
(s.vf particle-color sp 160)
(nop!))
;; Auxiliary-list particles retain their original RGB; ordinary particles clamp it to 255.
;; For an orbiting particle, x selects a horizontal axis (cos x, 0, -sin x), y is the angle
;; about that axis, and z becomes the orbit radius.
(label make-axis-rotation)
(b.eq launch-control-reg s7 bind-particle :delay (and.i v1 particle-flags (sp-cpuinfo-flag orbit)))
(b.z v1 bind-particle :delay (l.w a0 sp 128))
(m! t9 cos)
(nop!)
(jalr ra t9 :delay (nop!))
(m a1 v0)
(l.w a0 sp 128)
(m! t9 sin)
(nop!)
(jalr ra t9 :delay (nop!))
(ori v1 r0 #x8000)
(sll v1 v1 16)
(l.w a0 sp 136)
(xor a3 v0 v1)
(s.w a0 cpuinfo-reg 8)
(m a2 r0)
(l.w t0 sp 132)
(m! t9 quaternion-axis-angle!)
(add.i a0 cpuinfo-reg 80)
(jalr ra t9 :delay (nop!))
(l.w v1 launch-state-reg 16)
(nop!)
(l.w v1 v1)
(nop!)
(s.w v1 cpuinfo-reg 108)
(nop!)
(label bind-particle)
(s.w s7 cpuinfo-reg 136)
(nop!)
(b.eq launch-state-reg s7 apply-launch-fields :delay (l.w a0 launch-state-reg))
(l.w a1 a0 24)
(nop!)
(b.z a1 apply-launch-fields :delay (nop!))
(l.w a2 launch-control-reg)
(add.i a3 launch-control-reg 60)
;; A launch-control binding records this particle in the first matching free state slot. The state
;; then owns the CPU record and, for ordinary births, the allocated sprite.
(label scan-bindings)
(l.w v1 a3)
(add.i a2 a2 -1)
(l.w v1 v1)
(nop!)
(b.ne v1 a1 next-binding :delay (l.h v1 a3 4))
(and.i a0 v1 (sp-launch-state-flags enabled))
(ori v1 v1 (sp-launch-state-flags enabled))
(b.nz a0 next-binding :delay (nop!))
(s.h v1 a3 4)
(nop!)
(l.w v1 cpuinfo-reg)
(nop!)
(s.w v1 a3 8)
(m a0 s7)
(b.nel is-3d s7 install-binding :delay (l.w a0 cpuinfo-reg))
(label install-binding)
(s.w a0 a3 12)
(nop!)
(s.w cpuinfo-reg a3 16)
(nop!)
(s.w a3 cpuinfo-reg 136)
(nop!)
(b apply-launch-fields :delay (nop!))
(label next-binding)
(b.nz a2 scan-bindings :delay (add.i a3 a3 32))
;; Launch-field specs follow the CPU range. Apply them only when that range is present, then fold
;; in 3D Euler rotation and the owning launch-control transform.
(label apply-launch-fields)
(l.w a2 sp 80)
(add.i a0 sp 128)
(l.h v1 a2)
(m a1 cpuinfo-reg)
(m! t9 sp-adjust-launch)
(add.i v1 v1 -64)
(b.ge v1 r0 after-launch-adjust :delay (nop!))
(jalr ra t9 :delay (nop!))
(label after-launch-adjust)
(b.eq is-3d s7 after-euler-convert :delay (nop!))
(m! t9 sp-euler-convert)
(add.i a0 sp 128)
(jalr ra t9 :delay (m a1 cpuinfo-reg))
(label after-euler-convert)
(b.eq launch-state-reg s7 after-system-rotation :delay (l.w a2 launch-state-reg 12))
(b.eq a2 s7 after-system-rotation :delay (add.i a0 sp 128))
(b.z a2 after-system-rotation :delay (m a1 cpuinfo-reg))
(m! t9 sp-rotate-system)
(nop!)
(jalr ra t9 :delay (nop!))
;; Offset the initialized position by the requested origin. The shader cache expands the compact
;; texture ID into the five-quadword ADGIF used by the sprite renderer.
(label after-system-rotation)
(rlet ((launch-position :reg vf4))
(l.vf launch-position sp 128)
(nop!)
(add.vf.xyz launch-position launch-position origin-vf)
(l.w a0 cpuinfo-reg 4)
(l.w a1 sp 96)
(nop!)
(m! t9 particle-adgif)
(nop!)
(s.vf launch-position sp 128)
(nop!)
(jalr ra t9 :delay (nop!)))
;; Override the texture's ALPHA register for erase-destination, subtractive, or additive
;; blending, in that priority order. write-depth replaces the ZBUF setting.
(l.w a2 cpuinfo-reg 4)
(lui a3 256)
(l.w v1 a2 64)
(and.i a0 particle-flags (sp-cpuinfo-flag blend-erase-dest))
(add.i a1 r0 134)
(nop!)
(movn v1 a1 a0)
(and.i a0 particle-flags (sp-cpuinfo-flag subtract-blend))
(add.i a1 r0 66)
(nop!)
(movn v1 a1 a0)
(and.i a0 particle-flags (sp-cpuinfo-flag glow))
(add.i a1 r0 72)
(nop!)
(movn v1 a1 a0)
(and.i a0 particle-flags (sp-cpuinfo-flag write-depth))
(s.w v1 a2 64)
(ori a1 a3 448)
(b.nzl a0 after-blend-mode :delay (s.d a1 a2 48))
;; The optional birth callback runs after all standard fields and transforms, so it can make
;; effect-specific final changes to both the CPU record and the scratch sprite packet.
(label after-blend-mode)
(l.w t9 sp 108) ;; sparticle-birthinfo.birth-func
(m a1 cpuinfo-reg)
(m a0 system-reg)
(m t0 launch-state-reg)
(b.z t9 after-birth-callback :delay (add.i a2 sp 128))
(jalr ra t9 :delay (m a3 launcher-reg))
(label after-birth-callback)
(l.w a0 sp 120) ;; sparticle-birthinfo.sound
(nop!)
(l.s f2 launcher-reg 4)
(lui a1 #x3f80)
(b.z a0 after-sound :delay (nop!))
(l.s f3 a0 4)
(nop!)
(add.s f2 f2 f3)
(nop!)
(s.s f2 launcher-reg 4)
(m f3 a1)
(sub.s f2 f2 f3)
(nop!)
(m a1 f2)
(nop!)
(b.lt a1 r0 after-sound :delay (m! t9 new-sound-id))
(s.w a1 launcher-reg 4)
(nop!)
(jalr ra t9 :delay (nop!))
(l.w a0 sp 120)
(m a1 v0)
(m! t9 sound-play-by-spec)
(add.i a2 sp 64)
(jalr ra t9 :delay (nop!))
;; Auxiliary-list particles use a separate update callback and minimum lifetime. Level ownership
;; bits select the heap range that contains the launcher so relaunch pointers remain valid.
(label after-sound)
(add.i a0 r0 4)
(and.i v1 particle-flags (sp-cpuinfo-flag aux-list))
(b.z v1 after-aux-list :delay (sub a0 r0 a0))
(m! v1 add-to-sprite-aux-list)
(add.i a0 a0 -1)
(s.w v1 cpuinfo-reg 112)
(and particle-flags particle-flags a0)
(s.w r0 sp 172)
(nop!)
(s.w r0 cpuinfo-reg 60)
(nop!)
(l.w v1 sp 144)
(add.i a0 r0 3)
(max.w v1 v1 a0)
(add.i a0 r0 11)
(pminw v1 v1 a0)
(nop!)
(s.w v1 sp 144)
(nop!)
(label after-aux-list)
(m! a0 *level*)
(nop!)
;; A statically defined launcher belongs to a level when its address lies between that
;; level's heap base and top-base. Tag it so level unload can kill or relaunch it safely.
(l.w v1 a0 124) ;; level0.heap.base
(nop!)
(sub v1 launcher-reg v1)
(nop!)
(b.lt v1 r0 test-level1 :delay (nop!))
(l.w v1 a0 128) ;; level0.heap.top-base
(nop!)
(sub v1 launcher-reg v1)
(nop!)
(b.ge v1 r0 test-level1 :delay (nop!))
(b apply-time-of-day :delay (ori particle-flags particle-flags (sp-cpuinfo-flag level0)))
(label test-level1)
(l.w v1 a0 2732) ;; level1.heap.base
(nop!)
(sub v1 launcher-reg v1)
(nop!)
(b.lt v1 r0 apply-time-of-day :delay (nop!))
(l.w v1 a0 2736) ;; level1.heap.top-base
(nop!)
(sub v1 launcher-reg v1)
(nop!)
(b.ge v1 r0 apply-time-of-day :delay (nop!))
(ori particle-flags particle-flags (sp-cpuinfo-flag level1))
(nop!)
;; Time-of-day-enabled particles outside the auxiliary list are tinted. Apply the same RGB
;; scale to both the initial color and its fade vector.
(label apply-time-of-day)
(and.i v1 particle-flags (sp-cpuinfo-flag aux-list use-time-of-day-color))
(add.i a0 r0 (sp-cpuinfo-flag use-time-of-day-color))
(b.ne v1 a0 write-sprite :delay (m! a0 *time-of-day-context*))
(rlet ((time-color :reg vf4)
(particle-color :reg vf5)
(particle-fade :reg vf6))
(l.vf time-color a0 108)
(nop!)
(l.vf particle-color sp 160)
(nop!)
(l.vf particle-fade cpuinfo-reg 48)
(nop!)
(mul.vf.xyz particle-color particle-color time-color)
(nop!)
(mul.vf.xyz particle-fade particle-fade time-color)
(nop!)
(s.vf particle-color sp 160)
(nop!)
(s.vf particle-fade cpuinfo-reg 48)
(nop!))
;; Publish the three sprite vectors last. Alpha starts at zero; cache-alpha retains the requested
;; value until the update pass processes just-launched.
(label write-sprite)
(m v1 launch-control-reg)
(sub a0 launch-control-reg s7)
(mov.z v1 r0 a0)
(nop!)
(s.w v1 cpuinfo-reg 132)
(nop!)
(l.w v1 cpuinfo-reg)
(nop!)
(rlet ((sprite-position :reg vf4)
(sprite-orientation :reg vf5)
(sprite-color :reg vf6))
(l.vf sprite-position sp 128)
(nop!)
(l.vf sprite-orientation sp 144)
(nop!)
(l.vf sprite-color sp 160)
(nop!)
(s.vf sprite-position v1)
(nop!)
(s.vf sprite-orientation v1 16)
(nop!)
(sub.w.vf.w sprite-color vf0 vf0)
(nop!)
(s.vf sprite-color v1 32)
(nop!))
(ori particle-flags particle-flags (sp-cpuinfo-flag just-launched))
(l.w a0 sp 172)
(s.w particle-flags cpuinfo-reg 104)
(nop!)
(s.w a0 cpuinfo-reg 124)
(nop!)
;; Consume one birth and continue while the selected accumulator still has a whole unit.
(b.eq launch-state-reg s7 decrement-launch-state :delay (lui v1 #x3f80))
(l.s f2 launch-state-reg 24)
(m f3 v1)
(sub.s f2 f2 f3)
(nop!)
(s.s f2 launch-state-reg 24)
(cvt.w.s f2 f2)
(m v1 f2)
(nop!)
(b.nz v1 particle-loop :delay (nop!))
(b finish :delay (nop!))
(label decrement-launch-state)
(l.s f2 launcher-reg)
(m f3 v1)
(sub.s f2 f2 f3)
(nop!)
(s.s f2 launcher-reg)
(cvt.w.s f2 f2)
(m v1 f2)
(nop!)
(b.nz v1 particle-loop :delay (nop!))
(label finish)
(l.w ra sp)
(nop!)
(l.q launch-state-reg sp 16)
(nop!)
(l.q launch-control-reg sp 32)
(nop!)
(l.q cpuinfo-reg sp 48)
(nop!)
(l.q system-reg sp 176)
(nop!)
(l.q launcher-reg sp 192)
(nop!)
(l.q particle-flags sp 208)
(nop!)
(l.q is-3d sp 224)
(nop!)
(jr ra :delay (add.i sp sp 256))
(jr ra :delay (add sp sp r0))
(nop! :count 2))))
File diff suppressed because it is too large Load Diff
@@ -18,9 +18,9 @@
(defun sp-particle-copy! ((dst sparticle-cpuinfo) (src sparticle-cpuinfo))
"Copy src's rendered sprite, shader, and simulated CPU state to dst without replacing dst's
pool bookkeeping, ownership key, binding, or cached record pointers."
(let ((position-and-scale (-> src sprite x-y-z-sx quad))) (set! (-> dst sprite x-y-z-sx quad) position-and-scale))
(let ((rotation-and-scale (-> src sprite flag-rot-sy quad))) (set! (-> dst sprite flag-rot-sy quad) rotation-and-scale))
(let ((color (-> src sprite color quad))) (set! (-> dst sprite color quad) color))
(vector-copy! (-> dst sprite x-y-z-sx) (-> src sprite x-y-z-sx))
(vector-copy! (-> dst sprite flag-rot-sy) (-> src sprite flag-rot-sy))
(vector-copy! (-> dst sprite color) (-> src sprite color))
(dotimes (i 10)
(set! (-> dst adgif prims i) (-> src adgif prims i)))
(vector-copy! (-> dst vel-sxvel) (-> src vel-sxvel))
@@ -143,67 +143,6 @@
0
(none))
(#unless PC_PORT
(defun sp-get-particle ((sys sparticle-system) (group int) (binding sparticle-launch-state))
"Allocate a free particle in group, beginning at binding's rotating block index when supplied.
Clear its bitmap bit, mark its CPU record valid, and return it; return false when full."
(let ((block-offset 0)
(num-blocks (-> sys blocks 0))
(block-index 0))
(when (= group 1)
(set! block-offset num-blocks)
(set! num-blocks (-> sys blocks 1)))
;; Rotate the first block examined so repeated launchers spread their allocations.
(when binding
(set! block-index (the int (-> binding randomize)))
(+! (-> binding randomize) 1)
(when (= (-> binding randomize) num-blocks)
(set! (-> binding randomize) (the uint 0))))
(dotimes (i num-blocks)
(let ((free-bits (-> sys alloc-table (+ block-offset block-index))))
(when (nonzero? free-bits)
;; Find the least-significant set bit in six branchless halves. Shifting a candidate
;; half to the sign end lets mov.nz keep it when that half contains a free slot;
;; mov.z advances the bit index only when the candidate half is empty.
(let ((free-bit (rlet ((bits :reg t1)
(shifted :reg t2)
(candidate-index :reg t3)
(bit-index :reg a2))
(m! bits free-bits)
(m! bit-index 0)
(sll32 shifted bits)
(add.i candidate-index bit-index 32)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 16)
(add.i candidate-index bit-index 16)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 8)
(add.i candidate-index bit-index 8)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 4)
(add.i candidate-index bit-index 4)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 2)
(add.i candidate-index bit-index 2)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
(sll shifted bits 1)
(add.i candidate-index bit-index 1)
(mov.nz bits shifted shifted)
(mov.z bit-index candidate-index shifted)
bit-index)))
(let ((slot (+ free-bit (* (+ block-offset block-index) 64))))
(logxor! (-> sys alloc-table (+ block-offset block-index)) (ash 1 free-bit))
(+! (-> sys num-alloc group) 1)
(let ((cpuinfo (-> sys cpuinfo-table slot))) (set! (-> cpuinfo valid) #t) (return cpuinfo))))))
(+! block-index 1)
(if (= block-index num-blocks) (set! block-index 0))))
(the-as sparticle-cpuinfo #f)))
(#when PC_PORT
(defun sp-get-particle ((sys sparticle-system) (group int) (binding sparticle-launch-state))
"Allocate the first free particle in group, clear its bitmap bit, and return its CPU record.
@@ -237,7 +176,7 @@
timer so its block update can perform the release safely."
(cond
;; modified to check the end of the spad too.
((in-scratchpad? cpuinfo) ;;(>= (the-as int cpuinfo) #x70000000) ;; TODO PC port
((in-scratchpad? cpuinfo)
;; If in the scratchpad, just set the timer to 0.
(set! (-> cpuinfo timer) 0)
0)
@@ -295,588 +234,232 @@
;; particle update
;;;;;;;;;;;;;;;;;;;;;
(#unless PC_PORT
(defun sp-process-block-2d ((sys sparticle-system)
(defun sp-process-block-2d ((sys sparticle-system)
(scratchpad-cpuinfo int)
(scratchpad-sprite-data int)
(start-slot int)
(count int)
(paused? symbol))
"Update count 2D particle records in scratchpad: age, integrate, fade, invoke callbacks, handle
death or relaunch, and write their billboard sprite data."
(declare (asm-func none))
;; Each iteration walks one 144-byte CPU record beside one 48-byte sprite record. The sprite
;; record is three quadwords:
;;
;; +0 position.xyz, x-scale
;; +16 sprite flags/matrix mode, rotation, y-scale
;; +32 color.rgba
;;
;; The matching CPU record keeps velocity.xyz/x-scale velocity at +16, rotation/y-scale
;; velocity at +32, color fade at +48, acceleration at +64, friction at +96, timer at +100,
;; flags at +104, the per-particle update function at +112, relaunch state at +116/+120, cached
;; launch alpha at +124, and validity at +128. Keeping these records separate lets the sprite
;; record remain ready for the renderer while simulation-only state stays in the CPU record.
;;
;; *sp-frame-time*.x carries the integer 300 Hz tick count in its low byte. Its y lane scales
;; position, rotation, scale, and color increments, while z and w express the same interval in
;; 60 Hz frames for acceleration and friction. Invalid slots are skipped. Pausing freezes a
;; particle unless run-while-paused is set. A timer of -1 never expires; zero frees the particle.
;; On the first visible update, just-launched restores the intended alpha from cache-alpha.
(rlet ((sys :reg gp)
(cpuinfo :reg s5 :type sparticle-cpuinfo)
(sprite-data :reg s4 :type sprite-vec-data-2d)
(slot :reg s1)
(remaining :reg s3)
(paused? :reg s2)
(ticks :reg s0)
(frame-time :reg vf9)
(sprite-pos-scale :reg vf1)
(sprite-rot-scale :reg vf2)
(sprite-color :reg vf3)
(velocity-scale-rate :reg vf4)
(rotation-scale-rate :reg vf5)
(color-fade-rate :reg vf6)
(acceleration :reg vf7)
(friction-factor :reg vf8)
(position-step :reg vf10)
(rotation-scale-step :reg vf11)
(color-step :reg vf12))
;; Establish the scratchpad cursors and recover this frame's integer tick count.
(asm-block setup-2d
(label particle-update-2d-entry)
(add.i sp sp -128)
(s.d ra sp)
(s.q s0 sp 16)
(s.q s1 sp 32)
(s.q s2 sp 48)
(s.q s3 sp 64)
(s.q s4 sp 80)
(s.q s5 sp 96)
(s.q gp sp 112)
(m sys a0)
(m cpuinfo a1)
(m sprite-data a2)
(m slot a3)
(m remaining t0)
(m paused? t1)
(m! v1 *sp-frame-time*)
(l.vf frame-time v1)
(m v1 frame-time)
(and.i ticks v1 255))
;; Reject unused slots, then decide whether a paused particle is allowed to simulate.
(asm-block particle-lifetime-2d
(label particle-loop)
(l.w.field v1 cpuinfo (valid))
(b.eq v1 s7 next-particle :delay (nop!))
(b.eq paused? s7 simulate-particle :delay (l.w.field v1 cpuinfo (flags)))
(and.i v1 v1 #x2000)
(b.nz v1 simulate-particle :delay (nop!))
(l.w.field v1 cpuinfo (timer))
(add.i a0 r0 -1)
(b.eq v1 a0 reveal-new-particle :delay (nop!))
(b.z v1 free-particle :delay (nop!))
;; A newly launched particle is hidden with alpha zero. Reveal it without advancing time when
;; the rest of its simulation is frozen.
(label reveal-new-particle)
(l.w.field a0 cpuinfo (flags))
(and.i v1 a0 64)
(xor a0 a0 v1)
(b.z v1 next-particle :delay (s.w.field a0 cpuinfo (flags)))
(l.w.field v1 cpuinfo (cache-alpha))
(b next-particle :delay (s.w.field v1 sprite-data (a)))
;; Age finite particles in 300 Hz ticks. The packed maximum clamps an overshoot to zero so the
;; next iteration follows the ordinary zero-timer death path.
(label simulate-particle)
(l.w.field v1 cpuinfo (timer))
(add.i a0 r0 -1)
(b.eq v1 a0 run-callback :delay (sub a0 v1 ticks))
(b.z v1 free-particle :delay (max.w v1 a0 r0))
(s.w.field v1 cpuinfo (timer))
;; Restore the launch alpha before callbacks so custom particle logic sees the visible state.
(label run-callback)
(l.w.field a0 cpuinfo (flags))
(and.i v1 a0 64)
(xor a0 a0 v1)
(b.z v1 call-update-function :delay (s.w.field a0 cpuinfo (flags)))
(l.w.field v1 cpuinfo (cache-alpha))
(s.w.field v1 sprite-data (a)))
;; The callback may edit either the CPU record or its renderer-facing sprite record.
(asm-block callback-and-relaunch-2d
(label call-update-function)
(l.w.field t9 cpuinfo (func))
(b.z t9 check-relaunch :delay (nop!))
(add.i sp sp -80)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(m a0 sys)
(m a1 cpuinfo)
(m a2 sprite-data)
(jalr ra t9 :delay (s.q remaining sp 64))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(add.i sp sp 80)
;; A nonfalse next-launcher owns a countdown independent of the particle lifetime. Relaunch as
;; soon as subtracting this frame's ticks carries it below zero.
(label check-relaunch)
(l.w.field a1 cpuinfo (next-launcher))
(l.w.field v1 cpuinfo (next-time))
(b.z a1 integrate :delay (sub a0 v1 ticks))
(add.i v1 a0 -1)
(b.ge v1 r0 integrate :delay (s.w.field a0 cpuinfo (next-time)))
(add.i sp sp -96)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(s.q remaining sp 64)
(s.q paused? sp 80)
(m a0 sys)
(m a3 sprite-data)
(m a2 cpuinfo)
(m! t9 sp-relaunch-particle-2d)
(jalr ra t9 :delay (sll v0 ra 0))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(l.q paused? sp 80)
(add.i sp sp 96))
;; Integrate the four adjacent CPU vectors directly into the three sprite quadwords.
(asm-block integrate-motion-2d
(label integrate)
(l.vf.field sprite-pos-scale sprite-data (x-y-z-sx))
(l.vf.field sprite-rot-scale sprite-data (flag-rot-sy))
(l.vf.field sprite-color sprite-data (r-g-b-a))
(l.vf.field velocity-scale-rate cpuinfo (vel-sxvel))
(l.vf.field rotation-scale-rate cpuinfo (rot-syvel))
(l.vf.field color-fade-rate cpuinfo (fade))
(l.vf.field acceleration cpuinfo (acc))
(l.s.field f0 cpuinfo (friction))
(m v1 f0)
;; Acceleration uses 60 Hz frame units. Nonzero friction is converted to a retained-velocity
;; factor for this interval, then applied to xyz without disturbing x-scale velocity in w.
(mul.z.vf acceleration acceleration frame-time)
(b.z v1 no-friction :delay (add.vf.xyz velocity-scale-rate velocity-scale-rate acceleration))
(m friction-factor v1)
(sub.x.vf.w friction-factor vf0 friction-factor)
(mul.w.vf friction-factor friction-factor frame-time)
(sub.w.vf.w friction-factor vf0 friction-factor)
(mul.w.vf.xyz velocity-scale-rate velocity-scale-rate friction-factor)
(label no-friction)
;; Apply velocity, rotation/scale velocity, and fade with the full 300 Hz tick count. Clamp
;; color to zero so the packed death tests below can compare its float bits as integers.
(mul.y.vf position-step velocity-scale-rate frame-time)
(mul.y.vf rotation-scale-step rotation-scale-rate frame-time)
(mul.y.vf color-step color-fade-rate frame-time)
(add.vf sprite-pos-scale sprite-pos-scale position-step)
(add.vf.zw sprite-rot-scale sprite-rot-scale rotation-scale-step)
(add.vf sprite-color sprite-color color-step)
(max.x.vf sprite-color sprite-color vf0)
(s.vf.field velocity-scale-rate cpuinfo (vel-sxvel))
(s.vf.field sprite-pos-scale sprite-data (x-y-z-sx))
(s.vf.field sprite-rot-scale sprite-data (flag-rot-sy))
(s.vf.field sprite-color sprite-data (r-g-b-a)))
;; 2D rotation is kept as an integer-valued float, matching the sprite packet format.
(asm-block billboard-post-update
(l.s.field f0 sprite-data (rot))
(cvt.w.s f0 f0)
(m v1 f0)
(sll32 v1 v1 16)
(sra32 v1 v1 16)
(m f0 v1)
(cvt.s.w f0 f0)
(s.s.field f0 sprite-data (rot))
;; Orbiting is a post-integration position adjustment around the particle's stored center.
(l.w.field v1 cpuinfo (flags))
(and.i v1 v1 128)
(b.z v1 test-death-flags :delay (m! t9 sp-orbiter))
(add.i sp sp -96)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(s.q remaining sp 64)
(m a0 sys)
(m a1 cpuinfo)
(m a2 sprite-data)
(jalr ra t9 :delay (s.q paused? sp 80))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(l.q paused? sp 80)
(add.i sp sp 96))
;; Packed extracts test RGB together, then alpha, then x- and y-scale. These correspond to
;; die-when-color-0, die-when-faded, and die-when-scale-0.
(asm-block death-tests-2d
(label test-death-flags)
(l.q.field v1 sprite-data (r-g-b-a))
(l.w.field a0 cpuinfo (flags))
(and.i a1 a0 2)
(b.z a1 test-fade-death :delay (and.i a1 a0 4))
(b.nz v1 test-fade-death :delay (pextuw t4 v1 r0))
(b.z t4 free-particle :delay (nop!))
(label test-fade-death)
(b.z a1 test-scale-death :delay (and.i a0 a0 1))
(pcpyud t4 v1 r0)
(pexew t4 t4)
(b.le t4 r0 free-particle :delay (nop!))
(label test-scale-death)
(b.z a0 next-particle :delay (nop!))
(m v1 sprite-pos-scale)
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.lt v1 r0 free-particle :delay (m v1 sprite-rot-scale))
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.ge v1 r0 next-particle :delay (nop!))
;; Return dead particles to the bitmap-managed pool.
(label free-particle)
(m! t9 sp-free-particle)
(m a0 sys)
(m a1 slot)
(m a2 cpuinfo)
(m a3 sprite-data)
(jalr ra t9 :delay (sll v0 ra 0)))
;; Advance every parallel cursor even when this slot was invalid.
(asm-block advance-and-return-2d
(label next-particle)
(add.i remaining remaining -1)
(add.i cpuinfo cpuinfo 144)
(add.i sprite-data sprite-data 48)
(b.nz remaining particle-loop :delay (add.i slot slot 1))
;; Return the first slot after the processed block.
(m v0 slot)
(l.d ra sp)
(l.q sys sp 112)
(l.q cpuinfo sp 96)
(l.q sprite-data sp 80)
(l.q remaining sp 64)
(l.q paused? sp 48)
(l.q slot sp 32)
(l.q ticks sp 16)
(jr ra :delay (add.i sp sp 128))
(nop! :count 3)))))
"Update a scratchpad block of 2D particles using the original simulation rules."
(let ((cpuinfos (the-as (inline-array sparticle-cpuinfo) scratchpad-cpuinfo))
(sprites (the-as (inline-array sprite-vec-data-2d) scratchpad-sprite-data))
(ticks (logand (the-as int (-> *sp-frame-time* x)) 255))
(tick-scale (-> *sp-frame-time* y))
(frame-scale (-> *sp-frame-time* z)))
(dotimes (i count)
(let ((cpuinfo (-> cpuinfos i))
(sprite (-> sprites i))
(slot (+ start-slot i)))
(when (-> cpuinfo valid)
(cond
;; Paused particles do not age or simulate, but a zero timer still frees them and
;; a newly launched particle still becomes visible.
((and paused?
(not (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag run-while-paused))))
(cond
((= (-> cpuinfo timer) 0)
(sp-free-particle sys slot cpuinfo sprite))
((logtest? (-> cpuinfo flags) (sp-cpuinfo-flag just-launched))
(logclear! (-> cpuinfo flags) (sp-cpuinfo-flag just-launched))
(set! (-> sprite a) (-> cpuinfo cache-alpha)))))
;; A timer which reached zero on the preceding update dies before doing any more work.
((= (-> cpuinfo timer) 0)
(sp-free-particle sys slot cpuinfo sprite))
(else
;; -1 is an infinite lifetime. Finite timers clamp at zero and are freed next update.
(when (!= (-> cpuinfo timer) -1)
(set! (-> cpuinfo timer) (max 0 (- (-> cpuinfo timer) ticks))))
(#when PC_PORT
(def-mips2c sp-process-block-2d (function sparticle-system int int int int symbol none)))
(when (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag just-launched))
(logclear! (-> cpuinfo flags) (sp-cpuinfo-flag just-launched))
(set! (-> sprite a) (-> cpuinfo cache-alpha)))
(#unless PC_PORT
(defun sp-process-block-3d ((sys sparticle-system)
(when (nonzero? (the-as uint (-> cpuinfo func)))
((the-as (function sparticle-system
sparticle-cpuinfo
sprite-vec-data-3d
none)
(-> cpuinfo func))
sys
cpuinfo
(the-as sprite-vec-data-3d sprite)))
;; The 2D original relaunches when the decremented countdown is zero or negative.
(when (nonzero? (the-as uint (-> cpuinfo next-launcher)))
(let ((next-time (- (the-as int (-> cpuinfo next-time)) ticks)))
(set! (-> cpuinfo next-time) (the-as uint next-time))
(when (< next-time 1)
(sp-relaunch-particle-2d sys
(the-as sparticle-launcher (-> cpuinfo next-launcher))
cpuinfo
(the-as sprite-vec-data-3d sprite)))))
;; Acceleration is measured in 60 Hz frames. Friction retains
;; 1 - (1 - friction) * frame-scale of the xyz velocity.
(+! (-> cpuinfo vel-sxvel x) (* (-> cpuinfo acc x) frame-scale))
(+! (-> cpuinfo vel-sxvel y) (* (-> cpuinfo acc y) frame-scale))
(+! (-> cpuinfo vel-sxvel z) (* (-> cpuinfo acc z) frame-scale))
(when (nonzero? (the-as int (-> cpuinfo friction)))
(let ((retained (- 1.0 (* (- 1.0 (-> cpuinfo friction)) frame-scale))))
(*! (-> cpuinfo vel-sxvel x) retained)
(*! (-> cpuinfo vel-sxvel y) retained)
(*! (-> cpuinfo vel-sxvel z) retained)))
;; Velocity, scale velocity, billboard rotation, and fade use 300 Hz ticks.
(+! (-> sprite x) (* (-> cpuinfo vel-sxvel x) tick-scale))
(+! (-> sprite y) (* (-> cpuinfo vel-sxvel y) tick-scale))
(+! (-> sprite z) (* (-> cpuinfo vel-sxvel z) tick-scale))
(+! (-> sprite sx) (* (-> cpuinfo vel-sxvel w) tick-scale))
(+! (-> sprite rot) (* (-> cpuinfo rot-syvel z) tick-scale))
(+! (-> sprite sy) (* (-> cpuinfo rot-syvel w) tick-scale))
(+! (-> sprite r) (* (-> cpuinfo fade x) tick-scale))
(+! (-> sprite g) (* (-> cpuinfo fade y) tick-scale))
(+! (-> sprite b) (* (-> cpuinfo fade z) tick-scale))
(+! (-> sprite a) (* (-> cpuinfo fade w) tick-scale))
(set! (-> sprite r) (fmax (-> sprite r) 0.0))
(set! (-> sprite g) (fmax (-> sprite g) 0.0))
(set! (-> sprite b) (fmax (-> sprite b) 0.0))
(set! (-> sprite a) (fmax (-> sprite a) 0.0))
(set! (-> sprite rot) (unwrap-angle (-> sprite rot)))
(when (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag orbit))
(sp-orbiter sys cpuinfo (-> sprite x-y-z-sx)))
(let ((dies?
(or
(and (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag die-when-color-0))
(= (-> sprite r) 0.0)
(= (-> sprite g) 0.0)
(= (-> sprite b) 0.0))
(and (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag die-when-faded))
(<= (the-as int (-> sprite a)) 0))
(and (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag die-when-scale-0))
(or (< (the-as int (-> sprite sx)) 0)
(< (the-as int (-> sprite sy)) 0))))))
(when dies?
(sp-free-particle sys slot cpuinfo sprite))))))))
(none)))
(defun sp-process-block-3d ((sys sparticle-system)
(scratchpad-cpuinfo int)
(scratchpad-sprite-data int)
(start-slot int)
(count int)
(paused? symbol))
"Update count 3D particle records in scratchpad: age, integrate, rotate, fade, invoke callbacks,
handle death or relaunch, and write their oriented sprite data."
(declare (asm-func none))
;; The 3D path shares the 144-byte CPU record and 48-byte renderer record with the 2D path. The
;; renderer record still stores position.xyz/x-scale at +0 and color.rgba at +32, but its middle
;; quadword holds quaternion.xyz/y-scale rather than a billboard rotation. The complete
;; rotational velocity quaternion is kept in the CPU record at +80.
;;
;; Quaternion w is omitted from the renderer record because a unit quaternion has only three
;; independent components. Rebuild |w| from xyz, compose the rotational step, then choose the
;; sign of the stored xyz so the omitted w can remain nonnegative. q and -q represent the same
;; rotation, so the renderer can reconstruct an equivalent orientation without spending a sign
;; bit.
;;
;; Lifetime, pause, callback, relaunch, integration, and packed death tests otherwise match the
;; 2D update. A long frame of at least ten 300 Hz ticks composes the rotation step twice.
(rlet ((sys :reg gp)
(cpuinfo :reg s5 :type sparticle-cpuinfo)
(sprite-data :reg s4 :type sprite-vec-data-3d)
(slot :reg s0)
(remaining :reg s3)
(paused? :reg s2)
(rotation :reg s1)
(frame-time :reg vf16)
(sprite-pos-scale :reg vf8)
(sprite-quat-scale :reg vf9)
(sprite-color :reg vf10)
(velocity-scale-rate :reg vf11)
(rotation-scale-rate :reg vf12)
(color-fade-rate :reg vf13)
(acceleration :reg vf14)
(friction-factor :reg vf15)
(position-step :reg vf17)
(rotation-scale-step :reg vf18)
(color-step :reg vf19)
(stored-quat-scale :reg vf1)
(full-quaternion :reg vf2))
;; Establish the scratchpad cursors, reserve a complete quaternion, and cache the tick count.
(asm-block setup-3d
(label particle-update-3d-entry)
(add.i sp sp -160)
(s.d ra sp)
(s.d fp sp 8)
(m fp t9)
(s.q s0 sp 48)
(s.q s1 sp 64)
(s.q s2 sp 80)
(s.q s3 sp 96)
(s.q s4 sp 112)
(s.q s5 sp 128)
(s.q gp sp 144)
(m sys a0)
(m cpuinfo a1)
(m sprite-data a2)
(m slot a3)
(m remaining t0)
(m paused? t1)
(add.i rotation sp 16)
(s.q r0 rotation)
(m! v1 *sp-frame-time*)
(l.vf frame-time v1)
(m v1 frame-time)
(and.i v1 v1 255)
(s.q v1 sp 32)
(nop!))
;; Reject unused slots, then decide whether a paused particle is allowed to simulate.
(asm-block particle-lifetime-3d
(label particle-loop-3d)
(l.w.field v1 cpuinfo (valid))
(b.eq v1 s7 next-particle-3d :delay (nop!))
(b.eq paused? s7 simulate-particle-3d :delay (l.w.field v1 cpuinfo (flags)))
(and.i v1 v1 #x2000)
(b.nz v1 simulate-particle-3d :delay (nop!))
(l.w.field v1 cpuinfo (timer))
(add.i a0 r0 -1)
(b.eq v1 a0 reveal-new-particle-3d :delay (nop!))
(b.z v1 free-particle-3d :delay (nop!))
;; Reveal a newly launched particle without advancing it when simulation is frozen.
(label reveal-new-particle-3d)
(l.w.field a0 cpuinfo (flags))
(and.i v1 a0 64)
(xor a0 a0 v1)
(b.z v1 next-particle-3d :delay (s.w.field a0 cpuinfo (flags)))
(l.w.field v1 cpuinfo (cache-alpha))
(b next-particle-3d :delay (s.w.field v1 sprite-data (a)))
;; Age finite particles in 300 Hz ticks and clamp an overshoot to zero.
(label simulate-particle-3d)
(l.w.field v1 cpuinfo (timer))
(add.i a0 r0 -1)
(b.eq v1 a0 run-callback-3d :delay (l.q a0 sp 32))
(sub a0 v1 a0)
(b.z v1 free-particle-3d :delay (max.w v1 a0 r0))
(s.w.field v1 cpuinfo (timer))
;; Restore the launch alpha before custom particle logic runs.
(label run-callback-3d)
(l.w.field a0 cpuinfo (flags))
(and.i v1 a0 64)
(xor a0 a0 v1)
(b.z v1 call-update-function-3d :delay (s.w.field a0 cpuinfo (flags)))
(l.w.field v1 cpuinfo (cache-alpha))
(s.w.field v1 sprite-data (a)))
;; The callback may edit either the CPU record or its renderer-facing sprite record.
(asm-block callback-3d
(label call-update-function-3d)
(l.w.field t9 cpuinfo (func))
(b.z t9 check-relaunch-3d :delay (nop!))
(add.i sp sp -96)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(s.q remaining sp 64)
(m a0 sys)
(m a1 cpuinfo)
(m a2 sprite-data)
(jalr ra t9 :delay (s.q paused? sp 80))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(l.q paused? sp 80)
(add.i sp sp 96))
;; Relaunch on the first frame that carries the independent next-time countdown below zero.
(asm-block relaunch-3d
(label check-relaunch-3d)
(l.w.field a1 cpuinfo (next-launcher))
(l.w.field v1 cpuinfo (next-time))
(b.z a1 integrate-3d :delay (l.q a0 sp 32))
(sub v1 v1 a0)
(b.ge v1 r0 integrate-3d :delay (s.w.field v1 cpuinfo (next-time)))
(add.i sp sp -96)
(s.q sys sp)
(s.q cpuinfo sp 16)
(s.q sprite-data sp 32)
(s.q slot sp 48)
(s.q remaining sp 64)
(s.q paused? sp 80)
(m a0 sys)
(m a3 sprite-data)
(m a2 cpuinfo)
(m! t9 sp-relaunch-particle-3d)
(jalr ra t9 :delay (sll v0 ra 0))
(l.q sys sp)
(l.q cpuinfo sp 16)
(l.q sprite-data sp 32)
(l.q slot sp 48)
(l.q remaining sp 64)
(l.q paused? sp 80)
(add.i sp sp 96))
;; Integrate the four adjacent CPU vectors directly into the three sprite quadwords.
(asm-block integrate-motion-3d
(label integrate-3d)
(l.vf.field sprite-pos-scale sprite-data (x-y-z-sx))
(l.vf.field sprite-quat-scale sprite-data (qx-qy-qz-sy))
(l.vf.field sprite-color sprite-data (r-g-b-a))
(l.vf.field velocity-scale-rate cpuinfo (vel-sxvel))
(l.vf.field rotation-scale-rate cpuinfo (rot-syvel))
(l.vf.field color-fade-rate cpuinfo (fade))
(l.vf.field acceleration cpuinfo (acc))
(l.s.field f0 cpuinfo (friction))
(m v1 f0)
;; Acceleration uses 60 Hz frame units. Nonzero friction becomes a retained-velocity factor
;; for this interval and applies to xyz without disturbing x-scale velocity in w.
(mul.z.vf acceleration acceleration frame-time)
(b.z v1 no-friction-3d :delay (add.vf.xyz velocity-scale-rate velocity-scale-rate acceleration))
(m friction-factor v1)
(sub.x.vf.w friction-factor vf0 friction-factor)
(mul.w.vf friction-factor friction-factor frame-time)
(sub.w.vf.w friction-factor vf0 friction-factor)
(mul.w.vf.xyz velocity-scale-rate velocity-scale-rate friction-factor)
(label no-friction-3d)
;; Apply velocity, y-scale velocity, and fade with the full tick count. The xyz lanes of
;; rotation-scale-rate are not used here; orientation advances from rotvel3d below.
(mul.y.vf position-step velocity-scale-rate frame-time)
(mul.y.vf rotation-scale-step rotation-scale-rate frame-time)
(mul.y.vf color-step color-fade-rate frame-time)
(add.vf sprite-pos-scale sprite-pos-scale position-step)
(add.w.vf.w sprite-quat-scale sprite-quat-scale rotation-scale-step)
(add.vf sprite-color sprite-color color-step)
(max.x.vf sprite-color sprite-color vf0)
(s.vf.field velocity-scale-rate cpuinfo (vel-sxvel))
(s.vf.field sprite-pos-scale sprite-data (x-y-z-sx))
(s.vf.field sprite-quat-scale sprite-data (qx-qy-qz-sy))
(s.vf.field sprite-color sprite-data (r-g-b-a)))
;; Rebuild w = sqrt(1-x^2-y^2-z^2) in the stack quaternion. The constant is loaded relative to
;; this function because the hand-written function keeps 1.0 in its local literal pool.
(asm-block update-quaternion
(m v1 rotation)
(m a0 sprite-data)
(l.s f0 a0 16)
(l.s f1 a0 20)
(l.s f3 a0 24)
(s.s f0 v1)
(s.s f1 v1 4)
(s.s f3 v1 8)
(l.s f2 fp L157)
(mul.s f3 f3 f3)
(sub.s f2 f2 f3)
(mul.s f1 f1 f1)
(sub.s f1 f2 f1)
(mul.s f0 f0 f0)
(sub.s f0 f1 f0)
(sqrt.s f0 f0)
(s.s f0 v1 12)
(m a0 f0)
;; Compose the particle's rotational velocity once, or twice when a long frame crosses the
;; ten-tick threshold.
(m! v1 *sp-frame-time*)
(l.s f0 v1)
(m v1 f0)
(and.i v1 v1 255)
(add.i v1 v1 -10)
(b.lt v1 r0 apply-rotation-step :delay (nop!))
(m! t9 quaternion*!)
(m a0 rotation)
(m a1 rotation)
(lea.field a2 cpuinfo (rotvel3d))
(jalr ra t9 :delay (sll v0 ra 0))
(label apply-rotation-step)
(m! t9 quaternion*!)
(m a0 rotation)
(m a1 rotation)
(lea.field a2 cpuinfo (rotvel3d))
(jalr ra t9 :delay (sll v0 ra 0)))
;; q and -q are equivalent. Store the sign whose omitted w is nonnegative so the renderer can
;; reconstruct the same convention from xyz alone; preserve y-scale in the untouched w lane.
(asm-block store-compact-quaternion
(m a0 sprite-data)
(m v1 rotation)
(l.s f0 v1 12)
(m f1 r0)
(c.lt.s f0 f1)
(b.fpf store-positive-quaternion :delay (nop!))
(l.vf stored-quat-scale a0 16)
(l.vf full-quaternion v1)
(sub.vf.xyz stored-quat-scale vf0 full-quaternion)
(s.vf stored-quat-scale a0 16)
(m a0 stored-quat-scale)
(b test-death-flags-3d :delay (nop!))
(label store-positive-quaternion)
(l.vf stored-quat-scale a0 16)
(l.vf full-quaternion v1)
(add.vf.xyz stored-quat-scale vf0 full-quaternion)
(s.vf stored-quat-scale a0 16)
(m a0 stored-quat-scale))
;; Packed extracts test RGB together, then alpha, then x- and y-scale. These correspond to
;; die-when-color-0, die-when-faded, and die-when-scale-0.
(asm-block death-tests-3d
(label test-death-flags-3d)
(m v1 sprite-color)
(l.w.field a0 cpuinfo (flags))
(and.i a1 a0 2)
(b.z a1 test-fade-death-3d :delay (and.i a1 a0 4))
(b.nz v1 test-fade-death-3d :delay (pextuw a2 v1 r0))
(b.z a2 free-particle-3d :delay (nop!))
(label test-fade-death-3d)
(b.z a1 test-scale-death-3d :delay (and.i a0 a0 1))
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.le v1 r0 free-particle-3d :delay (nop!))
(label test-scale-death-3d)
(b.z a0 next-particle-3d :delay (nop!))
(m v1 sprite-pos-scale)
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.lt v1 r0 free-particle-3d :delay (m v1 sprite-quat-scale))
(pcpyud v1 v1 r0)
(pexew v1 v1)
(b.ge v1 r0 next-particle-3d :delay (nop!))
;; Return dead particles to the bitmap-managed pool.
(label free-particle-3d)
(m! t9 sp-free-particle)
(m a0 sys)
(m a1 slot)
(m a2 cpuinfo)
(m a3 sprite-data)
(jalr ra t9 :delay (sll v0 ra 0)))
;; Advance every parallel cursor even when this slot was invalid.
(asm-block advance-and-return-3d
(label next-particle-3d)
(add.i remaining remaining -1)
(add.i cpuinfo cpuinfo 144)
(add.i sprite-data sprite-data 48)
(b.nz remaining particle-loop-3d :delay (add.i slot slot 1))
;; Return the first slot after the processed block.
(m v0 slot)
(l.d ra sp)
(l.d fp sp 8)
(l.q sys sp 144)
(l.q cpuinfo sp 128)
(l.q sprite-data sp 112)
(l.q remaining sp 96)
(l.q paused? sp 80)
(l.q rotation sp 64)
(l.q slot sp 48)
(jr ra :delay (add.i sp sp 160))
(nop! :count 3)))))
"Update a scratchpad block of oriented 3D particles using the original simulation rules."
(let ((cpuinfos (the-as (inline-array sparticle-cpuinfo) scratchpad-cpuinfo))
(sprites (the-as (inline-array sprite-vec-data-3d) scratchpad-sprite-data))
(ticks (logand (the-as int (-> *sp-frame-time* x)) 255))
(tick-scale (-> *sp-frame-time* y))
(frame-scale (-> *sp-frame-time* z)))
(dotimes (i count)
(let ((cpuinfo (-> cpuinfos i))
(sprite (-> sprites i))
(slot (+ start-slot i)))
(when (-> cpuinfo valid)
(cond
((and paused?
(not (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag run-while-paused))))
(cond
((= (-> cpuinfo timer) 0)
(sp-free-particle sys slot cpuinfo (the-as sprite-vec-data-2d sprite)))
((logtest? (-> cpuinfo flags) (sp-cpuinfo-flag just-launched))
(logclear! (-> cpuinfo flags) (sp-cpuinfo-flag just-launched))
(set! (-> sprite a) (-> cpuinfo cache-alpha)))))
((= (-> cpuinfo timer) 0)
(sp-free-particle sys slot cpuinfo (the-as sprite-vec-data-2d sprite)))
(else
(when (!= (-> cpuinfo timer) -1)
(set! (-> cpuinfo timer) (max 0 (- (-> cpuinfo timer) ticks))))
(#when PC_PORT
(def-mips2c sp-process-block-3d (function sparticle-system int int int int symbol none)))
(when (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag just-launched))
(logclear! (-> cpuinfo flags) (sp-cpuinfo-flag just-launched))
(set! (-> sprite a) (-> cpuinfo cache-alpha)))
(when (nonzero? (the-as uint (-> cpuinfo func)))
((the-as (function sparticle-system
sparticle-cpuinfo
sprite-vec-data-3d
none)
(-> cpuinfo func))
sys
cpuinfo
sprite))
;; Unlike the 2D path, the 3D original waits until next-time is strictly negative.
(when (nonzero? (the-as uint (-> cpuinfo next-launcher)))
(let ((next-time (- (the-as int (-> cpuinfo next-time)) ticks)))
(set! (-> cpuinfo next-time) (the-as uint next-time))
(when (< next-time 0)
(sp-relaunch-particle-3d sys
(the-as sparticle-launcher (-> cpuinfo next-launcher))
cpuinfo
sprite))))
(+! (-> cpuinfo vel-sxvel x) (* (-> cpuinfo acc x) frame-scale))
(+! (-> cpuinfo vel-sxvel y) (* (-> cpuinfo acc y) frame-scale))
(+! (-> cpuinfo vel-sxvel z) (* (-> cpuinfo acc z) frame-scale))
(when (nonzero? (the-as int (-> cpuinfo friction)))
(let ((retained (- 1.0 (* (- 1.0 (-> cpuinfo friction)) frame-scale))))
(*! (-> cpuinfo vel-sxvel x) retained)
(*! (-> cpuinfo vel-sxvel y) retained)
(*! (-> cpuinfo vel-sxvel z) retained)))
(+! (-> sprite x) (* (-> cpuinfo vel-sxvel x) tick-scale))
(+! (-> sprite y) (* (-> cpuinfo vel-sxvel y) tick-scale))
(+! (-> sprite z) (* (-> cpuinfo vel-sxvel z) tick-scale))
(+! (-> sprite sx) (* (-> cpuinfo vel-sxvel w) tick-scale))
(+! (-> sprite sy) (* (-> cpuinfo rot-syvel w) tick-scale))
(+! (-> sprite r) (* (-> cpuinfo fade x) tick-scale))
(+! (-> sprite g) (* (-> cpuinfo fade y) tick-scale))
(+! (-> sprite b) (* (-> cpuinfo fade z) tick-scale))
(+! (-> sprite a) (* (-> cpuinfo fade w) tick-scale))
(set! (-> sprite r) (fmax (-> sprite r) 0.0))
(set! (-> sprite g) (fmax (-> sprite g) 0.0))
(set! (-> sprite b) (fmax (-> sprite b) 0.0))
(set! (-> sprite a) (fmax (-> sprite a) 0.0))
;; Reconstruct the omitted quaternion w, apply one rotational step (two after a
;; long frame), then store the equivalent sign with nonnegative omitted w.
(let ((rotation (new 'stack-no-clear 'quaternion)))
(set-vector! rotation
(-> sprite qx)
(-> sprite qy)
(-> sprite qz)
(sqrtf (- (- (- 1.0 (square (-> sprite qz)))
(square (-> sprite qy)))
(square (-> sprite qx)))))
(when (>= ticks 10)
(quaternion*! rotation rotation (-> cpuinfo rotvel3d)))
(quaternion*! rotation rotation (-> cpuinfo rotvel3d))
(cond
((< (-> rotation w) 0.0)
(set! (-> sprite qx) (- 0.0 (-> rotation x)))
(set! (-> sprite qy) (- 0.0 (-> rotation y)))
(set! (-> sprite qz) (- 0.0 (-> rotation z))))
(else
(set! (-> sprite qx) (-> rotation x))
(set! (-> sprite qy) (-> rotation y))
(set! (-> sprite qz) (-> rotation z)))))
(let ((dies?
(or
(and (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag die-when-color-0))
(= (-> sprite r) 0.0)
(= (-> sprite g) 0.0)
(= (-> sprite b) 0.0))
(and (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag die-when-faded))
(<= (the-as int (-> sprite a)) 0))
(and (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag die-when-scale-0))
(or (< (the-as int (-> sprite sx)) 0)
(< (the-as int (-> sprite sy)) 0))))))
(when dies?
(sp-free-particle sys
slot
cpuinfo
(the-as sprite-vec-data-2d sprite))))))))))
(none))
(defun sp-copy-to-spr ((scratchpad-offset int) (memory pointer) (size int))
"Copy size bytes from main memory to a scratchpad offset, rounded up to whole quadwords."
@@ -900,59 +483,6 @@
0
(none))
(#unless PC_PORT
(defun memcpy ((dst pointer) (src pointer) (size int))
"Copy size rounded-up bytes from src to dst in 128-byte bursts followed by 16-byte quadwords."
(declare (asm-func none))
;; Particle scratchpad records are all quadword-padded, so this copier deliberately rounds size
;; upward. Large transfers use eight quadword loads followed by eight stores per iteration.
(rlet ((dst :reg a0)
(src :reg a1)
(size :reg a2)
(quadwords :reg v1)
(data0 :reg a2 :type i128)
(data1 :reg a3 :type i128)
(data2 :reg t0 :type i128)
(data3 :reg t1 :type i128)
(data4 :reg t2 :type i128)
(data5 :reg t3 :type i128)
(data6 :reg t4 :type i128)
(data7 :reg t5 :type i128))
(add.i quadwords size 15)
(sra quadwords quadwords 4)
(add.i quadwords quadwords -8)
(b.lt quadwords r0 finish-bursts :delay (at-label copy-burst (l.q data0 src)))
(l.q data1 src 16)
(l.q data2 src 32)
(l.q data3 src 48)
(l.q data4 src 64)
(l.q data5 src 80)
(l.q data6 src 96)
(l.q data7 src 112)
(s.q data0 dst)
(s.q data1 dst 16)
(s.q data2 dst 32)
(s.q data3 dst 48)
(s.q data4 dst 64)
(s.q data5 dst 80)
(s.q data6 dst 96)
(s.q data7 dst 112)
(add.i src src 128)
(add.i quadwords quadwords -8)
(b.ge quadwords r0 copy-burst :delay (add.i dst dst 128))
(label finish-bursts)
(add.i quadwords quadwords 8)
(b.z quadwords done-copying :delay (at-label copy-quadword (l.q data0 src)))
(add.i src src 16)
(s.q data0 dst)
(add.i quadwords quadwords -1)
(nop!)
(b.nz quadwords copy-quadword :delay (add.i dst dst 16))
(label done-copying)
(m v0 r0)
(jr ra :delay (add sp sp r0))
(nop! :count 3))))
(defun sp-process-block ((sys sparticle-system) (start-slot int) (sprite-array-data sprite-array-2d) (count int))
"Copy count parallel particle records beginning at start-slot into scratchpad, update them, and
copy the mutable CPU and sprite records back. Shader records are input-only."
@@ -976,8 +506,10 @@
(set! scratchpad-cpuinfo (+ (the int (scratchpad-start)) cpu-offset))
(set! scratchpad-sprite-data (+ (the int (scratchpad-start)) sprite-offset))
(cond
((-> sys is-3d) (sp-process-block-3d sys scratchpad-cpuinfo scratchpad-sprite-data start-slot count (paused?)))
(else (sp-process-block-2d sys scratchpad-cpuinfo scratchpad-sprite-data start-slot count (paused?))))
((-> sys is-3d)
(sp-process-block-3d sys scratchpad-cpuinfo scratchpad-sprite-data start-slot count (paused?)))
(else
(sp-process-block-2d sys scratchpad-cpuinfo scratchpad-sprite-data start-slot count (paused?))))
(sp-copy-from-spr cpu-offset (the-as pointer (-> sys cpuinfo-table start-slot)) cpu-size)
(sp-copy-from-spr sprite-offset (&+ (-> sys vecdata-table) (* 48 start-slot)) sprite-size)))
0
@@ -1150,11 +682,11 @@
(defun process-particles ()
"Update particle timing, all 2D, HUD, and 3D particle groups, profiling bars, and deferred
launches for one display frame."
(local-vars (end-cycles int) (start-cycles int))
(local-vars (start-ticks uint))
(when *particles-flag*
0
0
;;(.mfc0 start-cycles Count)
(set! start-ticks (get-cpu-clock))
(set! *sp-launcher-lock* #t)
;; start a profile frame.
(if *debug-segment*
@@ -1181,15 +713,17 @@
(set! *sp-launcher-lock* #f)
;; launch queued particles
(sp-clear-queue)
;;(.mfc0 end-cycles Count)
(let ((elapsed-cycles (- end-cycles start-cycles)))
(let ((elapsed-usec
(* 1000000.0 (cpu-ticks-to-seconds (- (get-cpu-clock) start-ticks)))))
(if *display-sprite-info*
(format *stdcon*
"Particle time = ~D cycles for ~D 2D [~D warp] and ~D HUD and ~D 3D~%"
elapsed-cycles
"Particle time = ~F us for ~D 2D [~D warp] and ~D HUD and ~D 3D~%"
elapsed-usec
(-> *sp-particle-system-2d* num-alloc 0)
(-> *sprite-aux-list* entry)
(-> *sp-particle-system-2d* num-alloc 1)
(-> *sp-particle-system-3d* num-alloc 0)))))
(#when PARTICLE_ADGIF_CACHE_STATS
(particle-adgif-cache-stats-frame))
0
(none))
+3 -1
View File
@@ -7,7 +7,9 @@
(defmacro square (x)
"Multiply x by itself."
`(* ,x ,x))
(with-gensyms (value)
`(let ((,value ,x))
(* ,value ,value))))
;; DECOMP BEGINS
+5 -9
View File
@@ -8,8 +8,7 @@
;; DECOMP BEGINS
(defmethod eval-position! ((this trajectory) (elapsed-time float) (result vector))
"Evaluate position after elapsed-time game-time ticks into result. Vertical motion includes
one half gravity times time squared; evaluation is not clamped to the solved flight duration."
"Evaluate position after elapsed-time game-time ticks into result."
(vector-copy! result (-> this initial-position))
(+! (-> result x) (* elapsed-time (-> this initial-velocity x)))
(+! (-> result y) (* elapsed-time (-> this initial-velocity y)))
@@ -18,16 +17,14 @@
result)
(defmethod eval-velocity! ((this trajectory) (elapsed-time float) (result vector))
"Evaluate velocity after elapsed-time game-time ticks into result. Only the vertical
component changes under gravity."
"Evaluate velocity after elapsed-time game-time ticks into result."
(vector-copy! result (-> this initial-velocity))
(+! (-> result y) (* elapsed-time (-> this gravity)))
result)
(defmethod setup-from-to-duration! ((this trajectory) (start vector) (destination vector) (duration float) (gravity float))
"Solve the initial velocity that moves from start to destination in duration game-time ticks
under constant vertical gravity. Store the supplied duration; callers must provide a positive,
nonzero duration."
under constant vertical gravity."
(vector-copy! (-> this initial-position) start)
(set! (-> this gravity) gravity)
(set! (-> this time) duration)
@@ -41,7 +38,7 @@
(defmethod setup-from-to-xz-vel! ((this trajectory) (start vector) (destination vector) (xz-speed float) (gravity float))
"Solve a trajectory from start to destination at the requested nonzero horizontal speed
under constant vertical gravity. Horizontal distance divided by xz-speed determines the duration."
under constant vertical gravity."
(let ((duration (/ (vector-vector-xz-distance destination start) xz-speed)))
(setup-from-to-duration! this start destination duration gravity))
0
@@ -74,8 +71,7 @@
(none))
(defmethod debug-draw! ((this trajectory))
"Draw the solved flight as ten translucent red line segments from time zero through the
stored duration, without depth testing."
"Draw the solved flight as ten translucent red line segments."
(let ((previous-position (new 'stack-no-clear 'vector))
(position (new 'stack-no-clear 'vector))
(num-segments 10))
+1 -1
View File
@@ -854,7 +854,7 @@
(go flutflutegg-physics)))
:trans
(behavior ()
(let* ((target-offset (ambient-control-method-10 (-> self ambient) (new 'stack-no-clear 'vector) (seconds 3) 368640.0 self))
(let* ((target-offset (ambient-chatter-ready? (-> self ambient) (new 'stack-no-clear 'vector) (seconds 3) 368640.0 self))
(random-mantissa (/ (the-as int (rand-uint31-gen *random-generator*)) 256))
(random-bits (the-as number (logior #x3f800000 random-mantissa)))
(random-choice (+ -1.0 (the-as float random-bits))))
+1 -1
View File
@@ -102,7 +102,7 @@
(defmethod try-play-ambient-chatter ((this bird-lady))
"When the target is within thirty metres and the thirty-second cooldown has elapsed, choose one
of three Bird Lady idle lines with approximately equal probability."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((choice (rand-float-gen)))
(cond
((< 0.66 choice) (play-ambient (-> this ambient) "BIR-LO02" #f (-> this root trans)))
+1 -1
View File
@@ -607,7 +607,7 @@
"When the target is within thirty metres and the thirty-second cooldown has elapsed, choose
one of nine Mayor idle lines with equal probability. After the Lurker-machine reminder is
complete, six task-specific choices become silence while the three generic lines remain."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((choice (rand-float-gen)))
(cond
((< 0.8888889 choice)
+1 -1
View File
@@ -141,7 +141,7 @@
(defmethod try-play-ambient-chatter ((this sculptor))
"When the target is within thirty metres and the thirty-second cooldown has elapsed, choose
one of seven Sculptor idle lines with approximately equal probability."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((choice (rand-float-gen)))
(cond
((< 0.85714287 choice) (play-ambient (-> this ambient) "SCU-LO01" #f (-> this root trans)))
@@ -445,7 +445,7 @@ Sages."
"When the beam is off, try a thirty-second, thirty-metre ambient gate and choose one of three Red
Sage lines with equal probability."
(if (-> this beam-on) (return #f))
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((random-choice (rand-float-gen)))
(cond
((< 0.6666667 random-choice) (play-ambient (-> this ambient) "RED-AM01" #f (-> this root trans)))
@@ -538,7 +538,7 @@ Sages."
"When the beam is off, try a thirty-second, thirty-metre ambient gate and choose one of three
Blue Sage lines with equal probability."
(if (-> this beam-on) (return #f))
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((random-choice (rand-float-gen)))
(cond
((< 0.6666667 random-choice) (play-ambient (-> this ambient) "BLU-AM01" #f (-> this root trans)))
@@ -622,7 +622,7 @@ Sages."
"When the beam is off, try a thirty-second, thirty-metre ambient gate and choose one of three
Yellow Sage lines with equal probability."
(if (-> this beam-on) (return #f))
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((random-choice (rand-float-gen)))
(cond
((< 0.6666667 random-choice) (play-ambient (-> this ambient) "YEL-AM01" #f (-> this root trans)))
+2 -2
View File
@@ -1719,7 +1719,7 @@
(when (not (or (ja-group? robotboss-blue-roar-ja) (ja-group? robotboss-blue-last-hit-ja))))
(robotboss-blue-beam 8 #t)
(robotboss-blue-beam 9 #f)
(when (ambient-control-method-10 (-> self ambient) (new 'stack-no-clear 'vector) (seconds 10) (the-as float 327680.0) self)
(when (ambient-chatter-ready? (-> self ambient) (new 'stack-no-clear 'vector) (seconds 10) (the-as float 327680.0) self)
(let ((f0-2 (rand-float-gen)))
(cond
((< 0.8 f0-2) (play-ambient (-> self ambient) "GOL-AM02" #t (the-as vector #f)))
@@ -1977,7 +1977,7 @@
(set! (-> this blue-smoke) #f)
(set! (-> this red-smoke) #f)
(set! (-> this yellow-smoke) #f)
(ambient-control-method-9 (-> this ambient))
(reset-ambient-cooldown! (-> this ambient))
(set! (-> this yellow-gun) (new 'process 'joint-mod (joint-mod-handler-mode flex-blend) this 16))
(set-vector! (-> this yellow-gun twist-max) 8192.0 8192.0 0.0 1.0)
(set! (-> this yellow-gun up) (the-as uint 1))
+2 -2
View File
@@ -1346,13 +1346,13 @@
"Play fishing-specific reminder or ambient speech when its distance and cooldown permit."
(cond
((closed? (-> this tasks) (game-task jungle-fishgame) (task-status need-reminder))
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 1) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 1) 122880.0 this)
(let ((f0-2 (rand-float-gen)))
(if (< 0.5 f0-2)
(play-ambient (-> this ambient) "FIS-LO03" #f (-> this root trans))
(play-ambient (-> this ambient) "FIS-LO05" #f (-> this root trans))))))
(else
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((f0-5 (rand-float-gen)))
(cond
((< 0.875 f0-5) (play-ambient (-> this ambient) "FIS-LO01" #f (-> this root trans)))
+1 -1
View File
@@ -678,7 +678,7 @@
(defmethod try-play-ambient-chatter ((this billy))
"Occasionally play a random Billy ambient line while he is idle."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((rng (rand-float-gen)))
(cond
((< 0.9411765 rng) (play-ambient (-> this ambient) "BIL-AM01" #f (-> this root trans)))
+1 -1
View File
@@ -125,7 +125,7 @@
(defmethod try-play-ambient-chatter ((this assistant))
"When the target is within thirty metres, the thirty-second cooldown has elapsed, and the target
is not more than four metres below her, choose one of five workshop lines."
(let ((target-offset (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)))
(let ((target-offset (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)))
(when target-offset
(let ((choice (rand-float-gen)))
(cond
+1 -1
View File
@@ -104,7 +104,7 @@
(defmethod try-play-ambient-chatter ((this explorer))
"When the target is within thirty metres and the thirty-second cooldown has elapsed, choose
one of seven Explorer lines; two task-specific lines are silent after their reminder closes."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((choice (rand-float-gen)))
(cond
((< 0.85714287 choice) (play-ambient (-> this ambient) "EXP-AM05" #f (-> this root trans)))
+1 -1
View File
@@ -56,7 +56,7 @@
(defmethod try-play-ambient-chatter ((this farmer))
"When the target is within thirty metres and the thirty-second cooldown has elapsed, choose
one of four Farmer idle lines or remain silent."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((choice (rand-float-gen)))
(cond
((< 0.8333333 choice) (play-ambient (-> this ambient) "FAR-LO1A" #f (-> this root trans)))
+1 -1
View File
@@ -343,7 +343,7 @@
(defmethod try-play-ambient-chatter ((this sage))
"When the target is within thirty metres, the thirty-second cooldown has elapsed, and the target
is not more than three metres above him, choose one of five hut lines."
(let ((target-offset (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)))
(let ((target-offset (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)))
(when target-offset
(let ((choice (rand-float-gen)))
(cond
@@ -351,7 +351,7 @@
(defmethod try-play-ambient-chatter ((this assistant-bluehut))
"After the ambient cooldown and distance check pass, choose one of two welding lines."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((roll (rand-float-gen)))
(cond
((= (get-task-status (game-task village2-levitator)) (task-status invalid)) #f)
+1 -1
View File
@@ -91,7 +91,7 @@
(defmethod try-play-ambient-chatter ((this gambler))
"After the ambient cooldown and distance checks pass, choose one of thirteen Gambler lines;
suppress GAM-AM10 after the ogre-boss reminder is complete."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 61440.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 61440.0 this)
(let ((roll (rand-float-gen)))
(cond
((< 0.9230769 roll) (play-ambient (-> this ambient) "GAM-AM01" #f (-> this root trans)))
+1 -1
View File
@@ -83,7 +83,7 @@
(defmethod try-play-ambient-chatter ((this geologist))
"After the ambient cooldown and distance check pass, choose a Geologist idle line, subject
to the current mole and money task progress."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((chatter-roll (rand-float-gen)))
(cond
((< 0.8888889 chatter-roll) (play-ambient (-> this ambient) "GEO-AM01" #f (-> this root trans)))
@@ -155,7 +155,7 @@
(defmethod try-play-ambient-chatter ((this sage-bluehut))
"After the ambient cooldown and distance check pass, choose one of five Blue Sage idle lines."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((roll (rand-float-gen)))
(cond
((< 0.8 roll) (play-ambient (-> this ambient) "SAGELP20" #f (-> this root trans)))
+1 -1
View File
@@ -114,7 +114,7 @@
(defmethod try-play-ambient-chatter ((this warrior))
"After the two-second ambient cooldown and distance check pass, choose one of three Warrior
idle lines."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 2) 61440.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 2) 61440.0 this)
(let ((roll (rand-float-gen)))
(cond
((< 0.66 roll) (play-ambient (-> this ambient) "WAR-LO1A" #f (-> this root trans)))
@@ -72,7 +72,7 @@
(defmethod try-play-ambient-chatter ((this assistant-villagec))
"Try to play one of the assistant's ambient voice lines after the chatter cooldown."
(local-vars (lavatube-status task-status))
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((choice (rand-float-gen)))
(cond
((< 0.85714287 choice) (play-ambient (-> this ambient) "ASSTLP31" #f (-> this root trans)))
+1 -1
View File
@@ -353,7 +353,7 @@
(defmethod try-play-ambient-chatter ((this minershort))
"Try to play one of the miners' ambient voice lines after the chatter cooldown."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((choice (rand-float-gen)))
(cond
((< 0.9655172 choice) (play-ambient (-> this ambient) "MIN-LO01" #f (-> this root trans)))
@@ -149,7 +149,7 @@
(defmethod try-play-ambient-chatter ((this sage-villagec))
"Try to play a task-appropriate Samos ambient voice line after the chatter cooldown."
(when (ambient-control-method-10 (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(when (ambient-chatter-ready? (-> this ambient) (new 'stack-no-clear 'vector) (seconds 30) 122880.0 this)
(let ((choice (rand-float-gen)))
(cond
((< 0.875 choice) (play-ambient (-> this ambient) "SAGELP31" #f (-> this root trans)))