yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_room_loader.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <future>
5#include <map>
6#include <mutex>
7#include <optional>
8#include <thread>
9#include <utility>
10
11#ifdef __EMSCRIPTEN__
13#endif
14
17#include "util/log.h"
18#include "util/macro.h"
19#include "zelda3/dungeon/room.h"
20
21namespace yaze::editor {
22
23absl::Status DungeonRoomLoader::LoadRoom(int room_id, zelda3::Room& room) {
24 if (!rom_ || !rom_->is_loaded()) {
25 return absl::FailedPreconditionError("ROM not loaded");
26 }
27 if (room_id < 0 || room_id >= 0x128) {
28 return absl::InvalidArgumentError("Invalid room ID");
29 }
30
31 // Imports can materialize a room without loading its objects. Preserve the
32 // entire pending map (including explicit 0x00 presence and an empty Clear)
33 // across replacement by the fully loaded room.
34 std::optional<zelda3::CustomCollisionMap> pending_collision;
35 if (room.id() == room_id && room.custom_collision_dirty()) {
36 pending_collision = room.custom_collision();
37 }
38
39 room = zelda3::LoadRoomFromRom(rom_, room_id);
40 room.SetGameData(game_data_); // Ensure room has access to GameData
41 if (pending_collision.has_value()) {
42 room.custom_collision() = std::move(*pending_collision);
44 }
45
46 return absl::OkStatus();
47}
48
50 if (!rom_ || !rom_->is_loaded()) {
51 return absl::FailedPreconditionError("ROM not loaded");
52 }
53
54 constexpr int kTotalRooms = 0x100 + 40; // 296 rooms
55
56 // Data structures for collecting results
57 std::vector<std::pair<int, zelda3::RoomSize>> room_size_results;
58 std::vector<std::pair<int, ImVec4>> room_palette_results;
59
60#ifdef __EMSCRIPTEN__
61 // WASM: Sequential loading to avoid Web Worker explosion
62 // std::async creates pthreads which become Web Workers in browsers,
63 // causing excessive worker spawning and main thread blocking.
64 LOG_DEBUG("Dungeon", "Loading %d dungeon rooms sequentially (WASM build)",
65 kTotalRooms);
66
67 if (!game_data_) {
68 return absl::FailedPreconditionError("GameData not available");
69 }
70 auto dungeon_man_pal_group = game_data_->palette_groups.dungeon_main;
71
72 // Create loading indicator for progress feedback
73 auto loading_handle =
74 app::platform::WasmLoadingManager::BeginLoading("Loading Dungeon Rooms");
75
76 for (int i = 0; i < kTotalRooms; ++i) {
77 // Update progress every 10 rooms to reduce overhead
78 if (i % 10 == 0) {
79 float progress = static_cast<float>(i) / static_cast<float>(kTotalRooms);
80 app::platform::WasmLoadingManager::UpdateProgress(loading_handle,
81 progress);
82
83 // Check for cancellation
84 if (app::platform::WasmLoadingManager::IsCancelled(loading_handle)) {
85 app::platform::WasmLoadingManager::EndLoading(loading_handle);
86 return absl::CancelledError("Dungeon room loading cancelled by user");
87 }
88 }
89
90 // Lazy load: Only load header/metadata, not objects/graphics
92 rooms[i].SetGameData(game_data_); // Ensure room has access to GameData
93 auto room_size = zelda3::CalculateRoomSize(rom_, i);
94 // rooms[i].LoadObjects(); // DEFERRED: Load on demand
95
96 const int p_id = rooms[i].ResolveDungeonPaletteId();
97 if (p_id >= 0 && p_id < static_cast<int>(dungeon_man_pal_group.size())) {
98 auto color = dungeon_man_pal_group[p_id][3];
99 room_size_results.emplace_back(i, room_size);
100 room_palette_results.emplace_back(rooms[i].palette(), color.rgb());
101 }
102 }
103
104 app::platform::WasmLoadingManager::EndLoading(loading_handle);
105#else
106 // Native: Parallel loading for performance
107 constexpr int kMaxConcurrency =
108 8; // Reasonable thread limit for room loading
109
110 // Determine optimal number of threads
111 const int max_concurrency = std::min(
112 kMaxConcurrency, static_cast<int>(std::thread::hardware_concurrency()));
113 const int rooms_per_thread =
114 (kTotalRooms + max_concurrency - 1) / max_concurrency;
115
116 LOG_DEBUG("Dungeon",
117 "Loading %d dungeon rooms using %d threads (%d rooms per thread)",
118 kTotalRooms, max_concurrency, rooms_per_thread);
119
120 // Thread-safe data structures for collecting results
121 std::mutex results_mutex;
122
123 // Process rooms in parallel batches
124 std::vector<std::future<absl::Status>> futures;
125
126 for (int thread_id = 0; thread_id < max_concurrency; ++thread_id) {
127 auto task = [this, &rooms, thread_id, rooms_per_thread, &results_mutex,
128 &room_size_results, &room_palette_results,
129 kTotalRooms]() -> absl::Status {
130 const int start_room = thread_id * rooms_per_thread;
131 const int end_room = std::min(start_room + rooms_per_thread, kTotalRooms);
132
133 if (!game_data_) {
134 return absl::FailedPreconditionError("GameData not available");
135 }
136 auto dungeon_man_pal_group = game_data_->palette_groups.dungeon_main;
137
138 for (int i = start_room; i < end_room; ++i) {
139 // Lazy load: Only load header/metadata
140 rooms[i] = zelda3::LoadRoomHeaderFromRom(rom_, i);
141 rooms[i].SetGameData(game_data_); // Ensure room has access to GameData
142
143 // Calculate room size
144 auto room_size = zelda3::CalculateRoomSize(rom_, i);
145
146 // Load room objects - DEFERRED
147 // rooms[i].LoadObjects();
148
149 // Process palette (ResolveDungeonPaletteId handles the two-level
150 // lookup with out-of-range fallback; skip if group is empty).
151 const int p_id = rooms[i].ResolveDungeonPaletteId();
152 if (p_id >= 0 &&
153 p_id < static_cast<int>(dungeon_man_pal_group.size())) {
154 auto color = dungeon_man_pal_group[p_id][3];
155
156 // Thread-safe collection of results
157 {
158 std::lock_guard<std::mutex> lock(results_mutex);
159 room_size_results.emplace_back(i, room_size);
160 room_palette_results.emplace_back(rooms[i].palette(), color.rgb());
161 }
162 }
163 }
164
165 return absl::OkStatus();
166 };
167
168 futures.emplace_back(std::async(std::launch::async, task));
169 }
170
171 // Wait for all threads to complete
172 for (auto& future : futures) {
173 RETURN_IF_ERROR(future.get());
174 }
175#endif
176
177 // Process collected results on main thread
178 {
179 gfx::ScopedTimer postprocess_timer("DungeonRoomLoader::PostProcessResults");
180
181 // Sort results by room ID for consistent ordering
182 std::sort(room_size_results.begin(), room_size_results.end(),
183 [](const auto& a, const auto& b) { return a.first < b.first; });
184 std::sort(room_palette_results.begin(), room_palette_results.end(),
185 [](const auto& a, const auto& b) { return a.first < b.first; });
186
187 // Process room size results
188 for (const auto& [room_id, room_size] : room_size_results) {
189 room_size_pointers_.push_back(room_size.room_size_pointer);
190 room_sizes_.push_back(room_size.room_size);
191 if (room_size.room_size_pointer != 0x0A8000) {
192 room_size_addresses_[room_id] = room_size.room_size_pointer;
193 }
194 }
195
196 // Process palette results
197 for (const auto& [palette_id, color] : room_palette_results) {
198 room_palette_[palette_id] = color;
199 }
200 }
201
203 return absl::OkStatus();
204}
205
207 std::array<zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots>&
208 entrances) {
209 if (!rom_ || !rom_->is_loaded()) {
210 return absl::FailedPreconditionError("ROM not loaded");
211 }
212
213 // Validate before publishing any entries, including the spawn navigation
214 // views, so a malformed expansion cannot leave a partially replaced array.
216 auto loaded = entrances;
217 // Load entrances
218 for (int i = 0; i < zelda3::kNumDungeonSpawnPoints; ++i) {
219 loaded[i] = zelda3::RoomEntrance(rom_, i, true);
220 RETURN_IF_ERROR(loaded[i].load_status());
221 }
222
223 for (int i = 0; i < zelda3::kNumRegularDungeonEntrances; ++i) {
225 zelda3::RoomEntrance(rom_, i, false);
226 RETURN_IF_ERROR(loaded[i + zelda3::kNumDungeonSpawnPoints].load_status());
227 }
228 entrances = std::move(loaded);
229 return absl::OkStatus();
230}
231
233 std::array<zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints>&
234 spawn_points) {
235 if (!rom_ || !rom_->is_loaded()) {
236 return absl::FailedPreconditionError("ROM not loaded");
237 }
238
239 for (int spawn_id = 0; spawn_id < zelda3::kNumDungeonSpawnPoints;
240 ++spawn_id) {
241 ASSIGN_OR_RETURN(spawn_points[spawn_id],
243 }
244 return absl::OkStatus();
245}
246
248 std::map<int, std::vector<int>> rooms_by_bank;
249 for (const auto& room : room_size_addresses_) {
250 int bank = room.second >> 16;
251 rooms_by_bank[bank].push_back(room.second);
252 }
253
254 // Process and calculate room sizes within each bank
255 for (auto& bank_rooms : rooms_by_bank) {
256 std::ranges::sort(bank_rooms.second);
257
258 for (size_t i = 0; i < bank_rooms.second.size(); ++i) {
259 int room_ptr = bank_rooms.second[i];
260
261 // Identify the room ID for the current room pointer
262 int room_id =
263 std::ranges::find_if(room_size_addresses_, [room_ptr](
264 const auto& entry) {
265 return entry.second == room_ptr;
266 })->first;
267
268 if (room_ptr != 0x0A8000) {
269 if (i < bank_rooms.second.size() - 1) {
270 room_sizes_[room_id] = bank_rooms.second[i + 1] - room_ptr;
271 } else {
272 int bank_end_address = (bank_rooms.first << 16) | 0xFFFF;
273 room_sizes_[room_id] = bank_end_address - room_ptr + 1;
274 }
275 total_room_size_ += room_sizes_[room_id];
276 } else {
277 room_sizes_[room_id] = 0x00;
278 }
279 }
280 }
281}
282
284 if (!rom_ || !rom_->is_loaded()) {
285 return absl::FailedPreconditionError("ROM not loaded");
286 }
287
288 room.ReloadGraphics();
289
290 return absl::OkStatus();
291}
292
294 if (!rom_ || !rom_->is_loaded()) {
295 return absl::FailedPreconditionError("ROM not loaded");
296 }
297
298 // Reload graphics for all rooms
299 for (int room_id = 0; room_id < static_cast<int>(rooms.size()); ++room_id) {
300 auto* room = rooms.GetIfLoaded(room_id);
301 if (room == nullptr) {
302 continue;
303 }
304 auto status = LoadAndRenderRoomGraphics(*room);
305 if (!status.ok()) {
306 continue; // Log error but continue with other rooms
307 }
308 }
309
310 return absl::OkStatus();
311}
312
313} // namespace yaze::editor
const auto & vector() const
Definition rom.h:217
bool is_loaded() const
Definition rom.h:199
absl::Status ReloadAllRoomGraphics(DungeonRoomStore &rooms)
absl::Status LoadRoom(int room_id, zelda3::Room &room)
std::unordered_map< int, int > room_size_addresses_
std::vector< int64_t > room_size_pointers_
absl::Status LoadRoomEntrances(std::array< zelda3::RoomEntrance, zelda3::kNumDungeonEntranceSlots > &entrances)
absl::Status LoadAllRooms(DungeonRoomStore &rooms)
absl::Status LoadDungeonSpawnPoints(std::array< zelda3::DungeonSpawnPoint, zelda3::kNumDungeonSpawnPoints > &spawn_points)
std::unordered_map< int, ImVec4 > room_palette_
absl::Status LoadAndRenderRoomGraphics(zelda3::Room &room)
zelda3::Room * GetIfLoaded(int room_id)
void SetGameData(zelda3::GameData *game_data)
RAII timer for automatic timing management.
static absl::StatusOr< DungeonSpawnPoint > Load(const Rom &rom, int spawn_id)
Dungeon Room Entrance or Spawn Point.
const CustomCollisionMap & custom_collision() const
Definition room.h:559
bool custom_collision_dirty() const
Definition room.h:596
void ReloadGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:1047
void MarkCustomCollisionDirty()
Definition room.h:598
void SetGameData(GameData *data)
Definition room.h:1088
int id() const
Definition room.h:1012
#define LOG_DEBUG(category, format,...)
Definition log.h:133
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
Editors are the view controllers for the application.
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:680
RoomSize CalculateRoomSize(Rom *rom, int room_id)
Definition room.cc:630
constexpr int kNumRegularDungeonEntrances
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:655
constexpr int kNumDungeonSpawnPoints
absl::StatusOr< EntranceLayout > ResolveEntranceLayout(std::span< const uint8_t > data)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
gfx::PaletteGroupMap palette_groups
Definition game_data.h:95