Русский
Русский
English
Статистика
Реклама

Перевод Эмуляция компьютера интерпретатор CHIP-8, графика и стриминг текстур

В прошлый раз мы остановились на том, что создали интерпретатор CHIP-8 и оснастили его системой для формирования кадров. Видеть то, что должно попасть на экран, можно в консоли. Теперь же мы собираемся взять то, что формирует интерпретатор, вынести это за пределы консоли и показать на экране.

Решать вышеозначенные задачи мы будем с помощью библиотеки SDL, которая умеет выводить графические данные на экран, принимать то, что вводит пользователь, и проигрывать звуки. Настройка SDL-проекта может вызвать некоторые сложности. Поэтому я рекомендую перед началом работы с библиотекой почитать мой материал о ней.



Есть много способов вывести что-либо на экран с использованием SDL. В играх, в основном, изображения не формируются, как в нашем случае, средствами CPU. Но при эмуляции и (что встречается чаще) при воспроизведении видео изображение (вполне возможно сжатое) готовится к выводу средствами CPU. Такое изображение, для вывода его на экране, нужно загрузить в GPU. После того, как изображение попадёт в GPU, мы называем его текстурой, а весь этот процесс называют стримингом текстур.

Изображение, формируемое средствами класса Image, представлено в некоем графическом формате. Но SDL понимает лишь определённый набор пиксельных форматов. Если взглянуть на эти форматы, то окажется, что нам вполне может подойти SDL_PIXELFORMAT_RGB24. Настроим класс SDLViewer, который будет стримить изображения в этом формате, а чуть позже поразмыслим о том, как преобразовать данные нашего кадрового буфера в RGB24.

// sdl_viewer.h// SDL-окно RAII с поддержкой аппаратного ускорения.// Оптимизировано для стриминга RGB24-текстур.class SDLViewer {public:// Ширина и высота должны быть равны параметрам изображения, загружаемого// через SetFrameRGB24.SDLViewer(const std::string& title, int width, int height, int window_scale = 1);~SDLViewer();// Рендеринг текущего кадра, возврат списка событий.std::vector<SDL_Event> Update();// Предполагается, что это - 8-битное RGB-изображение, ширина которого в байтах равна его ширине в пикселях (без необходимости использовать заполнители).void SetFrameRGB24(uint8_t* rgb24, int height);private:SDL_Window* window_ = nullptr;SDL_Renderer* renderer_ = nullptr;SDL_Texture* window_tex_ = nullptr;};

Мы планируем использовать этот класс как SDL-окно RAII, которое получает актуальные сведения о текстурах и выполняет рендеринг. Конструктор принимает показатель масштабирования окна, так как если попытаться вывести на экран изображение размером 64x32 пикселя без масштабирования, оно окажется очень маленьким.

