yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
object_geometry.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <limits>
5
6#include "absl/status/status.h"
7#include "absl/strings/str_format.h"
11
12namespace yaze {
13namespace zelda3 {
14
15namespace {
16
17constexpr int kDummyTileCount = 512;
18
19struct AnchorPos {
20 int x = 0;
21 int y = 0;
22};
23
24std::vector<gfx::TileInfo> MakeDummyTiles() {
25 std::vector<gfx::TileInfo> tiles;
26 tiles.reserve(kDummyTileCount);
27 for (int i = 0; i < kDummyTileCount; ++i) {
28 // Non-zero tile IDs so writes are detectable in the buffer
29 tiles.push_back(gfx::TileInfo(static_cast<uint16_t>(i + 1), 0,
30 /*v=*/false, /*h=*/false, /*o=*/false));
31 }
32 return tiles;
33}
34
35// Choose an anchor (x, y) that avoids buffer clipping for routines that draw
36// leftward or upward from the object origin. Without enough headroom, the
37// off-screen replay in MeasureRoutine loses rows/columns that the real draw
38// routine would have emitted, producing undersized bounds.
40 const RoomObject& object) {
41 AnchorPos anchor;
42 const int size_nibble = object.size_ & 0x0F;
43
44 // Acute diagonals (ids 5, 17) move upward by (y - s) per step.
46 (routine.id == 5 || routine.id == 17)) {
47 const int count = (routine.id == 5) ? (size_nibble + 7) : (size_nibble + 6);
48 const int max_anchor =
49 DrawContext::kMaxTilesY - 5; // 4 rows headroom below
50 anchor.y = std::clamp(count - 1, 0, std::max(0, max_anchor));
51 return anchor;
52 }
53
54 // Diagonal ceilings (Corner category, ids 75-78) anchor at the visual corner.
55 // The draw routine offsets base by -(side-1) for the mirrored axis, so
56 // bottom-anchored (76, 78) writes into negative Y and right-anchored (77, 78)
57 // writes into negative X without headroom. See corner_routines.cc.
59 routine.id >= 75 && routine.id <= 78) {
60 const int side = size_nibble + 4;
61 const bool mirror_x = (routine.id == 77 || routine.id == 78);
62 const bool mirror_y = (routine.id == 76 || routine.id == 78);
63 if (mirror_x) {
64 anchor.x = std::clamp(side - 1, 0, DrawContext::kMaxTilesX - 1);
65 }
66 if (mirror_y) {
67 anchor.y = std::clamp(side - 1, 0, DrawContext::kMaxTilesY - 1);
68 }
69 return anchor;
70 }
71
72 // Default: top-left of canvas.
73 return anchor;
74}
75
76} // namespace
77
79 static ObjectGeometry instance;
80 return instance;
81}
82
86
88 routines_.clear();
89 routine_map_.clear();
90
91 // Use the unified DrawRoutineRegistry to ensure consistent routine IDs
92 // between ObjectGeometry and ObjectDrawer
93 const auto& registry = DrawRoutineRegistry::Get();
94 routines_ = registry.GetAllRoutines();
95
96 for (const auto& info : routines_) {
97 routine_map_[info.id] = info;
98 }
99}
100
101const DrawRoutineInfo* ObjectGeometry::LookupRoutine(int routine_id) const {
102 auto it = routine_map_.find(routine_id);
103 if (it == routine_map_.end()) {
104 return nullptr;
105 }
106 return &it->second;
107}
108
109absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByRoutineId(
110 int routine_id, const RoomObject& object) const {
111 const DrawRoutineInfo* info = LookupRoutine(routine_id);
112 if (info == nullptr) {
113 return absl::InvalidArgumentError(
114 absl::StrFormat("Unknown routine id %d", routine_id));
115 }
116 auto bounds = MeasureRoutine(*info, object);
117 if (!bounds.ok()) {
118 return bounds;
119 }
120 return ApplySelectionBounds(*bounds, routine_id);
121}
122
123absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureRoutine(
124 const DrawRoutineInfo& routine, const RoomObject& object) const {
125 // Anchor object so routines that move upward or leftward stay within bounds.
126 RoomObject adjusted = object;
127 const AnchorPos anchor = ChooseAnchor(routine, object);
128 adjusted.x_ = anchor.x;
129 adjusted.y_ = anchor.y;
130
131 // Allocate a dummy tile list large enough for every routine.
132 static const std::vector<gfx::TileInfo> kTiles = MakeDummyTiles();
133
136
137 DrawContext ctx{
138 .target_bg = bg,
139 .object = adjusted,
140 .tiles = std::span<const gfx::TileInfo>(kTiles.data(), kTiles.size()),
141 .state = nullptr,
142 .rom = nullptr,
143 .room_id = 0,
144 .room_gfx_buffer = nullptr,
145 .secondary_bg = nullptr,
146 };
147
148 // Execute the routine to mark tiles in the buffer.
149 routine.function(ctx);
150
151 // Scan buffer for written tiles.
152 const int tiles_w = DrawContext::kMaxTilesX;
153 const int tiles_h = DrawContext::kMaxTilesY;
154
155 int min_x = std::numeric_limits<int>::max();
156 int min_y = std::numeric_limits<int>::max();
157 int max_x = std::numeric_limits<int>::min();
158 int max_y = std::numeric_limits<int>::min();
159
160 for (int y = 0; y < tiles_h; ++y) {
161 for (int x = 0; x < tiles_w; ++x) {
162 if (bg.GetTileAt(x, y) == 0)
163 continue;
164 min_x = std::min(min_x, x);
165 min_y = std::min(min_y, y);
166 max_x = std::max(max_x, x);
167 max_y = std::max(max_y, y);
168 }
169 }
170
171 // Handle routines that intentionally draw nothing.
172 if (max_x == std::numeric_limits<int>::min()) {
173 return GeometryBounds{};
174 }
175
176 GeometryBounds bounds;
177 bounds.min_x_tiles = min_x - anchor.x;
178 bounds.min_y_tiles = min_y - anchor.y;
179 bounds.width_tiles = (max_x - min_x) + 1;
180 bounds.height_tiles = (max_y - min_y) + 1;
181 bounds.is_bg2_overlay = false; // Default, set by MeasureForLayerCompositing
182 return bounds;
183}
184
185absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureByObjectId(
186 const RoomObject& object) const {
187 int routine_id = DrawRoutineRegistry::Get().GetRoutineIdForObject(object.id_);
188 if (routine_id < 0) {
189 return absl::NotFoundError(
190 absl::StrFormat("No routine mapping for object 0x%03X", object.id_));
191 }
192
193 // Check cache
194 CacheKey key{routine_id, object.id_, object.size_};
195 auto cache_it = cache_.find(key);
196 if (cache_it != cache_.end()) {
197 return cache_it->second;
198 }
199
200 // Measure and cache
201 auto result = MeasureByRoutineId(routine_id, object);
202 if (result.ok()) {
203 cache_[key] = *result;
204 }
205 return result;
206}
207
209 cache_.clear();
210}
211
212absl::StatusOr<GeometryBounds> ObjectGeometry::MeasureForLayerCompositing(
213 int routine_id, const RoomObject& object) const {
214 auto result = MeasureByRoutineId(routine_id, object);
215 if (!result.ok()) {
216 return result;
217 }
218
219 GeometryBounds bounds = *result;
220
221 // Mark as BG2 overlay if the object's layer indicates Layer 1 (BG2)
222 // Layer 1 objects write to the lower tilemap (BG2) and need BG1 transparency
223 bounds.is_bg2_overlay = (object.layer_ == RoomObject::LayerType::BG2);
224
225 return bounds;
226}
227
229 // Layer 1 routines are those that explicitly draw to BG2 only.
230 // Most objects draw to the current layer pointer; this list is for
231 // routines that have special BG2-only behavior.
232 //
233 // From ASM analysis:
234 // - Objects decoded with $BF == $4000 (lower_layer) are Layer 1/BG2
235 // - The routine itself doesn't determine the layer; the object's position
236 // in the room data determines which layer pointer it uses
237 //
238 // This method is primarily for documentation; actual layer determination
239 // comes from the object's layer_ field set during room loading.
240 (void)
241 routine_id; // Currently unused - layer determined by object, not routine
242 return false;
243}
244
246 // Diagonal ceiling routines from draw_routine_registry.h
247 // kDiagonalCeilingTopLeft = 75
248 // kDiagonalCeilingBottomLeft = 76
249 // kDiagonalCeilingTopRight = 77
250 // kDiagonalCeilingBottomRight = 78
251 return routine_id >= 75 && routine_id <= 78;
252}
253
255 GeometryBounds render_bounds, int routine_id) {
256 if (!IsDiagonalCeilingRoutine(routine_id)) {
257 // Not a diagonal ceiling - return render bounds unchanged
258 return render_bounds;
259 }
260
261 // For diagonal ceilings, compute a tighter selection box.
262 // The visual triangle fills roughly 50% of the bounding box area.
263 // We use a selection rectangle that's 70% of the size, centered,
264 // to provide a reasonable hit target without excessive false positives.
265
266 int reduced_width = std::max(1, (render_bounds.width_tiles * 7) / 10);
267 int reduced_height = std::max(1, (render_bounds.height_tiles * 7) / 10);
268
269 // Center the reduced selection box within the render bounds
270 int offset_x = (render_bounds.width_tiles - reduced_width) / 2;
271 int offset_y = (render_bounds.height_tiles - reduced_height) / 2;
272
273 SelectionRect selection;
274 selection.x_tiles = render_bounds.min_x_tiles + offset_x;
275 selection.y_tiles = render_bounds.min_y_tiles + offset_y;
276 selection.width_tiles = reduced_width;
277 selection.height_tiles = reduced_height;
278
279 render_bounds.selection_bounds = selection;
280 return render_bounds;
281}
282
283} // namespace zelda3
284} // namespace yaze
uint16_t GetTileAt(int x, int y) const
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
int GetRoutineIdForObject(int16_t object_id) const
static DrawRoutineRegistry & Get()
Side-car geometry engine that replays draw routines against an off-screen buffer to calculate real ex...
static bool IsDiagonalCeilingRoutine(int routine_id)
Check if a routine ID corresponds to a diagonal ceiling.
std::vector< DrawRoutineInfo > routines_
absl::StatusOr< GeometryBounds > MeasureForLayerCompositing(int routine_id, const RoomObject &object) const
Measure bounds for a BG2 overlay object and mark it for masking.
absl::StatusOr< GeometryBounds > MeasureRoutine(const DrawRoutineInfo &routine, const RoomObject &object) const
std::unordered_map< int, DrawRoutineInfo > routine_map_
absl::StatusOr< GeometryBounds > MeasureByObjectId(const RoomObject &object) const
const DrawRoutineInfo * LookupRoutine(int routine_id) const
absl::StatusOr< GeometryBounds > MeasureByRoutineId(int routine_id, const RoomObject &object) const
static GeometryBounds ApplySelectionBounds(GeometryBounds render_bounds, int routine_id)
Compute tighter selection bounds for diagonal shapes.
static ObjectGeometry & Get()
std::unordered_map< CacheKey, GeometryBounds, CacheKeyHash > cache_
static bool IsLayerOneRoutine(int routine_id)
Get list of routine IDs that draw to BG2 layer.
const std::vector< gfx::TileInfo > & tiles() const
AnchorPos ChooseAnchor(const DrawRoutineInfo &routine, const RoomObject &object)
Context passed to draw routines containing all necessary state.
static constexpr int kMaxTilesY
gfx::BackgroundBuffer & target_bg
static constexpr int kMaxTilesX
Metadata about a draw routine.
Bounding box result for a draw routine execution.
std::optional< SelectionRect > selection_bounds
Simple rectangle for selection bounds.