Самая продаваемая консоль поколения имела худшую архитектуру памяти, самая технически грамотная продалась хуже всех, а самая простая в разработке принадлежала компании которая никогда раньше не делала консолей. Вы наверное узнали тут PS2, GameCube и Xbox.

Иногда шутят, что когда разработчик переносил игру с PS2 на Xbox, то первое что он делал это выбрасывал систему управления памятью и писал новую с нуля, потому что 32Мб плюс 4Мб плюс 2Мб не помещается в 64Мб.

Для чтения этой статьи вам не потребуется знать ассемблер или работать с конкретными SDK. Достаточно понимать, что такое указатель, чем стек отличается от кучи и что рендерить геометрию параллельно с её обновлением плохая идея, и что классические GPU и CPU паттерны работы по-разному нагружают память.

Там, где в тексте встречается псевдокод, он нужен для иллюстрации концепции, а не как готовый код для компиляции. Я уже подзабыл конкретные константы и названия буферов на каждой платформе, но паттерн работы всегда был примерно одинаковый.


Почему архитектура памяти вообще имеет значение

Процессор делает одно ровно одну вещь, он берёт данные из памяти, как-то их изменяет и кладёт результат обратно в память. Всё, что он делает, так или иначе связано с чтением из памяти и записью в память, но не вся память одинакова и разные физические типы памяти работают с принципиально разной скоростью, разной шириной шины и разными ограничениями по адресации.

Поэтому когда процессор вынужден ждать ответа от медленной памяти, он не делает ничего полезного. Такие пустые такты называют stall, они напрямую съедают производительность, и разработчики всеми силами пытаются от них избавиться, но поле боя двигается то в одну, то в другую сторону. На PC с многоуровневыми кешами большую часть stall’ов скрывает железо, скрывает тут надо читать в обоих смыслах, как "скрывает" в плане усредняет время получениея данных, так и "скрывает" в плане фиг поймшешь почему появился stall. На консолях cо stall исторически боролись уменьшением слоев между железом и SDK и разными наборами кешей или похожими механизами.

На ранних консолях, кешей почти не было, и разработчик должен был сам думать о том, откуда процессор в каждый конкретный момент читает данные. И все решения в архитектуре памяти консолей разных эпох это просто разные ответы на один и тот же вопрос: как дать процессору данные достаточно быстро данных при ограниченном бюджете. Ответы менялись от поколения к поколению, что и часто объясняет, почему код, написанный под одну консоль, с проблемами переносится на другую или требует серьёзной переработки.

Atari 2600 и 8-битные чудеса 80-х

Atari 2600 запустился в 1977 году с процессором MOS 6507 на частоте 1.19 МГц и 128 байтами оперативной памяти, 128 байт доступной памяти и тогда было немного, но Atari решили что 128 байт хватит всем. ROM картриджа был емкостью до 4 килобайт, хотя позже появились схемы bank-switching, расширившие адресное пространство. Никакого выделенного видеопроцессора в современном понимании не было, а вывод картинки генерировал чип TIA (Television Interface Adapter), и разработчик должен был синхронизировать свой код с электронным лучом, буквально просчитывая на листе бумаги, где будет физически находиться луч в конктретный момент кадра.

У TIA не было ни видеобуфера, ни буфера кадра, сигнал генерировался на лету, и если в нужный момент в нужные регистры не были записаны данные, то на экране появлялись глитчи (визуальные артифакты).

Одна нога здесь, другая там
Одна нога здесь, другая там

Игровой цикл Atari 2600 выглядел примерно так:

ВЕРТИКАЛЬНАЯ СИНХРОНИЗАЦИЯ (3 строки)
  записать WSYNC               // дождаться строки
  записать VSYNC = 1           // начало кадра
  записать WSYNC
  записать VSYNC = 0

ВЕРТИКАЛЬНЫЙ БЛАНК (37 строк)
  // вся игровая логика: движение, коллизии, AI
  // есть 2280 тактов процессора на логику

ВИДИМЫЕ СТРОКИ (192 строки)
  для каждой строки:
    записать WSYNC             // синхронизироваться с лучом
    записать цвет фона         // успеть до начала строки
    обновить позицию спрайтов  // строго в правильные такты
    записать данные спрайтов

ГОРИЗОНТАЛЬНЫЙ БЛАНК
  // ещё немного времени на дополнительную логику

Вертикальный бланк (VBlank) это время пока электронный луч возвращается из правого нижнего угла экрана в левый верхний чтобы начать новый кадр, и в аналоговом телевизоре в этот момент луч гасится, чтобы не оставить видимую диагональную линию на экране. На Atari 2600 этот период длится 37 строк и это единственное время когда программа может заниматься игровой логикой, не беспокоясь о том что происходит на экране, потому что луч всё равно не рисует ничего видимого.

Горизонтальный бланк (HBlank) то же самое, но в конце каждой строки: луч доходит до правого края, гасится и возвращается к левому краю чтобы начать следующую строку, и в этот короткий промежуток у процессора есть несколько тактов для дополнительных вычислений между отрисовкой строк.

Разработка под Atari 2600 было, по сути, программированием для системы реального времени, только без операционной системы, заставляя считать такты вручную. Если логика в вертикальном бланке занимала на 10 тактов больше, чем было допустимо, следующая строка рисовалась неправильно.

Немного не успели с заполнением буфера строки
Немного не успели с заполнением буфера строки

Помните про 128 байт? Так вот они были поделены между стеком и игровыми переменными. Обычно стек занимал 20–32 байт, а на всё остальное, вроде позиций объектов, счёта, флагов состояния и данных уровня оставалась еще куча памяти.

ROM на картридже был исполняемым кодом, который процессор читал напрямую и сразу выполнял. Никакой загрузки в RAM не было, потому что загружать было просто некуда, а еще это означало, что любая самомодификация кода была физически невозможна, потому что у вас код прошит в микросхеме. Еще было такое направление как хакинг lookup-таблиц в ROM, но оно было больше инструментом оптимизации и уделом тогдашних гиков.

