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 bool should_remove = true;
56
57 // Skip stale commands where bitmap was reallocated since queuing
58 if (command.bitmap && command.bitmap->generation() != command.generation) {
59 LOG_DEBUG("Arena", "Skipping stale texture command (gen %u != %u)",
60 command.generation, command.bitmap->generation());
61 texture_command_queue_.erase(it);
62 return false;
63 }
64
65 switch (command.type) {
67 if (command.bitmap && command.bitmap->surface() &&
68 command.bitmap->surface()->format && command.bitmap->is_active() &&
69 command.bitmap->width() > 0 && command.bitmap->height() > 0) {
70 const auto old_texture = command.bitmap->texture();
71 TextureHandle replacement = nullptr;
72 try {
73 replacement = active_renderer->CreateTexture(
74 command.bitmap->width(), command.bitmap->height());
75 } catch (...) {
76 LOG_ERROR("Arena", "Exception during single texture creation");
77 should_remove = false;
78 break;
79 }
80 if (!replacement) {
81 should_remove = false;
82 break;
83 }
84 try {
85 active_renderer->UpdateTexture(replacement, *command.bitmap);
86 } catch (...) {
87 LOG_ERROR("Arena", "Exception updating new single texture");
88 if (replacement != old_texture) {
89 try {
90 active_renderer->DestroyTexture(replacement);
91 } catch (...) {
92 LOG_ERROR("Arena", "Exception cleaning up failed single texture");
93 }
94 }
95 should_remove = false;
96 break;
97 }
98
99 command.bitmap->set_texture(replacement);
100 if (old_texture && old_texture != replacement) {
101 try {
102 active_renderer->DestroyTexture(old_texture);
103 } catch (...) {
104 LOG_ERROR("Arena", "Exception destroying replaced single texture");
105 }
106 }
107 processed = true;
108 }
109 break;
110 }
112 if (command.bitmap && command.bitmap->texture() &&
113 command.bitmap->surface() && command.bitmap->surface()->format &&
114 command.bitmap->is_active()) {
115 try {
116 active_renderer->UpdateTexture(command.bitmap->texture(),
117 *command.bitmap);
118 processed = true;
119 } catch (...) {
120 LOG_ERROR("Arena", "Exception during single texture update");
121 }
122 }
123 break;
124 }
126 if (command.bitmap && command.bitmap->texture()) {
127 try {
128 active_renderer->DestroyTexture(command.bitmap->texture());
129 command.bitmap->set_texture(nullptr);
130 processed = true;
131 } catch (...) {
132 LOG_ERROR("Arena", "Exception during single texture destruction");
133 }
134 }
135 break;
136 }
137 }
138
139 if (should_remove) {
140 texture_command_queue_.erase(it);
141 }
142 return processed;
143}
144
146 // Use provided renderer if available, otherwise use stored renderer
147 IRenderer* active_renderer = renderer ? renderer : renderer_;
148
149 if (!active_renderer) {
150 // Arena not initialized yet - defer processing
151 return;
152 }
153
154 if (texture_command_queue_.empty()) {
155 return;
156 }
157
158 // Performance optimization: Batch process textures with limits
159 // Process up to 8 texture operations per frame to avoid frame drops
160 constexpr size_t kMaxTexturesPerFrame = 8;
161 size_t processed = 0;
162
163 auto it = texture_command_queue_.begin();
164 while (it != texture_command_queue_.end() &&
165 processed < kMaxTexturesPerFrame) {
166 const auto& command = *it;
167 bool should_remove = true;
168
169 // Skip stale commands where bitmap was reallocated since queuing
170 if (command.bitmap && command.bitmap->generation() != command.generation) {
171 LOG_DEBUG("Arena", "Skipping stale texture command (gen %u != %u)",
172 command.generation, command.bitmap->generation());
173 it = texture_command_queue_.erase(it);
174 continue;
175 }
176
177 // CRITICAL: Replicate the exact short-circuit evaluation from working code
178 // We MUST check command.bitmap AND command.bitmap->surface() in one
179 // expression to avoid dereferencing invalid pointers
180
181 switch (command.type) {
183 // Create a new texture and update it with bitmap data
184 // Use short-circuit evaluation - if bitmap is invalid, never call
185 // ->surface()
186 if (command.bitmap && command.bitmap->surface() &&
187 command.bitmap->is_active() && command.bitmap->width() > 0 &&
188 command.bitmap->height() > 0) {
189
190 // DEBUG: Log texture creation with palette validation
191 auto* surf = command.bitmap->surface();
192 SDL_Palette* palette = platform::GetSurfacePalette(surf);
193 bool has_palette = palette != nullptr;
194 int color_count = has_palette ? palette->ncolors : 0;
195
196 // Log detailed surface state for debugging
198 "Arena::ProcessTextureQueue (CREATE)", surf);
200 "Arena::ProcessTextureQueue", has_palette, color_count);
201
202 // WARNING: Creating texture without proper palette will produce wrong
203 // colors
204 if (!has_palette) {
205 LOG_WARN("Arena",
206 "Creating texture from surface WITHOUT palette - "
207 "colors will be incorrect!");
209 "Arena::ProcessTextureQueue", 0, false,
210 "Surface has NO palette");
211 } else if (color_count < 90) {
212 LOG_WARN("Arena",
213 "Creating texture with only %d palette colors (expected "
214 "90 for dungeon)",
215 color_count);
217 "Arena::ProcessTextureQueue", 0, false,
218 absl::StrFormat("Low color count: %d", color_count));
219 }
220
222 "Arena::ProcessTextureQueue", 0, true,
223 "Calling CreateTexture...");
224 const auto old_texture = command.bitmap->texture();
225 TextureHandle replacement = nullptr;
226 try {
227 replacement = active_renderer->CreateTexture(
228 command.bitmap->width(), command.bitmap->height());
229 } catch (...) {
230 LOG_ERROR("Arena", "Exception during texture creation");
232 "Arena::ProcessTextureQueue", 0, false,
233 "EXCEPTION during texture creation");
234 should_remove = false;
235 break;
236 }
237
238 if (!replacement) {
240 "Arena::ProcessTextureQueue", 0, false,
241 "CreateTexture returned NULL");
242 should_remove = false;
243 break;
244 }
245
246 try {
247 active_renderer->UpdateTexture(replacement, *command.bitmap);
248 } catch (...) {
249 LOG_ERROR("Arena", "Exception updating new texture");
250 if (replacement != old_texture) {
251 try {
252 active_renderer->DestroyTexture(replacement);
253 } catch (...) {
254 LOG_ERROR("Arena", "Exception cleaning up failed texture");
255 }
256 }
257 should_remove = false;
258 break;
259 }
260
261 command.bitmap->set_texture(replacement);
262 if (old_texture && old_texture != replacement) {
263 try {
264 active_renderer->DestroyTexture(old_texture);
265 } catch (...) {
266 LOG_ERROR("Arena", "Exception destroying replaced texture");
267 }
268 }
270 "Arena::ProcessTextureQueue", 0, true, "CreateTexture SUCCESS");
271 processed++;
272 }
273 break;
274 }
276 // Update existing texture with current bitmap data
277 if (command.bitmap && command.bitmap->texture() &&
278 command.bitmap->surface() && command.bitmap->surface()->format &&
279 command.bitmap->is_active()) {
280 try {
281 active_renderer->UpdateTexture(command.bitmap->texture(),
282 *command.bitmap);
283 processed++;
284 } catch (...) {
285 LOG_ERROR("Arena", "Exception during texture update");
286 }
287 }
288 break;
289 }
291 if (command.bitmap && command.bitmap->texture()) {
292 try {
293 active_renderer->DestroyTexture(command.bitmap->texture());
294 command.bitmap->set_texture(nullptr);
295 processed++;
296 } catch (...) {
297 LOG_ERROR("Arena", "Exception during texture destruction");
298 }
299 }
300 break;
301 }
302 }
303
304 if (should_remove) {
305 it = texture_command_queue_.erase(it);
306 } else {
307 ++it;
308 }
309 }
310}
311
313 float budget_ms) {
314 using Clock = std::chrono::high_resolution_clock;
315 using Microseconds = std::chrono::microseconds;
316
317 IRenderer* active_renderer = renderer ? renderer : renderer_;
318 if (!active_renderer) {
319 return texture_command_queue_.empty();
320 }
321
322 if (texture_command_queue_.empty()) {
323 return true; // Queue is empty, all done
324 }
325
326 // Convert budget to microseconds for precise timing
327 const auto budget_us = static_cast<long long>(budget_ms * 1000.0f);
328 const auto start_time = Clock::now();
329
330 size_t textures_this_call = 0;
331
332 // Process textures until budget exhausted or queue empty
333 while (!texture_command_queue_.empty()) {
334 // Check time budget before each texture (not after, to avoid overshoot)
335 if (textures_this_call > 0) { // Always process at least one
336 auto elapsed =
337 std::chrono::duration_cast<Microseconds>(Clock::now() - start_time);
338 if (elapsed.count() >= budget_us) {
339 LOG_DEBUG("Arena",
340 "Budget exhausted: processed %zu textures in %.2fms, "
341 "%zu remaining",
342 textures_this_call, elapsed.count() / 1000.0f,
344 break;
345 }
346 }
347
348 // A failed CREATE remains queued for a later frame. Stop this budget pass
349 // when no command was removed so a retry cannot spin on the same front
350 // entry indefinitely.
351 const size_t queue_size_before = texture_command_queue_.size();
352 if (ProcessSingleTexture(active_renderer)) {
353 textures_this_call++;
354 } else if (texture_command_queue_.size() == queue_size_before) {
355 break;
356 }
357 }
358
359 // Update statistics
360 if (textures_this_call > 0) {
361 auto total_elapsed =
362 std::chrono::duration_cast<Microseconds>(Clock::now() - start_time);
363 float elapsed_ms = total_elapsed.count() / 1000.0f;
364
365 texture_queue_stats_.textures_processed += textures_this_call;
368
369 if (elapsed_ms > texture_queue_stats_.max_frame_time_ms) {
371 }
372
376 static_cast<float>(texture_queue_stats_.textures_processed);
377 }
378 }
379
380 return texture_command_queue_.empty();
381}
382
383SDL_Surface* Arena::AllocateSurface(int width, int height, int depth,
384 int format) {
385 // Try to get a surface from the pool first
386 for (auto it = surface_pool_.available_surfaces_.begin();
387 it != surface_pool_.available_surfaces_.end(); ++it) {
388 auto& info = surface_pool_.surface_info_[*it];
389 if (std::get<0>(info) == width && std::get<1>(info) == height &&
390 std::get<2>(info) == depth && std::get<3>(info) == format) {
391 SDL_Surface* surface = *it;
393 return surface;
394 }
395 }
396
397 // Create new surface if none available in pool
398 Uint32 sdl_format = GetSnesPixelFormat(format);
399 SDL_Surface* surface =
400 platform::CreateSurface(width, height, depth, sdl_format);
401
402 if (surface) {
403 auto surface_ptr =
404 std::unique_ptr<SDL_Surface, util::SDL_Surface_Deleter>(surface);
405 surfaces_[surface] = std::move(surface_ptr);
407 std::make_tuple(width, height, depth, format);
408 }
409
410 return surface;
411}
412
413void Arena::FreeSurface(SDL_Surface* surface) {
414 if (!surface)
415 return;
416
417 // Return surface to pool if space available
419 surface_pool_.available_surfaces_.push_back(surface);
420 } else {
421 // Remove from tracking maps
422 surface_pool_.surface_info_.erase(surface);
423 surfaces_.erase(surface);
424 }
425}
426
428 // Process any remaining batch updates before shutdown
430
431 // Clear LRU cache tracking (doesn't destroy textures, just tracking)
433
434 // Clear pool references first to prevent reuse during shutdown
439
440 // CRITICAL FIX: Clear containers in reverse order to prevent cleanup issues
441 // This ensures that dependent resources are freed before their dependencies
442 textures_.clear();
443 surfaces_.clear();
444
445 // Clear any remaining queue items
447}
448
449void Arena::NotifySheetModified(int sheet_index) {
450 if (sheet_index < 0 || sheet_index >= 223) {
451 LOG_WARN("Arena", "Invalid sheet index %d, ignoring notification",
452 sheet_index);
453 return;
454 }
455
456 auto& sheet = gfx_sheets_[sheet_index];
457 if (!sheet.is_active() || !sheet.surface()) {
458 LOG_DEBUG("Arena",
459 "Sheet %d not active or no surface, skipping notification",
460 sheet_index);
461 return;
462 }
463
464 // Queue texture update so changes are visible in all editors
465 if (sheet.texture()) {
467 LOG_DEBUG("Arena", "Queued texture update for modified sheet %d",
468 sheet_index);
469 } else {
470 // Create texture if it doesn't exist
472 LOG_DEBUG("Arena", "Queued texture creation for modified sheet %d",
473 sheet_index);
474 }
475}
476
477// ========== Palette Change Notification System ==========
478
479void Arena::NotifyPaletteModified(const std::string& group_name,
480 int palette_index) {
481 LOG_DEBUG("Arena", "Palette modified: group='%s', palette=%d",
482 group_name.c_str(), palette_index);
483
484 // Notify all registered listeners
485 for (const auto& [id, callback] : palette_listeners_) {
486 try {
487 callback(group_name, palette_index);
488 } catch (const std::exception& e) {
489 LOG_ERROR("Arena", "Exception in palette listener %d: %s", id, e.what());
490 }
491 }
492
493 LOG_DEBUG("Arena", "Notified %zu palette listeners",
494 palette_listeners_.size());
495}
496
498 int id = next_palette_listener_id_++;
499 palette_listeners_[id] = std::move(callback);
500 LOG_DEBUG("Arena", "Registered palette listener with ID %d", id);
501 return id;
502}
503
504void Arena::UnregisterPaletteListener(int listener_id) {
505 auto it = palette_listeners_.find(listener_id);
506 if (it != palette_listeners_.end()) {
507 palette_listeners_.erase(it);
508 LOG_DEBUG("Arena", "Unregistered palette listener with ID %d", listener_id);
509 }
510}
511
512// ========== LRU Sheet Texture Cache ==========
513
514void Arena::TouchSheet(int sheet_index) {
515 if (sheet_index < 0 || sheet_index >= 223) {
516 return;
517 }
518
519 auto map_it = sheet_lru_map_.find(sheet_index);
520 if (map_it != sheet_lru_map_.end()) {
521 // Sheet already in cache - move to front (most recently used)
522 sheet_lru_list_.erase(map_it->second);
523 sheet_lru_list_.push_front(sheet_index);
524 map_it->second = sheet_lru_list_.begin();
525 } else {
526 // New sheet - add to front
527 sheet_lru_list_.push_front(sheet_index);
528 sheet_lru_map_[sheet_index] = sheet_lru_list_.begin();
529 }
530}
531
533 if (sheet_index < 0 || sheet_index >= 223) {
534 return nullptr;
535 }
536
537 auto& sheet = gfx_sheets_[sheet_index];
538
539 // Check if sheet already has texture (cache hit)
540 bool had_texture = sheet.texture() != nullptr;
541
542 // Touch to update LRU order
543 TouchSheet(sheet_index);
544
545 if (had_texture) {
547 } else {
549
550 // Queue texture creation if sheet has valid surface
551 if (sheet.is_active() && sheet.surface()) {
553 }
554
555 // Check if we need to evict LRU sheets
557 EvictLRUSheets(0); // Evict until under max
558 }
559 }
560
562 return &sheet;
563}
564
565void Arena::SetSheetCacheSize(size_t max_size) {
566 // Clamp to valid range
567 sheet_cache_max_size_ = std::clamp(max_size, size_t{16}, size_t{223});
568
569 // Evict if current cache exceeds new size
572 }
573
574 LOG_INFO("Arena", "Sheet cache size set to %zu", sheet_cache_max_size_);
575}
576
577size_t Arena::EvictLRUSheets(size_t count) {
578 size_t evicted = 0;
579 size_t target_evictions = count;
580
581 // If count is 0, evict until we're under the max size
582 if (count == 0 && sheet_lru_map_.size() > sheet_cache_max_size_) {
583 target_evictions = sheet_lru_map_.size() - sheet_cache_max_size_;
584 }
585
586 // Evict from back of list (least recently used)
587 while (!sheet_lru_list_.empty() && evicted < target_evictions) {
588 int sheet_index = sheet_lru_list_.back();
589 auto& sheet = gfx_sheets_[sheet_index];
590
591 // Destroy texture if it exists
592 if (sheet.texture()) {
594 LOG_DEBUG("Arena", "Evicted LRU sheet %d texture", sheet_index);
595 evicted++;
597 }
598
599 // Remove from LRU tracking
600 sheet_lru_map_.erase(sheet_index);
601 sheet_lru_list_.pop_back();
602 }
603
605
606 if (evicted > 0) {
607 LOG_DEBUG("Arena", "Evicted %zu LRU sheet textures, %zu remaining", evicted,
608 sheet_lru_map_.size());
609 }
610
611 return evicted;
612}
613
615 sheet_lru_list_.clear();
616 sheet_lru_map_.clear();
618 LOG_DEBUG("Arena", "Cleared sheet cache tracking");
619}
620
621} // namespace gfx
622} // 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:312
SDL_Surface * AllocateSurface(int width, int height, int depth, int format)
Definition arena.cc:383
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:532
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:36
int RegisterPaletteListener(PaletteChangeCallback callback)
Register a callback for palette change notifications.
Definition arena.cc:497
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:413
void ProcessTextureQueue(IRenderer *renderer)
Definition arena.cc:145
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:449
struct yaze::gfx::Arena::SurfacePool surface_pool_
void UnregisterPaletteListener(int listener_id)
Unregister a palette change listener.
Definition arena.cc:504
TextureQueueStats texture_queue_stats_
Definition arena.h:366
void Shutdown()
Definition arena.cc:427
void SetSheetCacheSize(size_t max_size)
Set the maximum number of sheet textures to keep cached.
Definition arena.cc:565
static Arena & Get()
Definition arena.cc:21
size_t EvictLRUSheets(size_t count=0)
Evict least recently used sheet textures.
Definition arena.cc:577
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:479
void ClearSheetCache()
Clear all sheet texture cache tracking.
Definition arena.cc:614
void TouchSheet(int sheet_index)
Mark a graphics sheet as recently accessed.
Definition arena.cc:514
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
TextureHandle texture() const
Definition bitmap.h:401
uint32_t generation() const
Definition bitmap.h:406
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
void * TextureHandle
An abstract handle representing a texture.
Definition irenderer.h:47
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:439
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:392
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