///////////////////////////////////////////////////////////////////////////////// // // Persona 2 Eternal Punishment (PSX) JAP / Custom Characters/Data Patch // Author: Sergey Shemet 05/11/2025 // // v 1.1 - Grouped Chars VRAM Rendering // .psx // Определения функций .definelabel SetDrawTPage, 0x800578fc .definelabel storeColor, 0x8001c0b4 .definelabel initCopyCharChain, 0x8001b110 .definelabel MakeShadowSmallChar, 0x8001b2a8 .definelabel PrintBigDMAText, 0x8001a3a8 .definelabel MyAddr, 0x8009 .open "2_EP/BIN/SLPS_028.25", 0x8000F800 .org 0x80090000 ExternalPrint: lhu v0, 0x0(a0) // Читаем 2 байта текста nop srl v1, v0, 0xD // Проверяем 13-й бит (система 1 байт) bne v1, zero, MyPrintLineRoutine // Если установлен, переходим к нашей процедуре clear v1 j PrintBigDMAText // Иначе используем стандартную процедуру nop MyPrintLineRoutine: addiu sp, sp, -0x50 // Выделяем место в стеке sw ra, 0x48(sp) // Сохраняем регистры sw s0, 0x28(sp) sw s1, 0x2c(sp) sw s2, 0x30(sp) sw s3, 0x34(sp) sw s4, 0x38(sp) // Сохраняем регистры sw s5, 0x3c(sp) sw s6, 0x40(sp) sw s7, 0x44(sp) move s6, a0 // Адрес чтения текста -> s6 move s1, a1 move s2, a2 // Смещаем поинтер чтения andi v0, v0, 0xff // Берем младший байт addiu s6, s6, 0x2 // Сдвигаем указатель текста на +2 байта lui s3, MyAddr sh v0, -0x10(s3) // Сохраняем счетчик байтов @ 8008fff0 для общего количества sh v0, -0x0e(s3) // Сохраняем счетчик байтов @ 8008fff2 для обратного отсчёта lui t0, 0xff ori t0, t0, 0xffff // t0 = 00FFFFFF sw s0, 0x28(sp) // Сохраняем s0 в стеке andi s0, a3, 0xff // s0 - режим текста (цвет и тень) move a0, s0 // a0 = s0 (обрезанный цвет) lui a3, 0x8008 // a3 = 80080000 lui a2, 0xff00 // a2 = FF000000 //Начинаем формировать цепочки addiu t1, a3, -0x31a8 // t1 = 8007ce68 - Второй счётчик цепочек (корорый меньше) lw v1, 0x0(t1) // v1 = загружаем свободную цепочку lbu a1, 0x60(sp) // Флаг тени из стека -> a1 lui v0, 0x8000 // v0 = 80000000 and a2, v1, a2 // a2 = v1 & ff000000 andi a1, a1, 0xff // Очищаем режим текста and v1, v1, t0 // v1 & 00ffffff or s4, v1, v0 // s5 - основной адрес свободной цепочки lw v0, 0x4(t1) // Загружаем счетчик DMA lw v1, 0x0(s5) // v1 = следующая свободная цепочка addiu v0, v0, -0x1 // Уменьшаем счетчик DMA and v1, v1, t0 // v1 & 00ffffff or a2, a2, v1 // a2 | v1 = следующая свободная цепочка sw v0, 0x4(t1) // Сохраняем счетчик DMA jal storeColor //Сохраняем цвет sw a2, 0x0(t1) // Сохраняем следующую свободную цепочку move a0, s4 // Текущий адрес цепочки sll a1, s1, 0x10 // a1 = X << 16 sra a1, a1, 0x10 // a1 = X координата sll a2, s2, 0x10 // a2 = Y << 16 sra a2, a2, 0x10 // a2 = Y координата jal initCopyCharChain // Инициализация цепочки символов (в a0 адрес, a1 - X, a2 - Y) move a3, s0 //Режим cputovram_scratchpad_template: // Инициализация ScratchPad (CPU to VRAM) lui a3, 0x1f80 ori a3, a3, 0x0348 // a3 = CpuToVram cmd template addr lui a1, 0x0F00 // Количество команд без flushcache sw a1, 0x0(a3) // Сохраняем количество команд lui a0, 0xa000 // a0 = a0000000 sw a0, 0x4(a3) lui v0, 0x01f0 ori v0, 0x0130 sw v0, 0x8(a3) // сохраняем VU для CPUtoVRAM lui v0, 0xc ori v0, 0x2 sw v0, 0xc(a3) // сохраняем 000c0002 (ширина в 16-битных пикселях) _1bppTo4bpp_table_template: // Копирование таблицы преобразования 1bpp в 4bpp lui t8, 0x1f80 ori t8, t8, 0x390 lui t7, 0x8001 ori t7, t7, 0x3e8 lw t5, 0x0(t7) lw t6, 0x4(t7) sw t5, 0x0(t8) sw t6, 0x4(t8) lw t5, 0x8(t7) lw t6, 0xC(t7) sw t5, 0x8(t8) sw t6, 0xC(t8) lw t5, 0x10(t7) lw t6, 0x14(t7) sw t5, 0x10(t8) sw t6, 0x14(t8) lw t5, 0x18(t7) lw t6, 0x1C(t7) sw t5, 0x18(t8) sw t6, 0x1C(t8) rect_scratchpad_template: // Шаблон команды rect в scratchpad lui s3, 0x1f80 ori s3, 0x03d0 // s3 = 1f8003d0 - адрес rect в scratchpad lui v0, 0x0400 sw v0, 0x0(s3) // Длина цепочки = 4 lui v0, 0x6480 ori v0, 0x8080 sw v0, 0x4(s3) li v0, 0xf0c0 sh v0, 0xc(s3) // Сохраняем координаты VU (всегда f0c0) lhu v1, 0x9a(gp) // Загружаем CLUT из GP (всегда 7FD3) li v0, 0 sw v0, 0x8(s3) //Чистим экранные координаты sh s2, 0xa(s3) //И сразу устанавливаем Y lui v0, 0xc ori v0, 0x8 // Ширина спрайта = 8 sw v0, 0x10(s3) // Сохраняем ширину sh v1, 0xe(s3) // Сохраняем CLUT make_sprite_size_table: lui s3, 0x1f80 ori s3, 0x03f0 // Будем серить в scratch lui v0, 0x0403 // таблицей рассчитанных команд и ширин ori v0, 0x0201 // необходимых для спрайтов линии (cmdCount << 2) sw v0, 0x0(s3) lui v1, 0x0706 ori v1, 0x0504 sw v1, 0x4(s3) li v0, 0x0807 sh v0, 0x8(s3) misc_init: move t6, s1 // Текущий экранный X, который будет смещаться и писаться в команду rect // Устанавливаем, исходя из прилетевшей координаты X TextGroupReadLoop: // Смотрим количество символов до предела (не более 10) (проверяем MyAddr-0xE) // Отнимаем количество символов в MyAddr-0xE. Устанавливаем признак, если ещё не конец // Копируем символы во временный буфер MyAddr (-0x0с) lui s3, 0x1f80 ori s3, 0x03f0 // Перезадаём адрес таблицы количества спрайтов для нового цикла lui t1, MyAddr //Постоянный счётчик lh v1, -0x0e(t1) //Загружаем количество оставшихся символов lui t2, MyAddr //Сдвиг для буфера sltiu a0, v1, 0x0B //Меньше 10 в строке? bne a0, zero, readTextToBuffer clear s2 //Обнуляем счётчик символов li v1, 0xA //Установка максимального количество символов readTextToBuffer: lbu a0, 0x0(s6) // Основное чтение байта текста addiu s6, s6, 0x1 // Сдвиг адреса чтения на +1 байт addiu v1, v1, -0x1 // Осталось в этом блоке... sb a0, -0x0c(t2) // Запись символа в буфер addiu s2, s2, 0x1 // Счётчик прочитанного +1 lh v0, -0x0e(t1) // Грузим общий счётчик addiu t2, t2, 0x1 // Сдвиг записи +1 addiu v0, v0, -0x01 // Общий счётчик -1 bne v1, zero, readTextToBuffer // Проверяем частный счётчик != 0 sh v0, -0x0e(t1) // Сохраняем общий счётчик li a0, 0 sb a0, -0x0c(t2) //На всякий случай сохраняем 00-терминатор addu s3, s3, s2 // получаем адрес количества спрайтов из таблицы addiu s3, -0x1 // index -1 lbu a1, 0x0(s3) // Читаем количество в a1 nop move t5, a1 // Храним количество блоков для спрайта lui t3, 0x1f80 ori t3, t3, 0x1c0 // Начало данных спрайта в scratch form_char_data_in_scratch: jal make_char_line_in_scratch //Вызываем процедуру формирования строки move a0, s2 //Передаём в неё длину строки в символах (a0) и в спрайтах (a1) // (формируем спрайт размером до 60x12) rect_cmd_init: lui s3, 0x1f80 ori s3, 0x03d0 // s3 = 1f8003d0 - адрес rect в scratchpad lui s1, 0x8008 // Загружаем 80080000 lui s0, 0xff ori s0, s0, 0xffff // s0 = 00ffffff lui s7, 0x8000 // s7 = 80000000 lui t2, 0xff00 // t2 = ff000000 move t4, s4 // t4 - указатель на print params ОСНОВНОЙ move t1, s4 // t1 - print params для смещения страниц rect_set_X: sh t6, 0x8(s3) // Сохраняем X координату в текущий rect sll v0, s2, 1 addu v0, s2 // Умножаем количество обработанных символов на 6 sll v0, 1 addu t6, v0 // Сдвигаем X (прочитаем со следующим блоком символов) rect_set_width: sll v0, t5, 3 //Количество блоков спрайта * 8 = ширина спрайта sh v0, 0x10(s3) // Сохраняем ширину спрайта PageLoop: ////// Балансировщик нагрузки выбора цепочек для равномерного размещения команд (2 команды в 7ce58 + 1 команда в 7ce68) //////////////////////////////////////// TODO: CHAIN BALANCER addiu s5, s1, -0x3198 // s5 = 7ce68 (Второй свободный адрес dma) //s5 - содержит указатель на адрес следующей свободной цепочки. В зависимости от балансировщика может быть 7ce58 или 7ce68 // Создание цепочки копирования спрайта lw v0, 0x0(s5) // v0 = *адрес свободной цепочки nop and a0, v0, t2 // a0 = v0 & ff00.. and v0, v0, s0 // v0 & 00FFFFFF or a1, v0, s7 // a1 = v0 | s7 (80000000) lw v0, 0x4(s5) // Счетчик команд lw v1, 0x0(a1) // Новый адрес свободной цепочки addiu v0, v0, -0x1 // Уменьшаем счетчик and v1, v1, s0 // v1 & 00FFFFFF or a0, a0, v1 // a0 & v1 sw v0, 0x4(s5) // Сохраняем счетчик DMA sw a0, 0x0(s5) // Сохраняем новую свободную цепочку rect_copy_scratch_to_ram: lw t8, 0x0(s3) // Количество команд DMA lw t7, 0x4(s3) // Команда копирования спрайта sw t8, 0x0(a1) sw t7, 0x4(a1) lw a3, 0x8(s3) // Координаты экрана XY lw t7, 0xc(s3) // CLUT и координаты текстуры sw a3, 0x8(a1) sw t7, 0xc(a1) lw t8, 0x10(s3) // Размер спрайта после копирования nop sw t8, 0x10(a1) // Сохраняем последнюю команду rect_scratch_connect: lw v1, 0x18(t1) // Загружаем адрес след цепочки из print params (cur page) lw v0, 0x0(a1) // Текущий новый адрес цепочки lw v1, 0x0(v1) // Разыменование and v0, v0, t2 // v0 & ff000000 and v1, v1, s0 // v1 & 00FFFFFF or v0, v0, v1 // Команды с FFFFFF sw v0, 0x0(a1) // Обновляем текущий адрес цепочки lw a0, 0x18(t1) // Загружаем адрес след цепочки из print params (cur page) nop lw v0, 0x0(a0) // Разыменование and v1, a1, s0 // v1 = a1 & 00FFFFFF and v0, v0, t2 // v0 & FF000000 - чистим кол-во команд or v0, v0, v1 // прикручиваем кол-во команд к след адресу sw v0, 0x0(a0) // Сохраняем в цепочку ссылку sw a1, 0x18(t1) // Сохраняем адрес след актуальной цепочки в print params (cur page) lhu v0, 0x4(t4) // Проверяем счетчик символов в print params nop bne v0, zero, cpu2vram_cmd_loop // Если счетчик символов ≠ 0, переходим (уже инициализировали) clear s8 // Сбрасываем счётчик команд спрайтов sw a1, 0x28(t1) // Устанавливаем адрес начала цепочки символов в print_params (для хранения значения "цепочка от адреса...") sw a3, 0xc(t4) // Сохраняем экранные координаты в print params cpu2vram_cmd_loop: // Цикл команд cpu_to_vram (формирование спрайтов текста в VRAM) ////// Балансировщик нагрузки выбора цепочек для равномерного размещения команд (2 команды в 7ce58 + 1 команда в 7ce68) //////////////////////////////////////// TODO: CHAIN BALANCER addiu s5, s1, -0x31a8 // s5 = 7ce58 (Первый свободный адрес dma) //s5 - содержит указатель на адрес следующей свободной цепочки. В зависимости от балансировщика может быть 7ce58 или 7ce68 lw v0, 0x0(s5) // Следующая свободная цепочка 7ce58 () nop and a0, v0, t2 // a0 = v0 & FF000000 and v0, v0, s0 // v0 & 00FFFFFF or a3, v0, s7 // a3 = следующая цепочка & 80... move a2, a3 // a2 = a3 (следующая цепочка) lw v0, 0x4(s5) // Счетчик команд lw v1, 0x0(a3) // Новый адрес свободной цепочки addiu v0, v0, -0x1 // Уменьшаем счетчик and v1, v1, s0 // v1 & 00FFFFFF or a0, a0, v1 // a0 & v1 sw v0, 0x4(s5) // Сохраняем счетчик DMA sw a0, 0x0(s5) // Сохраняем новую свободную цепочку //Копируем шаблон cpu2vram lui a1, 0x1f80 ori a1, a1, 0x348 // 1f800348 = Scratch команда cpu2vram lw t8, 0x0(a1) lw t7, 0x4(a1) sw t8, 0x0(a2) sw t7, 0x4(a2) lw t8, 0x8(a1) //Сохраняем базу команды cpu2vram lw t7, 0xc(a1) sw t8, 0x8(a2) sw t7, 0xc(a2) sll v1, s8, 1 //Текущий индекс блока * 2 addiu v0, v1, 0x130 //складываем со 0x130 (X начала спрайта в VRAM) sh v0, 0x8(a2) //Обновляем VU X прямо в RAM copy_char_data_start: lui t3, 0x1f80 ori t3, t3, 0x1c0 // Начало данных спрайта в scratch addiu a2, 0x10 //Адрес для записи данных после заголовка команды clear v0 //Основной счётчик строк (0-11) sll v1, t5, 2 //v1 = количество байт в одной строке (блоки * 4) (инкремент) sll a1, s8, 2 //Вычисляем смещение начала чтения addu a1, t3 //a1 = адрес начала чтения в scratch copy_char_data_loop: lw t7, 0x0(a1) //Грузим данные addu a1, v1 //Смещаем адрес чтения sw t7, 0x0(a2) //Сохраняем данные addiu a2, 0x04 //Смещаем адрес записи addiu v0, 0x01 //Инкремент счётчика строк bne v0, 0x0c, copy_char_data_loop nop bne s8, zero, cpu2vram_dma_link // Если это первая команда (последняя выполняемая, то добавляем flushcache) nop set_flush_cache_cmd: li t8, 0x10 sb t8, -0x3d(a2) //Обновляем длину команды до 0x10 lui t8, 0x0100 sw t8, 0x0(a2) //Устанавливаем команду flush cache в конце команды cpu2vram_dma_link: // Связывание предыдущей команды с текущей (формирование ссылки в заголовке команды) lw v1, 0x18(t1) // v1 <- 18+(t1) - указатель на конец цепочки в текущей странице) lw v0, 0x0(a3) // v0 <- 0+(a3) - снова читаем первую команду (в данном случае - количество команд) lw v1, 0x0(v1) // Разыменование указателя на след свободный адрес в цепочке (из print_params) and v0, v0, t2 // v0 & FF..... Чистим количество команд and v1, v1, s0 // v1 & 00FFFFFF - чистим след свободный адрес or v0, v0, v1 // Совмещаем ссылку - Cmd+ADDR sw v0, 0x0(a3) // Сохраняем ссылку в цепочку //Связывание текущей команды со следующей (формирование ссылки в заголовке команды + запись в print params) lw a0, 0x18(t1) // v1 <- 18+(t1) - указатель на конец цепочки в текущей странице) nop lw v0, 0x0(a0) // Читаем то, на что указывает указатель (след адрес в цепочке из цепочки) and v1, a3, s0 // v1 = a3 & 00FFFFFF - от следующего адреса свободной команды отрезаем 80 and v0, v0, t2 // v0 & FF000000 (количество команд) от след адреса or v0, v0, v1 // Совмещаем ссылку - Cmd+ADDR sw v0, 0x0(a0) // Сохраняем ссылку в цепочку sw a3, 0x18(t1) // Сохраняем адрес последней команды в print params //Конец цикла команд cpu_to_vram addiu s8, 0x1 //Смешаем индекс читаемого спрайта bne s8, t5, cpu2vram_cmd_loop //Проверка на достижение количества спрайтов nop addiu t1, t1, 0x4 // Сдвигаем print params для следующей страницы addiu v0, t4, 0x8 // v0 = t4 + 8 (t1 не должен быть больше, чем t4 + 4) sltu v0, t1, v0 // Проверяем прохождение первой страницы bne v0, zero, PageLoop // Если не прошли, продолжаем цикл страниц nop chunk_making_end: // Увеличиваем счетчик символов + проверка lhu v0, 0x4(s4) // Текущее количество спрайтов lui t3, MyAddr addiu v0, v0, 0x1 // Увеличиваем на 1 lhu v1, -0x0e(t3) // Количество оставшихся символов sh v0, 0x4(s4) // Сохраняем счетчик спрайтов в print_params bne v1, zero, TextGroupReadLoop // Если обработаны все буквы и созданы все спрайты, то выходим nop TextEnd: clear s2 lui s3, 0x8008 addiu s5, s3, -0x3198 // s5 = 7ce68 (Второй свободный адрес dma) lui s1, 0xff ori s1, s1, 0xffff // s1 = 00ffffff move s0, s4 // s0 = Указатель на print params (теперь они и в t1, s4 и s0) clear a1 PageTLoop: //Добавляем команду переключения текстурной страницы (SetDrawTPage) в обе страницы li a2, 0x1 // a2 = 1 addu s2, s2, a2 // Увеличиваем счетчик страниц lui v0, 0x8000 // v0 = 80000000 lui a0, 0xff00 // a0 = ff000000 lw v1, 0x0(s5) // Следующая свободная цепочка lw a3, 0x9c(gp) // Текстурная страница из параметров and v1, v1, s1 // v1 & 00FFFFFF or v1, v1, v0 // v1 | v0 = 80..... sw v1, 0x30(s0) // Сохраняем последний адрес в print params //Линкуем адреса lw v0, 0x0(s5) // Следующая свободная цепочка ещё раз! lw v1, 0x0(v1) // разыменование and v0, v0, a0 // v0 & ff000000 and v1, v1, s1 // v1 & 00FFFFFF lw a0, 0x4(s5) // Счетчик DMA or v0, v0, v1 // v0 | v1 sw v0, 0x0(s5) // Сохраняем свободный адрес addiu a0, -0x1 // Уменьшаем счетчик DMA sw a0, 0x4(s5) // Сохраняем счетчик DMA lw a0, 0x30(s0) // Читаем адрес команды в a0 для передачи в функцию установки texPage jal SetDrawTPage // Устанавливаем страницу отрисовки addiu s0, s0, 0x4 // Смещаем print params для следующей страницы sltiu v0, s2, 0x2 // Проверяем счетчик < 2 bne v0, zero, PageTLoop // Если да, продолжаем цикл clear a1 // a1 = 0 move v0, s5 // Возвращаем основные параметры DMA // Восстанавливаем регистры из стека lw ra, 0x48(sp) lw s7, 0x44(sp) lw s6, 0x40(sp) lw s5, 0x3c(sp) lw s4, 0x38(sp) lw s3, 0x34(sp) lw s2, 0x30(sp) lw s1, 0x2c(sp) lw s0, 0x28(sp) jr ra // Возврат addiu sp, sp, 0x50 // Восстанавливаем стек .include "2_EP/EP_charload_grouped.asm" .include "2_EP/EP_charCalcs.asm" .close .include "2_EP/EP_txtpatches.asm" // misc text patches // compile with ./armips -sym 2_EP/BUILD_LOGS/SLPS_028.25.map -temp 2_EP/BUILD_LOGS/SLPS_028.25.txt 2_EP/EP_main_grouped.asm