Так как каждый такт процессора был на счету, то одним из способов выжать из железа больше в плане производительности было хранить в ROM заранее вычисленные таблицы значений вместо того чтобы считать их в реальном времени, потому что чтение байта из таблицы это одна инструкция, а синус или умножение это несколько тактов, которых может не хватить в бланке. Поэтому типичные таблицы содержали позиции спрайтов, значения синусоиды для анимации покачивания, заранее рассчитанные маски столкновений, цвета для градиентов неба или воды, данные для генерации звука. Всё это надо было уложить в 4 килобайта ROM, так что экономия на таблицах одновременно означала войну за каждый байт с данными самой игры.

Особым шиком считалось строить таблицы так чтобы они перекрывались между собой, когда один и тот же участок ROM читается со смещением и даёт разные данные в зависимости от стартового адреса, что позволяло хранить несколько логических таблиц в пространстве одной физической.

NES, Mega Drive, SNES (1985–1995, 8bit-16bit)

С приходом NES (1983 в Японии, 1985 в США) появилась возможность разделить процесс рисования экрана и обработки логики. На консоли был PPU (Picture Processing Unit), который получил свою отдельную VRAM объёмом 2 килобайта и генерировал изображение самостоятельно. Основная RAM тоже подросла до 2 килобайт. Данные на картриджах тоже лежали отдельно: PRG ROM (код) и CHR ROM (тайлы), и разработчик мог явно управлять, что откуда читается. Это разделение на CPU-сторону и GPU-сторону памяти держалось с разными вариациями вплоть до PS3, когда данные на CPU-стороне обрабатывает процессор, а данные на GPU-стороне обрабатывает видеочип, и передача между ними было отдельной задачей. SNES (1990) использовал аналогичную схему, добавив специализированный DSP для масштабирования и вращения спрайтов.

Другой представитель поколения (Sega Mega Drive, 1988) имел на борту проц на 7.67 МГц, уже 64 килобайта основной RAM, 64 килобайта VRAM и отдельный Z80 для звука с 8 килобайтами собственной памяти.

NES:
  CPU RAM:      2 KB (зеркалируется до 8 KB в адресном пространстве)
  PPU VRAM:     2 KB (таблицы и атрибуты)
  CHR ROM/RAM:  8 KB на картридже (тайлы)
  OAM:          256 байт (данные спрайтов)
  Картридж PRG/ROM: до 512 KB

SNES:
  Work RAM:     128 KB (основная оперативная память)
  VRAM:         64 KB (тайлы, тайлмапы)
  CGRAM:        512 байт (палитра — 256 цветов по 2 байта)
  OAM:          512 + 32 байт (данные спрайтов)
  Картридж ROM: до 4 MB с bank-switching

SNES хранит 256 записей по два байта, то есть 512 байт суммарно, но это не означает что на экране можно одновременно показать 256 произвольных цветов, потому что внутри эти 256 записей жёстко разбиты на палитры и каждый слой фона или спрайт может использовать только свою палитру целиком, не выбирая цвета из общего пула.

Для фоновых слоёв есть 8 палитр по 16 цветов, для спрайтов ещё 8 палитр по 16 цветов, и нулевой индекс в каждой палитре зарезервирован под прозрачность и не даёт видимого цвета, что оставляет 15 реальных цветов на палитру вместо 16. На практике это означало что художники не просто рисовали картинку, а подбирали какие именно цвета войдут в каждую палитру и как распределить персонажей и фоны между доступными слотами, потому что два спрайта использующие разные палитры могли стоять рядом на экране, но не могли разделить цвет из чужой палитры даже этот цвет физически присутствовал в CGRAM памяти.

Разработчику нужно было работать с несколькими адресными пространствами одновременно, записывая в один диапазон адресов данные для CPU RAM, а в другой диапазон данные для видеочипа. VRAM была доступна процессору в любое время, но записывать в неё можно было только в период бланка, иначе PPU и CPU одновременно обращались к одной шине и данные искажались.

Проблема совместного доступа к шине была фундаментальной проблемой для всех консолей того периода. На NES и SNES она решалась так, что PPU просто имел приоритет над шиной в период активного рисования, и если CPU пытался записать данные в VRAM в этот момент, результат был непредсказуемым. Часть байт могла попасть по неверным адресам, часть терялась, а на экране появлялись случайные тайлы или искажения цветов. Это не было багом в понимании разработчиков железа, просто делить шину по времени дешевле, чем давать каждому чипу отдельную память с независимым доступом, и разработчик был обязан знать это и планировать всю работу с видеопамятью строго вокруг периодов бланка.

На NES адресное пространство CPU занимало 64 KB, но физической RAM было только 2 KB, а остальное пространство заполнялось зеркалами, регистрами PPU, регистрами APU, и картриджем, причём PPU имел собственное отдельное 16 KB адресное пространство куда CPU вообще не мог обращаться напрямую, а только через восемь специальных регистров, и запись в VRAM выглядела как последовательная запись адреса в регистр PPUADDR а потом данных в PPUDATA, что само по себе создавало проблему: регистр адреса был двухбайтовым но записывался двумя последовательными однобайтовыми операциями, и если между ними происходило прерывание, то состояние внутреннего счётчика адреса сбивалось и следующая запись уходила по неверному адресу.

На SNES архитектуру усложнили до трёх отдельных шин, A-bus для картриджа и Work RAM, B-bus для регистров периферии включая PPU, и внутренняя шина самого CPU. Еще был отдельный DMA-контроллер, который умел перекачивать данные без участия процессора, что давало возможность запускать передачу тайлов в VRAM через DMA в начале VBlank, и пока данные идут заниматься игровой логикой, но и здесь был подводный камень.

DMA блокировал CPU на время передачи, и если объём данных превышал доступное время VBlank, PPU начинал рисовать раньше чем DMA заканчивал, и нижняя часть экрана получала мусор вместо правильных тайлов.