SDLViewer::SDLViewer(const std::string& title, int width, int height, int window_scale) :title_(title) {if(SDL_Init(SDL_INIT_VIDEO) < 0) {throw std::runtime_error(SDL_GetError());}// Создание SDL-окна с учётом коэффициента масштабирования.window_ = SDL_CreateWindow(title.c_str(), SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED, width * window_scale, height * window_scale, SDL_WINDOW_SHOWN);// Настройка аппаратной системы рендеринга и текстуры, которую мы будем стримить.renderer_ = SDL_CreateRenderer(window_, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);window_tex_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGB24,SDL_TEXTUREACCESS_STREAMING, width, height);}SDLViewer::~SDLViewer() {SDL_DestroyTexture(window_tex_);SDL_DestroyRenderer(renderer_);SDL_DestroyWindow(window_);SDL_Quit();}std::vector<SDL_Event> SDLViewer::Update() {std::vector<SDL_Event> events;SDL_Event e;while (SDL_PollEvent(&e)) { events.push_back(e); }// Рендеринг текстуры.SDL_RenderCopy(renderer_, window_tex_, NULL, NULL );SDL_RenderPresent(renderer_);return events;}void SDLViewer::SetFrameRGB24(uint8_t* rgb24, int height) {void* pixeldata;int pitch;// Блокировка текстуры и загрузка изображения в GPU.SDL_LockTexture(window_tex_, nullptr, &pixeldata, &pitch);std::memcpy(pixeldata, rgb24, pitch * height);SDL_UnlockTexture(window_tex_);}

Тут нужно выполнить некоторые стандартные процедуры по инициализации SDL-механизмов в конструкторе класса и по освобождению ресурсов в деструкторе. Метод Update будет представлять свежее изображение, отправленное SDLViewer. Он, кроме того, отвечает за приём событий, связанных с вводом данных.

Загрузка текстуры в GPU выполняется в SetFrameRGB24. Функция принимает сведения о фрагменте памяти, в котором хранится изображение в нужном формате, а так же сведения о высоте изображения. SDL_LockTexture возвращает CPU-память для копирования графических данных. Ещё эта функция возвращает длину строки изображения в байтах. После того, как изображение скопировано в выделенный участок памяти, мы вызываем функцию SDL_UnlockTexture, которая выгружает изображение в GPU в виде новой текстуры.

Теперь нам надо отредактировать код главного цикла, сделав так, чтобы в нём использовалось бы новое окно.

// main.cppvoid Run() {int width = 64;int height = 32;SDLViewer viewer("CHIP-8 Emulator", width, height, /*window_scale=*/8);uint8_t* rgb24 = static_cast<uint8_t*>(std::calloc(width * height * 3, sizeof(uint8_t)));viewer.SetFrameRGB24(rgb24, height);CpuChip8 cpu;cpu.Initialize("/path/to/program/file");bool quit = false;while (!quit) {cpu.RunCycle();cpu.GetFrame()->CopyToRGB24(rgb24, /*r=*/255, /*g=*/0, /*b=*/0);viewer.SetFrameRGB24(rgb24, height);auto events = viewer.Update();for (const auto& e : events) {if (e.type == SDL_QUIT) {quit = true;}}}}

Мы инициализируем RGB24-картинку пустым изображением (нулями, чёрным цветом). Обратите внимание на то, что размер этого изображения вычисляется не как width * height (ширина * высота), а как width * height * 3 (ширина * высота * 3). Мы ведь работаем с RGB-изображением, имеющим 3 цветовых канала. Загрузка текстуры и вывод её на экран выполняются в каждом цикле. Из-за использования vsync оказывается, что эмулятор работает очень медленно. Но мы это исправим, добравшись до настройки временных параметров работы эмулятора. Теперь нам осталось лишь разобраться в том, что собой представляет графический формат RGB24, и реализовать Image::CopyToRGB24.

При создании RGB-изображений данные красного (red), зелёного (green) и синего (blue) цветовых каналов каждого пикселя часто идут в памяти друг за другом. Поэтому простое добавление 1 к адресу памяти уже необязательно позволит нам получить значение, соответствующее следующему пикселю.

0x000 :|RGBRGBRGB...----------------------------------------|0x040*3:|RGBRGBRGB...                    |0x080*3:|RGBRGBRGB...                    |..0x7C0*3:|RGBRGBRGB...----------------------------------------|

Нам, прежде чем мы сможем это обсудить, понадобится ввести некоторые новые термины. То, что называется stride или pitch, представляет собой ширину строки изображения в байтах. В данном случае это 3 * width_px (3 * ширина в пикселях). Мы можем говорить о байтовой ширине строки изображения и в смысле её отношения к цветовым каналам. Для того чтобы перейти от одного значения красного цвета (канала) в некоем пикселе к такому же значению для следующего пикселя, мы должны прибавить к адресу этого первого значения 3 (это называется 0-dimension stride). То же самое справедливо и для синего, и для зелёного каналов. При этом каждое отдельное значение, как и прежде, представлено 8 битами (значение может находиться в диапазоне от 0 до 255), но для описания каждого пикселя теперь нужно 3 значения (число 24 в названии RGB24, в результате, означает результат умножения 3 каналов на 8 битов). Собственно говоря, теперь у нас, похоже, есть всё необходимое для того чтобы сгенерировать изображение нужного формата на основе нашего монохромного изображения.

// image.cppvoid Image::CopyToRGB24(uint8_t* dst, int red_scale, int green_scale, int blue_scale) {int cols = Cols();for (int row = 0; row < Rows(); row++) {for (int col = 0; col < cols; col++) {dst[(row * cols + col) * 3] = At(col, row) * red_scale;dst[(row * cols + col) * 3 + 1] = At(col, row) * green_scale;dst[(row * cols + col) * 3 + 2] = At(col, row) * blue_scale;}}}

Тут мы перебираем данные исходного изображения и создаём его эквивалент в dst. Каждый пиксель исходного изображения представляем в виде трёх байт нового изображения. Каждый из этих байтов соответствует одному из цветовых каналов. Здесь мы пользуемся знанием того, что пиксели исходного изображения могут пребывать либо в состоянии выключено, либо в состоянии включено, применяя коэффициенты при задании значений цветовых каналов и, таким образом, получая готовое изображение, окрашенное в какой-то цвет.

Итоги


Теперь на экране можно наблюдать за изображением, формируемым эмулятором. Мы даже пользуемся тут возможностями GPU! То, что выводит ваш вариант эмулятора, должно сильно напоминать, например, то, что показано в этом видеофрагменте. В следующий раз поговорим о временных параметрах работы эмулятора, о том, как заставить систему работать с нужной скоростью, и о том, как обрабатывать ввод данных пользователем.

Как вы думаете, почему эмулятор CHIP-8 столь популярен?
Источник: habr.com
К списку статей
Опубликовано: 10.01.2021 16:05:45
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Программирование

C++

Chip-8

Разработка

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru