背景
以前より安価なLinuxベースの中華ゲーム機をシンセサイザーに転用することを目論んできた。JUCEを使ったシンセサイザープラグインはLinuxではX11が必須となるので、まずX11を動作させなければいけないという大きなハードルがあった。そこで、これらのゲーム機で動いているゲームエミュレーター同様、SDL (Simple DirectMedia Layer) を使ってアプリを作れば良いかもしれないと思いそれを試すことにした。
基本的な作戦としては、簡単なコードが実機で動くことが確認できたら、あとはMac上でほとんど全ての開発および動作確認を行い、ちょいちょい実機での動作も確認する、という方式。実機ではデバッガを使った確認はできないと思うくらいの気持ちで。なんならprintfデバッグすらできないかもしれないという想定。
準備
Anbernic RG35XX H (他のRG35XXシリーズとかRG28XXシリーズとかでもいけそう)
Mac (開発に使う)
microSDカード (32GBくらいでいいと思う。速度優先おすすめ)
Anbernicのサイトからファームウェアのイメージをダウンロード。
https://jp.anbernic.com/pages/firmware
自分の持ってるデバイス用の32GBのイメージで。
microSDカードに入れる。
本体に刺して起動することを確認。
https://github.com/exdial/anbernic-apps からSSH-Enablerをダウンロード。
Place the contents of the SSH-Enabler directory in Roms/APPS inside the internal(TF1) or external(TF2) SD card and start it from the Anbernic APPS menu.
と書いてあるとおり、このようにmicroSDカードのRoms>APPSの中にコピーする。Imgに入っている2つの画像ファイルも入れておく。
アプリはシステムのSDカードでも2枚目のSDカードでも好きな方のAPPSフォルダに入れればよい。実行時にTF1かTF2かを選択する画面になる。
TF1とTF2へのアクセス方法
TF1 -> /mnt/mmc
TF2 -> /mnt/sdcarsd
WiFiに接続
SSH-Enablerを入れたSDカードを差した状態でRG35XXを起動し、メニューボタンを押してネットワーク接続設定から。SSIDとパスワードを入れて繋がったらIPアドレスが表示される。
SSHを有効にする
AppsからEnable SSHを選択する。しばらくすると戻ってくるのでそれでSSHが有効になる。無効にしたい場合はAppsに行きDisable SSH。
Macでターミナルを起動してSSH接続を試す。
ssh root@IPアドレス
パスワードはroot。繋がったら下記のように出てくるはず。Ubuntuベースだということがわかる。
Welcome to Ubuntu 22.04 LTS (GNU/Linux 4.9.170 aarch64)
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
root@ANBERNIC:~#
必要なツールをインストール
まずはアップデートとアップグレード。アップグレードはかなり時間かかる。正直やらなくてもいいのかもしれないが。
apt update -y
apt upgrade -y
コンパイルに必要なもの一式をインストール。
apt install -y build-essential cmake g++ nano git
SDLはSDL2の基本パッケージ以外に下記も入れておく。
- SDL2_mixer(オーディオミキシング)
- SDL2_image(画像読み込み)
- SDL2_ttf(フォントレンダリング)
- SDL2_net(ネットワーク)
apt install -y libsdl2-dev libsdl2-mixer-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-net-dev
OpenGLのために下記も入れる。
apt install -y libgl1-mesa-dev
ImGuiというGUIライブラリを使うので。これはプロジェクトのフォルダに入れた方がいいかも。
git clone https://github.com/ocornut/imgui.git
SDLとOpenGLを使ったアプリの確認
次のコードをmain.cppとして作成。ちなみにこのコードは100%AIが書いた。
#include <SDL2/SDL.h>
#include <GLES2/gl2.h>
#include <string>
#include <vector>
// シェーダーは変更なし
const char* vertexShaderSource = R"(
attribute vec2 position;
attribute vec4 color;
varying vec4 v_color;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
v_color = color;
}
)";
const char* fragmentShaderSource = R"(
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
)";
// 7セグメントの定義(各数字でどのセグメントが点灯するか)
// セグメントの順序: 上、右上、右下、下、左下、左上、中央
const bool SEGMENTS[10][7] = {
{1,1,1,1,1,1,0}, // 0
{0,1,1,0,0,0,0}, // 1
{1,1,0,1,1,0,1}, // 2
{1,1,1,1,0,0,1}, // 3
{0,1,1,0,0,1,1}, // 4
{1,0,1,1,0,1,1}, // 5
{1,0,1,1,1,1,1}, // 6
{1,1,1,0,0,0,0}, // 7
{1,1,1,1,1,1,1}, // 8
{1,1,1,1,0,1,1} // 9
};
// セグメントを描画する関数
void addSegment(std::vector<float>& vertices, float x, float y, float width, float height, int segmentType) {
float thickness = width * 0.15f; // セグメントの太さ
float gap = thickness * 0.5f; // セグメント間のギャップ
switch(segmentType) {
case 0: // 上横
vertices.push_back(x + gap); vertices.push_back(y);
vertices.push_back(x + width - gap); vertices.push_back(y);
break;
case 1: // 右上縦
vertices.push_back(x + width); vertices.push_back(y - gap);
vertices.push_back(x + width); vertices.push_back(y - height/2 + gap);
break;
case 2: // 右下縦
vertices.push_back(x + width); vertices.push_back(y - height/2 - gap);
vertices.push_back(x + width); vertices.push_back(y - height + gap);
break;
case 3: // 下横
vertices.push_back(x + gap); vertices.push_back(y - height);
vertices.push_back(x + width - gap); vertices.push_back(y - height);
break;
case 4: // 左下縦
vertices.push_back(x); vertices.push_back(y - height/2 - gap);
vertices.push_back(x); vertices.push_back(y - height + gap);
break;
case 5: // 左上縦
vertices.push_back(x); vertices.push_back(y - gap);
vertices.push_back(x); vertices.push_back(y - height/2 + gap);
break;
case 6: // 中央横
vertices.push_back(x + gap); vertices.push_back(y - height/2);
vertices.push_back(x + width - gap); vertices.push_back(y - height/2);
break;
}
}
// 数字一つを描画するための頂点を生成
void addNumber(std::vector<float>& vertices, float x, float y, float size, int number) {
float width = size * 0.6f;
float height = size;
for (int i = 0; i < 7; i++) {
if (SEGMENTS[number][i]) {
addSegment(vertices, x, y, width, height, i);
}
}
}
int main(int argc, char* argv[]) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) return 1;
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_Window* window = SDL_CreateWindow("FPS Test",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
1280, 720, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
if (!window) {
SDL_Quit();
return 1;
}
SDL_GLContext glContext = SDL_GL_CreateContext(window);
if (!glContext) {
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
SDL_GL_SetSwapInterval(1);
// シェーダープログラムのセットアップ
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// バッファの作成
GLuint VBO;
glGenBuffers(1, &VBO);
// 三角形の頂点データ
float triangleVertices[] = {
0.0f, 0.5f,
-0.5f, -0.5f,
0.5f, -0.5f
};
// メインループ
bool quit = false;
SDL_Event event;
Uint32 frameStart = SDL_GetTicks();
int frameCount = 0;
int fps = 0;
glLineWidth(3.0f); // 線を太くする
while (!quit) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT ||
(event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE)) {
quit = true;
}
}
// FPS計算
frameCount++;
Uint32 currentTime = SDL_GetTicks();
if (currentTime - frameStart >= 1000) {
fps = frameCount * 1000 / (currentTime - frameStart);
frameCount = 0;
frameStart = currentTime;
}
glClearColor(0.2f, 0.3f, 0.8f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
GLint colorAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(posAttrib);
// 三角形の描画
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVertices), triangleVertices, GL_STATIC_DRAW);
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
float red[] = {1.0f, 0.0f, 0.0f, 1.0f};
glVertexAttrib4fv(colorAttrib, red);
glDrawArrays(GL_TRIANGLES, 0, 3);
// FPS数字の描画
std::vector<float> numberVertices;
std::string fpsStr = std::to_string(fps);
float startX = -0.95f;
float startY = 0.9f;
float size = 0.15f;
for (char digit : fpsStr) {
addNumber(numberVertices, startX, startY, size, digit - '0');
startX += size * 0.8f;
}
if (!numberVertices.empty()) {
glBufferData(GL_ARRAY_BUFFER, numberVertices.size() * sizeof(float),
numberVertices.data(), GL_STATIC_DRAW);
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
float white[] = {1.0f, 1.0f, 1.0f, 1.0f};
glVertexAttrib4fv(colorAttrib, white);
glDrawArrays(GL_LINES, 0, numberVertices.size() / 2);
}
SDL_GL_SwapWindow(window);
Uint32 frameTime = SDL_GetTicks() - currentTime;
if (frameTime < 16) {
SDL_Delay(16 - frameTime);
}
}
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
glDeleteShader(fragmentShader);
glDeleteShader(vertexShader);
SDL_GL_DeleteContext(glContext);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
コンパイルする。
g++ -o sdltest main.cpp -lSDL2 -lGLESv2 -lEGL
このままリモートから実行するとエラーになるので、アプリを/mnt/mmc/Roms/APPS/にコピー。
cp sdltest /mnt/mmc/Roms/APPS/
実機で動作確認。
終了する方法がないので、プロセスを探してkillする。
root@ANBERNIC:/mnt/sdcard/home# ps -A | grep sdltest
23330 ? 00:00:25 sdltest
root@ANBERNIC:/mnt/sdcard/home# kill 23330