Исторически именно эти ограничения породили целый жанр технических трюков, которыми пользовались студии, рассчитывая сколько тактов занимает операция DMA, сколько строк бланка доступно на конкретной частоте телевизора, и разбивали передачу данных на несколько кадров, если объём не помещался в один VBlank. Подгрузка тайлов происходила постепенно и все выглядело как работа уровня без видимых пауз.

PS1, N64 и первые 3D игры (1994–1999)

Середина 90-х прошла под флаг 3Д, поставив перед архитекторами консолей соответствующую задачу рендеринга трёхмерной геометрии, но как обычно, каждая консоль решала её по-разному.

PS1 делал растеризацию через фиксированный конвейер с аффинным отображением текстур, когда текстуры натягивались на полигоны без перспективной коррекции. Это было осознанным упрощением ради скорости.

Результат виден на любом скриншоте с PS1 игрой: текстуры "плывут" и деформируются при движении камеры, особенно на крупных полигонах, потому что аффинное отображение не учитывает что дальний край полигона физически дальше от камеры чем ближний. Еще PS1 не имел буфера глубины (z-buffer), и порядок отрисовки полигонов определялся сортировкой по центру, что давало классические артефакты когда полигоны "пролезали" друг в друга или рисовались в неверном порядке.

Объяснение

Но зато у PS1 была быстрая и предсказуемая растеризация и разработчики быстро научились прятать артефакты аффинного маппинга дроблением полигонов на более мелкие.

N64 пошёл другим путём и поставил Reality Co-Processor с настоящей перспективной коррекцией текстур, z-buffer и трилинейной фильтрацией, то есть технически N64 рисовал геометрию правильнее, текстуры не плыли и полигоны не прорезали друг друга. Но за это пришлось платить маленьким объёмом текстурного кэша в 4 KB внутри RCP, и любая текстура которая не помещалась целиком в этот кэш вызывала дорогостоящую перекачку данных из основной памяти по медленной шине.

Поэтому приходилось либо использовать крошечные текстуры, либо вообще отказываться от них в пользу закрашенных полигонов, что давало узнаваемый "пластилиновый" вид игр на N64.

Принципиальная разница была ещё и в том как картриджи против CD влияли на весь подход к разработке. PS1 с CD мог хранить огромные объёмы текстур и аудио и подгружать их по мере необходимости, что компенсировало технические ограничения растеризатора возможностью использовать высокодетализированные текстуры хоть на каждом объекте, тогда как N64 с картриджем имел быстрый доступ к данным, но жёсткий потолок объёма, и маленький текстурный кэш RCP, буквально не позволяя делать детализированные текстуры ни по размеру кэша ни по объёму картриджа.

Быстрая, но медленная RDRAM на N64

RDRAM давала очень высокую пиковую пропускную способность, около 562 МБ/с, что было впечатляюще для 1996 года. Но у неё была высокая задержка при случайном доступе и если процессор читал данные последовательно, то делал это быстро, но если прыгал по случайным адресам, то очень медленно.

RDRAM появилась на N64 как результат совместной работа японских консолестроителей и инженерного гения Silicon Graphics, которая принимала участие в проектировании архитектуры консоли. Стандартная DRAM работала на частотах 60-70 МГц и давала пропускную способность порядка 100-150 МБ/с, чего катастрофически не хватало для трёхмерной графики, которой надо было одновременно читать геометрию, текстуры, z-buffer и писать результат в framebuffer.

Rambus предложила решение вместо широкой параллельной шины в 32 или 64 бита использовать узкую последовательную шину в 8-16 бит, но на очень высокой частоте и с конвейеризацией запросов. Пиковая пропускная способность получалась впечатляющей, когда данные идут потоком без пауз.

Silicon Graphics выбрала RDRAM для консоли, потому имела большой опыт работы с такой памятью и строила свои графические рабочие станции именно вокруг потоковой обработки данных, где графический конвейер последовательно читает вершины, трансформирует их, растеризует и пишет пиксели, и в этой модели RDRAM действительно работала хорошо.

Проблема была в том что реальная игровая нагрузка не бывает чисто потоковой и процессор 60% времени читает код из одного места, 20% данные из другого, а еще 20% пишет в третье, и все эти обращения в большинстве своем случайны. Но с RDRAM каждый такой случайный доступ требовал инициализации нового пакетного запроса, и задерки этой инициализации ложились тяжелыми камнями ожиданий в тележку производительности.

Разработчики игр под N64 быстро обнаружили, что написать код, который утилизирует заявленные 562 МБ/с в реальных условиях практически невозможно, и реальная эффективная пропускная способность в игровых сценариях была ближе к 200-250 МБ/с, то есть чуть лучше чем у PS1 с его обычной DRAM, но совсем не в разы лучше как звучало на маркетинговых презентациях. Разрыв между пиковой и реальной пропускной способностью стал одной из причин почему N64 воспринималась как сложная в освоении платформа, несмотря на технически превосходящее железо.

Чтобы получить заявленную пропускную скорость приходилось организовывать геометрию так, чтобы минимизировать случайный доступ: делать triangle strips вместо indexed meshes, сортировать вершины по порядку использования и предварительно обрабатывать текстуры на стороне билдфермы, чтобы они лежали последовательно в памяти.

// Это работало хорошо при последовательном чтении
for (int i = 0; i < vertex_count; i++) {
    process_vertex(vertex_array[i]);   // stride 1, кеш доволен
}

// Это работало плохо, при случайном доступе через индексный буфер
for (int i = 0; i < index_count; i++) {
    process_vertex(vertex_array[index_buffer[i]]);  // cache miss на каждой вершине
}

В похожую лепеху наступил Intel в 1999 году, когда заключил с Rambus соглашение и попытался сделать RDRAM стандартом для Pentium 4, что закончилось громким провалом по тем же причинам: высокая цена, высокая латентность на случайный доступ и разрыв между маркетинговыми цифрами и реальной производительностью в обычных приложениях, после чего рынок окончательно выбрал DDR и история RDRAM как массового стандарта на этом закончилась.

Dreamcast: консоль из будущего

Dreamcast оказался одной из самых необычных консолей своего времени именно потому, что её архитектура памяти и рендеринга выглядела скорее как прототип будущего поколения (PS2/Xbox), чем как прямой конкурент PlayStation и Nintendo конца девяностых.

Вышедшая в 1998 году консоль, использовала процессор Hitachi SH-4 с частотой 200 МГц и графический чип PowerVR2, основанный на тайловом рендере, который почти никто из разработчиков массовых игр не воспринимал как рабочее решение.

Большинство GPU той эпохи работали по схеме (написал команду - отрисовал), при которой каждый полигон после обработки почти растеризовался, что означало постоянные обращения к внешней памяти.

Если несколько полигонов перекрывали один и тот же пиксель, то видеочип несколько раз читал и перезаписывал один и тот же участок кадрового буфера и Z-буфера. А PowerVR2 сначала собирал геометрию кадра, затем разбивал экран на небольшие тайлы и только после этого рендерил каждый тайл отдельно, уже заранее отсекая полигоны невидимые полигоны.

Это позволяло резко сократить количество операций записи во внешнюю VRAM и уменьшало требования к пропускной способности памяти, что для конца девяностых с не самой быстрой памятью, было очень хорошим техническим решением.

Поэтому при относительно скромном объёме памяти консоль часто демонстрировал большую производительность и очень чистую картинку, особенно в сценах с большим количеством непрозрачной геометрии.

Однако тайловый рендер менял не только внутреннее устройство GPU, но и саму модель мышления разработчика, а техники, считавшиеся стандартными на PlayStation или PC того времени, не работали или работали неправильо.

Большие полупрозрачные поверхности, multipass-рендеринг и системы частиц становились неожиданно становились дорогими, потому что прозрачность разрушала преимущества такого подхода и GPU не мог заранее отбросить скрытые пиксели, что приводило к дополнительным проходам и более интенсивномиу использованию памяти.

В результате выяснилось, что проблема производительности всё чаще определяется не количеством арифметических операций, а объёмом данных, который необходимо перемещать между GPU и памятью.

Через двадцать лет практически все мобильные GPU, включая Apple GPU, Mali и Adreno, придут к очень похожим тайловым архитектурам, но уже по другой причине: стоимость доступа к памяти, особенно с точки зрения энергопотребления, окажется намного важнее стоимости вычислений.

PS2, Xbox, GameCube (2000–2005)

PS2, Xbox и GameCube вышли в рамках одного поколения, но имели настолько разную архитектуру памяти, что порт с одной платформы на другую часто требовал переписывания системы управления ресурсами с нуля.

PS2 имела три отдельных «острова» памяти: 32 мегабайта основной RDRAM для EE (Emotion Engine), 4 мегабайта VRAM для GS (Graphics Synthesizer) и 2 мегабайта IOP RAM для ввода-вывода, звука и CD-привода. Каждый остров обслуживался своей шиной, и передача между ними была явной операцией через DMA.

Исторический факт, что разработчики с опытом на N64 попадали на PS2 в знакомую, хотя и более сложную среду, потому что Nintendo последовательно строила свои консоли вокруг похожей идеи, что память это не однородный пул, а набор специализированных областей с разными характеристиками, и N64 с её разделением между картриджем, основной RDRAM и текстурным кэшем RCP уже приучила думать именно в таких категориях.

На PS2 та же логика просто стала более явной и три острова памяти с разными скоростями, разными шинами и явными DMA-передачами между ними были концептуально ближе и понятнее тем японской школе игростроения.

Это давало преимущество ветеранам N64, которым было естественно выстраивать систему управления ресурсами вокруг явных бюджетов для каждого острова и планировать DMA-передачи заранее, тогда как программист пришедший с PC инстинктивно пытался абстрагироваться от физической структуры памяти и получал задержки там, где не ожидал их увидеть.

Примерная схема передачи данных на PS2:

Диск CD/DVD
    │
    ▼
IOP (2 MB) ──DMA──► EE Main RAM (32 MB) ──DMA──► GS VRAM (4 MB)
                          │                            │
                          ▼                            ▼
                     EE (CPU Logic)              Рендеринг
                     VU0 / VU1 (геометрия)

Главной особенностью PS2 была пара векторных юнитов VU0 и VU1, способных работать параллельно с основным процессором. VU0 был плотно связан с CPU и использовался для физики и коллизий. VU1 был подключён к GS и занимался трансформацией геометрии и подготовкой примитивов.

У каждого VU было собственное маленькое хранилище микропрограмм: VU0 имел 4 килобайта, а VU1 16 килобайт. Это означало, что разработчик мог писать отдельные программы для VU (compute shader в современном понимании) и загружал их туда перед использованием. Это была форма аппаратного параллелизма, доступная задолго до того, как она стала нормой в PC-разработке с GPGPU, когда разработчики получали в свои руки физически выполнять параллельную логику.

// Концептуальная схема использования VU1
vu1_upload_program(skinning_microprogram, vu1_microprog_memory);
vu1_upload_data(vertex_buffer, vu1_input_buffer);
vu1_execute();
// пока VU1 трансформирует геометрию, CPU занимается логикой
cpu_update_game_state();
vu1_wait_complete();
vu1_kick_to_gs();   // отправить результат напрямую в GS

Xbox использовал обычную Pentium III-подобную архитектуру с унифицированными 64 мегабайтами DDR, разделёнными между CPU и GPU. Никаких «островов», просто одно адресное пространство, доступное всем и отовсюду и программисты, пришедшие с PC-разработки, чувствовали себя дома.

