yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
arena.cc
Go to the documentation of this file.
2
4
5#include <algorithm>
6#include <chrono>
7
8#include "absl/strings/str_format.h"
10#include "util/log.h"
11#include "util/sdl_deleter.h"
13
14namespace yaze {
15namespace gfx {
16
18 renderer_ = renderer;
19}
20
22 static Arena instance;
23 return instance;
24}
25
26Arena::Arena() : bg1_(512, 512), bg2_(512, 512) {
27 layer1_buffer_.fill(0);
28 layer2_buffer_.fill(0);
29}
30
32 // Use the safe shutdown method that handles cleanup properly
33 Shutdown();
34}
35
37 // Store generation at queue time for staleness detection
38 uint32_t gen = bitmap ? bitmap->generation() : 0;
39 texture_command_queue_.push_back({type, bitmap, gen});
40}
41
45
47 IRenderer* active_renderer = renderer ? renderer : renderer_;
48 if (!active_renderer || texture_command_queue_.empty()) {
49 return false;
50 }
51
52 auto it = texture_command_queue_.begin();
53 const auto& command = *it;
54 bool processed = false;
55
56 // Skip stale commands where bitmap was reallocated since queuing
57 if (command.bitmap && command.bitmap->generation() != command.generation) {
58 LOG_DEBUG("Arena", "Skipping stale texture command (gen %u != %u)",
59 command.generation, command.bitmap->generation());
60 texture_command_queue_.erase(it);
61 return false;
62 }
63
64 switch (command.type) {
66 if (command.bitmap && command.bitmap->surface() &&
67 command.bitmap->surface()->format && command.bitmap->is_active() &&
68 command.bitmap->width() > 0 && command.bitmap->height() > 0) {
69 try {
70 auto texture = active_renderer->CreateTexture(
71 command.bitmap->width(), command.bitmap->height());
72 if (texture) {
73 command.bitmap->set_texture(texture);
74 active_renderer->UpdateTexture(texture, *command.bitmap);
75 processed = true;
76 }
77 } catch (...) {
78 LOG_ERROR("Arena", "Exception during single texture creation");
79 }
80 }
81 break;
82 }
84 if (command.bitmap && command.bitmap->texture() &&
85 command.bitmap->surface() && command.bitmap->surface()->format &&
86 command.bitmap->is_active()) {
87 try {
88 active_renderer->UpdateTexture(command.bitmap->texture(),
89 *command.bitmap);
90 processed = true;
91 } catch (...) {
92 LOG_ERROR("Arena", "Exception during single texture update");
93 }
94 }
95 break;
96 }
98 if (command.bitmap && command.bitmap->texture()) {
99 try {
100 active_renderer->DestroyTexture(command.bitmap->texture());
101 command.bitmap->set_texture(nullptr);
102 processed = true;
103 } catch (...) {
104 LOG_ERROR("Arena", "Exception during single texture destruction");
105 }
106 }
107 break;
108 }
109 }
110
111 // Always remove the command after attempting (whether successful or not)
112 texture_command_queue_.erase(it);
113 return processed;
114}
115
117 // Use provided renderer if available, otherwise use stored renderer
118 IRenderer* active_renderer = renderer ? renderer : renderer_;
119
120 if (!active_renderer) {
121 // Arena not initialized yet - defer processing
122 return;
123 }
124
125 if (texture_command_queue_.empty()) {
126 return;
127 }
128
129 // Performance optimization: Batch process textures with limits
130 // Process up to 8 texture operations per frame to avoid frame drops
131 constexpr size_t kMaxTexturesPerFrame = 8;
132 size_t processed = 0;
133
134 auto it = texture_command_queue_.begin();
135 while (it != texture_command_queue_.end() &&
136 processed < kMaxTexturesPerFrame) {
137 const auto& command = *it;
138 bool should_remove = true;
139
140 // Skip stale commands where bitmap was reallocated since queuing
141 if (command.bitmap && command.bitmap->generation() != command.generation) {
142 LOG_DEBUG("Arena", "Skipping stale texture command (gen %u != %u)",
143 command.generation, command.bitmap->generation());
144 it = texture_command_queue_.erase(it);
145 continue;
146 }
147
148 // CRITICAL: Replicate the exact short-circuit evaluation from working code
149 // We MUST check command.bitmap AND command.bitmap->surface() in one
150 // expression to avoid dereferencing invalid pointers
151
152 switch (command.type) {
154 // Create a new texture and update it with bitmap data
155 // Use short-circuit evaluation - if bitmap is invalid, never call
156 // ->surface()
157 if (command.bitmap && command.bitmap->surface() &&
158 command.bitmap->is_active() && command.bitmap->width() > 0 &&
159 command.bitmap->height() > 0) {
160
161 // DEBUG: Log texture creation with palette validation
162 auto* surf = command.bitmap->surface();
163 SDL_Palette* palette = platform::GetSurfacePalette(surf);
164 bool has_palette = palette != nullptr;
165 int color_count = has_palette ? palette->ncolors : 0;
166
167 // Log detailed surface state for debugging
169 "Arena::ProcessTextureQueue (CREATE)", surf);
171 "Arena::ProcessTextureQueue", has_palette, color_count);
172
173 // WARNING: Creating texture without proper palette will produce wrong
174 // colors
175 if (!has_palette) {
176 LOG_WARN("Arena",
177 "Creating texture from surface WITHOUT palette - "
178 "colors will be incorrect!");
180 "Arena::ProcessTextureQueue", 0, false,
181 "Surface has NO palette");
182 } else if (color_count < 90) {
183 LOG_WARN("Arena",
184 "Creating texture with only %d palette colors (expected "
185 "90 for dungeon)",
186 color_count);
188 "Arena::ProcessTextureQueue", 0, false,
189 absl::StrFormat("Low color count: %d", color_count));
190 }
191
192 try {
194 "Arena::ProcessTextureQueue", 0, true,
195 "Calling CreateTexture...");
196
197 auto texture = active_renderer->CreateTexture(
198 command.bitmap->width(), command.bitmap->height());
199
200 if (texture) {
202 "Arena::ProcessTextureQueue", 0, true,
203 "CreateTexture SUCCESS");
204
205 command.bitmap->set_texture(texture);
206 active_renderer->UpdateTexture(texture, *command.bitmap);
207 processed++;
208 } else {
210 "Arena::ProcessTextureQueue", 0, false,
211 "CreateTexture returned NULL");
212 should_remove = false; // Retry next frame
213 }
214 } catch (...) {
215 LOG_ERROR("Arena", "Exception during texture creation");
217 "Arena::ProcessTextureQueue", 0, false,
218 "EXCEPTION during texture creation");
219 should_remove = true; // Remove bad command
220 }
221 }
222 break;
223 }
225 // Update existing texture with current bitmap data
226 if (command.bitmap && command.bitmap->texture() &&
227 command.bitmap->surface() && command.bitmap->surface()->format &&
228 command.bitmap->is_active()) {
229 try {
230 active_renderer->UpdateTexture(command.bitmap->texture(),
231 *command.bitmap);
232 processed++;
233 } catch (...) {
234 LOG_ERROR("Arena", "Exception during texture update");
235 }
236 }
237 break;
238 }
240 if (command.bitmap && command.bitmap->texture()) {
241 try {
242 active_renderer->DestroyTexture(command.bitmap->texture());
243 command.bitmap->set_texture(nullptr);
244 processed++;
245 } catch (...) {
246 LOG_ERROR("Arena", "Exception during texture destruction");
247 }
248 }
249 break;
250 }
251 }
252
253 if (should_remove) {
254 it = texture_command_queue_.erase(it);
255 } else {
256 ++it;
257 }
258 }
259}
260
262 float budget_ms) {
263 using Clock = std::chrono::high_resolution_clock;
264 using Microseconds = std::chrono::microseconds;
265
266 IRenderer* active_renderer = renderer ? renderer : renderer_;
267 if (!active_renderer) {
268 return texture_command_queue_.empty();
269 }
270
271 if (texture_command_queue_.empty()) {
272 return true; // Queue is empty, all done
273 }
274
275 // Convert budget to microseconds for precise timing
276 const auto budget_us = static_cast<long long>(budget_ms * 1000.0f);
277 const auto start_time = Clock::now();
278
279 size_t textures_this_call = 0;
280
281 // Process textures until budget exhausted or queue empty
282 while (!texture_command_queue_.empty()) {
283 // Check time budget before each texture (not after, to avoid overshoot)
284 if (textures_this_call > 0) { // Always process at least one
285 auto elapsed = std::chrono::duration_cast<Microseconds>(
286 Clock::now() - start_time);
287 if (elapsed.count() >= budget_us) {
288 LOG_DEBUG("Arena",
289 "Budget exhausted: processed %zu textures in %.2fms, "
290 "%zu remaining",
291 textures_this_call, elapsed.count() / 1000.0f,
293 break;
294 }
295 }
296
297 // Process one texture
298 if (ProcessSingleTexture(active_renderer)) {
299 textures_this_call++;
300 }
301 }
302
303 // Update statistics
304 if (textures_this_call > 0) {
305 auto total_elapsed = std::chrono::duration_cast<Microseconds>(
306 Clock::now() - start_time);
307 float elapsed_ms = total_elapsed.count() / 1000.0f;
308
309 texture_queue_stats_.textures_processed += textures_this_call;
312
313 if (elapsed_ms > texture_queue_stats_.max_frame_time_ms) {
315 }
316
320 static_cast<float>(texture_queue_stats_.textures_processed);
321 }
322 }
323
324 return texture_command_queue_.empty();
325}
326
327SDL_Surface* Arena::AllocateSurface(int width, int height, int depth,
328 int format) {
329 // Try to get a surface from the pool first
330 for (auto it = surface_pool_.available_surfaces_.begin();
331 it != surface_pool_.available_surfaces_.end(); ++it) {
332 auto& info = surface_pool_.surface_info_[*it];
333 if (std::get<0>(info) == width && std::get<1>(info) == height &&
334 std::get<2>(info) == depth && std::get<3>(info) == format) {
335 SDL_Surface* surface = *it;
337 return surface;
338 }
339 }
340
341 // Create new surface if none available in pool
342 Uint32 sdl_format = GetSnesPixelFormat(format);
343 SDL_Surface* surface =
344 platform::CreateSurface(width, height, depth, sdl_format);
345
346 if (surface) {
347 auto surface_ptr =
348 std::unique_ptr<SDL_Surface, util::SDL_Surface_Deleter>(surface);
349 surfaces_[surface] = std::move(surface_ptr);
351 std::make_tuple(width, height, depth, format);
352 }
353
354 return surface;
355}
356
357void Arena::FreeSurface(SDL_Surface* surface) {
358 if (!surface)
359 return;
360
361 // Return surface to pool if space available
363 surface_pool_.available_surfaces_.push_back(surface);
364 } else {
365 // Remove from tracking maps
366 surface_pool_.surface_info_.erase(surface);
367 surfaces_.erase(surface);
368 }
369}
370
372 // Process any remaining batch updates before shutdown
374
375 // Clear LRU cache tracking (doesn't destroy textures, just tracking)
377
378 // Clear pool references first to prevent reuse during shutdown
383
384 // CRITICAL FIX: Clear containers in reverse order to prevent cleanup issues
385 // This ensures that dependent resources are freed before their dependencies
386 textures_.clear();
387 surfaces_.clear();
388
389 // Clear any remaining queue items
391}
392
393void Arena::NotifySheetModified(int sheet_index) {
394 if (sheet_index < 0 || sheet_index >= 223) {
395 LOG_WARN("Arena", "Invalid sheet index %d, ignoring notification",
396 sheet_index);
397 return;
398 }
399
400 auto& sheet = gfx_sheets_[sheet_index];
401 if (!sheet.is_active() || !sheet.surface()) {
402 LOG_DEBUG("Arena",
403 "Sheet %d not active or no surface, skipping notification",
404 sheet_index);
405 return;
406 }
407
408 // Queue texture update so changes are visible in all editors
409 if (sheet.texture()) {
411 LOG_DEBUG("Arena", "Queued texture update for modified sheet %d",
412 sheet_index);
413 } else {
414 // Create texture if it doesn't exist
416 LOG_DEBUG("Arena", "Queued texture creation for modified sheet %d",
417 sheet_index);
418 }
419}
420
421// ========== Palette Change Notification System ==========
422
423void Arena::NotifyPaletteModified(const std::string& group_name,
424 int palette_index) {
425 LOG_DEBUG("Arena", "Palette modified: group='%s', palette=%d",
426 group_name.c_str(), palette_index);
427
428 // Notify all registered listeners
429 for (const auto& [id, callback] : palette_listeners_) {
430 try {
431 callback(group_name, palette_index);
432 } catch (const std::exception& e) {
433 LOG_ERROR("Arena", "Exception in palette listener %d: %s", id, e.what());
434 }
435 }
436
437 LOG_DEBUG("Arena", "Notified %zu palette listeners",
438 palette_listeners_.size());
439}
440
442 int id = next_palette_listener_id_++;
443 palette_listeners_[id] = std::move(callback);
444 LOG_DEBUG("Arena", "Registered palette listener with ID %d", id);
445 return id;
446}
447
448void Arena::UnregisterPaletteListener(int listener_id) {
449 auto it = palette_listeners_.find(listener_id);
450 if (it != palette_listeners_.end()) {
451 palette_listeners_.erase(it);
452 LOG_DEBUG("Arena", "Unregistered palette listener with ID %d", listener_id);
453 }
454}
455
456// ========== LRU Sheet Texture Cache ==========
457
458void Arena::TouchSheet(int sheet_index) {
459 if (sheet_index < 0 || sheet_index >= 223) {
460 return;
461 }
462
463 auto map_it = sheet_lru_map_.find(sheet_index);
464 if (map_it != sheet_lru_map_.end()) {
465 // Sheet already in cache - move to front (most recently used)
466 sheet_lru_list_.erase(map_it->second);
467 sheet_lru_list_.push_front(sheet_index);
468 map_it->second = sheet_lru_list_.begin();
469 } else {
470 // New sheet - add to front
471 sheet_lru_list_.push_front(sheet_index);
472 sheet_lru_map_[sheet_index] = sheet_lru_list_.begin();
473 }
474}
475
477 if (sheet_index < 0 || sheet_index >= 223) {
478 return nullptr;
479 }
480
481 auto& sheet = gfx_sheets_[sheet_index];
482
483 // Check if sheet already has texture (cache hit)
484 bool had_texture = sheet.texture() != nullptr;
485
486 // Touch to update LRU order
487 TouchSheet(sheet_index);
488
489 if (had_texture) {
491 } else {
493
494 // Queue texture creation if sheet has valid surface
495 if (sheet.is_active() && sheet.surface()) {
497 }
498
499 // Check if we need to evict LRU sheets
501 EvictLRUSheets(0); // Evict until under max
502 }
503 }
504
506 return &sheet;
507}
508
509void Arena::SetSheetCacheSize(size_t max_size) {
510 // Clamp to valid range
511 sheet_cache_max_size_ = std::clamp(max_size, size_t{16}, size_t{223});
512
513 // Evict if current cache exceeds new size
516 }
517
518 LOG_INFO("Arena", "Sheet cache size set to %zu", sheet_cache_max_size_);
519}
520
521size_t Arena::EvictLRUSheets(size_t count) {
522 size_t evicted = 0;
523 size_t target_evictions = count;
524
525 // If count is 0, evict until we're under the max size
526 if (count == 0 && sheet_lru_map_.size() > sheet_cache_max_size_) {
527 target_evictions = sheet_lru_map_.size() - sheet_cache_max_size_;
528 }
529
530 // Evict from back of list (least recently used)
531 while (!sheet_lru_list_.empty() && evicted < target_evictions) {
532 int sheet_index = sheet_lru_list_.back();
533 auto& sheet = gfx_sheets_[sheet_index];
534
535 // Destroy texture if it exists
536 if (sheet.texture()) {
538 LOG_DEBUG("Arena", "Evicted LRU sheet %d texture", sheet_index);
539 evicted++;
541 }
542
543 // Remove from LRU tracking
544 sheet_lru_map_.erase(sheet_index);
545 sheet_lru_list_.pop_back();
546 }
547
549
550 if (evicted > 0) {
551 LOG_DEBUG("Arena", "Evicted %zu LRU sheet textures, %zu remaining",
552 evicted, sheet_lru_map_.size());
553 }
554
555 return evicted;
556}
557
559 sheet_lru_list_.clear();
560 sheet_lru_map_.clear();
562 LOG_DEBUG("Arena", "Cleared sheet cache tracking");
563}
564
565} // namespace gfx
566} // namespace yaze
Resource management arena for efficient graphics memory handling.
Definition arena.h:47
IRenderer * renderer_
Definition arena.h:365
size_t sheet_cache_max_size_
Definition arena.h:377
std::unordered_map< SDL_Surface *, std::unique_ptr< SDL_Surface, util::SDL_Surface_Deleter > > surfaces_
Definition arena.h:348
void Initialize(IRenderer *renderer)
Definition arena.cc:17
struct yaze::gfx::Arena::TexturePool texture_pool_
bool ProcessTextureQueueWithBudget(IRenderer *renderer, float budget_ms)
Process texture queue with a time budget.
Definition arena.cc:261
SDL_Surface * AllocateSurface(int width, int height, int depth, int format)
Definition arena.cc:327
std::unordered_map< TextureHandle, std::unique_ptr< SDL_Texture, util::SDL_Texture_Deleter > > textures_
Definition arena.h:344
void ClearTextureQueue()
Definition arena.cc:42
Bitmap * GetSheetWithCache(int sheet_index)
Get a sheet with automatic LRU tracking and texture creation.
Definition arena.cc:476
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:441
std::array< uint16_t, kTotalTiles > layer2_buffer_
Definition arena.h:338
SheetCacheStats sheet_cache_stats_
Definition arena.h:378
std::function< void(const std::string &group_name, int palette_index)> PaletteChangeCallback
Definition arena.h:192
void FreeSurface(SDL_Surface *surface)
Definition arena.cc:357
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:116
std::array< gfx::Bitmap, 223 > gfx_sheets_
Definition arena.h:340
bool ProcessSingleTexture(IRenderer *renderer)
Process a single texture command for frame-budget-aware loading.
Definition arena.cc:46
std::unordered_map< int, std::list< int >::iterator > sheet_lru_map_
Definition arena.h:376
std::unordered_map< int, PaletteChangeCallback > palette_listeners_
Definition arena.h:369
std::vector< TextureCommand > texture_command_queue_
Definition arena.h:364
std::array< uint16_t, kTotalTiles > layer1_buffer_
Definition arena.h:337
void NotifySheetModified(int sheet_index)
Notify Arena that a graphics sheet has been modified.
Definition arena.cc:393
struct yaze::gfx::Arena::SurfacePool surface_pool_
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:448
TextureQueueStats texture_queue_stats_
Definition arena.h:366
void Shutdown()
Definition arena.cc:371
void SetSheetCacheSize(size_t max_size)
Set the maximum number of sheet textures to keep cached.
Definition arena.cc:509
static Arena & Get()
Definition arena.cc:21
size_t EvictLRUSheets(size_t count=0)
Evict least recently used sheet textures.
Definition arena.cc:521
std::list< int > sheet_lru_list_
Definition arena.h:374
int next_palette_listener_id_
Definition arena.h:370
void NotifyPaletteModified(const std::string &group_name, int palette_index=-1)
Notify all listeners that a palette has been modified.
Definition arena.cc:423
void ClearSheetCache()
Clear all sheet texture cache tracking.
Definition arena.cc:558
void TouchSheet(int sheet_index)
Mark a graphics sheet as recently accessed.
Definition arena.cc:458
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
TextureHandle texture() const
Definition bitmap.h:380
uint32_t generation() const
Definition bitmap.h:385
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
virtual TextureHandle CreateTexture(int width, int height)=0
Creates a new, empty texture.
virtual void UpdateTexture(TextureHandle texture, const Bitmap &bitmap)=0
Updates a texture with the pixel data from a Bitmap.
virtual void DestroyTexture(TextureHandle texture)=0
Destroys a texture and frees its associated resources.
void LogTextureCreation(const std::string &location, bool has_palette, int color_count)
static PaletteDebugger & Get()
void LogPaletteApplication(const std::string &location, int palette_id, bool success, const std::string &reason="")
void LogSurfaceState(const std::string &location, SDL_Surface *surface)
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
Uint32 GetSnesPixelFormat(int format)
Convert bitmap format enum to SDL pixel format.
Definition bitmap.cc:33
SDL_Surface * CreateSurface(int width, int height, int depth, uint32_t format)
Create a surface using the appropriate API.
Definition sdl_compat.h:418
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:375
SDL2/SDL3 compatibility layer.
static constexpr size_t MAX_POOL_SIZE
Definition arena.h:361
std::vector< SDL_Surface * > available_surfaces_
Definition arena.h:358
std::unordered_map< SDL_Surface *, std::tuple< int, int, int, int > > surface_info_
Definition arena.h:360
std::vector< TextureHandle > available_textures_
Definition arena.h:352
std::unordered_map< TextureHandle, std::pair< int, int > > texture_sizes_
Definition arena.h:353