/* * Copyright 2018 The Cartographer Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using CartographerSharp.Common.Math; using System; using System.Runtime.InteropServices; using RobotNet10.Shared.Numbers; namespace CartographerSharp.Mapping.D2D; /// /// Grid type enumeration. /// public enum GridType { ProbabilityGrid, TSDF } /// /// Base class for 2D grids. /// public abstract class Grid2D : IGrid { protected MapLimits _limits; protected List _correspondenceCostCells; protected double _minCorrespondenceCost; protected double _maxCorrespondenceCost; protected List _updateIndices; protected (int minX, int minY, int maxX, int maxY) _knownCellsBox; protected double[] _valueToCorrespondenceCostTable; private const ushort kUnknownCorrespondenceValue = 0; protected Grid2D( MapLimits limits, double minCorrespondenceCost, double maxCorrespondenceCost, ValueConversionTables conversionTables) { if (minCorrespondenceCost >= maxCorrespondenceCost) { throw new ArgumentException("min_correspondence_cost must be less than max_correspondence_cost"); } _limits = limits; _minCorrespondenceCost = minCorrespondenceCost; _maxCorrespondenceCost = maxCorrespondenceCost; _correspondenceCostCells = new List( limits.CellLimits.NumXCells * limits.CellLimits.NumYCells); for (int i = 0; i < _correspondenceCostCells.Capacity; i++) { _correspondenceCostCells.Add(kUnknownCorrespondenceValue); } _updateIndices = []; // Match C++: AlignedBox2i is empty by default (min > max) // Use sentinel values to mark empty: minX > maxX indicates empty box _knownCellsBox = (int.MaxValue, int.MaxValue, int.MinValue, int.MinValue); _valueToCorrespondenceCostTable = conversionTables.GetConversionTable( maxCorrespondenceCost, minCorrespondenceCost, maxCorrespondenceCost); } protected Grid2D( Models.Mapping.Grid2D proto, ValueConversionTables conversionTables) { _limits = new MapLimits(proto.Limits); // Match C++: MinCorrespondenceCostFromProto/MaxCorrespondenceCostFromProto - older proto had 0,0 if (proto.MinCorrespondenceCost == 0 && proto.MaxCorrespondenceCost == 0) { _minCorrespondenceCost = 0.1; _maxCorrespondenceCost = 0.9; } else { _minCorrespondenceCost = proto.MinCorrespondenceCost; _maxCorrespondenceCost = proto.MaxCorrespondenceCost; } if (_minCorrespondenceCost >= _maxCorrespondenceCost) { throw new ArgumentException("min_correspondence_cost must be less than max_correspondence_cost"); } // Match C++: Copy cells from proto _correspondenceCostCells = new List(proto.Cells?.Count ?? 0); if (proto.Cells != null) { foreach (var cell in proto.Cells) { _correspondenceCostCells.Add((ushort)cell); } } _updateIndices = []; // Match C++: Copy known_cells_box from proto if present var knownCellsBox = proto.KnownCellsBox; _knownCellsBox = (knownCellsBox.MinX, knownCellsBox.MinY, knownCellsBox.MaxX, knownCellsBox.MaxY); _valueToCorrespondenceCostTable = conversionTables.GetConversionTable( _maxCorrespondenceCost, _minCorrespondenceCost, _maxCorrespondenceCost); } /// /// Returns the limits of this Grid2D. /// public MapLimits Limits => _limits; /// /// Returns the correspondence cost cells. /// protected List CorrespondenceCostCells => _correspondenceCostCells; /// /// Returns the update indices. /// protected List UpdateIndices => _updateIndices; /// /// Returns the known cells box. /// protected (int minX, int minY, int maxX, int maxY) KnownCellsBox => _knownCellsBox; /// /// Returns mutable correspondence cost cells. /// protected List MutableCorrespondenceCostCells => _correspondenceCostCells; /// /// Returns mutable update indices. /// protected List MutableUpdateIndices => _updateIndices; /// /// Returns mutable known cells box. /// protected ref (int minX, int minY, int maxX, int maxY) MutableKnownCellsBox => ref _knownCellsBox; /// /// Returns the minimum possible correspondence cost. /// public double MinCorrespondenceCost => _minCorrespondenceCost; /// /// Returns the maximum possible correspondence cost. /// public double MaxCorrespondenceCost => _maxCorrespondenceCost; /// /// Gets the grid type. /// public abstract GridType GetGridType(); /// /// Returns the correspondence cost of the cell with 'cell_index'. /// public double GetCorrespondenceCost(Array2i cellIndex) { if (!_limits.Contains(cellIndex)) { return _maxCorrespondenceCost; } var flatIndex = ToFlatIndex(cellIndex); if (flatIndex >= _correspondenceCostCells.Count) { return _maxCorrespondenceCost; } var value = _correspondenceCostCells[flatIndex]; if (value >= _valueToCorrespondenceCostTable.Length) { return _maxCorrespondenceCost; } return _valueToCorrespondenceCostTable[value]; } /// /// Copies all correspondence cost values into the destination array using bulk access. /// Much faster than calling GetCorrespondenceCost per cell (avoids per-cell bounds checks, /// struct allocation, and virtual dispatch). Data is in row-major order matching the internal layout. /// /// Pre-allocated array of size >= NumXCells * NumYCells. public void CopyCorrespondenceCostData(double[] destination) { var numXCells = _limits.CellLimits.NumXCells; var numYCells = _limits.CellLimits.NumYCells; var totalCells = numXCells * numYCells; if (destination.Length < totalCells) throw new ArgumentException($"Destination array too small: {destination.Length} < {totalCells}", nameof(destination)); var cells = CollectionsMarshal.AsSpan(_correspondenceCostCells); var table = _valueToCorrespondenceCostTable; var maxCost = _maxCorrespondenceCost; var tableLen = table.Length; var count = Math.Min(totalCells, cells.Length); for (int i = 0; i < count; i++) { var value = cells[i]; destination[i] = value < tableLen ? table[value] : maxCost; } // Fill remaining if cells is shorter than expected (shouldn't happen normally) for (int i = count; i < totalCells; i++) { destination[i] = maxCost; } } /// /// Returns true if the probability at the specified index is known. /// public bool IsKnown(Array2i cellIndex) { if (!_limits.Contains(cellIndex)) { return false; } var flatIndex = ToFlatIndex(cellIndex); var cellValue = _correspondenceCostCells[flatIndex]; const ushort kUpdateMarker = (ushort)(1u << 15); var hasUpdateMarker = cellValue >= kUpdateMarker; var valueWithoutMarker = hasUpdateMarker ? (ushort)(cellValue - kUpdateMarker) : cellValue; var isKnown = valueWithoutMarker != kUnknownCorrespondenceValue; return isKnown; } /// /// Finishes the update sequence. /// Match C++: DCHECK_GE(correspondence_cost_cells_[update_indices_.back()], kUpdateMarker) /// Optimized: uses Span for batch processing instead of per-element RemoveAt. /// public void FinishUpdate() { const ushort kUpdateMarker = (ushort)(1u << 15); var indices = CollectionsMarshal.AsSpan(_updateIndices); var cells = CollectionsMarshal.AsSpan(_correspondenceCostCells); for (int i = indices.Length - 1; i >= 0; i--) { var idx = indices[i]; #if DEBUG if (cells[idx] < kUpdateMarker) { throw new InvalidOperationException($"Cell at index {idx} does not have update marker. Value: {cells[idx]}, Expected: >= {kUpdateMarker}"); } #endif cells[idx] -= kUpdateMarker; } _updateIndices.Clear(); } /// /// Fills in 'offset' and 'limits' to define a subregion that contains all known cells. /// Match C++: if (known_cells_box_.isEmpty()) - check by min > max /// public void ComputeCroppedLimits(out Array2i offset, out CellLimits limits) { // Match C++: AlignedBox2i::isEmpty() returns true if min > max // Empty box is marked by minX > maxX (or minY > maxY) if (_knownCellsBox.minX > _knownCellsBox.maxX || _knownCellsBox.minY > _knownCellsBox.maxY) { offset = Array2i.Zero; limits = new CellLimits(1, 1); return; } offset = new Array2i(_knownCellsBox.minX, _knownCellsBox.minY); limits = new CellLimits( _knownCellsBox.maxX - _knownCellsBox.minX + 1, _knownCellsBox.maxY - _knownCellsBox.minY + 1); } /// /// Grows the map as necessary to include 'point'. This changes the meaning of /// these coordinates going forward. This method must be called immediately /// after 'FinishUpdate', before any calls to 'ApplyLookupTable'. /// public virtual void GrowLimits(Vector2 point) { GrowLimits(point, [_correspondenceCostCells], [kUnknownCorrespondenceValue]); } /// /// Grows limits for multiple grids. /// protected void GrowLimits(Vector2 point, List[] grids, ushort[] gridsUnknownCellValues) { if (_updateIndices.Count > 0) { throw new InvalidOperationException("GrowLimits must be called after FinishUpdate"); } // Log before resize if point is outside current limits var needsGrow = !_limits.Contains(_limits.GetCellIndex(point)); while (!_limits.Contains(_limits.GetCellIndex(point))) { var xOffset = _limits.CellLimits.NumXCells / 2; var yOffset = _limits.CellLimits.NumYCells / 2; // CRITICAL FIX: Check for integer overflow before multiplying by 2 // Use long to prevent overflow during calculation long newNumXCellsLong = 2L * _limits.CellLimits.NumXCells; long newNumYCellsLong = 2L * _limits.CellLimits.NumYCells; // Check if result exceeds int.MaxValue if (newNumXCellsLong > int.MaxValue || newNumYCellsLong > int.MaxValue) { throw new InvalidOperationException( $"Cannot grow grid: new size would exceed int.MaxValue. " + $"Current: NumXCells={_limits.CellLimits.NumXCells}, NumYCells={_limits.CellLimits.NumYCells}. " + $"New would be: NumXCells={newNumXCellsLong}, NumYCells={newNumYCellsLong}"); } // Match C++: limits_.max() + limits_.resolution() * Eigen::Vector2d(y_offset, x_offset) // C++ uses Eigen::Vector2d(y_offset, x_offset), which means: // newMax.x() = oldMax.x() + resolution * y_offset // newMax.y() = oldMax.y() + resolution * x_offset var newMax = new Vector2( (_limits.Max.X + _limits.Resolution * yOffset), (_limits.Max.Y + _limits.Resolution * xOffset)); var newCellLimits = new CellLimits( (int)newNumXCellsLong, (int)newNumYCellsLong); var newLimits = new MapLimits(_limits.Resolution, newMax, newCellLimits); var stride = newLimits.CellLimits.NumXCells; var offset = xOffset + stride * yOffset; // CRITICAL FIX: Use long for newSize calculation to prevent overflow long newSizeLong = (long)newLimits.CellLimits.NumXCells * newLimits.CellLimits.NumYCells; if (newSizeLong <= 0 || newSizeLong > int.MaxValue) { throw new InvalidOperationException( $"New size is invalid: {newSizeLong}. " + $"NumXCells={newLimits.CellLimits.NumXCells}, NumYCells={newLimits.CellLimits.NumYCells}"); } var newSize = (int)newSizeLong; for (int gridIndex = 0; gridIndex < grids.Length; gridIndex++) { var oldGrid = grids[gridIndex]; if (oldGrid == null || oldGrid.Count == 0) { throw new InvalidOperationException($"Grid at index {gridIndex} is null or empty"); } // Allocate and initialize new cells without per-element Add() loop. // CollectionsMarshal.SetCount sets the internal _size field directly, // avoiding repeated bounds checks; the backing array is zero-initialised // by the runtime, so an explicit fill is only needed for non-zero values. var newCells = new List(newSize); CollectionsMarshal.SetCount(newCells, newSize); var newSpan = CollectionsMarshal.AsSpan(newCells); var unknownValue = gridsUnknownCellValues[gridIndex]; if (unknownValue != 0) newSpan.Fill(unknownValue); // Copy old rows into the correct offset in the new (larger) grid. // Using Span.CopyTo() per row lets the JIT emit a single memcpy/memmove // for each row instead of indexing element-by-element. var oldSpan = CollectionsMarshal.AsSpan(oldGrid); int numXCells = _limits.CellLimits.NumXCells; int numYCells = _limits.CellLimits.NumYCells; for (int i = 0; i < numYCells; i++) { var srcRow = oldSpan.Slice(i * numXCells, numXCells); var dstRow = newSpan.Slice(offset + i * stride, numXCells); srcRow.CopyTo(dstRow); } // THREAD-SAFETY FIX: Instead of Clear()+AddRange() which creates a // window where Count==0 (race with ConstraintBuilder2D ThreadPool readers), // assign the completed newCells to the array slot. The field references // are updated atomically via UpdateGridReferences below. grids[gridIndex] = newCells; } // Atomic field swap: update _correspondenceCostCells (and _weightCells for TSDF2D) // BEFORE updating _limits, so concurrent readers with old limits compute small // indices into the (larger) new cells list — always safe. UpdateGridReferences(grids); _limits = newLimits; // Match C++: if (!known_cells_box_.isEmpty()) { known_cells_box_.translate(...); } // Update known cells box offset only if box is not empty if (_knownCellsBox.minX <= _knownCellsBox.maxX && _knownCellsBox.minY <= _knownCellsBox.maxY) { _knownCellsBox = ( _knownCellsBox.minX + xOffset, _knownCellsBox.minY + yOffset, _knownCellsBox.maxX + xOffset, _knownCellsBox.maxY + yOffset ); } } } /// /// Updates field references after GrowLimits rebuilds the cells lists. /// Called with the new lists before _limits is updated, ensuring concurrent /// readers always see a valid (complete) cells list. /// Override in derived classes that hold additional grid lists (e.g., TSDF2D._weightCells). /// protected virtual void UpdateGridReferences(List[] grids) { _correspondenceCostCells = grids[0]; } /// /// Converts a 'cell_index' into an index into 'cells_'. /// protected int ToFlatIndex(Array2i cellIndex) { if (!_limits.Contains(cellIndex)) { throw new ArgumentOutOfRangeException(nameof(cellIndex), "Cell index out of bounds"); } return _limits.CellLimits.NumXCells * cellIndex.Y + cellIndex.X; } /// /// Lightweight, read-only snapshot of grid cell data used for thread-safe /// occupancy grid generation. Only the data needed for merging is captured; /// the underlying Grid2D remains unaffected. /// public sealed class GridCellSnapshot { public readonly ushort[] Cells; public readonly MapLimits Limits; public readonly (int minX, int minY, int maxX, int maxY) KnownCellsBox; public readonly double[] ValueToCorrespondenceCostTable; public readonly double MinCorrespondenceCost; public readonly double MaxCorrespondenceCost; internal GridCellSnapshot( ushort[] cells, MapLimits limits, (int minX, int minY, int maxX, int maxY) knownCellsBox, double[] valueToCorrespondenceCostTable, double minCorrespondenceCost, double maxCorrespondenceCost) { Cells = cells; Limits = limits; KnownCellsBox = knownCellsBox; ValueToCorrespondenceCostTable = valueToCorrespondenceCostTable; MinCorrespondenceCost = minCorrespondenceCost; MaxCorrespondenceCost = maxCorrespondenceCost; } public bool IsKnown(Array2i cellIndex) { if (!Limits.Contains(cellIndex)) return false; var flatIndex = Limits.CellLimits.NumXCells * cellIndex.Y + cellIndex.X; if (flatIndex < 0 || flatIndex >= Cells.Length) return false; const ushort kUpdateMarker = (ushort)(1u << 15); var value = Cells[flatIndex]; var raw = value >= kUpdateMarker ? (ushort)(value - kUpdateMarker) : value; return raw != 0; } public void ComputeCroppedLimits(out Array2i offset, out CellLimits limits) { if (KnownCellsBox.minX > KnownCellsBox.maxX || KnownCellsBox.minY > KnownCellsBox.maxY) { offset = Array2i.Zero; limits = new CellLimits(1, 1); return; } offset = new Array2i(KnownCellsBox.minX, KnownCellsBox.minY); limits = new CellLimits( KnownCellsBox.maxX - KnownCellsBox.minX + 1, KnownCellsBox.maxY - KnownCellsBox.minY + 1); } } /// /// Creates a snapshot of the current cell data for thread-safe reading. /// The returned snapshot is decoupled from the live grid and will not be /// affected by concurrent InsertRangeData / GrowLimits calls. /// Intended for occupancy grid generation on active (non-finished) submaps. /// public GridCellSnapshot SnapshotCellData() { // Capture references / value copies before allocating the array. var limits = _limits; var knownCellsBox = _knownCellsBox; var cells = CollectionsMarshal.AsSpan(_correspondenceCostCells); var copy = new ushort[cells.Length]; cells.CopyTo(copy); return new GridCellSnapshot( copy, limits, knownCellsBox, _valueToCorrespondenceCostTable, _minCorrespondenceCost, _maxCorrespondenceCost); } /// /// Computes a cropped grid containing only known cells. /// public abstract Grid2D ComputeCroppedGrid(); /// /// Converts to proto representation. /// Match C++: CHECK(update_indices().empty()) before serializing /// public virtual Models.Mapping.Grid2D ToProto() { // Match C++: CHECK(update_indices().empty()) << "Serializing a grid during an update is not supported. Finish the update first."; if (_updateIndices.Count > 0) { throw new InvalidOperationException("Serializing a grid during an update is not supported. Finish the update first."); } var cells = new List(_correspondenceCostCells.Count); foreach (var cell in _correspondenceCostCells) { cells.Add(cell); } var proto = new Models.Mapping.Grid2D { Limits = _limits.ToProto(), Cells = cells, MinCorrespondenceCost = _minCorrespondenceCost, MaxCorrespondenceCost = _maxCorrespondenceCost }; // Match C++: if (!known_cells_box().isEmpty()) { set known_cells_box } if (_knownCellsBox.minX <= _knownCellsBox.maxX && _knownCellsBox.minY <= _knownCellsBox.maxY) { proto.KnownCellsBox = new Models.Mapping.Grid2D.CellBox( _knownCellsBox.maxX, _knownCellsBox.maxY, _knownCellsBox.minX, _knownCellsBox.minY); } return proto; } }