Xbox был настолько близок к обычному PC, что Microsoft даже не скрывала этого. Внутри консоли стоял немного модифицированный Intel Pentium III на 733 МГц, видеокарта на базе GeForce 3 от Nvidia и обычный жёсткий диск от IBM, то есть железо, которое фактически можно было купить в любом компьютерном магазине с минимальными модификациями.

Это давало очевидные преимущества при портировании игр с PC, которое теперь занимало недели вместо месяцев, а DirectX API был знаком любому виндовому разработчику, и вся экосистема PC-разработки переезжала на консоль практически без изменений. Но вместе с этим переезжали и все проблемы PC-архитектуры, как то унифицированная память, борьба за шину между CPU и GPU, просадки в моменты пиковой нагрузки, кэш-промахи, и т.д. и т.п.

Вместе с разработчиками переехала и привычка не думать об управлении памятью явно, что приводило к тому что игры на Xbox часто использовали ресурсы менее эффективно чем конкуренты на PS2 или GameCube, выжимавшие максимум из своей специализированной архитектуры.

Последний из этой тройки, GameCube, мог предложить 24 мегабайта быстрой «Splash» SRAM прямо на чипе GPU (1T-SRAM с очень низкой латентностью) плюс 16 мегабайт внешней DRAM, тоже подключённых к GPU. Пропускная способность внутренней памяти была огромной, порядка 9.6 ГБ/с, что позволяло при меньшем объёме памяти конкурировать с PS2 по производительности.

24 мегабайта Splash SRAM GameCube были быстрее аналогов конкурентов и разработчики, сумевшие уложить активную сцену целиком в эту память, получали рендеринг заметно быстрее, чем на PS2 с её 4 мегабайтами VRAM, и уж тем более на XBox. Но 24 мегабайта на всё: геометрию, текстуры, рабочие буферы - это очень мало, и управление этим пространством требовало особой дисциплины разработки, что приводило к сложным подходам при работе с кадром и хранить в быстрой памяти только активные текстуры текущей сцены, а оставшиеся 16 мегабайт внешней памяти исользовать как большой промежуточный буфер загрузки с диска.

PS3, Xbox 360 и разделённая память

PS3 (2006) и Xbox 360 (2005) вышли практически одновременно по меркам нового поколения консолей, но имели при этом принципиально разные модели памяти, что сделало кроссплатформенную разработку этой эпохи очень болезненной.

Xbox 360 имел 512 мегабайт унифицированной GDDR3-памяти с пропускной способностью порядка 22.4 ГБ/с, физически доступной для CPU (Xenon) и GPU (Xenos) без дополнительный условий и копирований.

Xbox имел еще дополнительно 10 мегабайт EDRAM на чипе GPU, которые работали с пропускной способностью около 256 ГБ/с, на порядок быстрее основной памяти, и весь пайплайн кадра, включая буфер кадра и Z-буфер, происходил в этой EDRAM.

В конце кадра результат копировался в основную память для вывода на экран. Из-за высокой пропускной способности EDRAM приставка могла делать 4x MSAA практически бесплатно, потому что запись в 4 раза большего буфера не стоила ничего в терминах пропускной способности. На PS3, где буфер кадра хранился в GDDR3, тот же MSAA обходился ощутимо дороже. Но ограничение в 10 мегабайт - это буфер кадра 1280x720 @ 32bpp + 32-битный Z-буфер = 7.03 Мб и если разработчик хотел рендерить в нестандартное разрешение или добавлял ещё один тяжелый проход или таргет, то бюджет заканчивался, и приходилось ходить в медленную основную память.

PS3 имел 256 мегабайт XDR DRAM для Cell-процессора и отдельно 256 мегабайт GDDR3 для GPU (RSX). Суммарно столько же, что у Xbox 360, но она была разделена физически и процессор мог читать видеопамять только через PCIe-мост, а RSX мог читать системную память когда этого не делал CPU, что конечно же было медленно.

Cell BE (3.2 GHz)                    RSX (GPU)
  │                                    │
  ├─ PPU (основное ядро)               ├─ 256 MB GDDR3 VRAM
  │    └─ SPU 0...5                    │  (пропускная способность: ~22.4 GB/s)
  │         (локальные хранилища       │
  │          по 256 KB каждый)         │
  │                                    │
  └─ 256 MB XDR DRAM                  │
       (пропускная способность:        │
        ~25.6 GB/s для Cell)           │
                                       │
  ───────── PCIe x16 ─────────────────
  (пропускная способность: ~15 GB/s обе стороны)

Вдобавок процессор PS3 содержал шесть рабочих SPU (Synergistic Processing Units), каждый из которых имел 256 килобайт локального хранилища (Local Store). SPU не мог обращаться к основной памяти напрямую и работал только с данными в своём Local Store, запрашивая DMA-передачи туда и обратно явными командами.

// Схема работы SPU

// В Local Store (256 KB):
// [0..63 KB]   входной буфер — данные для обработки
// [64..127 KB] выходной буфер — результаты
// [128..192 KB] следующий входной буфер (DMA в процессе)
// [192..256 KB] код + стек SPU

// Основной цикл SPU-задачи:
while (has_work) {
    // Пока обрабатываем текущий буфер...
    process_data(input_buffer_a, output_buffer_a);
    
    // ...параллельно грузим следующий
    dma_get_async(next_input, main_memory_ptr, INPUT_SIZE);
    dma_put_async(output_buffer_a, result_memory_ptr, OUTPUT_SIZE);
    
    dma_wait_all();   // синхронизация перед следующей итерацией
    swap_buffers();
}

Этот паттерн двойной буферизации DMA-передач позволял перекрывать вычисления и передачу данных. Хорошо написанная SPU-задача никогда не простаивала в ожидании DMA, а плохо написанная сначала ждала DMA и частенько мешала основному процессору прерываниями, потом считала, потом снова ждала, работая не только медленнее, но и мешая основной логике. Сложность написания SPU-программ оттолкнула большинство разработчиков от такой схемы, и многие даже не открывали документацию на SPU, считая её экзотикой. Только треть игр для PS3 использовала SPU, и всего несколько игр полностью реализовали эти возможности.

