#ifndef PSX_MOD_CAPCOM_BG2D_H #define PSX_MOD_CAPCOM_BG2D_H #include "mod_plugins.h" #include "gpu.h" #include "ws_view_anchor.h" #include /* Capcom's 16px tile engine: resolve the authored map, never a wrapping * guest tile ring. The title supplies addresses, palette layout and policies. * Host packets use the modern GPU view metadata and snapshot-owned DMA RAM. * No guest camera, producer loop, ring, or native packet budget is changed. */ typedef struct PSXCapcomTileMap { uint32_t map, metatiles, descriptors; unsigned width, height, layer_stride, layer, left_screen, right_screen; } PSXCapcomTileMap; typedef struct PSXCapcomBackground { uint32_t heads, tails; unsigned layers, buckets, clut_base; } PSXCapcomBackground; #define PSX_CAPCOM_BG_LAYER_BYTES (1024u * 1024u) static inline int psx_capcom_ram_range(uint32_t p, uint32_t n) { p &= 0x1fffffffu; return p >= 0x10000u && p < 0x200000u && n <= 0x200000u - p; } static inline uint16_t psx_capcom_map_tile(const PSXCapcomTileMap *m, int x, int y) { if (x < 0 || y < 0 || !m->width || !m->height || !m->layer_stride || m->width > m->layer_stride / m->height) return 0; unsigned col = (unsigned)x / 256u, row = (unsigned)y / 256u; if (col < m->left_screen || col > m->right_screen || col >= m->width || row >= m->height) return 0; uint64_t cell = (uint64_t)m->map + m->layer * (uint64_t)m->layer_stride + row * m->width + col; if (cell > UINT32_MAX || !psx_capcom_ram_range((uint32_t)cell, 1)) return 0; uint64_t tile = (uint64_t)m->metatiles + psx_mod_read_byte((uint32_t)cell) * 512u + (((unsigned)y / 16u) & 15u) * 32u + (((unsigned)x / 16u) & 15u) * 2u; return tile <= UINT32_MAX && psx_capcom_ram_range((uint32_t)tile, 2) ? psx_mod_read_half((uint32_t)tile) : 0; } static inline uint32_t psx_capcom_tile_uvclut(uint32_t desc, unsigned clut_base) { unsigned page = desc >> 24; unsigned clut = clut_base + ((desc & 0xf000u) >> 6) + ((page & 0x40u) << 4); clut |= (desc >> 8) & 15u; return (clut << 16) | ((desc >> 12) & 0xf0u) | (((desc >> 16) & 0xf0u) << 8); } static inline int psx_capcom_floor_div(int n, int d) { return n >= 0 ? n / d : -((-n + d - 1) / d); } /* The title evaluates its retail scroll mapping at the camera and at the * wider view's left edge. Preserve its rounding and authored offsets instead * of assuming every plane scrolls at the foreground camera's speed. */ static inline WsViewAnchor psx_capcom_scroll_view(WsViewAnchor fg, int current_scroll, int view_scroll) { int extra = (fg.left + fg.right + fg.pad_left + fg.pad_right) / 2; int origin = current_scroll - view_scroll + fg.pad_left; /* A faster plane can crop part of the native window at a room end. * Emit enough source tiles; keep the true shift for GPU viewport clipping. */ int right = 2 * extra - origin; WsViewAnchor result = {origin > 0 ? origin : 0, right > 0 ? right : 0, origin - extra, fg.pad_left, fg.pad_right}; return result; } static inline WsViewAnchor psx_capcom_parallax_view(WsViewAnchor fg, int divisor, int camera) { return psx_capcom_scroll_view(fg, psx_capcom_floor_div(camera, divisor), psx_capcom_floor_div(camera - fg.left, divisor)); } /* Optional per-tile title policy (panorama wrapping, retained texture bank, * or authored region substitution). NULL uses live VRAM and the native map. */ typedef void (*PSXCapcomTilePolicy)(unsigned layer, int *x, int *y, unsigned *mirror_x, uint16_t *bank); static inline uint32_t psx_capcom_background_activate(unsigned layers) { if (!layers || layers > 8u) return 0; uint32_t bytes = 2u * layers * PSX_CAPCOM_BG_LAYER_BYTES; uint32_t arena = psx_mod_alloc_gpu_dma_memory(bytes, 32u); if (arena) gpu_ws_bg2d_set_host_arena(arena, bytes); return arena; } /* Return emitted packet count, or -1 if setup is invalid. All setup checks * precede replacing the current layer's OT lists. Both its priority groups * and the other display buffer stay distinct. Only the native callback that * follows this operation splices these groups into the final game OT. */ static inline int psx_capcom_background_render(const PSXCapcomBackground *b, uint32_t arena, unsigned buffer, const PSXCapcomTileMap *map, int sx, int sy, WsViewAnchor view, uint32_t native_packet, PSXCapcomTilePolicy policy) { if (!arena || !b->layers || b->layers > 8u || map->layer >= b->layers || buffer > 1u || !b->buckets || b->buckets > 64u || !map->width || !map->height || !psx_capcom_ram_range(map->map, 1) || !psx_capcom_ram_range(map->metatiles, 512) || !psx_capcom_ram_range(map->descriptors, 4)) return -1; if (view.left <= 0 && view.right <= 0) return 0; if (view.left < 0 || view.right < 0 || view.left > 32768 || view.right > 32768) return -1; int left = (view.left + 15) / 16, right = (view.right + 15) / 16; int columns = left + 21 + right; int screen_x = -(sx & 15), screen_y = -(sy & 15); if (columns > 2048 || screen_x - left * 16 < -32768 || screen_x + (20 + right) * 16 > 32767) return -1; unsigned group_bytes = b->buckets * 4u, buffer_bytes = 2u * b->layers * group_bytes; uint32_t ot_bytes = 2u * buffer_bytes; if (!psx_capcom_ram_range(b->heads, ot_bytes) || !psx_capcom_ram_range(b->tails, ot_bytes)) return -1; uint32_t cursor = arena + (buffer * b->layers + map->layer) * PSX_CAPCOM_BG_LAYER_BYTES; uint32_t first = cursor; uint32_t color = psx_mod_read_word(native_packet + 4u); if (((color >> 24) & 0xfdu) != 0x7du) color = 0x7d808080u; int start_col = sx / 16, start_row = sy / 16; /* Retail truncates negative scroll. */ for (unsigned group = map->layer; group < 2u * b->layers; group += b->layers) { for (unsigned i = 0; i < b->buckets; ++i) { uint32_t offset = buffer * buffer_bytes + group * group_bytes + i * 4u; psx_mod_write_word(b->heads + offset, 0); psx_mod_write_word(b->tails + offset, b->heads + offset); } } for (int row = 0; row < 16; ++row) for (int col = -left; col < 21 + right; ++col) { int x = (start_col + col) * 16, y = (start_row + row) * 16; unsigned mirror = 0; uint16_t bank = 0; if (policy) policy(map->layer, &x, &y, &mirror, &bank); uint16_t tile = psx_capcom_map_tile(map, x, y); if (!tile) continue; uint64_t desc_addr = (uint64_t)map->descriptors + (tile & 0x3fffu) * 4u; if (desc_addr > UINT32_MAX || !psx_capcom_ram_range((uint32_t)desc_addr, 4)) continue; uint32_t desc = psx_mod_read_word((uint32_t)desc_addr); unsigned bucket = (desc >> 24) & 0x3fu; if ((desc >> 24) == 255u || bucket >= b->buckets) continue; unsigned group = map->layer + ((tile & 0x8000u) ? b->layers : 0u); uint32_t tail_slot = b->tails + buffer * buffer_bytes + group * group_bytes + bucket * 4u; uint32_t tail = psx_mod_read_word(tail_slot); psx_mod_write_word(cursor, 0x03000000u); psx_mod_write_word(cursor + 4u, (color & ~0x02000000u) | ((tile & 0x4000u) ? 0x02000000u : 0u)); psx_mod_write_word(cursor + 8u, (uint16_t)(screen_x + col * 16) | ((uint32_t)(uint16_t)(screen_y + row * 16) << 16)); psx_mod_write_word(cursor + 12u, psx_capcom_tile_uvclut(desc, b->clut_base)); psx_mod_write_word(cursor + 16u, bank ? (uint16_t)view.shift | (uint32_t)bank << 16 : (uint32_t)view.shift); psx_mod_write_word(cursor + 20u, (uint32_t)view.pad_left); psx_mod_write_word(cursor + 24u, (uint32_t)view.pad_right); psx_mod_write_word(cursor + 28u, (bank ? GPU_WS_BG2D_BANK_PACKET_MAGIC : GPU_WS_BG2D_PACKET_MAGIC) | (mirror ? GPU_WS_BG2D_MIRROR_X : 0u)); psx_mod_write_word(tail, (psx_mod_read_word(tail) & 0xff000000u) | (cursor & 0xffffffu)); psx_mod_write_word(tail_slot, cursor); cursor += 32u; } return (int)((cursor - first) / 32u); } #endif