はじめに
C/C++でGUIアプリ作成においてはQtやXWindowsシステムなどの既存の描写ライブラリがあります.これらは大抵の場合,ソースコードが膨大で理解が大変,もしくはライセンス上ソースコードを見ることができないなどブラックボックス化しています.商用目的になると有償版等を用いるなどの制約があるでしょう.また,環境構築やバージョン違いによりそもそも実行自体に苦労することもしばしばあります.そこで筆者は,直接フレームバッファをいじることで描画するプログラムを作成しようと思いました.低レイヤーとIoTへの応用を兼ねてlinux上で動かすアプリとします.
本記事はフレームバッファをいじる際の備忘録として,また,GUIをフルスクラッチで書いてみたい初心者向けの記事となっております.私自身業務プログラミングが未経験ゆえにソースコード等拙い部分がありますがご了承ください.
本記事ではLinuxOSのフレームバッファ(/dev/fbXX)の読み書きを通して,ディスプレイに表示される内容を変化させるプログラムをC++で記載しています.この記事に書かれた内容を応用すれば,IoTのほかの機器(例: ラズベリーパイ)と組み合わせることにより,デジタルサイネージのプログラムをつくることも可能になります.
注意:デバイスに関わる操作を含むために,誤って書き換えるべきではない場所を書き換えることによるシステムの破壊がありえます.筆者は責任をとりません.本記事のプログラムを試す際には自己責任でお願いします.
フレームバッファ
フレームバッファとはコンピュータ内部においてディスプレイの表示内容を記憶しておくメモリ領域,もしくはメモリ装置のことを指します.Linux系OSにおいては/dev/fbXXが該当します.XXには数字が入り,接続先などのディスプレイによりXXは変化します.本記事では一貫として/dev/fb0とします.
/dev/fb0の対応するアドレスデータを適切に変更することで,最終的に思いのままの内容をディスプレイに表示可能です.
環境
筆者の環境を紹介します.
ubuntu22.04LTS(VMWare Workstation 16 Player上で動かしています.メモリは16GB,プロセッサのコア数は2,ハードディスクは20GB,64bit)
ホストOSはwindows10(メモリ32GB、64bit)を用いています.
プログラム
準備
まず初めに/dev/fb0ファイルオープンをします.続いて必要な情報であるディスプレイのx方向(幅)とy方向(高さ)のサイズを求めます.この際にioctlを利用し,フレームバッファの情報を構造であるfb_var_screeninfoの変数であるvinfoに格納します.筆者の環境では幅が1280px,高さが800pxとなりました.
int fb_fd = open(FRAME_BUFFER_PATH.c_str(), O_RDWR);
if (fb_fd == -1) {
perror("Error opening framebuffer device");
return 1;
}
struct fb_var_screeninfo vinfo;
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo)) {
perror("Error reading variable information");
close(fb_fd);
return 1;
}
height=vinfo.yres_virtual;
width=vinfo.xres_virtual;
続いて描画していきます.方法としては以下のstep1~step3を用います.
step1.
mmap()でディスプレイの幅 $\times$ 高さ $\times$ 4Bytes(不透明度とR,G,Bでそれぞれ1byteずつ)分をメモリに紐づける.
uint32_t *frameBuffer = (uint32_t*)mmap(NULL, h*w*4, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
if (frameBuffer == MAP_FAILED) {
perror("Error mapping framebuffer device to memory");
close(fb_fd);
return 1;
}
step2.
対応するバッファに書き込む.ここでは白色のカラーコードである(WHITE=0xFFFFFFFF)を書き込みます.
unsigned int fcolor=WHITE;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
frameBuffer[y*w+x]=fcolor;
}
}
step3
msync()によりフレームバッファの変更を/dev/fb0に同期させます.これによりディスプレイが更新されます.操作が終わったあとはmunmap()によりメモリを解放し,ファイルを閉じることを忘れないようにしましょう.
msync(frameBuffer,h*w*4,0);
munmap(frameBuffer, screensize);
close(fb_fd);
以上が原理の説明でした.
以下はプログラム全体です.
長方形の描画処理を関数draw_boxを通して変数bgimageへ書き込んでいます.
最終的にはshow_image関数によりbgimageの内容をフレームバッファへ反映させています.
この際にbgimageの内容が0の場合は白で塗りつぶすようにしています.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include<linux/omapfb.h>
#include <string.h>
#include <stdint.h>
#include<string>
#include<cassert>
#include<vector>
#include<cmath>
#include<iostream>
using namespace std;
const string FRAME_BUFFER_PATH="/dev/fb0";
const int FONT_PTSCALE=64;
const string FONT_PATH="/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf";
const unsigned int WHITE=0xFFFFFFFF;
const unsigned int BLACK=0xFF000000;
const unsigned int RED=0xFFFF0000;
const unsigned int YELLOW=0xFFFFFF00;
const unsigned int BLUE = 0xFF0000FF;
int fb_fd;//ファイル
int height;//ディスプレイ高さ
int width;//ディスプレイ幅
vector<vector<unsigned int>> bgimage;
//(px,py)から(px+w,py+h)を対角線にもつ長方形
void draw_box(int px,int py,int h,int w,unsigned int color){
for(int iy=py ;iy<py+h ;iy++){
for(int ix=px ;ix<px+w ;ix++){
if(0<=iy && iy<height && 0<=ix && ix<width){
bgimage[iy][ix]=color;
}
}
}
}
int show_image(unsigned int background_color)
{
uint32_t *frameBuffer = (uint32_t*)mmap(NULL, height*width*4, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
if (frameBuffer == MAP_FAILED) {
perror("Error mapping framebuffer device to memory");
close(fb_fd);
return 1;
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
//bgimageが0ならバックグラウンドの色
frameBuffer[y*width+x]=(bgimage[y][x]==0?background_color:bgimage[y][x]);
}
}
msync(frameBuffer,height*width*4,0);
munmap(frameBuffer, height*width*4);
close(fb_fd);
return 0;
}
int main() {
fb_fd = open(FRAME_BUFFER_PATH.c_str(), O_RDWR);
if (fb_fd == -1) {
perror("Error opening framebuffer device");
return 1;
}
struct fb_var_screeninfo vinfo;
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo)) {
perror("Error reading variable information");
close(fb_fd);
return 1;
}
height=vinfo.yres_virtual;
width=vinfo.xres_virtual;
bgimage = vector<vector<unsigned int>>(height,vector<unsigned int>(width,0));
printf("vinfo.yres_virtual=%d,vinfo.xres_virtual=%d,vinfo.bits_per_pixel=%d",vinfo.yres_virtual,vinfo.xres_virtual,vinfo.bits_per_pixel);
draw_box(0,50,width/4,height/4,BLUE);
draw_box(300,300,width/4,height/4,RED);
show_image(WHITE);
getchar();
return 0;
}
実行
コンパイルしましょう.
$g++ main.cpp
フレームバッファへの書き込み操作があるため,管理者権限で実行する必要があります.
$sudo ./a.out
なお,GUI画面ではプログラムの結果が反映されない可能性があります.このため,Ctrl+Alt+F5によりCUIの環境に切り替えておきましょう.(Ctrl+Alt+F1でGUIに戻せます.)
以下はプログラムを実行させた結果です.見てわかるように画面全体が描写されておらず,ところどころでもともとのコンソール画面が見えてしまっています.この現象が起こる場所はプログラムを実行するたびに変化するため,再現性がありません.
排他処理
上記のプログラムを実行させた際に全部が塗りつぶされなかった理由は,コンソールの様々なプロセスが並行して動いているためにa.out実行で画面描写した後に上からコンソール画面の描写が起こるためです.そこでa.outが実行中は画面がロックされるようにしましょう.これによりほかのプロセスは画面描写をすることができなくなるようにします.
以下の処理step1~step3を行います.
step1
現在のプログラムのプロセスidを調べ,そのid以外からの書き込みを禁止するようにファイルディスクリプタfp_fdをロックする.
struct flock fl;
fl.l_type = F_WRLCK; // 書き込みロックを取得
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
fl.l_pid = getpid();
// ファイルロックを設定
if (fcntl(fb_fd, F_SETLKW, &fl) == -1) {
perror("Error locking framebuffer device");
close(fb_fd);
return 1;
}
step2
frameBufferに書き込んでmsync()で実行.
step3
標準入力が来る間はフレームバッファをロックした状態にします.サイネージに応用する場合等はsleep等などで時間制限をするといいかもしれません.
char c;
cin>>c;//waiting for input. 入力が加わるまでの間は画面がこのプログラムでロックされる
// ファイルロックの解除
fl.l_type = F_UNLCK;
if (fcntl(fb_fd, F_SETLK, &fl) == -1) {
perror("Error unlocking framebuffer device");
}
close(fb_fd);
実行結果
なお,上図の左下の黒い部分は入力待ちの際のコンソール画面においてカーソルが点滅する部分です.
最後にプログラム全貌を示します.show_image()は省略し,代わりにshow_image2()を載せています.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include<linux/omapfb.h>
#include <string.h>
#include <stdint.h>
#include<string>
#include<cassert>
#include<vector>
#include<cmath>
#include<iostream>
using namespace std;
const string FRAME_BUFFER_PATH="/dev/fb0";
const int FONT_PTSCALE=64;
const string FONT_PATH="/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf";
const unsigned int WHITE=0xFFFFFFFF;
const unsigned int BLACK=0xFF000000;
const unsigned int RED=0xFFFF0000;
const unsigned int YELLOW=0xFFFFFF00;
const unsigned int BLUE = 0xFF0000FF;
int fb_fd;//ファイル
int height;
int width;
vector<vector<unsigned int>> bgimage;
//(px,py)から(px+w,py+h)を対角線にもつ長方形
void draw_box(int px,int py,int h,int w,unsigned int color){
for(int iy=py ;iy<py+h ;iy++){
for(int ix=px ;ix<px+w ;ix++){
if(0<=iy && iy<height && 0<=ix && ix<width){
bgimage[iy][ix]=color;
}
}
}
}
int show_image2(unsigned int background_color)
{
struct flock fl;
fl.l_type = F_WRLCK; // 書き込みロックを取得
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
fl.l_pid = getpid();
// ファイルロックを設定
if (fcntl(fb_fd, F_SETLKW, &fl) == -1) {
perror("Error locking framebuffer device");
close(fb_fd);
return 1;
}
//ここからフレームバッファをいじる
uint32_t *frameBuffer = (uint32_t*)mmap(NULL, height*width*4, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
if (frameBuffer == MAP_FAILED) {
perror("Error mapping framebuffer device to memory");
close(fb_fd);
return 1;
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
//bgimageが0ならバックグラウンドの色
frameBuffer[y*width+x]=(bgimage[y][x]==0?background_color:bgimage[y][x]);
}
}
msync(frameBuffer,height*width*4,0);
munmap(frameBuffer, height*width*4);
char c;
cin>>c;//waiting for input
// ファイルロックの解除
fl.l_type = F_UNLCK;
if (fcntl(fb_fd, F_SETLK, &fl) == -1) {
perror("Error unlocking framebuffer device");
}
close(fb_fd);
return 0;
}
int main() {
fb_fd = open(FRAME_BUFFER_PATH.c_str(), O_RDWR);
if (fb_fd == -1) {
perror("Error opening framebuffer device");
return 1;
}
struct fb_var_screeninfo vinfo;
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo)) {
perror("Error reading variable information");
close(fb_fd);
return 1;
}
height=vinfo.yres_virtual;
width=vinfo.xres_virtual;
bgimage = vector<vector<unsigned int>>(height,vector<unsigned int>(width,0));
//printf("vinfo.yres_virtual=%d,vinfo.xres_virtual=%d,vinfo.bits_per_pixel=%d",vinfo.yres_virtual,vinfo.xres_virtual,vinfo.bits_per_pixel);
draw_box(0,50,width/4,height/4,BLUE);
draw_box(300,300,width/4,height/4,RED);
show_image2(WHITE);
return 0;
}
最後に
本記事ではフレームバッファの描写プログラムをC++で作成しました.
このプログラムを出発点としていろいろなことができると思います.
最初に考えられることは,今回は図形だけを載せましたが,文字列表示もできるようになります(下図).文字列描写の際にはFreeTypeライブラリを用いるとよいでしょう.
参考までにソースコードをgithub上で公開しています.
(ラズベリーパイで実行する場合はch1/Manual.mdを参考にしてください)
次に考えられることがスクロール処理です.現段階ではshow_image2()内部においてスクロールのたびにいちいちframeBufferへbufferすべての内容を描写させています.これでは処理量がかかるため工夫する余地があるでしょう.このヒントとしてはゼロから作るOS自作入門が参考になると思います.
さらなる発展としては,マウスのクリックとマウス座標の取得などの関数と組み合わせるとお絵描きアプリ等をつくることが可能になります.二点間をクリックして線を引く処理などを組み合わせれば,最終的にはCADのようなソフトウェアをつくることも可能です.
別の発展としてはキー入力を反映させてキャラクターを動かすゲームなども作成可能です.
このように,プログラムを作る際には最初から完璧を作ろうとせずに少しずつ改良を加えていくことが大事になってきます.
最後に,ここまで読んでいただきありがとうございました.この記事が読者のGUIプログラミング等の助けになれば幸いです.それではよい低レイヤーライフを!
参考
https://www.kernel.org/doc/html/v4.19/driver-api/frame-buffer.html
https://qiita.com/iwatake2222/items/0a7a2fefec9d93cdf6db
http://archive.linux.or.jp/JF/JFdocs/kernel-docs-2.6/fb/framebuffer.txt.html
カラーコード
https://www.conifer.jp/tool/color_name.html