nx_bed_mesh.nx source
↩ module page · 207 lines · 7866 B
1// nx_bed_mesh.nx -- slicer-side per-XY Z compensation from a probed
2// bed-mesh model.
3//
4// =====================================================================
5// EXCEED axis (research baseline)
6// =====================================================================
7//
8// Industry treatment of bed warp (as of 2026-05):
9//
10// Klipper: BED_MESH_PROFILE_LOAD + Z_TILT_ADJUST -- mesh is
11// probed by firmware, Z-correction applied at GCODE
12// EXECUTION time. Slicer is bed-naive.
13// Marlin: M420 S1 -- runtime correction, same model.
14// RepRapFirmware: M557 + bed-mesh compensation, runtime.
15// OrcaSlicer 2.3.2: single-number "First Layer Print Height
16// Compensation" -- one global offset, not per-XY.
17// PrusaSlicer 2.7: mesh-leveling AWARE (knows the printer does it)
18// but emits FLAT first-layer G-code; relies on
19// firmware to compensate.
20// Bambu Studio: no slicer-side mesh compensation.
21//
22// All four major slicers EMIT FLAT G-code for the first layer and
23// rely on firmware-side Z correction. This breaks for:
24// - Firmware without mesh leveling (older boards, custom builds)
25// - G-code archived / shared / printed on a different machine
26// - Printers with mesh data the operator wants embedded for
27// reproducibility
28//
29// This file ships slicer-side bilinear-interp per-XY Z compensation
30// baked DIRECTLY into the Z coordinates of the emitted G-code, with
31// layer-tapered fade-out so the model still meets nominal Z by the
32// time bed warp stops mattering (typically 1-2 mm above bed).
33//
34// =====================================================================
35//
36// license_tier: ORIGINAL
37
38import "nx_syscalls.nx"
39const NX_MAGIC_16384: i64 = 16384
40
41// ===== verdicts ====================================================
42
43const NX_BED_MESH_OK: i64 = 0
44const NX_BED_MESH_ERR_BAD_INPUT: i64 = 1
45const NX_BED_MESH_ERR_BAD_INDEX: i64 = 2
46
47// ===== struct ======================================================
48//
49// z_deltas is a flat n_x * n_y array indexed as [iy * n_x + ix].
50// Q14 throughout (1 unit = 1/16384 mm). Typical bed warp magnitude:
51// ±300 μm = ±0.3 mm = ±4915 Q14.
52
53struct NxBedMesh {
54 n_x: i64,
55 n_y: i64,
56 x_min_q14: i64,
57 x_max_q14: i64,
58 y_min_q14: i64,
59 y_max_q14: i64,
60 z_deltas: *i64,
61 taper_layers: i64,
62}
63
64const NX_BED_MESH_BYTES: i64 = 64
65
66// Default taper: 10 layers at typical 0.2 mm layer height = 2 mm of
67// compensation fade-out. Matches Klipper's default `fade_end = 2.0`
68// (mesh-leveling literature consensus).
69const NX_BED_MESH_TAPER_LAYERS: i64 = 10
70
71// ===== construction ================================================
72
73func nx_bed_mesh_new(n_x: i64, n_y: i64,
74 x_min_q14: i64, x_max_q14: i64,
75 y_min_q14: i64, y_max_q14: i64,
76 taper_layers: i64) -> *NxBedMesh {
77 if n_x < 2 { return 0 as *NxBedMesh }
78 if n_y < 2 { return 0 as *NxBedMesh }
79 if x_max_q14 <= x_min_q14 { return 0 as *NxBedMesh }
80 if y_max_q14 <= y_min_q14 { return 0 as *NxBedMesh }
81 if taper_layers <= 0 { return 0 as *NxBedMesh }
82
83 let m: *NxBedMesh = (sys_mmap(NX_BED_MESH_BYTES)) as *NxBedMesh
84 m.n_x = n_x
85 m.n_y = n_y
86 m.x_min_q14 = x_min_q14
87 m.x_max_q14 = x_max_q14
88 m.y_min_q14 = y_min_q14
89 m.y_max_q14 = y_max_q14
90 m.z_deltas = (sys_mmap(n_x * n_y * 8)) as *i64
91 m.taper_layers = taper_layers
92
93 // Zero-init z_deltas (flat bed default).
94 var i: i64 = 0
95 let total: i64 = n_x * n_y
96 while i < total {
97 m.z_deltas[i] = 0
98 i = i + 1
99 }
100 return m
101}
102
103func nx_bed_mesh_set_point(m: *NxBedMesh, ix: i64, iy: i64,
104 dz_q14: i64) -> i64 {
105 if (m as i64) == 0 { return NX_BED_MESH_ERR_BAD_INPUT }
106 if ix < 0 { return NX_BED_MESH_ERR_BAD_INDEX }
107 if iy < 0 { return NX_BED_MESH_ERR_BAD_INDEX }
108 if ix >= m.n_x { return NX_BED_MESH_ERR_BAD_INDEX }
109 if iy >= m.n_y { return NX_BED_MESH_ERR_BAD_INDEX }
110 m.z_deltas[iy * m.n_x + ix] = dz_q14
111 return NX_BED_MESH_OK
112}
113
114// ===== bilinear Z lookup ===========================================
115//
116// Returns the Z compensation (Q14 mm) at the given (x, y) by bilinear
117// interpolation over the four nearest grid corners. Points outside
118// the mesh return the nearest-edge value (NOT extrapolated -- mesh
119// data outside the probed region is undefined; we cap to the edge).
120
121func nx_bed_mesh_z_offset(m: *NxBedMesh, x_q14: i64, y_q14: i64) -> i64 {
122 if (m as i64) == 0 { return 0 }
123
124 // Map (x, y) to fractional grid index (gx, gy) in Q14.
125 let span_x: i64 = m.x_max_q14 - m.x_min_q14
126 let span_y: i64 = m.y_max_q14 - m.y_min_q14
127 let nx_m1: i64 = m.n_x - 1
128 let ny_m1: i64 = m.n_y - 1
129
130 // gx = (x - x_min) * (n_x - 1) / span_x (Q14)
131 let dx: i64 = x_q14 - m.x_min_q14
132 var gx_q14: i64 = (dx * nx_m1) / (span_x / NX_MAGIC_16384)
133 let dy: i64 = y_q14 - m.y_min_q14
134 var gy_q14: i64 = (dy * ny_m1) / (span_y / NX_MAGIC_16384)
135
136 // Clamp to [0, (n_x-1) * Q14] / [0, (n_y-1) * Q14]
137 let gx_max: i64 = nx_m1 * NX_MAGIC_16384
138 let gy_max: i64 = ny_m1 * NX_MAGIC_16384
139 if gx_q14 < 0 { gx_q14 = 0 }
140 if gy_q14 < 0 { gy_q14 = 0 }
141 if gx_q14 > gx_max { gx_q14 = gx_max }
142 if gy_q14 > gy_max { gy_q14 = gy_max }
143
144 let ix0: i64 = gx_q14 / NX_MAGIC_16384
145 let iy0: i64 = gy_q14 / NX_MAGIC_16384
146 var ix1: i64 = ix0 + 1
147 var iy1: i64 = iy0 + 1
148 if ix1 > nx_m1 { ix1 = nx_m1 }
149 if iy1 > ny_m1 { iy1 = ny_m1 }
150
151 // Fractional parts in Q14 (0..16384).
152 let fx_q14: i64 = gx_q14 - ix0 * NX_MAGIC_16384
153 let fy_q14: i64 = gy_q14 - iy0 * NX_MAGIC_16384
154 let inv_fx_q14: i64 = NX_MAGIC_16384 - fx_q14
155 let inv_fy_q14: i64 = NX_MAGIC_16384 - fy_q14
156
157 let z00: i64 = m.z_deltas[iy0 * m.n_x + ix0]
158 let z10: i64 = m.z_deltas[iy0 * m.n_x + ix1]
159 let z01: i64 = m.z_deltas[iy1 * m.n_x + ix0]
160 let z11: i64 = m.z_deltas[iy1 * m.n_x + ix1]
161
162 // Bilinear: z = (1-fx)(1-fy) z00 + fx (1-fy) z10 +
163 // (1-fx) fy z01 + fx fy z11
164 // Q14 multiplication: (a_q14 * b_q14) / 16384 = (a*b)_q14
165 let w00_num: i64 = inv_fx_q14 * inv_fy_q14
166 let w00: i64 = w00_num / NX_MAGIC_16384
167 let w10_num: i64 = fx_q14 * inv_fy_q14
168 let w10: i64 = w10_num / NX_MAGIC_16384
169 let w01_num: i64 = inv_fx_q14 * fy_q14
170 let w01: i64 = w01_num / NX_MAGIC_16384
171 let w11_num: i64 = fx_q14 * fy_q14
172 let w11: i64 = w11_num / NX_MAGIC_16384
173
174 let c00_num: i64 = w00 * z00
175 let c00: i64 = c00_num / NX_MAGIC_16384
176 let c10_num: i64 = w10 * z10
177 let c10: i64 = c10_num / NX_MAGIC_16384
178 let c01_num: i64 = w01 * z01
179 let c01: i64 = c01_num / NX_MAGIC_16384
180 let c11_num: i64 = w11 * z11
181 let c11: i64 = c11_num / NX_MAGIC_16384
182
183 let sum_a: i64 = c00 + c10
184 let sum_b: i64 = c01 + c11
185 return sum_a + sum_b
186}
187
188// ===== layer-tapered compensation =================================
189//
190// Linearly fades compensation from FULL at layer 0 to ZERO at
191// layer_idx == taper_layers. Above taper_layers, returns 0
192// (model meets nominal Z above the fade region).
193//
194// Returns Q14 Z compensation to be ADDED to the emitter's z_q14.
195
196func nx_bed_mesh_z_at_layer(m: *NxBedMesh, x_q14: i64, y_q14: i64,
197 layer_idx: i64) -> i64 {
198 if (m as i64) == 0 { return 0 }
199 if layer_idx < 0 { return 0 }
200 if layer_idx >= m.taper_layers { return 0 }
201
202 let raw_dz: i64 = nx_bed_mesh_z_offset(m, x_q14, y_q14)
203 let remaining: i64 = m.taper_layers - layer_idx
204 let scaled_num: i64 = raw_dz * remaining
205 let dz: i64 = scaled_num / m.taper_layers
206 return dz
207}