PS4 и Xbox One и общая унификация

PS4 и Xbox One (2013) сделали то, о чём разработчики просили еще с эпохи PS1 и обе консоли получили единое адресное пространство с общей памятью для CPU и GPU. Этот шаг, казавшийся очевидным для больниства разработчиков, стал возможен благодаря несколькими факторам: xbox захватил половину рынка консолей и мог диктовать условия Nintendo и Sony, на их историческом поле, x86-64 архитектура стала массовой и дешевой, и последнее - из Sony ушли последние приверженцы собственного пути в архитектуре, и более некому было остановить продвижений идей интела в светоче японского кораблеконсолестроения.

Так PS4 получила 8 гигабайт GDDR5 с единой шиной для CPU (Jaguar, 8 ядер x86-64) и GPU, пропускной способностью до 176 ГБ/с в пике на потоковых данных, а Xbox One получила теже 8 гигабайт, но разделённых: 5 гигабайт DDR3 для приложений и 3 гигабайта DDR3 для системы, плюс 32 мегабайта встроенной ESRAM (снова, как EDRAM у 360, но больше). Схема оказалась сложнее, чем у PS4, и теперь уже разработчики мелкомягких жаловались на необходимость явно управлять тем, что попадает в ESRAM.

Процессор Jaguar был изначально создан для ноутбуков и планшетов с низким TDP, а не для игровых консолей и имел относительно небольшие кеши, что означало, что код для PS4 нуждался в тщательной организации данных под кеш, диктуя определенные структуры данных, AoS vs SoA, минимальные footprint-ы для hot-path структур и явнуя предвыборку горячих данных. Движки, пришедшие с PC-разработки, где разработчики привыкли полагаться на бОльшие кеши, выдавали на Jaguar ожидаемо плохие результаты до соответствующих оптимизаций.

// AoS (Array of Structures) плохо для Jaguar
struct Entity {
    float x, y, z;      // позиция
    float vx, vy, vz;   // скорость
    int   health;
    int   type;
    char  name[32];      // 32 байта имени тянутся в кеш при доступе к позиции
};
Entity entities[1000];

// Обновление позиций тянет всю структуру в кеш,
// хотя нужны только x/y/z и vx/vy/vz

// SoA (Structure of Arrays) — хорошо для Jaguar
struct EntityPositions  { float x[1000], y[1000], z[1000]; };
struct EntityVelocities { float vx[1000], vy[1000], vz[1000]; };
struct EntityMisc       { int health[1000], type[1000]; };

// Обновление позиций читает только плотный массив float,
// кеш-линия содержит 16 позиций вместо 1 структуры

Самое важное изменение этого поколение было в том, что исчезла необходимость явно копировать данные между «системной» и «видеопамятью». На PS3 текстура сначала существовала в XDR DRAM, потом копировалась в GDDR3 для использования GPU, а на PS4 текстура создаётся один раз и GPU читает её из того же адреса, куда её положил CPU.

CPU (Jaguar, x86-64)           GPU
  │                             │
  └─────── общая шина ──────────┘
                │
        8 GB GDDR5 @ 176 GB/s
        
// Нет явного разделения и CPU, и GPU видят один адресный диапазон.
// GPU получает буфер через handle, а не через отдельную копию.

CommandBuffer* cb = gpu_alloc_command_buffer();
gpu_set_vertex_buffer(cb, shared_vertex_buffer_handle);
gpu_set_shader(cb, vertex_shader);
gpu_draw(cb, index_count);
gpu_submit(cb);
// CPU не ждёт и продолжает логику следующего кадра

Это упрощает управление и ресурсами и потоковую загрузку, но создает другую проблему. Появилась конкуренция за шину памяти, потому что CPU и GPU физически делят одни и те же провода и если GPU в разгаре рендеринга гонит много данных, а CPU в это время хочет загрузить текстуру с диска в память, то они начинают мешать друг другу и оба делают работу хуже и дольше, чем если бы это делалось последовательными операцими.

PS5, Xbox Series и SSD как новый уровень памяти

Главной болью предыдущего поколения была уже не нехватка VRAM, большинство игр не использовали даже половины доступной RAM/VRAM, а скорость потоковой загрузки. PS4 и Xbox One имели обычные HDD с пропускной способностью порядка, 100 МБ/с, что при 8 гигабайтах оперативной памяти и больших открытых мирах означало постоянный стриминг, когда одни ресурсы загружались, а другие выгружались.

Приходилось тратить усилия на отдельные и сложные системы приоритизации загрузок и маскировки задержек для открытых миров. Веяние в разработке игр не осталось незамеченным и уже PS5 ответил на это кастомным NVMe SSD с большими кешами, пропускной способностью до 9 ГБ/с (с аппаратной декомпрессией) и специализированным чипом для аппаратной декомпрессии Kraken прямо на пути от SSD к оперативной памяти.

Т.е. фактически весь уровень со всеми его текстурами, моделями, нпс и прочим барахлом, можно было запечь в архив, отдать приставке и сказать, положи его в оперативку начиная с такого-то адреса и через полсекунды иметь полностью загруженный уровень, что возвращает нас (по отзывчивости) в эпоху SNES, когда уровень начинался сразу после нажатия кнопки старт без всяких экранов загрузок и переходов между областями.

Xbox Series X предложил Xbox Velocity Architecture с NVMe SSD на 2.4 ГБ/с, похожей схемой аппаратной декомпрессии и прямым DMA в память.

Традиционная PS4 (упрощённо):
HDD (100 MB/s)
    │ загрузка
    ▼
RAM 8GB GDDR5
    │ GPU читает текстуры из RAM
    ▼
GPU рендеринг

