code wiki / _hdl_build / nx_vnsprite.nx
nx_vnsprite.nx source
↩ module page · 150 lines · 6570 B
1// nx_vnsprite.nx -- certified composable part: the VN SPRITE COMPOSITOR (gamebench bit 13).
2// REFERENCE BAR RESEARCHED 2026-07-29 (never invent a ruler, applied to the build itself): the field
3// standard is Ren'Py's layeredimage -- layers organized into ATTRIBUTE GROUPS (expression / outfit /
4// pose) where selecting one attribute auto-excludes the rest of its group, so a 4-outfit x 6-emotion
5// character is 10 layers, not 24 images -- plus dissolve transitions as the core scene grammar
6// (renpy.org/doc/html/layeredimage.html). This part is those semantics, sovereign and integer:
7// z-ordered clipped alpha blits + GROUP-EXCLUSIVE attribute selection + integer dissolve, with the
8// layer STATE a pure i64 vector so a scene round-trips through nx_gamesave (parts doctrine).
9// HONEST DELTA declared: commercial 2026 SOTA adds Live2D-class mesh-deformed "living" sprites --
10// that is the animation-skeletal / soft-tissue axis (separate bits), not flat compositing.
11// LIB, no main. KEY color, alpha, groups are PARAMETERS (rule 11); art quality is the critic's axis,
12// composition correctness is THIS axis.
13// license_tier: ORIGINAL expect_exit: 0
14import "nx_syscalls.nx"
15const VSP_65536: i64 = 65536 // packed-RGB blue-channel multiplier (ecosystem fb convention)
16const VSP_A1: i64 = 256 // alpha denominator (256 = fully opaque)
17const VSP_LROW: i64 = 7 // layer row: [spr-index, x, y, alpha, z, visible, group]
18
19func vsp_clear(fb: *i64, W: i64, H: i64, c: i64) -> i64 {
20 var i: i64 = 0
21 let n: i64 = W*H
22 while i < n { fb[i] = c; i = i + 1 }
23 return 0
24}
25// clipped alpha blit: skip pixels equal to the KEY color; blend s over d by alpha/256 per channel.
26// Out-of-bounds target pixels are clipped BY CONSTRUCTION -- the loop bounds are intersected with the
27// framebuffer rectangle before any write, so a sprite can hang off any edge safely.
28func vsp_blit(fb: *i64, W: i64, H: i64, spr: *i64, sw: i64, sh: i64, x: i64, y: i64, key: i64, alpha: i64) -> i64 {
29 var sy: i64 = 0
30 if y < 0 { sy = 0 - y }
31 var ey: i64 = sh
32 if y + sh > H { ey = H - y }
33 while sy < ey {
34 var sx: i64 = 0
35 if x < 0 { sx = 0 - x }
36 var ex: i64 = sw
37 if x + sw > W { ex = W - x }
38 while sx < ex {
39 let s: i64 = spr[sy*sw + sx]
40 if s != key {
41 let di: i64 = (y + sy)*W + (x + sx)
42 if alpha >= VSP_A1 {
43 fb[di] = s
44 } else {
45 let d: i64 = fb[di]
46 let sr: i64 = s % 256
47 let sg: i64 = (s/256) % 256
48 let sb: i64 = (s/VSP_65536) % 256
49 let dr: i64 = d % 256
50 let dg: i64 = (d/256) % 256
51 let db: i64 = (d/VSP_65536) % 256
52 let orr: i64 = (sr*alpha + dr*(VSP_A1 - alpha))/VSP_A1
53 let og: i64 = (sg*alpha + dg*(VSP_A1 - alpha))/VSP_A1
54 let ob: i64 = (sb*alpha + db*(VSP_A1 - alpha))/VSP_A1
55 fb[di] = orr + og*256 + ob*VSP_65536
56 }
57 }
58 sx = sx + 1
59 }
60 sy = sy + 1
61 }
62 return 0
63}
64// compose n layers in ascending z (stable: equal z keeps table order). layers = flat VSP_LROW rows;
65// sprites/sprw/sprh are parallel tables indexed by the layer's spr-index. Invisible layers skipped.
66func vsp_compose(fb: *i64, W: i64, H: i64, sprites: *i64, sprw: *i64, sprh: *i64, layers: *i64, n: i64, key: i64) -> i64 {
67 let done: *i64 = sys_mmap(n*8) as *i64
68 var i: i64 = 0
69 while i < n { done[i] = 0; i = i + 1 }
70 var emitted: i64 = 0
71 var rounds: i64 = 0
72 while rounds < n {
73 var best: i64 = 0 - 1
74 var bz: i64 = 0
75 i = 0
76 while i < n {
77 if done[i] == 0 {
78 let z: i64 = layers[i*VSP_LROW + 4]
79 var take: i64 = 0
80 if best < 0 { take = 1 }
81 if best >= 0 { if z < bz { take = 1 } }
82 if take == 1 { best = i; bz = z }
83 }
84 i = i + 1
85 }
86 if best >= 0 {
87 done[best] = 1
88 if layers[best*VSP_LROW + 5] == 1 {
89 let si: i64 = layers[best*VSP_LROW]
90 vsp_blit(fb, W, H, sprites[si] as *i64, sprw[si], sprh[si],
91 layers[best*VSP_LROW + 1], layers[best*VSP_LROW + 2],
92 key, layers[best*VSP_LROW + 3])
93 emitted = emitted + 1
94 }
95 }
96 rounds = rounds + 1
97 }
98 return emitted
99}
100// ---- ATTRIBUTE GROUPS (the Ren'Py layeredimage signature): show EXACTLY ONE layer of a group.
101// group 0 = ungrouped (visibility managed by the caller); group > 0 = exclusive attribute group.
102// vsp_setg(layers, n, grp, want) makes row `want` the group's visible attribute and hides the rest --
103// ONE call swaps an expression/outfit with no manual layer bookkeeping (the 96-combos-no-96-files law).
104func vsp_setg(layers: *i64, n: i64, grp: i64, want: i64) -> i64 {
105 var i: i64 = 0
106 var shown: i64 = 0
107 while i < n {
108 if layers[i*VSP_LROW + 6] == grp {
109 if i == want { layers[i*VSP_LROW + 5] = 1; shown = shown + 1 }
110 if i != want { layers[i*VSP_LROW + 5] = 0 }
111 }
112 i = i + 1
113 }
114 return shown
115}
116// ---- DISSOLVE (the core VN transition): out = A*(256-t)/256 + B*t/256 per channel, integer-exact.
117// t=0 -> exactly A, t=256 -> exactly B; a scene change is a t-ramp over frames.
118func vsp_dissolve(fa: *i64, fbb: *i64, out: *i64, n: i64, t: i64) -> i64 {
119 var i: i64 = 0
120 while i < n {
121 let a: i64 = fa[i]
122 let b: i64 = fbb[i]
123 let ar: i64 = a % 256
124 let ag: i64 = (a/256) % 256
125 let ab2: i64 = (a/VSP_65536) % 256
126 let br: i64 = b % 256
127 let bg: i64 = (b/256) % 256
128 let bb2: i64 = (b/VSP_65536) % 256
129 let orr: i64 = (ar*(VSP_A1 - t) + br*t)/VSP_A1
130 let og: i64 = (ag*(VSP_A1 - t) + bg*t)/VSP_A1
131 let ob: i64 = (ab2*(VSP_A1 - t) + bb2*t)/VSP_A1
132 out[i] = orr + og*256 + ob*VSP_65536
133 i = i + 1
134 }
135 return 0
136}
137// bounded positive rolling checksum over a frame (independent-walker shape, cf. nx_collide2d)
138const VSP_CKMOD: i64 = 1000000007
139func vsp_fbck(fb: *i64, W: i64, H: i64) -> i64 {
140 var ck: i64 = 0
141 var i: i64 = 0
142 let n: i64 = W*H
143 while i < n {
144 var v: i64 = fb[i] % VSP_CKMOD
145 if v < 0 { v = v + VSP_CKMOD }
146 ck = (ck*31 + v) % VSP_CKMOD
147 i = i + 1
148 }
149 return ck
150}