diff --git a/decompiler/config/jak1/all-types.gc b/decompiler/config/jak1/all-types.gc index 1693538901..d2206368b6 100644 --- a/decompiler/config/jak1/all-types.gc +++ b/decompiler/config/jak1/all-types.gc @@ -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 diff --git a/goal_src/goal-lib.gc b/goal_src/goal-lib.gc index 7c11d0d862..e336f4bcd8 100644 --- a/goal_src/goal-lib.gc +++ b/goal_src/goal-lib.gc @@ -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) diff --git a/goal_src/jak1/engine/common-obs/process-taskable.gc b/goal_src/jak1/engine/common-obs/process-taskable.gc index 3c9db1d26f..b3975c40f5 100644 --- a/goal_src/jak1/engine/common-obs/process-taskable.gc +++ b/goal_src/jak1/engine/common-obs/process-taskable.gc @@ -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) diff --git a/goal_src/jak1/engine/entity/entity-table.gc b/goal_src/jak1/engine/entity/entity-table.gc index 81525a1c79..14290b4fe5 100644 --- a/goal_src/jak1/engine/entity/entity-table.gc +++ b/goal_src/jak1/engine/entity/entity-table.gc @@ -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 diff --git a/goal_src/jak1/engine/game/game-info.gc b/goal_src/jak1/engine/game/game-info.gc index 042d50401f..4ecb1ae782 100644 --- a/goal_src/jak1/engine/game/game-info.gc +++ b/goal_src/jak1/engine/game/game-info.gc @@ -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)) diff --git a/goal_src/jak1/engine/game/task/task-control-h.gc b/goal_src/jak1/engine/game/task/task-control-h.gc index 5841fcc164..2c606fe6a6 100644 --- a/goal_src/jak1/engine/game/task/task-control-h.gc +++ b/goal_src/jak1/engine/game/task/task-control-h.gc @@ -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)) diff --git a/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-ee-asm.gc b/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-ee-asm.gc new file mode 100644 index 0000000000..c07682423e --- /dev/null +++ b/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-ee-asm.gc @@ -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)))) \ No newline at end of file diff --git a/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-launcher-ee-asm.gc b/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-launcher-ee-asm.gc new file mode 100644 index 0000000000..accf6b1f30 --- /dev/null +++ b/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-launcher-ee-asm.gc @@ -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)))) \ No newline at end of file diff --git a/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-launcher.gc b/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-launcher.gc index bd8bcb2e76..1cfadbbef5 100644 --- a/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-launcher.gc +++ b/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle-launcher.gc @@ -28,8 +28,7 @@ (define *particle-300hz-timer* 0) ;; The decoded "misc fields" (misc-fields-start..end of sp-field-id) of a launcher, filled in by -;; the launch kernel via sp-init-fields! before it births particles: texture/anim selection, -;; birth-func, launch joint, particle count per birth, and sound to play. +;; sp-init-fields!: texture/anim selection, birth-func, launch joint, count per birth, and sound (deftype sparticle-birthinfo (structure) ((sprite texture-id) (anim int32) @@ -77,7 +76,9 @@ (let* ((table *part-group-id-table*) (len (-> table length))) (dotimes (i len) - (let ((group (-> table i))) (if (and (nonzero? group) (string= name (-> group name))) (return (the-as basic group)))))) + (let ((group (-> table i))) + (if (and (nonzero? group) (string= name (-> group name))) + (return (the-as basic group)))))) (the-as sparticle-launch-group #f)) (defun lookup-part-group-pointer-by-name ((name string)) @@ -177,242 +178,6 @@ (&+! cur-spec 16)) cur-spec))) -(#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)))) - ;;;;;;;;;;;;;;;;;;;;;;; ;; launch queue ;;;;;;;;;;;;;;;;;;;;;;; @@ -482,121 +247,73 @@ (set! (-> *particle-adgif-cache* used) 0) -;; Cache particle shaders by a folded 16-bit texture ID. Populated entries are searched linearly; -;; the folded key is treated as the texture's identity, so particle texture IDs must not collide -;; under this fold. A miss fills the next free entry, or builds directly into the output when all -;; eighty entries are occupied. -(#when PC_PORT - (def-mips2c particle-adgif (function adgif-shader texture-id none))) +(defglobalconstant PARTICLE_ADGIF_CACHE_STATS #f) -(#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. +(#when PARTICLE_ADGIF_CACHE_STATS + + (define *particle-adgif-stat-calls* 0) + (define *particle-adgif-stat-hits* 0) + (define *particle-adgif-stat-misses* 0) + + (defun particle-adgif-cache-stats-frame () + "Print and reset the per-frame particle ADGIF cache counters." + (format *stdcon* + "particle-adgif: ~D calls, ~D hits, ~D misses, cache capacity ~D/80~%" + *particle-adgif-stat-calls* + *particle-adgif-stat-hits* + *particle-adgif-stat-misses* + (-> *particle-adgif-cache* used)) + (set! *particle-adgif-stat-calls* 0) + (set! *particle-adgif-stat-hits* 0) + (set! *particle-adgif-stat-misses* 0) + (none))) + +;; Cache particle shaders by a folded 16-bit texture ID. First check the most recently used +;; texture, then linearly scan through the 80 entry cache. Once the cache is full, all +;; future misses fall back to rebuilding the shader. In jak2/jak3 the cache is cleared on level +;; unload, but for Jak 1, the 80 entry cache seems large enough to hold most (or all?) textures. +(#when PC_PORT (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)))) + (#when PARTICLE_ADGIF_CACHE_STATS + (+! *particle-adgif-stat-calls* 1)) + (let* ((cache *particle-adgif-cache*) + (texture-hash + (the-as uint16 + (logand (logxor (sar (the-as int tex-id) 20) (sar (the-as int tex-id) 8)) + #xffff))) + (cached-shader (the-as adgif-shader #f))) + ;; last is zero-initialized, so the hash alone is not enough to prove that lastgif is valid. + (when (and (= texture-hash (-> cache last)) (nonzero? (-> cache lastgif))) + (set! cached-shader (-> cache lastgif))) + (let ((i 0)) + (while (and (not cached-shader) (< i (-> cache used))) + (when (= texture-hash (-> cache tidhash i)) + (set! cached-shader (-> cache spadgif i))) + (+! i 1))) + (cond + (cached-shader + (#when PARTICLE_ADGIF_CACHE_STATS + (+! *particle-adgif-stat-hits* 1)) + (quad-copy! (the-as pointer shader-out) (the-as pointer cached-shader) 5) + (set! (-> cache last) texture-hash) + (set! (-> cache lastgif) cached-shader)) + ((< (-> cache used) 80) + (#when PARTICLE_ADGIF_CACHE_STATS + (+! *particle-adgif-stat-misses* 1)) + (let* ((cache-index (-> cache used)) + (new-shader (-> cache spadgif cache-index))) + (set! (-> cache tidhash cache-index) texture-hash) + (+! (-> cache used) 1) + (particle-setup-adgif new-shader tex-id) + (quad-copy! (the-as pointer shader-out) (the-as pointer new-shader) 5) + (set! (-> cache last) texture-hash) + (set! (-> cache lastgif) new-shader))) + (else + (#when PARTICLE_ADGIF_CACHE_STATS + (+! *particle-adgif-stat-misses* 1)) + (particle-setup-adgif shader-out tex-id)))) + (none))) ;;;;;;;;;;;;;;;;;;; ;; launch @@ -701,60 +418,28 @@ 0 (none))) -(defun sp-rotate-system ((launchinfo sparticle-launchinfo) (cpuinfo sparticle-cpuinfo) (rot transformq)) +(defun sp-rotate-system ((launchinfo sparticle-launchinfo) (cpuinfo sparticle-cpuinfo) (rot sprite-vec-data-3d)) "Rotate a particle's launch offset, velocity, and local acceleration by the owning system's xyz-only quaternion." (let ((rot-mat (new 'stack-no-clear 'matrix))) (let ((quat (new 'stack-no-clear 'quaternion))) - ;; sparticle quaternions don't store w, so figure it out. (let* ((dst-quat quat) (src-rot rot) - (qx (-> src-rot quat x)) - (qy (-> src-rot quat y)) - (qz (-> src-rot quat z))) - (set! (-> dst-quat x) qx) - (set! (-> dst-quat y) qy) - (set! (-> dst-quat z) qz) - (set! (-> dst-quat w) (sqrtf (- (- (- 1.0 (square qz)) (square qy)) (square qx))))) + (qx (-> src-rot qx-qy-qz-sy x)) + (qy (-> src-rot qx-qy-qz-sy y)) + (qz (-> src-rot qx-qy-qz-sy z))) + (set-vector! dst-quat qx qy qz (sqrtf (- (- (- 1.0 (square qz)) (square qy)) (square qx))))) (quaternion->matrix rot-mat quat)) - ;; Rotate xyz without disturbing the adjacent packed fourth component. (vector3s-rotate*! (the-as vector3s (-> launchinfo launchrot)) (the-as vector3s (-> launchinfo launchrot)) rot-mat) (vector3s-rotate*! (the-as vector3s (-> cpuinfo vel-sxvel)) (the-as vector3s (-> cpuinfo vel-sxvel)) rot-mat) - ;; also rotate acc if we would have done it previously. (if (not (logtest? (sp-cpuinfo-flag use-global-acc) (-> cpuinfo flags))) - (vector3s-rotate*! (the-as vector3s (-> cpuinfo acc)) (the-as vector3s (-> cpuinfo acc)) rot-mat))) + (vector3s-rotate*! (the-as vector3s (-> cpuinfo acc)) (the-as vector3s (-> cpuinfo acc)) rot-mat))) 0 (none)) ;; Decode the launcher's misc fields once, accumulate fractional births, then initialize each new -;; particle's sprite and CPU fields. The launch path also applies group bindings, launch and system -;; rotations, shader state, sound, level ownership, time-of-day color, and the optional birth -;; callback before exposing the sprite to the update pass. +;; particle. (#when PC_PORT - (def-mips2c sp-launch-particles-var - (function sparticle-system sparticle-launcher vector sparticle-launch-state sparticle-launch-control float none))) - -(#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) @@ -763,540 +448,263 @@ (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!) + (unless *sp-launcher-enable* + (return 0)) + + ;; If a particle's update function launches a particle, we can't safely run the code below. + ;; During particle updates, *sp-launcher-lock* is set, and we fall back to queuing the particle + ;; for later lanuching. + (when *sp-launcher-lock* + ;; the queue doesn't store launch-state, launch-control, or rate, so if we + ;; have a non-default here, crash and alert the user. + (when (or launch-state launch-control (!= rate 1.0)) + (format 0 "Crashing: particle launched with non-default with lock held~%") + (set! (-> (the-as (pointer uint64) 2) 0) 0)) + (sp-queue-launch sys launcher pos) + (return 0)) - ;; 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!) + (let ((birthinfo (new 'stack-no-clear 'sparticle-birthinfo)) + (sprite-tmp (new 'stack-no-clear 'sprite-vec-data-3d)) + (origin (new 'stack-no-clear 'vector))) + ;; Store origin before any decoding + (vector-copy! origin pos) + (let ((sprite-2d (the-as sprite-vec-data-2d sprite-tmp)) + (launchinfo (the-as sparticle-launchinfo sprite-tmp)) + ;; decode misc fields + (sprite-spec + (sp-init-fields! (&-> birthinfo dataf 0) + (-> launcher init-specs) + (sp-field-id misc-fields-start) + (sp-field-id misc-fields-end) + #t))) + ;; update birth counter + (let ((birth-increment (* (-> birthinfo num-to-birth) rate))) + ;; if a launch-state is provided, birth increment is stored there, otherwise + ;; use the one in the launcher object itself (shared across all places the launcher is used) + (if launch-state + (+! (-> launch-state accum) birth-increment) + (+! (-> launcher birthaccum) birth-increment))) + ;; Cast to int to truncate and see if accumulator has reached 1.0 or more. + (while (nonzero? (the int (if launch-state (-> launch-state accum) (-> launcher birthaccum)))) + + ;; prepase to allocate + (let ((group-index 0) + (allocation-state (the-as sparticle-launch-state #f))) + (when (and launch-control (-> launch-control group)) + (let ((group-flags (-> launch-control group flags))) + ;; select screen-space group + (when (logtest? group-flags (sp-group-flag screen-space)) + (set! group-index 1)) + ;; if allocation order is "randomized", provide the launch state to + ;; the allocator, which stashes the last position in the buffer the + ;; allocator checked. + (when (logtest? group-flags (sp-group-flag randomize-order)) + (set! allocation-state launch-state)))) - (label finish-disabled) - (jr ra :delay (add.i sp sp 256)) + ;; Allocate! If it fails, return immediately, leaving the birth accumulator >1 so + ;; we try again. + (let ((cpuinfo (sp-get-particle sys group-index allocation-state))) + (unless cpuinfo + (return 0)) + + ;; decode sprite fields (passed to renderer) + (let* ((cpu-spec (sp-init-fields! (&-> sprite-tmp x-y-z-sx x) + sprite-spec + (sp-field-id sprite-fields-start) + (sp-field-id sprite-fields-end) + #t)) + ;; decode sparticle fields + (launch-spec (sp-init-fields! (&-> cpuinfo omega) + cpu-spec + (sp-field-id cpu-fields-start) + (sp-field-id cpu-fields-end) + #t)) + (particle-flags (-> cpuinfo flags))) + (unless (-> sys is-3d) + ;; 2D sprites need to populate a matrix for the VU program. + (cond + ;; for screen space, we use the matrix provided, or 1 for aux-list (ignored?) + ((and launch-control + (-> launch-control group) + (logtest? (-> launch-control group flags) (sp-group-flag screen-space))) + ;; grab 2D matrix, or use default camera matrix for aux (distort) sprites + (set! (-> sprite-2d matrix) + (the-as sprite-matrix-mode + (if (logtest? particle-flags (sp-cpuinfo-flag aux-list)) + 1 + (-> launch-control matrix))))) + (else + ;; for camera space, the upper bits of matrix are a flag to disable fogging. + (set! (-> sprite-2d matrix) + (the-as sprite-matrix-mode + (shr (logand particle-flags (sp-cpuinfo-flag no-fog)) 14))))) + ;; unwrap angle here, VU1 program expects this to be 16-bit within a single rotation. + (set! (-> sprite-2d rot) (unwrap-angle (-> sprite-2d rot)))) - ;; 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!) + ;; truncate RGB values. aux-list (distorter) use this for radius, and don't need need clamping. + (unless (logtest? particle-flags (sp-cpuinfo-flag aux-list)) + (set! (-> sprite-tmp r) (fmin (-> sprite-tmp r) 255.0)) + (set! (-> sprite-tmp g) (fmin (-> sprite-tmp g) 255.0)) + (set! (-> sprite-tmp b) (fmin (-> sprite-tmp b) 255.0))) + + ;; set up orbit (see sparticle-launcher-h.gc) + (when (and launch-control (logtest? particle-flags (sp-cpuinfo-flag orbit))) + (let* ((axis-angle (-> launchinfo launchrot x)) + (axis-x (cos axis-angle)) + (axis-z (- (sin axis-angle)))) + (set! (-> cpuinfo radius) (-> launchinfo launchrot z)) + (quaternion-axis-angle! (-> cpuinfo rotvel3d) + axis-x + 0.0 + axis-z + (-> launchinfo launchrot y))) + ;; Orbit updates use the parent particle's renderer record as their center. + (set! (-> cpuinfo user-sprite) (-> launch-state sprite sprite))) + + (set! (-> cpuinfo binding) (the-as sparticle-launch-state #f)) + + ;; handle bound sprites + (when (and launch-state (nonzero? (-> launch-state group-item binding))) + (let ((binding-id (-> launch-state group-item binding)) + (state-index 0) + (binding-installed? #f)) + ;; iterate through launcher looking for a dead launcher matching binding + (while (and (< state-index (-> launch-control length)) (not binding-installed?)) + (let ((candidate (-> launch-control data state-index))) + (when (and (= (-> candidate group-item launcher) binding-id) + (not (logtest? (-> candidate flags) (sp-launch-state-flags enabled)))) + ;; found it, enable! + (logior! (-> candidate flags) (sp-launch-state-flags enabled)) + ;; link + (set! (-> candidate origin) (-> cpuinfo sprite x-y-z-sx)) + (set! (-> candidate sprite3d) + (if (-> sys is-3d) + (the-as sprite-vec-data-3d (-> cpuinfo sprite)) + (the-as sprite-vec-data-3d #f))) + (set! (-> candidate sprite) cpuinfo) + (set! (-> cpuinfo binding) candidate) + (set! binding-installed? #t))) + (+! state-index 1)))) - ;; 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!) + ;; check if there are any leftover fields that adjust the launch + (when (< (the-as int16 (-> launch-spec 0 field)) 64) + (sp-adjust-launch launchinfo cpuinfo launch-spec)) + ;; 3D euler -> quaternion update + (when (-> sys is-3d) + (sp-euler-convert launchinfo cpuinfo)) + (when (and launch-state + (!= (-> launch-state sprite3d) #f) + (nonzero? (-> launch-state sprite3d))) + (sp-rotate-system launchinfo cpuinfo (-> launch-state sprite3d))) - ;; 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!)) + ;; add origin position after rotations + (+! (-> launchinfo launchrot x) (-> origin x)) + (+! (-> launchinfo launchrot y) (-> origin y)) + (+! (-> launchinfo launchrot z) (-> origin z)) + + ;; get ADGIF shader info for this particle's texture. + (particle-adgif (-> cpuinfo adgif) (-> birthinfo sprite)) + ;; Set blending mode on ADGIF based on flags. + (when (logtest? particle-flags (sp-cpuinfo-flag blend-erase-dest)) + (set! (-> cpuinfo adgif alpha) (new 'static 'gs-alpha :a 2 :b 1 :d 2))) + (when (logtest? particle-flags (sp-cpuinfo-flag subtract-blend)) + (set! (-> cpuinfo adgif alpha) (new 'static 'gs-alpha :a 2 :d 1))) + (when (logtest? particle-flags (sp-cpuinfo-flag glow)) + (set! (-> cpuinfo adgif alpha) (new 'static 'gs-alpha :b 2 :d 1))) + (when (logtest? particle-flags (sp-cpuinfo-flag write-depth)) + (set! (-> cpuinfo adgif clamp) + (the gs-clamp (new 'static 'gs-zbuf :zbp #x1c0 :psm (gs-psm ct24))))) - (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!)) + ;; run user-provided birth callback + (when (nonzero? (the-as uint (-> birthinfo birth-func))) + ((the-as (function sparticle-system + sparticle-cpuinfo + sprite-vec-data-3d + sparticle-launcher + sparticle-launch-state + none) + (-> birthinfo birth-func)) + sys + cpuinfo + sprite-tmp + launcher + launch-state)) + + ;; play sound. Use a single birth accumulator in the launcher def itself. + (when (nonzero? (the-as uint (-> birthinfo sound))) + (+! (-> launcher soundaccum) (-> (the-as sound-spec (-> birthinfo sound)) num)) + (let ((sound-remainder (- (-> launcher soundaccum) 1.0))) + ;; The assembly tests the sign bit of the subtraction result directly. + (when (>= (the-as int sound-remainder) 0) + (set! (-> launcher soundaccum) sound-remainder) + (sound-play-by-spec (the-as sound-spec (-> birthinfo sound)) (new-sound-id) origin)))) - ;; 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!) + ;; Handle distortion sprites - these are processed like normal on CPU, + ;; but passed to a different list for rendering + (when (logtest? particle-flags (sp-cpuinfo-flag aux-list)) + ;; set up callback to add to aux list + (set! (-> cpuinfo func) add-to-sprite-aux-list) + ;; alpha is 0 for these particles, we don't want that to kill them + (logclear! particle-flags (sp-cpuinfo-flag die-when-faded)) + (set! (-> sprite-tmp a) 0.0) + (set! (-> cpuinfo fade a) 0.0) + ;; clamp number of "turns" (number of sides on distort polygon) + (set! (-> sprite-2d flag) (max 3 (min 11 (-> sprite-2d flag))))) - ;; 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!) + ;; check if particle is in each level and set appropriate flags. + ;; this is used to kill particles when a level unloads. + (cond + ((and (>= (the-as int launcher) (the-as int (-> *level* level0 heap base))) + (< (the-as int launcher) (the-as int (-> *level* level0 heap top-base)))) + (logior! particle-flags (sp-cpuinfo-flag level0))) + ((and (>= (the-as int launcher) (the-as int (-> *level* level1 heap base))) + (< (the-as int launcher) (the-as int (-> *level* level1 heap top-base)))) + (logior! particle-flags (sp-cpuinfo-flag level1)))) + + ;; apply time-of-day coloring + (when (= (logand particle-flags + (sp-cpuinfo-flag aux-list use-time-of-day-color)) + (sp-cpuinfo-flag use-time-of-day-color)) + (let ((time-color (-> *time-of-day-context* current-prt-color))) + (*! (-> sprite-tmp r) (-> time-color x)) + (*! (-> sprite-tmp g) (-> time-color y)) + (*! (-> sprite-tmp b) (-> time-color z)) + (*! (-> cpuinfo fade r) (-> time-color x)) + (*! (-> cpuinfo fade g) (-> time-color y)) + (*! (-> cpuinfo fade b) (-> time-color z)))) - ;; 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)))) + ;; Set the launch control as the "key", essentially the owner for the particle. + ;; We can later look it up by key to kill associated particles. + (set! (-> cpuinfo key) + (if launch-control launch-control (the-as sparticle-launch-control 0))) + + ;; copy sprite data out (there's a race on reading this data with DMA, this + ;; provides a more atomic update after all updates/processing) + (let ((sprite-out (the-as sprite-vec-data-3d (-> cpuinfo sprite)))) + (vector-copy! (-> sprite-out x-y-z-sx) (-> sprite-tmp x-y-z-sx)) + (vector-copy! (-> sprite-out qx-qy-qz-sy) (-> sprite-tmp qx-qy-qz-sy)) + (vector-copy! (-> sprite-out r-g-b-a) (-> sprite-tmp r-g-b-a)) + ;; set alpha to zero, initially disabled, it will get restored on first update. + (set! (-> sprite-out a) 0.0)) + (logior! particle-flags (sp-cpuinfo-flag just-launched)) + (set! (-> cpuinfo flags) particle-flags) + (set! (-> cpuinfo cache-alpha) (-> sprite-tmp a)))) + ;; decrement birth accumulators after a successful birth. + (if launch-state + (+! (-> launch-state accum) -1.0) + (+! (-> launcher birthaccum) -1.0))))) + (none)))) ;;;;;;;;;;;;;;; ;; enemy death ;;;;;;;;;;;;;;; -;; Enemy-death particles share one shader instead of retaining a cache entry per texture. +;; Enemy-death particles are a special case. Particles are launched from positions on the mesh to +;; make it look like the enemy is dissolving. These bypass the usual launcher system and instead +;; launch exactly one particle per sp-launch-particles-death call. (define *death-adgif* (the-as adgif-shader #f)) -;; WARN: Function sp-launch-particles-death has a return type of none, but the expression builder found a return statement. (defun sp-launch-particles-death ((sys sparticle-system) (launcher sparticle-launcher) (pos vector)) "Launch the enemy-death effect as one 2D particle, runs the launcher's sprite+cpu init specs, applies time-of-day tint, points its adgif at the @@ -1315,13 +723,14 @@ ;; vf30 = launch position (pos). vf31 = 255.0 broadcast (#x437f0000 is the float 255.0), ;; used below as the saturation ceiling for the 8-bit color components. (.lvf vf30 (&-> pos quad)) - (let ((max-color-bits #x437f0000)) (.mov vf31 max-color-bits)) + (let ((max-color-bits #x437f0000)) + (.mov vf31 max-color-bits)) (let ((sprite-tmp (new 'stack-no-clear 'sprite-vec-data-2d)) ;; get a particle, from the particle system ;; this gets us a "cpuinfo", which is the state of a particle. (cpuinfo (sp-get-particle sys 0 (the-as sparticle-launch-state #f)))) (if (not cpuinfo) - ;; if it fails, give up. + ;; if we're out of particles, just igve up. (return 0)) (let* ((first-spec (-> launcher init-specs 0)) ;; initialize the sprite fields. this is done to a temporary structure on the stack @@ -1359,7 +768,7 @@ (let ((adgif-q2 (-> *death-adgif* quad 2 quad))) (set! (-> cpuinfo adgif quad 2 quad) adgif-q2)) (let ((adgif-q3 (-> *death-adgif* quad 3 quad))) (set! (-> cpuinfo adgif quad 3 quad) adgif-q3)) (let ((adgif-q4 (-> *death-adgif* quad 4 quad))) (set! (-> cpuinfo adgif quad 4 quad) adgif-q4)) - ;; set up some color stuff. + ;; tint by time of day (.lvf vf4 (&-> (-> *time-of-day-context* current-prt-color) quad)) (.lvf vf5 (&-> sprite-tmp color quad)) (.lvf vf6 (&-> cpuinfo fade quad)) @@ -1367,7 +776,7 @@ (.mul.vf.xyz vf6 vf6 vf4) (.svf (&-> sprite-tmp color quad) vf5) (.svf (&-> cpuinfo fade quad) vf6) - ;; + ;; no key for these particles (set! (-> cpuinfo key) (the-as sparticle-launch-control 0)) (set! (-> cpuinfo binding) #f) ;; copy the sprite data @@ -1384,12 +793,12 @@ (set! (-> cpuinfo cache-alpha) (-> sprite-tmp a)) ;; The particle is born invisible (its real alpha stashed in cache-alpha, sprite alpha 0). ;; On the next process-particles pass, the per-block update function sees just-launched, - ;; restores the alpha from cache-alpha, and clears - ;; the bit -- that is all the flag does; it does NOT trigger the sp-relaunch functions. + ;; restores the alpha from cache-alpha, and clears the bit. ) 0 (none))) + (defun sp-clear-queue () "Launch every deferred particle request and empty the launch queue." (let ((queue *sp-launch-queue*)) @@ -1409,11 +818,11 @@ ;; Each particle carries two 300hz countdowns, decremented by the update kernel ;; (sp-process-block-2d/3d): `timer` is the lifetime -- when it hits 0 the particle is ;; freed -- and `next-time` is the relaunch timer. When next-time expires and the particle has a -;; `next-launcher` set (and is not on the aux-list), the kernel calls sp-relaunch-particle-2d/3d -;; with that launcher: the particle is NOT freed, its fields are re-initialized in place from the +;; `next-launcher` set, the kernel calls sp-relaunch-particle-2d/3d +;; with that next-launcher: the particle is NOT freed, its fields are re-initialized in place from the ;; new launcher's init-specs (sp-relaunch-setup-fields, write-missing-fields=#f so unspecified -;; fields keep their current values), and it keeps aging. Chaining next-launcher ids is how -;; defparts express multi-phase effects (e.g. smoke that changes color and slows down). +;; fields keep their current values), and it keeps aging. This is how the game creates +;; multi-phase effects (e.g. smoke that changes textures/colors and slows down). (defun sp-relaunch-setup-fields ((user object) (launcher sparticle-launcher) (cpuinfo sparticle-cpuinfo) (sprite sprite-vec-data-3d)) "Reset the sprite+cpu fields of an existing (live) particle from a launcher's init-specs. @@ -1449,7 +858,10 @@ (sp-init-fields! (&-> cpuinfo omega) next-spec (sp-field-id cpu-fields-start) (sp-field-id cpu-fields-end) #f)) ;; clear all flags except for the level. (logior! (-> cpuinfo flags) level-flags) + ;; unused... (if (logtest? (-> cpuinfo flags) (sp-cpuinfo-flag level1)) (-> *level* level1) (-> *level* level0)) + + ;; apply time-of-day tinting (let ((tod-color (-> *time-of-day-context* current-prt-color))) (if (= (-> sprite r-g-b-a x) 99999.0) (set! (-> sprite r-g-b-a x) saved-r) @@ -1470,6 +882,7 @@ (set! (-> cpuinfo fade z) saved-fade-b) (set! (-> cpuinfo fade z) (* (-> cpuinfo fade z) (-> tod-color z))))))) (else + ;; no time-of-day trick needed, just re-init fields. (let ((next-spec (sp-init-fields! (&-> sprite x-y-z-sx x) (the-as (inline-array sp-field-init-spec) first-spec) (sp-field-id sprite-fields-start) @@ -1586,15 +999,18 @@ (set! (-> state randomize) (the-as uint 0)) (cond ((logtest? (-> item flags) (sp-group-item-flag child)) - ;; if we aren't immediate + ;; start dead, another particle will bind this one and set enabled + ;; and also set origin/sprite3d/sprite to the parent. (logclear! (-> state flags) (sp-launch-state-flags enabled)) (set! (-> state origin) #f) (set! (-> state sprite3d) #f) (set! (-> state sprite) #f)) (else + ;; not child, enable the launcher (logior! (-> state flags) (sp-launch-state-flags enabled)) ;; set the position now. (set! (-> state origin) (-> this center)) + ;; set #f in the link to parent fields, this isn't a child. (set! (-> state sprite3d) #f) (set! (-> state sprite) #f))) (+! state-idx 1)) @@ -1617,13 +1033,15 @@ optional sprite transform." ;; clear the flag. (countdown (i (-> this length)) - (let ((state (-> this data i))) (logclear! (-> state flags) (sp-launch-state-flags launched)))) + (let ((state (-> this data i))) + (logclear! (-> state flags) (sp-launch-state-flags launched)))) (set! (-> this local-clock) 0) (set! (-> this fade) 1.0) ;; kill the particles (let them go back to the sparticle system.) (kill-all-particles-with-key this) ;; release matrices - (if (> (-> this matrix) 0) (sprite-release-user-hvdf (-> this matrix))) + (if (> (-> this matrix) 0) + (sprite-release-user-hvdf (-> this matrix))) 0 (none)) @@ -1671,7 +1089,8 @@ (vector-copy! (-> this center) pos) ;; og:preserve-this ;; check if we are visible (remove this check to force particles to be drawn.) - (if (not (or (is-visible? this pos) (logtest? (-> this group flags) (sp-group-flag always-process screen-space)))) + (if (not (or (is-visible? this pos) + (logtest? (-> this group flags) (sp-group-flag always-process screen-space)))) (return (the-as object 0))) (let ((now *particle-300hz-timer*) (last-time (-> this last-spawn-time))) @@ -1681,6 +1100,9 @@ (set! last-time (- now (logand (the-as int (-> *sp-frame-time* x)) 255))))) (set! (-> this last-spawn-frame) (the-as int (-> *display* real-actual-frame-counter))) (set! (-> this last-spawn-time) now) + + ;; if we're using the local clock, update the clock in the launch-control (only ticks when visible, starts at 0 + ;; when launch-control is first run) (when (logtest? (-> this group flags) (sp-group-flag use-local-clock)) (set! last-time (-> this local-clock)) (+! (-> this local-clock) (logand (the-as int (-> *sp-frame-time* x)) 255)) @@ -1730,6 +1152,7 @@ this :rate rate))) + (b! #t next-item :delay (nop!)) (label periodic-path) ;; reject if bad time of day @@ -1886,16 +1309,13 @@ (qx (-> spr qx-qy-qz-sy x)) (qy (-> spr qx-qy-qz-sy y)) (qz (-> spr qx-qy-qz-sy z))) - (set! (-> quat x) qx) - (set! (-> quat y) qy) - (set! (-> quat z) qz) - (set! (-> quat w) (sqrtf (- (- (- 1.0 (square qz)) (square qy)) (square qx))))) + (set-vector! quat qx qy qz (sqrtf (- (- (- 1.0 (square qz)) (square qy)) (square qx))))) (let ((euler (new-stack-vector0))) (set! (-> euler y) (-> parent sprite rot)) (set! (-> euler z) (if (logtest? *global-toggle* 1) - (the float (sar (shl (the int (- 16384.0 (-> parent sprite sx))) 48) 48)) - (the float (sar (shl (the int (+ 16384.0 (-> parent sprite sx))) 48) 48)))) + (unwrap-angle (- 16384.0 (-> parent sprite sx))) + (unwrap-angle (+ 16384.0 (-> parent sprite sx))))) (quaternion-zxy! (the-as quaternion quat) euler)) (let ((spr-out sprite3d)) (cond diff --git a/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle.gc b/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle.gc index ec3cfef064..db9371704a 100644 --- a/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle.gc +++ b/goal_src/jak1/engine/gfx/sprite/sparticle/sparticle.gc @@ -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)) diff --git a/goal_src/jak1/engine/math/math.gc b/goal_src/jak1/engine/math/math.gc index 522177c8f6..d64411ba7b 100644 --- a/goal_src/jak1/engine/math/math.gc +++ b/goal_src/jak1/engine/math/math.gc @@ -7,7 +7,9 @@ (defmacro square (x) "Multiply x by itself." - `(* ,x ,x)) + (with-gensyms (value) + `(let ((,value ,x)) + (* ,value ,value)))) ;; DECOMP BEGINS diff --git a/goal_src/jak1/engine/physics/trajectory.gc b/goal_src/jak1/engine/physics/trajectory.gc index 1e57fa02a6..4d4c93fc51 100644 --- a/goal_src/jak1/engine/physics/trajectory.gc +++ b/goal_src/jak1/engine/physics/trajectory.gc @@ -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)) diff --git a/goal_src/jak1/levels/beach/beach-obs.gc b/goal_src/jak1/levels/beach/beach-obs.gc index 0aed01c1b1..939d68e9da 100644 --- a/goal_src/jak1/levels/beach/beach-obs.gc +++ b/goal_src/jak1/levels/beach/beach-obs.gc @@ -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)))) diff --git a/goal_src/jak1/levels/beach/bird-lady.gc b/goal_src/jak1/levels/beach/bird-lady.gc index 1271d54483..3447993e00 100644 --- a/goal_src/jak1/levels/beach/bird-lady.gc +++ b/goal_src/jak1/levels/beach/bird-lady.gc @@ -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))) diff --git a/goal_src/jak1/levels/beach/mayor.gc b/goal_src/jak1/levels/beach/mayor.gc index f2e0e5a8a4..5d1f4a0012 100644 --- a/goal_src/jak1/levels/beach/mayor.gc +++ b/goal_src/jak1/levels/beach/mayor.gc @@ -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) diff --git a/goal_src/jak1/levels/beach/sculptor.gc b/goal_src/jak1/levels/beach/sculptor.gc index a1504c8998..72cf5b4668 100644 --- a/goal_src/jak1/levels/beach/sculptor.gc +++ b/goal_src/jak1/levels/beach/sculptor.gc @@ -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))) diff --git a/goal_src/jak1/levels/citadel/citadel-sages.gc b/goal_src/jak1/levels/citadel/citadel-sages.gc index 2753e309ea..9d60c6abf4 100644 --- a/goal_src/jak1/levels/citadel/citadel-sages.gc +++ b/goal_src/jak1/levels/citadel/citadel-sages.gc @@ -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))) diff --git a/goal_src/jak1/levels/finalboss/robotboss.gc b/goal_src/jak1/levels/finalboss/robotboss.gc index 570ec9fa6c..db5e34cdb4 100644 --- a/goal_src/jak1/levels/finalboss/robotboss.gc +++ b/goal_src/jak1/levels/finalboss/robotboss.gc @@ -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)) diff --git a/goal_src/jak1/levels/jungle/fisher.gc b/goal_src/jak1/levels/jungle/fisher.gc index 57a1106649..5ad98f1be4 100644 --- a/goal_src/jak1/levels/jungle/fisher.gc +++ b/goal_src/jak1/levels/jungle/fisher.gc @@ -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))) diff --git a/goal_src/jak1/levels/swamp/billy.gc b/goal_src/jak1/levels/swamp/billy.gc index 1c534c7f02..316629099f 100644 --- a/goal_src/jak1/levels/swamp/billy.gc +++ b/goal_src/jak1/levels/swamp/billy.gc @@ -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))) diff --git a/goal_src/jak1/levels/village1/assistant.gc b/goal_src/jak1/levels/village1/assistant.gc index d4a8b326b9..5f18632f05 100644 --- a/goal_src/jak1/levels/village1/assistant.gc +++ b/goal_src/jak1/levels/village1/assistant.gc @@ -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 diff --git a/goal_src/jak1/levels/village1/explorer.gc b/goal_src/jak1/levels/village1/explorer.gc index 558cdd09e2..c58787bb36 100644 --- a/goal_src/jak1/levels/village1/explorer.gc +++ b/goal_src/jak1/levels/village1/explorer.gc @@ -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))) diff --git a/goal_src/jak1/levels/village1/farmer.gc b/goal_src/jak1/levels/village1/farmer.gc index 00b1911f4c..3f9afcb790 100644 --- a/goal_src/jak1/levels/village1/farmer.gc +++ b/goal_src/jak1/levels/village1/farmer.gc @@ -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))) diff --git a/goal_src/jak1/levels/village1/sage.gc b/goal_src/jak1/levels/village1/sage.gc index dc003a0995..55f511908b 100644 --- a/goal_src/jak1/levels/village1/sage.gc +++ b/goal_src/jak1/levels/village1/sage.gc @@ -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 diff --git a/goal_src/jak1/levels/village2/assistant-village2.gc b/goal_src/jak1/levels/village2/assistant-village2.gc index c4083a60e4..d3e78f6088 100644 --- a/goal_src/jak1/levels/village2/assistant-village2.gc +++ b/goal_src/jak1/levels/village2/assistant-village2.gc @@ -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) diff --git a/goal_src/jak1/levels/village2/gambler.gc b/goal_src/jak1/levels/village2/gambler.gc index 70d9bb8cde..1916395ebd 100644 --- a/goal_src/jak1/levels/village2/gambler.gc +++ b/goal_src/jak1/levels/village2/gambler.gc @@ -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))) diff --git a/goal_src/jak1/levels/village2/geologist.gc b/goal_src/jak1/levels/village2/geologist.gc index 6251d1ca6b..20d80d23e2 100644 --- a/goal_src/jak1/levels/village2/geologist.gc +++ b/goal_src/jak1/levels/village2/geologist.gc @@ -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))) diff --git a/goal_src/jak1/levels/village2/sage-bluehut.gc b/goal_src/jak1/levels/village2/sage-bluehut.gc index 45a6c937d6..ca4228d3cd 100644 --- a/goal_src/jak1/levels/village2/sage-bluehut.gc +++ b/goal_src/jak1/levels/village2/sage-bluehut.gc @@ -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))) diff --git a/goal_src/jak1/levels/village2/warrior.gc b/goal_src/jak1/levels/village2/warrior.gc index 99ca2a603d..bf937670e0 100644 --- a/goal_src/jak1/levels/village2/warrior.gc +++ b/goal_src/jak1/levels/village2/warrior.gc @@ -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))) diff --git a/goal_src/jak1/levels/village3/assistant-village3.gc b/goal_src/jak1/levels/village3/assistant-village3.gc index 6c940e182a..17f7cd6b53 100644 --- a/goal_src/jak1/levels/village3/assistant-village3.gc +++ b/goal_src/jak1/levels/village3/assistant-village3.gc @@ -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))) diff --git a/goal_src/jak1/levels/village3/miners.gc b/goal_src/jak1/levels/village3/miners.gc index f6fcfdb5aa..3898237a58 100644 --- a/goal_src/jak1/levels/village3/miners.gc +++ b/goal_src/jak1/levels/village3/miners.gc @@ -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))) diff --git a/goal_src/jak1/levels/village3/sage-village3.gc b/goal_src/jak1/levels/village3/sage-village3.gc index c23c76598a..95522cff7f 100644 --- a/goal_src/jak1/levels/village3/sage-village3.gc +++ b/goal_src/jak1/levels/village3/sage-village3.gc @@ -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)))