PS5 (упрощённо):
NVMe SSD (5.5 GB/s)
    │ аппаратная декомпрессия Kraken
    │ прямой DMA
    ▼
RAM 16GB GDDR6 (448 GB/s)
    │ GPU читает текстуры из RAM
    ▼
GPU рендеринг

Скорость SSD выросла в ~55 раз.

Принципиальное изменение этого поколения, что SSD стал рассматриваться не как «медленный диск, с которым нужно работать позадачно», а как ещё один уровень памяти в иерархии. Если предыдущее поколение заставляло строить сложные системы стриминга с предсказанием и буферами, то PS5 позволял перейти к прямолинейной модели грузить нужное тогда, когда оно нужно, а ненужное просто убирать с уровня и не хранить его в кешах на будущее.

Тут складывается интересная ситуация, что упор разработчиков на большие открыте миры определил развитие железа, когда до PS5 такие миры требовали видимых экранов загрузки при переходах, или маленьких ячеек мира, или хитрых corridors-of-loading («лифты» в Halo, узкие проходы в The Last of Us), которые были инженерными решения для маскировки что HDD не успевал. А теперь с быстрым SSD и аппаратной декомпрессией нужда в большинстве этих трюков исчезла.

Ratchet & Clank: Rift Apart на PS5 демонстрировала телепортации между мирами без какой-либо загрузки именно потому, что SSD грузил новую сцену за доли секунды, просто пока воспроизводилась анимация перемещения.

Как обычно, тут тоже не обошлось без костылей. Если переносить на PS5 движок, написанный под PS4, с его системами стриминга «один в один» и не использовать возможности SSD вообще, то это не дает ощутимого прироста. Вы просто получите свои 15%-20% за счет выросшей мощности железа. И многие ранние PS5-игры это просто cross-gen-проекты, ограниченные моделью памяти предыдущего поколения, они просто грузились быстрее, но не меняли архитектуру своих миров.

Game Boy поколение и снова считаем байты

Вернемся немного назад, потому что, кроме больших консолей, у нас есть еще и мобильные и полумобильные платформы. Которые также популярны у очень большого числа игроков, и ситуация с ними если не кардинально отличается, то все равно очень интересная.

Game Boy (1989) имел 8-килобайтную рабочую RAM и 8-килобайтную VRAM. Это было больше, чем у Atari 2600, но меньше, чем у NES. Game Boy Advance (2001) принёс ARM7TDMI на 16.78 МГц, 256 килобайт внутренней WRAM, 32 килобайта IWRAM (быстрая, доступная за 1 такт) и 96 килобайт VRAM.

Разделение на WRAM и IWRAM позволяет критичный код положить в IWRAM для однотактового доступа, а всё остальное в более медленную WRAM. Картридж ROM был доступен тоже, но с задержкой в 3–8 тактов в зависимости от режима доступа, и многие разработчики копировали критичные функции из ROM в IWRAM при старте.

// GBA: разница между выполнением из ROM и IWRAM

// Если функция выполняется из ROM (картриджа):
//   каждая инструкция Thumb: 3 такта  (3 + wait states)
//   каждая инструкция ARM:   4 такта  (1 + 3 для 32-bit ROM access)

// Если функция скопирована в IWRAM и выполняется оттуда:
//   каждая инструкция Thumb: 1 такт
//   каждая инструкция ARM:   1 такт

// Атрибут для копирования функции в IWRAM:
IWRAM_CODE void critical_update_function(void) {
    // эта функция при старте копируется в IWRAM
}

Nintendo DS (2004) стал первой массовой консолью с двумя экранами, но нам для статьи интереснее его архитектура памяти: ARM9 на 67 МГц и ARM7 на 33 МГц, работающие параллельно с раздельными задачами.

ARM9 занимался игровой логикой и рендерингом верхнего экрана, ARM7 обслуживал Wi-Fi, звук и тачскрин. Разработчик надо было явно распределять задачи между двумя процессорами и управлять, в какой RAM живёт код каждого из них. Это была первая "многопроцессорная" разработка для широкой аудитории мобильных разработчиков, за несколько лет до Cell на PS3.

Switch (2017) использует чип Nvidia Tegra X1 с 4 гигабайтами LPDDR4, разделёнными между CPU и GPU. Тот же принцип единого адресного пространства, что и PS4, только в портативном формфакторе, а переключение между режимами (портативный / TV) меняет частоты GPU, но не меняет модель памяти.

Современные GPU и «виртуальная» память видеокарты

К современному поколению консолей изменилась не только скорость памяти, но и сама идея того, как GPU должен работать с ресурсами. На ранних системах разработчик почти всегда мыслил о памяти физическими метрками, потому что существовал конкретный фреймбуфер, конкретные текстуры и атласы и конкретный объём видеопамяти, внутри которого должны были одновременно помещаться все активные ресурсы сцены.

Современные GPU работают ближе к модели виртуальной памяти CPU, и теперь текстурки больше не обязана полностью существовать в VRAM вообще, а движок может загружать только те части текстур, которые действительно видимы в текущем кадре.

Это называется virtual texturing, sparse textures и residency management (виртуальные текстуры), они построены вокруг идеи, что видеопамять больше не является местом постоянного хранения всех данных сцены, а становится быстрым кешем для тех ресурсов, которые нужны GPU прямо сейчас. Это важно для современных open-world игр, где полный набор текстур и геометрии может занимать сотни гигабайт и физически не поместится ни в одну видеопамять.

SSD следующего поколения (на условной PS6 сделают подобную модель физически реализуемой) и если на PS3/PS4/PS5 нам еще надо заботиться о загрузке данных с носителя, предугадывать будущие загрузки или строить сложные streaming-системы вокруг медленного диска (современный SSD хоть и быстрый, но все ёще не так быстр как нужно), то будущие PS6, и частично уже PS5 и Xbox Series позволяют перейти к demand-driven streaming, при котором данные загружаются в момент их реального использования.

