From 143954560986af6d5f95c202fd519afc0efdedaf Mon Sep 17 00:00:00 2001 From: PenguinAkiko <124635421+PenguinAkiko@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:15:22 +0800 Subject: [PATCH 1/4] Fix bilinear ABL grid_start probe/nozzle coordinate mixup The variable abl.probe_position_lf is in probe-space, but bedlevel.grid_start is consumed as a nozzle-space origin for mesh interpolation. This caused the entire bilinear mesh to be shifted by the probe offset on machines with a non-zero XY offset. Convert the bilinear lower-left grid origin before comparing or storing the mesh, and apply the same conversion in M420 S2. Add Probe::convert_to_nozzle_xy() to centralize the conversion. --- Marlin/src/gcode/bedlevel/M420.cpp | 2 +- Marlin/src/gcode/bedlevel/abl/G29.cpp | 6 ++++-- Marlin/src/module/probe.h | 5 +++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Marlin/src/gcode/bedlevel/M420.cpp b/Marlin/src/gcode/bedlevel/M420.cpp index 680d99fe469c..bfc2cf39f8df 100644 --- a/Marlin/src/gcode/bedlevel/M420.cpp +++ b/Marlin/src/gcode/bedlevel/M420.cpp @@ -74,7 +74,7 @@ void GcodeSuite::M420() { start.set(x_min, y_min); spacing.set((x_max - x_min) / (GRID_MAX_CELLS_X), (y_max - y_min) / (GRID_MAX_CELLS_Y)); - bedlevel.set_grid(spacing, start); + bedlevel.set_grid(spacing, Probe::convert_to_nozzle_xy(start)); #endif GRID_LOOP(x, y) { bedlevel.z_values[x][y] = 0.001 * random(-200, 200); diff --git a/Marlin/src/gcode/bedlevel/abl/G29.cpp b/Marlin/src/gcode/bedlevel/abl/G29.cpp index 77809605a4ac..5da442efb4bc 100644 --- a/Marlin/src/gcode/bedlevel/abl/G29.cpp +++ b/Marlin/src/gcode/bedlevel/abl/G29.cpp @@ -520,7 +520,9 @@ G29_TYPE GcodeSuite::G29() { #endif #if ENABLED(AUTO_BED_LEVELING_BILINEAR) - if (!abl.dryrun && (abl.gridSpacing != bedlevel.grid_spacing || abl.probe_position_lf != bedlevel.grid_start)) { + // Convert the probe-space grid origin to the motion/nozzle coordinate + const xy_pos_t grid_start = Probe::convert_to_nozzle_xy(abl.probe_position_lf); + if (!abl.dryrun && (abl.gridSpacing != bedlevel.grid_spacing || grid_start != bedlevel.grid_start)) { reset_bed_level(); // Reset grid to 0.0 or "not probed". (Also disables ABL) abl.reenable = false; // Can't re-enable (on error) until the new grid is written } @@ -883,7 +885,7 @@ G29_TYPE GcodeSuite::G29() { if (abl.dryrun) bedlevel.print_leveling_grid(&abl.z_values); else { - bedlevel.set_grid(abl.gridSpacing, abl.probe_position_lf); + bedlevel.set_grid(abl.gridSpacing, Probe::convert_to_nozzle_xy(abl.probe_position_lf)); COPY(bedlevel.z_values, abl.z_values); TERN_(IS_KINEMATIC, bedlevel.extrapolate_unprobed_bed_level()); bedlevel.refresh_bed_level(); diff --git a/Marlin/src/module/probe.h b/Marlin/src/module/probe.h index 4f2a10eb1aac..3f4cde94b472 100644 --- a/Marlin/src/module/probe.h +++ b/Marlin/src/module/probe.h @@ -244,6 +244,11 @@ class Probe { static constexpr xy_pos_t offset_xy = xy_pos_t({ 0, 0 }); // See #16767 #endif + // Convert a probe-space XY point to the active nozzle's motion-space coordinates. + static xy_pos_t convert_to_nozzle_xy(const xy_pos_t &probe_xy) { + return probe_xy - DIFF_TERN(HAS_HOTEND_OFFSET, offset_xy, xy_pos_t(motion.active_hotend_offset())); + } + static bool deploy(const bool no_return=false) { return set_deployed(true, no_return); } static bool stow(const bool no_return=false) { return set_deployed(false, no_return); } From 449935e0b3444ffb65ee30b31725ebc624b14df4 Mon Sep 17 00:00:00 2001 From: Scott Lahteine Date: Mon, 24 Aug 2026 01:43:49 -0500 Subject: [PATCH 2/4] tweak and comment --- Marlin/src/gcode/bedlevel/M420.cpp | 4 +++- Marlin/src/gcode/bedlevel/abl/G29.cpp | 4 ++-- Marlin/src/module/probe.h | 12 +++++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Marlin/src/gcode/bedlevel/M420.cpp b/Marlin/src/gcode/bedlevel/M420.cpp index bfc2cf39f8df..34460e01c8b0 100644 --- a/Marlin/src/gcode/bedlevel/M420.cpp +++ b/Marlin/src/gcode/bedlevel/M420.cpp @@ -67,6 +67,7 @@ void GcodeSuite::M420() { #if ENABLED(MARLIN_DEV_MODE) if (parser.intval('S') == 2) { + // Farthest XY coordinates that the probe can reach const float x_min = probe.min_x(), x_max = probe.max_x(), y_min = probe.min_y(), y_max = probe.max_y(); #if ENABLED(AUTO_BED_LEVELING_BILINEAR) @@ -74,7 +75,8 @@ void GcodeSuite::M420() { start.set(x_min, y_min); spacing.set((x_max - x_min) / (GRID_MAX_CELLS_X), (y_max - y_min) / (GRID_MAX_CELLS_Y)); - bedlevel.set_grid(spacing, Probe::convert_to_nozzle_xy(start)); + // Shift the grid bounds by the probe offset because...? + bedlevel.set_grid(spacing, Probe::tool_xy_for_probe_xy(start)); #endif GRID_LOOP(x, y) { bedlevel.z_values[x][y] = 0.001 * random(-200, 200); diff --git a/Marlin/src/gcode/bedlevel/abl/G29.cpp b/Marlin/src/gcode/bedlevel/abl/G29.cpp index 5da442efb4bc..e4e7cf479633 100644 --- a/Marlin/src/gcode/bedlevel/abl/G29.cpp +++ b/Marlin/src/gcode/bedlevel/abl/G29.cpp @@ -521,7 +521,7 @@ G29_TYPE GcodeSuite::G29() { #if ENABLED(AUTO_BED_LEVELING_BILINEAR) // Convert the probe-space grid origin to the motion/nozzle coordinate - const xy_pos_t grid_start = Probe::convert_to_nozzle_xy(abl.probe_position_lf); + const xy_pos_t grid_start = Probe::tool_xy_for_probe_xy(abl.probe_position_lf); if (!abl.dryrun && (abl.gridSpacing != bedlevel.grid_spacing || grid_start != bedlevel.grid_start)) { reset_bed_level(); // Reset grid to 0.0 or "not probed". (Also disables ABL) abl.reenable = false; // Can't re-enable (on error) until the new grid is written @@ -885,7 +885,7 @@ G29_TYPE GcodeSuite::G29() { if (abl.dryrun) bedlevel.print_leveling_grid(&abl.z_values); else { - bedlevel.set_grid(abl.gridSpacing, Probe::convert_to_nozzle_xy(abl.probe_position_lf)); + bedlevel.set_grid(abl.gridSpacing, Probe::tool_xy_for_probe_xy(abl.probe_position_lf)); COPY(bedlevel.z_values, abl.z_values); TERN_(IS_KINEMATIC, bedlevel.extrapolate_unprobed_bed_level()); bedlevel.refresh_bed_level(); diff --git a/Marlin/src/module/probe.h b/Marlin/src/module/probe.h index 3f4cde94b472..1a2cfae868d0 100644 --- a/Marlin/src/module/probe.h +++ b/Marlin/src/module/probe.h @@ -244,9 +244,15 @@ class Probe { static constexpr xy_pos_t offset_xy = xy_pos_t({ 0, 0 }); // See #16767 #endif - // Convert a probe-space XY point to the active nozzle's motion-space coordinates. - static xy_pos_t convert_to_nozzle_xy(const xy_pos_t &probe_xy) { - return probe_xy - DIFF_TERN(HAS_HOTEND_OFFSET, offset_xy, xy_pos_t(motion.active_hotend_offset())); + // Input: A coordinate destination for the probe. + // Return: The tool position that will put the probe there. + // Also adjusted for the tool offset unless PROBING_TOOL is used. + // With PROBING_TOOL the probe offsets are presumed relative to that tool, not T0. + static xy_pos_t tool_xy_for_probe_xy(const xy_pos_t &probe_xy) { + xy_pos_t nozPos = DIFF_TERN(HAS_PROBE_XY_OFFSET, probe_xy, offset_xy); + #if DISABLED(DO_TOOLCHANGE_FOR_PROBING) + nozPos += xy_pos_t(motion.active_hotend_offset()); + #endif } static bool deploy(const bool no_return=false) { return set_deployed(true, no_return); } From 74f793726fc15d79b8ee161596e7c0a5b6637043 Mon Sep 17 00:00:00 2001 From: Scott Lahteine Date: Mon, 24 Aug 2026 01:52:50 -0500 Subject: [PATCH 3/4] this helps --- Marlin/src/module/probe.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Marlin/src/module/probe.h b/Marlin/src/module/probe.h index 1a2cfae868d0..116e46a45318 100644 --- a/Marlin/src/module/probe.h +++ b/Marlin/src/module/probe.h @@ -253,6 +253,7 @@ class Probe { #if DISABLED(DO_TOOLCHANGE_FOR_PROBING) nozPos += xy_pos_t(motion.active_hotend_offset()); #endif + return nozPos; } static bool deploy(const bool no_return=false) { return set_deployed(true, no_return); } From bcd6c35b159dc354349a486d9cca639ec497520a Mon Sep 17 00:00:00 2001 From: Scott Lahteine Date: Mon, 24 Aug 2026 21:24:36 -0500 Subject: [PATCH 4/4] macro fix --- Marlin/src/module/settings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marlin/src/module/settings.cpp b/Marlin/src/module/settings.cpp index a83740b5d028..ac08111dc3c0 100644 --- a/Marlin/src/module/settings.cpp +++ b/Marlin/src/module/settings.cpp @@ -3190,7 +3190,7 @@ void MarlinSettings::postprocess() { #define MESH_STORE_SIZE sizeof(TERN(OPTIMIZED_MESH_STORAGE, mesh_store_t, bedlevel.z_values)) uint16_t MarlinSettings::calc_num_meshes() { - return MIN(MAX_SAVED_MESHES, (meshes_end - meshes_start_index()) / MESH_STORE_SIZE); + return _MIN(MAX_SAVED_MESHES, (meshes_end - meshes_start_index()) / MESH_STORE_SIZE); } int MarlinSettings::mesh_slot_offset(const int8_t slot) {