Общая память тоже решила далеко не все проблемы, хотя может показаться, что единое адресное пространство полностью устраняет сложности взаимодействия CPU и GPU. Но даже если оба процессора работают с одним и тем же адресом памяти, то у каждого из них всё ещё остаются собственные кеши, а значит данные не становятся автоматически видимыми другой стороне сразу после записи.

Если CPU обновил буфер частиц, но не выполнил необходимые flush или invalidate операции, то GPU будет продолжать читать старую версию данных из своего кеша, и это часто проявляется как странные однокадровые артефакты, нестабильные фреймы или визуальное мерцание.

Частично эту задачу решают современные низкоуровневые API вроде Vulkan, DX12 и Metal, требуя от разработчика явно описывать владение ресурсом, точки синхронизации и барьеры, но концептуально эта проблема не решена, просто мы вынесли проблемы железа на уровень разработчика.

Sony видит решение это проблемы в еще одном слое DMA, только теперь уже на отдельной шине между CPU-GPU, а Мicrosoft и Apple в унификации APU, который мог бы решать оба набора задач. Но фундаментальная задача осталась той же самой - данные должны оказаться в правильном месте, в правильный момент и в правильной форме, иначе о производительности можно забыть.

З.Ы. Как правильно заметил @XenRE без маппера (NROM) адресное пространство CPU жёстко ограничено диапазоном 0x8000–0xFFFF, что даёт максимум 32 КБ для PRG, и 8 КБ для CHR через диапазон PPU 0x0000–0x1FFF. Цифра 512 КБ относится к маpперу MMC3, как одному из самых распространённых в коммерческой библиотеке NES (Super Mario Bros. 3, Mega Man 3–6, Contra и сотни других). MMC3 переключает банки PRG и CHR на лету, обходя физические ограничения шины. Теоретического предела для PRG/CHR ROM с маппером не существует и всё упирается только в конкретную схему маппера.

Комментарии (6)


  1. lgorSL
    12.05.2026 10:45

    Кстати, а что вы думаете про архитектуры типа apple m5 и ryzen ai, где память общая для всего? И в стимдеке тоже так.

    Там, с одной стороны, процессор, gpu и npu могут конкурировать за память, а с другой - шина памяти более широкая и в итоге и процессору веселее, и необходимость перекладывать данные между gpu и cpu пропадает, только синхронизацию сделал и читай?

    Так для справки - у райзена память 8 Ггц и 4 канала (в сумме вроде 256 ГБ/сек), у маков зависит от объёма оперативки (у модели на 128 ГБ вроде 600 ГБ/сек).


    1. dalerank Автор
      12.05.2026 10:45

      А тут все очень интересное начинается, физически память одна, но это не значит что ВСЕ читают из одного места одинаково. У текущего Xbox каждый "читатель" cpu, gpu, декомпрессор, h264 модуль подключен своим каналом к единому мемори контроллеру. Это уже получается больше "звезда", чем шина, но исторически просто консоли более оптимизированы с точки зрения латенси, да и шишки уже все набили и знают чего делать не надо.
      На стимдек другая проблема, приходится агрессивно сжимать таргет рендер и текстуры чтобы выдавать стабильные 60фпс, но экран маленький и игрок этого попросту не видит.
      Опять же на всех консолях с единой памятью вылазит другая проблема overhead memory visibilty, на дискретной карточке как было, видяхе данные отправил, сабмит или как там его вызвал и они там сами както варятся, про них можно забыть.
      А с единой памятью gpu и cpu могут смотреть на одну страницу, и cpu уже положил туда данные, т.е. они там физически изменились, а вот когда их "увидит" gpu теперь полностью зависит от протокола когерентности кешей. И получается что если ты хочешь быть уверен что gpu сразу увидит эти данные, то приходится либо флашить кеш, либо использовать когерентные области, которые гпу/цпу видят одинаково, что сильно дороже обычной аллокации.
      Как обычно бесплатный сыр бывает только мышке ловкой, так что unified memory никак не отменяет необходимости думать о доступе к памяти, просто меняет точку где надо думать.


  1. Nemoumbra
    12.05.2026 10:45

    Охохох... И ничего про Playstation Portable не сказали...(

    Не вдаваясь в детали железа, я могу сказать, что там есть SRAM (Scratchpad: по адресу 0x10000, длиной 0x4000 байт), EDRAM aka VRAM (0x0400_0000, 0x20_0000 байт, где можно выделить 0xCC000 байт там на два 16-битных фреймбуфера и 16-битный z-буфер, а остальное использовать в своё удовольствие), Kernel RAM (от 0x08000000 до 0x08400000, потом ещё до 0x08800000 т.н. Volatile RAM, который можно попросить у ядра) и пользовательская оператива до 0x0A000000. Есть ещё система зеркал с флагами (cached/uncached, kernel/user) и какие-то специальные адреса для работы с железом. Разумеется, играм в жезело низя лезть, но оно есть. На GE (Graphics engine) дисплей лист с гпушными инструкциями пересылается через сискол sceGeListEnqueue. Есть понятие "vblank callback", который можно зарегать тоже через сискол.


    1. dalerank Автор
      12.05.2026 10:45

      Интересно, расскажето поподробнее? просто с PSP никогда не сталкивался вообще. Более-менее близко с консолями работаю только от третьей плойки


  1. XenRE
    12.05.2026 10:45

    NES: CHR ROM/RAM: 8 KB на картридже (тайлы)
    Картридж PRG/ROM: до 512 KB

    Откуда взялась цифра 512 KB? Максимальный объем PRG ROM безмапперного картриджа 32Кб, если же использовать мапперы то лимит зависит от маппера, а теоретического предела вообще нет - хоть гигабайт поставь (то же относится и к CHR ROM).


    1. dalerank Автор
      12.05.2026 10:45

      угу, вы правы без мапера там только 32кб. Емнип для хомбрю массово использовали MMC3, который позволял как раз адресовать 512кб. Сейчас поправлю.