apps/bbench - CPU, graphics, memory and disk benchmark for KolibriOS (#561)
Build system / Build (en_US) (push) Successful in 2m19s
Build system / Build (es_ES) (push) Successful in 2m22s
Build system / Build (ru_RU) (push) Successful in 2m30s
Build system / Publish Images (push) Successful in 2m32s

- Add `bbench` to IMG and System Panel
  - Universal benchmark with 17 tests for CPU, graphics, memory and disk
  - Tests can be toggled and configured
  - Generates derailed reports in HTML
- Remove `MGB` and `FSPEED` from System Panel, move them from IMG to ISO

---------

Co-authored-by: Burer <burer@kolibrios.org>
Reviewed-on: #561
Reviewed-by: bad_Dr3dd0x <1702+bad_dr3dd0x@noreply.localhost>
Reviewed-by: Burer <burer@kolibrios.org>
Co-authored-by: leency <lipatov.kiril@gmail.com>
This commit was merged in pull request #561.
This commit is contained in:
2026-07-23 13:51:18 +00:00
committed by Burer
co-authored by Burer
parent 00fb7afe33
commit 7c4d3f57e6
13 changed files with 1698 additions and 8 deletions
+3 -2
View File
@@ -428,7 +428,6 @@ tup.append_table(img_files, {
{"DISPTEST", VAR_PROGS .. "/testing/disptest/disptest"},
{"END", VAR_PROGS .. "/system/end/light/end"},
{"ESKIN", VAR_PROGS .. "/system/eskin/eskin"},
{"FSPEED", VAR_PROGS .. "/testing/fspeed/fspeed"},
{"GMON", VAR_PROGS .. "/system/gmon/gmon"},
{"HDD_INFO", VAR_PROGS .. "/system/hdd_info/hdd_info"},
{"KBD", VAR_PROGS .. "/testing/kbd/kbd"},
@@ -437,7 +436,6 @@ tup.append_table(img_files, {
{"LOADDRV", VAR_PROGS .. "/system/loaddrv/loaddrv"},
{"MAGNIFY", VAR_PROGS .. "/system/magnify/magnify"},
{"MADMOUSE", VAR_PROGS .. "/other/madmouse/madmouse"},
{"MGB", VAR_PROGS .. "/testing/mgb/mgb"},
{"MOUSEMUL", VAR_PROGS .. "/system/mousemul/mousemul"},
{"MOUSEPOS", VAR_PROGS .. "/other/mousepos/mousepos"},
{"MYKEY", VAR_PROGS .. "/system/MyKey/MyKey"},
@@ -642,6 +640,8 @@ tup.append_table(extra_files, {
{"kolibrios/media/zsea/plugins/rotate.obj", VAR_PROGS .. "/media/zsea/plugins/rotate/rotate.obj"},
{"kolibrios/media/zsea/plugins/scaling.obj", VAR_PROGS .. "/media/zsea/plugins/scaling/scaling.obj"},
{"kolibrios/utils/AMDtemp", VAR_PROGS .. "/system/amd_temp_view/AMDtemp"},
{"kolibrios/utils/fspeed", VAR_PROGS .. "/testing/fspeed/fspeed"},
{"kolibrios/utils/mgb", VAR_PROGS .. "/testing/mgb/mgb"},
{"kolibrios/utils/kfm/kfm", VAR_PROGS .. "/fs/kfm/kfm"},
{"kolibrios/utils/tedit/t_edit", VAR_PROGS .. "/other/t_edit/t_edit"},
{"kolibrios/3D/blocks/block.bin", VAR_PROGS .. "/bcc32/games/blocks/block.bin"},
@@ -683,6 +683,7 @@ tup.append_table(img_files, {
{"EASYSHOT", VAR_PROGS .. "/cmm/misc/easyshot.com"},
{"MOUSECFG", VAR_PROGS .. "/cmm/mousecfg/mousecfg.com"},
{"BARSCFG", VAR_PROGS .. "/cmm/barscfg/barscfg.com"},
{"BBENCH", VAR_PROGS .. "/cmm/bbench/bbench.com"},
{"SEARCH", VAR_PROGS .. "/cmm/misc/search.com"},
{"SYSPANEL", VAR_PROGS .. "/cmm/misc/software_widget.com"},
{"SYSMON", VAR_PROGS .. "/cmm/sysmon/sysmon.com"},
+1 -2
View File
@@ -35,7 +35,6 @@ Console Board=develop/dbgboard,19
PCI Devices=pcidev,38
CPU Info=cpuid,37
Display Test=disptest,113
Graphic Bench=mgb,121
Benchmark=bbench,121
HDD Info=hdd_info,50
File Speed=fspeed,45
Network Speed=network/dl|-test,51
+1 -2
View File
@@ -35,7 +35,6 @@ Consola de depuracion=develop/dbgboard,19
Dispositivos PCI=pcidev,38
CPU Info=cpuid,37
Prueba de pantalla=disptest,113
Rendimiento grafico=mgb,121
Benchmark=bbench,121
Info de disco=hdd_info,50
Velocidad FS=fspeed,45
Velocidad de red=network/dl|-test,51
+1 -2
View File
@@ -35,7 +35,6 @@ AMDtemp=/k/utils/AMDtemp,128
Устройства PCI=pcidev,38
CPU Info=cpuid,37
Тест экрана=disptest,113
Скорость графики=mgb,121
Benchmark=bbench,121
Информация о дисках=hdd_info,50
Скорость ФС=fspeed,45
Скорость сети=network/dl|-test,51
+6
View File
@@ -0,0 +1,6 @@
if tup.getconfig("NO_CMM") ~= "" then return end
if tup.getconfig("LANG") == "ru_RU"
then C_LANG = "LANG_RUS"
else C_LANG = "LANG_ENG" -- this includes default case without config
end
tup.rule("bbench.c", "c-- /D=$(C_LANG) /OPATH=%o %f" .. tup.getconfig("KPACK_CMD"), "bbench.com")
+616
View File
@@ -0,0 +1,616 @@
//========================================================//
// //
// BirdBench - a modular system benchmark //
// The window is a launcher: tick the tests you want, //
// press Run - results open as an HTML report in //
// WebView (clearer there than in the tiny window). //
// //
// Add a test : edit tests_cpu.h / tests_gpu.h / ... //
// Add a rival : edit cpudb.h //
// //
//========================================================//
#define MEMSIZE 4096*1536 // ~6 MB heap (2+2 MB work buffers + canvas)
#include "../lib/kolibri.h" // must be first (sets entry point)
#include "../lib/mem.h"
#include "../lib/strings.h"
#include "../lib/fs.h"
#include "../lib/io.h"
#include "../lib/gui.h"
#include "../lib/system.h"
#include "../lib/gui/checkbox.h"
#include "../lib/gui/menu.h"
#include "bbench.h"
#include "tests_cpu.h"
#include "tests_gpu.h"
#include "tests_disk.h"
#include "cpudb.h"
//---------------- layout ----------------//
#define PAD 16
#define COL2_X 170 // second column (Graphics)
#define COL3_X 328 // third column (Disk)
#define HEADER_H 42
#define ROW_H 22
#define WIN_W GCV_W
#define WIN_H GCV_H + HEADER_H
#define BUTTON_Y WIN_H - 62
#define BUTTON_W 70
#define BUTTON_H 24
#define STATUSBAR_Y WIN_H - 25
#define BAR_FULL 3000 // score that fills a full HTML bar
#define BTN_RUN 30
#define BTN_ALL 33
#define BTN_NONE 34
#define BTN_DISK_DROP 39 // disk dropdown toggle
#define MAX_DISKS 20
#define BTN_SECT_BASE 50 // 50..52 section masters
#define BTN_CHECK_BASE 60 // 60.. per-test checkboxes
checkbox cb; // reused as a renderer for every checkbox
byte t_enabled[MAX_TESTS];
dword sums[SECT_NUM];
dword cnts[SECT_NUM];
char sysinfo[] = "CPU: %d MHz RAM: %d MB Screen: %dx%d@%db";
char revinfo[] = " BB Revision: %d";
//---------------- disk list (LMENU dropdown) ----------------//
char disk_store[MAX_DISKS*24];
dword disk_dirp[MAX_DISKS];
int disk_count = 0;
int disk_sel = 0;
byte disk_menu_open = 0; // an LMENU popup is currently open
char disk_menu[MAX_DISKS*26]; // "\n"-separated item list for /sys/develop/menu
dword disk_drop_x, disk_drop_y;
char probe_buf[16];
int DiskWritable(dword dir)
{
char pf[80];
dword r;
strcpy(#pf, dir); strcat(#pf, "/bbprobe.tmp");
r = FileWrite(#pf, #probe_buf, 16);
if (r==0) dk_delete(#pf);
if (r==0) return 1;
return 0;
}
void AddDisk(dword dir)
{
dword slot;
if (disk_count >= MAX_DISKS) return;
slot = disk_count * 24; slot = slot + #disk_store;
strcpy(slot, dir);
disk_dirp[disk_count] = slot;
disk_count++;
}
void TryDisk(dword dir) { if (DiskWritable(dir)) AddDisk(dir); }
void ScanDisks() // enumerate real mounts under "/", keep writable
{
int i, j;
dword nm, l;
char base[48];
char dig[2];
byte c0, c1, skip;
disk_count = 0;
dig[1] = 0;
io.dir.load("/", DIR_ONLYREAL);
for (i=0; i<io.dir.count; i++) {
nm = io.dir.position(i);
c0 = DSBYTE[nm]; c1 = DSBYTE[nm+1];
skip = 0;
l = strlen(nm);
if (l > 12) skip = 1; // path slots are fixed 24 bytes
if (c0=='c') && (c1=='d') skip = 1; // CD (read-only)
if (c0=='f') && (c1=='d') skip = 1; // floppy
if (c0=='r') && (c1=='d') skip = 1; // ramdisk /rd (system)
if (!skip) {
// whole-disk FS (rare: unpartitioned media)
strcpy(#base, "/"); strcat(#base, nm);
TryDisk(#base);
// probe partitions /1../9 - the writable one is NOT always #1
// (e.g. #1 = EFI/NTFS-reserved, data partition = #2 or #3)
for (j=1; j<=9; j++) {
dig[0] = j + '0';
strcpy(#base, "/"); strcat(#base, nm);
strcat(#base, "/"); strcat(#base, #dig);
TryDisk(#base);
}
}
}
if (disk_count < 1) AddDisk("/tmp0/1");
disk_sel = 0;
disk_dir = disk_dirp[0];
disk_menu[0] = 0; // "\n"-joined paths for the LMENU app
for (i=0; i<disk_count; i++) {
strcat(#disk_menu, disk_dirp[i]);
if (i < disk_count-1) strcat(#disk_menu, "\n");
}
}
//---------------- helpers ----------------//
void bench_exit() // single exit point: never leave bbtst.tmp behind
{
Disk_Cleanup();
ExitProcess();
}
void FlatButton(dword x, id, bg, tc, label)
{
DefineButton(x, BUTTON_Y, BUTTON_W, BUTTON_H - 1, id, bg);
WriteText(-strlen(label)*8 + BUTTON_W / 2 + x,
BUTTON_H - 16 / 2 + BUTTON_Y, 0x90, tc, label);
}
//---------------- checkbox model ----------------//
byte SectionAllOn(dword sect)
{
int i;
for (i=0; i<t_count; i++) if (t_sect[i]==sect) && (!t_enabled[i]) return 0;
return 1;
}
void ToggleSection(dword sect)
{
int i;
byte v;
v = SectionAllOn(sect); v = v ^ 1;
for (i=0; i<t_count; i++) if (t_sect[i]==sect) t_enabled[i] = v;
}
void SetAll(byte v)
{
int i;
for (i=0; i<t_count; i++) t_enabled[i] = v;
}
//========================================================//
// HTML export //
//========================================================//
#define ICON_CPU 48
#define ICON_GPU 52
#define ICON_DISK 50
#define EBUF_SIZE 16384 // report buffer; a full 48-test run needs ~6 KB
#define BAR_W 26 // bar width in characters, same for every row
#define COL_NAME 20 // same columns for test and compare rows, so
#define COL_SCORE 8 // numbers and bars line up down the whole page
#define COL_INFO 16
// one copy of each colour; html_head repeats them for browsers that honour
// <style> - WebView reads bg= straight off the tag
char c_bar1[] = "#2c7be5"; char c_trk1[] = "#cdd2da";
char c_bar2[] = "#4a90e2"; char c_trk2[] = "#e4e7ec";
char c_cmp [] = "#8fb8e8";
dword ebuf = 0;
dword epos = 0;
char etmp[176];
char epath[64];
void AppendBcd(dword dst, byte b) // BCD byte -> 2 ASCII digits
{
dword hi, lo;
hi = b >> 4; hi = hi & 0xF;
lo = b & 0xF;
DSBYTE[dst] = hi + '0';
DSBYTE[dst+1] = lo + '0';
}
void BuildExportPath() // unique name from date + time
{
dword dt, tm, adr, v;
EAX = 29; $int 0x40; dt = EAX; // fn 29: 0x00DDMMYY (BCD)
EAX = 3; $int 0x40; tm = EAX; // fn 3: 0x00SSMMHH (BCD)
strcpy(#epath, "/tmp0/1/bb_");
adr = #epath; adr = adr + strlen(#epath);
v = dt; v = v & 0xFF; AppendBcd(adr, v); adr = adr + 2; // YY
v = dt >> 8; v = v & 0xFF; AppendBcd(adr, v); adr = adr + 2; // MM
v = dt >> 16; v = v & 0xFF; AppendBcd(adr, v); adr = adr + 2; // DD
DSBYTE[adr] = '_'; adr = adr + 1;
v = tm; v = v & 0xFF; AppendBcd(adr, v); adr = adr + 2; // HH
v = tm >> 8; v = v & 0xFF; AppendBcd(adr, v); adr = adr + 2; // MM
v = tm >> 16; v = v & 0xFF; AppendBcd(adr, v); adr = adr + 2; // SS
DSBYTE[adr] = 0;
strcat(#epath, ".htm");
}
void Hput(dword s)
{
dword l = strlen(s);
if (epos + l >= EBUF_SIZE) return; // truncate the report, never the heap
strcpy(ebuf+epos, s);
epos = epos + l;
}
void Hfield(dword s, dword width)
{
dword l = strlen(s);
Hput(s);
while (l < width) { Hput(" "); l++; }
}
// one bar for test rows (max = BAR_FULL) and compare rows (max = fastest CPU)
void Hbar(dword val, max, fillcol, trackcol)
{
dword filled, track, i;
if (max < 1) max = 1;
filled = muldiv(val, BAR_W, max); if (filled > BAR_W) filled = BAR_W;
track = BAR_W - filled;
Hput("<font bg="); Hput(fillcol); Hput(">");
for (i=0; i<filled; i++) Hput(" ");
Hput("</font><font bg="); Hput(trackcol); Hput(">");
for (i=0; i<track; i++) Hput(" ");
Hput("</font>\n");
}
void ExportSection(dword sect, title, icon)
{
int i, r;
dword whole, frac, barcol, trkcol;
if (!SectionRan(sect)) return; // no completed test here -> no header at all
if (sect==SECT_DISK) // show which disk was actually tested
sprintf(#etmp, "<h3><kosicon n=%d><b>%s</b> (%s): %d</h3><blockquote>\n", icon, title, disk_dir, sect_score[sect]);
else
sprintf(#etmp, "<h3><kosicon n=%d><b>%s</b>: %d</h3><blockquote>\n", icon, title, sect_score[sect]);
Hput(#etmp);
r = 0;
for (i=0; i<t_count; i++) {
if (t_sect[i]==sect) {
if (t_done[i]==2) { // not applicable here
Hfield(t_name[i], COL_NAME);
Hfield("skipped", COL_SCORE);
Hput("\n");
}
if (t_done[i]==1) {
whole = t_raw[i] / 100; frac = t_raw[i] % 100;
if (r & 1) { barcol = #c_bar2; trkcol = #c_trk2; }
else { barcol = #c_bar1; trkcol = #c_trk1; }
Hfield(t_name[i], COL_NAME);
sprintf(#etmp, "%d", t_score[i]); Hfield(#etmp, COL_SCORE);
if (frac < 10) sprintf(#etmp, "%d.0%d %s", whole, frac, t_unit[i]);
else sprintf(#etmp, "%d.%d %s", whole, frac, t_unit[i]);
Hfield(#etmp, COL_INFO);
Hbar(t_score[i], BAR_FULL, barcol, trkcol);
r++;
}
}
}
Hput("</blockquote>\n");
}
void CompareRow(dword name, mhz, score, maxscore, fillcol, trkcol)
{
char sb[20];
Hfield(name, COL_NAME);
sprintf(#sb, "%d", score); Hfield(#sb, COL_SCORE);
sprintf(#sb, "%d MHz", mhz); Hfield(#sb, COL_INFO);
Hbar(score, maxscore, fillcol, trkcol);
}
#ifdef BB_CALIB
// Ready-to-paste block: without it calibration means transcribing every
// number by hand, so nobody does it and the REF_ values stay guesses.
void ExportCalib()
{
int i;
Hput("<h3><b>Calibration</b></h3><blockquote>\n");
Hput("This machine's metrics. Put each one in the matching REF_ define to\n");
Hput("score it 1000 everywhere, then bump BB_REVISION and re-measure cpudb.\n\n");
for (i=0; i<t_count; i++) {
if (t_done[i]==1) {
Hfield(t_name[i], COL_NAME);
sprintf(#etmp, "%d", t_raw[i]); Hput(#etmp);
Hput("\n");
}
}
if (SectionRan(SECT_CPU)) {
sprintf(#etmp, "\ncpudb_add(\"CPU name here\", %d, %d, %d);\n",
sys_cpu_mhz, sect_score[SECT_CPU], BB_REVISION);
Hput(#etmp);
}
Hput("</blockquote>\n");
}
#endif
void ExportCompare()
{
int i;
dword mx, thisc;
thisc = sect_score[SECT_CPU];
Hput("<h3><kosicon n=47><b>CPU compare</b></h3><blockquote>\n");
mx = thisc;
for (i=0; i<cpudb_count; i++) if (cpudb_score[i] > mx) mx = cpudb_score[i];
if (mx < 1) mx = 1;
CompareRow("This PC", sys_cpu_mhz, thisc, mx, #c_bar1, #c_trk1);
for (i=0; i<cpudb_count; i++) {
if (i < 8) CompareRow(cpudb_name[i], cpudb_mhz[i], cpudb_score[i], mx, #c_cmp, #c_trk2);
}
Hput("</blockquote>\n");
}
// styles for Chrome/Firefox (WebView ignores <style>, uses bg= directly)
char html_head[] = "</title><style>h3{margin:0}
font{margin-bottom:1px;display:inline-block;}
font[bg='#2c7be5']{background:#2c7be5}font[bg='#4a90e2']{background:#4a90e2}
font[bg='#8fb8e8']{background:#8fb8e8}font[bg='#cdd2da']{background:#cdd2da}
font[bg='#e4e7ec']{background:#e4e7ec}
</style><body bgcolor=#ffffff><pre><h2>BirdBench results</h2>";
void ExportHTML()
{
dword sp, fname;
if (!ebuf) ebuf = malloc(EBUF_SIZE);
if (!ebuf) return;
BuildExportPath(); // need the name for <title>
sp = strrchr(#epath, '/');
fname = #epath + sp; // -> "bb_YYMMDD_HHMMSS.htm"
epos = 0;
DSBYTE[ebuf] = 0;
Hput("<html><title>"); Hput(fname);
Hput(#html_head);
Hput("<font color=#86868b>");
sprintf(#etmp, #sysinfo, sys_cpu_mhz, sys_ram_mb, screen.w, screen.h, sys_bpp);
Hput(#etmp);
sprintf(#etmp, #revinfo, BB_REVISION);
Hput(#etmp);
Hput("</font>\n\n");
ExportSection(SECT_CPU, "CPU", ICON_CPU); // each self-guards on t_done
ExportSection(SECT_GPU, "Graphics", ICON_GPU);
ExportSection(SECT_DISK, "Disk", ICON_DISK);
if (SectionRan(SECT_CPU)) ExportCompare();
#ifdef BB_CALIB
ExportCalib();
#endif
Hput("</pre></body></html>");
FileWrite(#epath, ebuf, epos);
RunProgram("/sys/network/webview", #epath);
}
//========================================================//
// the panel //
//========================================================//
void DrawDiskButton(dword x, y) // combobox trigger (Eolite style) -> opens LMENU
{
#define DISK_BW 100
#define DISK_BH 18
dword ax;
disk_drop_x = x; disk_drop_y = y;
ax = x + DISK_BW - DISK_BH; // arrow-cell left
// sunken field with a light background
DrawRectangle3D(x, y, DISK_BW-1, DISK_BH, sc.dark, sc.light);
DrawBar(x+1, y+1, DISK_BW-2, DISK_BH-1, sc.light);
WriteText(x+5, y+3, 0x90, sc.work_text, disk_dirp[disk_sel]);
// raised arrow button on the right
DrawRectangle3D(x-1, y-1, DISK_BW+1, DISK_BH+2, sc.line, sc.line);
DrawRectangle3D(ax-1, y-1, DISK_BH+1, DISK_BH+2, sc.line, sc.line);
DrawRectangle3D(ax, y, DISK_BH-1, DISK_BH, sc.light, sc.dark);
DrawBar(ax+1, y+1, DISK_BH-2, DISK_BH-1, sc.work);
WriteText(ax+6, y+6, 0x80, sc.work_text, "\x19"); // CP866 down arrow
DefineHiddenButton(x, y, DISK_BW-1, DISK_BH, BTN_DISK_DROP);
}
dword DrawSectionChecks(dword sect, title, x, y)
{
int i;
cb.disabled = 0;
cb.text = title;
cb.checked = SectionAllOn(sect);
cb.id = BTN_SECT_BASE + sect;
cb.draw(x, y);
y = y + 30; // bigger gap after the master (the first)
for (i=0; i<t_count; i++) {
if (t_sect[i]==sect) {
cb.disabled = 0;
cb.text = t_name[i];
cb.checked = t_enabled[i];
cb.id = BTN_CHECK_BASE + i;
cb.draw(x, y); // aligned with the master (no extra indent)
y = y + ROW_H;
}
}
DrawBar(x-1, HEADER_H+32, math.min(140, WIN_W-x-10), 1, sc.light); // line under the top (master) checkboxes
DrawBar(x-1, HEADER_H+33, math.min(140, WIN_W-x-10), 1, sc.dark); // line under the top (master) checkboxes
if (sect==SECT_DISK) { // dropdown under bottom checkbox
DrawDiskButton(x, y+2); y = y + 24;
}
return y;
}
void DrawPanel()
{
char line[96];
// Icons
draw_icon_32(67, 6, sc.work, 37); // CPU chip, centered over column 1
draw_icon_32(225, 6, sc.work, 27); // Graphics card, over column 2
draw_icon_32(362, 6, sc.work, 50); // Disk, over column 3
// Main Checkboxes
DrawSectionChecks(SECT_CPU, "CPU", PAD, HEADER_H + 10);
DrawSectionChecks(SECT_GPU, "Graphics", COL2_X, HEADER_H + 10);
DrawSectionChecks(SECT_DISK, "Disk", COL3_X, HEADER_H + 10);
// Buttons
FlatButton(PAD, BTN_ALL, sc.work, sc.work_text, "All");
FlatButton(PAD + BUTTON_W + 9, BTN_NONE, sc.work, sc.work_text, "None");
FlatButton(WIN_W - BUTTON_W - PAD, BTN_RUN, 0x4A90E2, 0xFFFFFF, "Run");
// Foter
DrawBar(0, STATUSBAR_Y, WIN_W, 1, sc.light);
DrawBar(0, STATUSBAR_Y+1, WIN_W, 1, sc.dark);
sprintf(#line, #sysinfo, sys_cpu_mhz, sys_ram_mb, screen.w, screen.h, sys_bpp);
WriteText(PAD, STATUSBAR_Y+7, 0x90, sc.work_text, #line);
// Disk open
}
proc_info Form; // global: menu.h reads Form.left/top for LMENU
void draw_window()
{
dword wx, wy;
sc.get();
RefreshScreen();
wx = 0; if (screen.w > WIN_W) { wx = screen.w - WIN_W; wx = wx/2; }
wy = 0; if (screen.h > WIN_H) { wy = screen.h - WIN_H; wy = wy/2; }
DefineAndDrawWindow(wx, wy, WIN_W+9, WIN_H+skin_h+4, 0x34, sc.work, "BirdBench", 0);
GetProcessInfo(#Form, SelfInfo);
DrawPanel();
}
//---------------- run flow ----------------//
char bench_caption[] = "This Bird is Benching";
void DrawRunScreen() // once per run: clear the body + draw the mascot
{
dword tx;
DrawBar(0, 0, WIN_W, STATUSBAR_Y, sc.work);
draw_icon_32(WIN_W-32/2, 96, sc.work, 121);
tx = strlen(#bench_caption)*8; // 0x90 = 8x16 font
tx = WIN_W - tx; tx = tx/2;
WriteText(tx, 140, 0x90, sc.work_text, #bench_caption);
}
void DrawRunStatus(dword name, idx, total, sect)
{
char msg[80];
dword icon;
icon = ICON_CPU;
if (sect==SECT_GPU) icon = ICON_GPU;
if (sect==SECT_DISK) icon = ICON_DISK;
DrawBar(0, 0, WIN_W, HEADER_H, sc.work); // header only
draw_icon_16w(PAD, 11, icon); // section icon, left of text
sprintf(#msg, "Running %s (%d/%d)", name, idx, total);
WriteText(PAD+24, 13, 0x90, sc.work_text, #msg);
}
// drain events queued while a test was running; returns 1 if ESC = abort
byte AbortRequested()
{
dword ev, id;
byte stop = 0;
loop() {
ev = CheckEvent();
if (!ev) break;
if (ev==evButton) {
id = GetButtonID();
if (id==1) bench_exit(); // window X pressed mid-run
}
if (ev==evKey) {
GetKeys();
if (key_scancode==SCAN_CODE_ESC) stop = 1;
}
if (ev==evReDraw) { draw_window(); DrawRunScreen(); } // keep the run look
}
return stop;
}
void RunSelected()
{
int i, idx, total;
dword s, v;
byte aborted;
total = 0;
for (i=0; i<t_count; i++) if (t_enabled[i]) total++;
if (total < 1) return;
for (s=0; s<SECT_NUM; s++) { sums[s] = 0; cnts[s] = 0; }
for (i=0; i<t_count; i++) t_done[i] = 0;
// occlusion skews fn13/fn7/fn38/fn4 (the kernel only draws visible
// parts) - keep the window on top for the duration of the run
SetWindowLayerBehaviour(-1, ZPOS_ALWAYS_TOP);
DrawRunScreen(); // the mascot, once per run
aborted = 0;
idx = 0;
for (i=0; i<t_count; i++) {
if (t_enabled[i]) {
if (AbortRequested()) { aborted = 1; break; }
idx++;
DrawRunStatus(t_name[i], idx, total, t_sect[i]);
RunOne(i);
if (t_done[i]==1) { // skipped carry no score
s = t_sect[i];
v = t_score[i]; if (v < 1) v = 1;
sums[s] = sums[s] + ilog2_16(v); // geometric mean accumulator
cnts[s] = cnts[s] + 1;
}
}
}
Disk_Cleanup(); // remove the 8 MB temp file
SetWindowLayerBehaviour(-1, ZPOS_NORMAL);
// section score = geomean of its tests (robust to single-test outliers)
for (s=0; s<SECT_NUM; s++) {
if (cnts[s] > 0) { v = sums[s] / cnts[s]; sect_score[s] = iexp2_16(v); }
else sect_score[s] = 0;
}
draw_window();
if (!aborted) ExportHTML(); // auto-open the report (ESC = silent abort)
}
//---------------- events ----------------//
void HandleButton(dword id)
{
if (id==1) bench_exit();
if (id==BTN_DISK_DROP) { // open the LMENU popup below the combobox
open_lmenu(disk_drop_x, disk_drop_y+DISK_BH+1, MENU_TOP_LEFT, disk_sel+1, #disk_menu);
disk_menu_open = 1;
return;
}
if (id==BTN_RUN) RunSelected();
if (id==BTN_ALL) { SetAll(1); draw_window(); }
if (id==BTN_NONE) { SetAll(0); draw_window(); }
if (id>=BTN_SECT_BASE) && (id<BTN_SECT_BASE+SECT_NUM) {
ToggleSection(id - BTN_SECT_BASE);
draw_window();
}
if (id>=BTN_CHECK_BASE) && (id<BTN_CHECK_BASE+MAX_TESTS) {
t_enabled[id-BTN_CHECK_BASE] ^= 1;
draw_window();
}
}
void main()
{
int i;
dword click;
GetSysInfo();
if (!BenchAllocBuffers()) {
notify("'BirdBench\nNot enough memory for the work buffers!' -E");
ExitProcess();
}
cpudb_init();
Register_CPU();
Register_GPU();
Register_DISK();
ScanDisks();
for (i=0; i<t_count; i++) t_enabled[i] = 1;
loop() switch(WaitEvent())
{
case evButton:
HandleButton(GetButtonID());
break;
case evKey:
GetKeys();
if (key_scancode==SCAN_CODE_ESC) bench_exit();
if (key_scancode==SCAN_CODE_ENTER) RunSelected();
break;
case evReDraw:
if (disk_menu_open) { // an LMENU popup was opened
click = get_menu_click();
if (click) { // user picked a disk (1-based)
disk_sel = click - 1;
disk_dir = disk_dirp[disk_sel];
}
if (!menu_process_id) disk_menu_open = 0; // popup closed
}
draw_window();
break;
}
}
+199
View File
@@ -0,0 +1,199 @@
#ifndef INCLUDE_BENCH_H
#define INCLUDE_BENCH_H
//========================================================//
// KolibriMark - benchmark engine //
// Test registry, timing, scoring, system info. //
// (No UI here - see kbench.c) //
//========================================================//
#define SECT_CPU 0
#define SECT_GPU 1
#define SECT_DISK 2
#define SECT_NUM 3
#define MAX_TESTS 48
//#define BB_CALIB // uncomment to add the calibration block to the report
#define BB_REVISION 1 // version of the tests + REF_ values, shown in the report
#define BB_SKIP 0xFFFFFFFF // test cannot run here: no score, out of the geomean
// ---- shared work buffers ----
#define GCV_X 0 // graphics canvas = window body (WIN_W/H tied to these)
#define GCV_Y 42
#define GCV_W 455 // wide enough for 3 columns of checkboxes
#define GCV_H 240
#define CPY_BYTES 2097152 // 2 MB: copy/latency working set (> retro-era caches)
// buf_a: copy source, hash input, disk-write source (content never matters)
// buf_b: copy dest, sieve + pointer-chase workspace (each test re-inits it)
dword buf_a, buf_b, buf_canvas;
// ---- parallel-array registry (modular: RegisterTest appends) ----
dword t_name [MAX_TESTS]; // -> name string
dword t_unit [MAX_TESTS]; // -> unit string, e.g. "MB/s"
dword t_fn [MAX_TESTS]; // -> test function (returns metric x100)
dword t_ref [MAX_TESTS]; // reference metric x100 that scores 1000
dword t_raw [MAX_TESTS]; // last measured metric x100
dword t_score[MAX_TESTS]; // last computed score
byte t_sect [MAX_TESTS];
byte t_done [MAX_TESTS]; // 0 = not run, 1 = has a result, 2 = skipped
int t_count = 0;
dword sect_score[SECT_NUM]; // aggregate score per section
void RegisterTest(dword sect, name, unit, ref, fn)
{
t_sect [t_count] = sect;
t_name [t_count] = name;
t_unit [t_count] = unit;
t_ref [t_count] = ref;
t_fn [t_count] = fn;
t_raw [t_count] = 0;
t_score[t_count] = 0;
t_done [t_count] = 0;
t_count++;
}
byte SectionRan(dword sect) // true if any test here produced a result
{
int i;
for (i=0; i<t_count; i++) if (t_sect[i]==sect) && (t_done[i]==1) return 1;
return 0;
}
// ---- 64-bit-safe a*b/c ----
dword muldiv(dword a, b, c)
{
if (c<1) c = 1;
EAX = a;
ECX = b;
$mul ecx
ECX = c;
$div ecx
}
// ---- integer log2 / exp2 in 16.16 fixed point ----
// Used for the geometric mean of section scores: with an arithmetic mean
// one outlier test (VRAM write spans 2 orders of magnitude between WC and
// non-WC machines) would dominate the whole section.
// log2(1+f) ~ f + 0.343*f*(1-f), error < 0.5% after the exp2 round-trip.
dword pw2tab[32]; // 2^n table (C-- has no variable shifts)
dword ilog2_16(dword s) // log2(s) in 16.16, s >= 1
{
dword e, m, x, c;
if (s < 2) return 0;
e = 0; m = s;
while (m > 1) { m = m / 2; e++; }
x = muldiv(s, 65536, pw2tab[e]);
x = x - 65536; // fractional mantissa, 16.16
c = 65536 - x;
c = muldiv(x, c, 65536);
c = muldiv(c, 22479, 65536); // 0.343 in 16.16
x = x + c;
e = e << 16;
return e + x;
}
dword iexp2_16(dword v) // 2^(v/65536), inverse of ilog2_16
{
dword e, f, c, m;
e = v >> 16;
f = v & 0xFFFF;
c = 65536 - f;
c = muldiv(f, c, 65536);
c = muldiv(c, 22479, 65536);
f = f - c;
m = 65536 + f; // 2^frac in 16.16
if (e > 30) return 0xFFFFFFFF;
return muldiv(m, pw2tab[e], 65536);
}
// ---- timing (system fn 26.9: 1/100 s counter) ----
dword bench_t0;
void BenchBegin() { bench_t0 = GetStartTime(); }
dword BenchTicks() // elapsed 1/100 s, min 1
{
dword e = GetStartTime() - bench_t0;
if (e < 1) e = 1;
return e;
}
// count = amount processed in DISPLAY units; returns units/sec x100
dword PerSecX100(dword count)
{
return muldiv(count, 10000, BenchTicks());
}
// ---- call a registered test by pointer, return its metric x100 ----
dword CallFn(dword fn)
{
ESI = fn;
$call esi
}
// ---- run one registered test ----
void RunOne(int i)
{
dword raw = CallFn(t_fn[i]);
if (raw == BB_SKIP) {
t_raw[i] = 0;
t_score[i] = 0;
t_done[i] = 2;
return;
}
t_raw[i] = raw;
t_score[i] = muldiv(raw, 1000, t_ref[i]);
t_done[i] = 1;
}
// ---- allocate shared buffers + seed with data ----
byte BenchAllocBuffers() // 0 = out of memory
{
dword i, n;
buf_a = malloc(CPY_BYTES);
buf_b = malloc(CPY_BYTES);
buf_canvas = malloc(GCV_W*GCV_H*3);
if (!buf_a) || (!buf_b) || (!buf_canvas) return 0;
i = 0;
while (i < CPY_BYTES) {
ESDWORD[buf_a+i] = i;
i += 4;
}
n = GCV_W*GCV_H; n = n*3;
for (i=0; i<n; i++) DSBYTE[buf_canvas+i] = i; // gradient BBGGRR
pw2tab[0] = 1;
for (i=1; i<32; i++) { n = pw2tab[i-1]; pw2tab[i] = n + n; }
return 1;
}
//======================= system info =======================//
dword sys_cpu_mhz;
dword sys_ram_mb;
dword sys_bpp; // current video mode bits per pixel
// re-read screen resolution + bpp (they change on a mode switch)
void RefreshScreen()
{
dword d;
EAX = 14; $int 0x40 // screen size: (w-1)<<16 + (h-1)
d = EAX;
screen.h = d & 0xFFFF; screen.h++;
screen.w = d >> 16; screen.w++;
EAX = 61; EBX = 2; $int 0x40 // fn 61.2 = bits per pixel
sys_bpp = EAX;
}
// RAM + CPU clock (fn 18.5). No CPUID/RDTSC -> fully 386-compatible (no
// "CPU required: Pentium"). The processor is just labelled "CPU" in the UI.
void GetSysInfo()
{
dword hz;
sys_ram_mb = GetTotalRAM() / 1024;
EAX = 18; EBX = 5; $int 0x40; // fn 18.5 = CPU clock rate, Hz
hz = EAX;
sys_cpu_mhz = hz / 1000000;
}
#endif
+5
View File
@@ -0,0 +1,5 @@
@del *.
@..\c--\c--.exe bbench.c
@rename *.com *.
@del warning.txt
if not exist bbench ( @pause )
+39
View File
@@ -0,0 +1,39 @@
#ifndef INCLUDE_CPUDB_H
#define INCLUDE_CPUDB_H
//========================================================//
// CPU comparison database -- EDIT ME //
// //
// One line per reference CPU in cpudb_init(), copied straight //
// from the Calibration block of a run on that machine. Entries //
// measured under another BB_REVISION are hidden. //
// The values below are placeholders - replace them. //
//========================================================//
#define CPUDB_MAX 24
dword cpudb_name [CPUDB_MAX];
dword cpudb_mhz [CPUDB_MAX];
dword cpudb_score[CPUDB_MAX];
int cpudb_count = 0;
// entries from another revision are dropped: their numbers mean something else
void cpudb_add(dword name, mhz, score, rev)
{
if (cpudb_count >= CPUDB_MAX) return;
if (rev != BB_REVISION) return;
cpudb_name [cpudb_count] = name;
cpudb_mhz [cpudb_count] = mhz;
cpudb_score[cpudb_count] = score;
cpudb_count++;
}
void cpudb_init()
{
cpudb_add("Intel Core i5-2400", 3100, 1600, 1);
cpudb_add("AMD Athlon XP 2500+", 1833, 360, 1);
cpudb_add("Intel Pentium 4", 2400, 300, 1);
cpudb_add("Intel Pentium III", 1000, 180, 1);
cpudb_add("Intel Atom N270", 1600, 420, 1);
cpudb_add("VIA C7", 1200, 150, 1);
}
#endif
+124
View File
@@ -0,0 +1,124 @@
# BirdBench
A small, modular system benchmark for KolibriOS, styled after Geekbench.
Single-threaded (KolibriOS exposes no SMP to apps). 386-compatible
(no CPUID/RDTSC — the MHz value comes from sysfn 18.5).
## Sections
* **CPU** — Integer, Floating Point, Memory Copy, Memory Latency,
Prime Sieve, Hash (FNV-1a).
Integer/Float run **4 independent chains** so superscalar CPUs (P5+) can
pair/overlap them — a strictly serial chain would only measure latency.
Float has no division on purpose: one fdiv (1540+ cycles) would dominate
the chain and the test would degenerate into an fdiv-latency meter.
Memory Copy is a **forward** `rep movsd` over 2 MB (lib `memmov` copies
backward when dst>src, which never hits the fast-string path on modern
CPUs). Memory Latency is a serial pointer chase over a 2 MB random cycle
(Sattolo shuffle) — defeats prefetch and cache, measures dependent-load
latency; `100 / (Macc/s) = ns` per access.
* **Graphics** — Fill (f13), Blit (f7), VRAM Read (f36), VRAM Write (gs:),
Lines (f38), Text (f4).
Fill/Blit = writes through the driver's blitter (may be HW-accelerated),
VRAM Read (f36) = read-back, VRAM Write (gs:) = raw CPU `rep stosd` straight
into the LFB via the GS selector — this one exposes write-combining (MTRR/PAT):
no WC and it collapses ~10-50x. The report shows the current video mode —
switch modes with the vidmode picker and re-run to compare VESA-LFB vs a
native driver vs KMS at the same resolution.
Before writing, the GS segment limit is checked with `lsl` — on banked-VGA
modes (no LFB) the test skips instead of #GP-faulting. Color counters are
masked to 24 bits: fn13 treats bit 31 as the gradient-fill flag and fn38
treats bit 24 as the inversed-line flag — unmasked they silently switch the
kernel to a different (slower) operation mid-test.
Text draws **both kernel fonts** (6×9 and 8×16) and forces **subpixel font
smoothing** for the duration (fn 48.9 saves the user's setting, fn 48.10
sets/restores it) — the heaviest glyph path, so machines are compared on
equal footing regardless of their smoothing setting.
* **Disk** — Sequential write / read (2 MB ops) + Random write / read
(4 KB blocks at fixed-seed random offsets in an 8 MB file, via fn 70
positioned I/O) + **File System** (create 10 files + 10 folders, delete
them all — metadata op/s, fn 70.2/70.9/70.8; names are pre-built so no
sprintf lands in the timed loop). Folder support is probed first: not every
writable FS driver implements fn 70.9 (exFAT has no CreateFolder at all) —
there the test degrades to files-only instead of scoring 0. Pick the target
disk in the UI — the report's Disk header shows which disk was tested. All
disk rates are MiB/s.
Temp files are deleted when the run finishes.
NB: KolibriOS apps can't drop the FS cache, so on a real disk the random
numbers reflect the FS small-op path (not raw seek/IOPS), and on the RAM
disk random ≈ sequential (no seek penalty).
The window is a **launcher**: tick the tests you want (per-test checkboxes,
per-section master checkbox, or All / None), pick a disk target, press **Run**.
Only ticked tests run; **Esc aborts** between tests. During a run the window
is forced always-on-top: the kernel only draws the visible parts of a window,
so an occluded window would inflate the Graphics scores. When a run finishes,
an HTML report is written to `/tmp0/1/bb_YYMMDD_HHMMSS.htm` (unique name) and
**auto-opened in WebView**.
The report (not the window) shows the results: per-section score, one row per
test with a colored bar (alternating shades) plus a gray/light-gray track, and
a **CPU compare** chart of your score vs the CPUs in `cpudb.h` (with a MHz
column; the compare bars line up with the test bars). Results are only
in the web page — clearer there than in the small window.
Each test reports a raw throughput (MB/s, MPix/s, ...) and a *score*
(1000 = the reference machine defined by the `REF_*` constants in the
module). The section score is the **geometric mean** of its ticked tests'
scores — a single outlier test (VRAM Write spans two orders of magnitude
between WC and non-WC machines) can't dominate the section the way it
would with an arithmetic mean.
## How to add a test
1. Open the section module (`tests_cpu.h`, `tests_gpu.h` or `tests_disk.h`).
2. Write a function returning the metric ×100 (two decimals):
```c
dword t_cpu_mytest()
{
dword c = 0;
BenchBegin();
do { /* one unit of work */ c++; } while (BenchTicks() < 100);
return PerSecX100(c); // units/sec ×100
}
```
`c` counts how many *display units* you processed (1 MB, 1 Mpixel,
1 Kline, ...). The framework turns that into a per-second rate.
3. Register it and give it a reference value:
```c
#define REF_MYTEST 5000 // 50.00 units/s -> score 1000
RegisterTest(SECT_CPU, "My Test", "u/s", REF_MYTEST, #t_cpu_mytest);
```
That's it — it shows up in the list automatically.
## How to add a comparison CPU
Edit `cpudb.h`, add a line to `cpudb_init()`:
```c
cpudb_add("Intel Core 2 Duo E8400", 3000, 520); // name, MHz, measured CPU score
```
NB: any change to the test definitions invalidates previously measured
db entries — re-measure them with the same binary you compare against.
## Build
Run `build.bat` (needs `../c--/c--.exe`). Output: `bbench`.
## Notes / calibration
* The `REF_*` constants are placeholders. Pick a machine as your baseline,
run it, and set each `REF_*` to that machine's raw metric ×100 so it
scores ~1000. Everything else scales relative to it.
* All "MB" are MiB (2^20) across CPU, VRAM Write and Disk tests.
* Random-I/O offsets and the latency chain use **fixed seeds** — every run
and every machine gets the same access pattern (reproducibility).
* On machines with a large L3 the 2 MB working set of Memory Copy/Latency
measures the cache, not RAM — unavoidable at sizes that still fit
retro-era boxes; scores stay comparable within an era.
* Needs ~6 MB RAM (see `MEMSIZE`).
+188
View File
@@ -0,0 +1,188 @@
#ifndef INCLUDE_TESTS_CPU_H
#define INCLUDE_TESTS_CPU_H
//========================================================//
// CPU test module. //
// Each test returns its metric x100 (two decimals). //
// To add a test: write t_cpu_xxx() then RegisterTest(). //
//========================================================//
// reference metric x100 that should score 1000 (calibration)
#define REF_INT 5000 // 50.00 MOps/s
#define REF_FLOAT 2000 // 20.00 MFLOP/s
#define REF_MEM 80000 // 800.00 MB/s
#define REF_LAT 1000 // 10.00 Macc/s (= 100 ns per access)
#define REF_SIEVE 3000 // 30.00 Mcell/s
#define REF_HASH 10000 // 100.00 MB/s
#define CPY_MB 2 // CPY_BYTES in whole MiB (see bbench.h)
#define LAT_N 524288 // nodes in the 2 MB pointer chain
dword cpu_sink;
float cpu_fsink;
dword lat_rnd;
// forward rep movsd block copy. NB: lib memmov() copies BACKWARD when
// dst>src (std; rep movsd), which never hits the fast-string path on
// modern CPUs and would understate them - so we do our own, forward.
inline fastcall copy_fwd(EDI, ESI, ECX) // dst, src, bytes (mult of 4)
{
asm {
CLD
SHR ECX, 2
REP MOVSD
}
}
void sieve1m(dword buf)
{
dword i, j;
EDI = buf;
EAX = 0;
ECX = 262144; // 1 MB / 4
asm {
CLD
REP STOSD
}
for (i=2; i*i<1000000; i++) {
if (DSBYTE[buf+i]==0) {
j = i*i;
while (j<1000000) { DSBYTE[buf+j]=1; j += i; }
}
}
}
dword hash1mb(dword buf)
{
dword i, h;
h = 2166136261;
for (i=0; i<1048576; i++) {
h = h ^ DSBYTE[buf+i];
h = h * 16777619;
}
return h;
}
// build one random cycle of byte-offsets over buf (Sattolo shuffle):
// following p = [buf+p] visits every node in random order - defeats
// both the prefetcher and the cache -> pure dependent-load latency.
void lat_build(dword buf)
{
dword i, j, t, ai, aj;
for (i=0; i<LAT_N; i++) { ai = i*4; ESDWORD[buf+ai] = ai; }
lat_rnd = 0x1A2B3C4D; // fixed seed: same chain on every machine
i = LAT_N;
while (i > 1) {
i = i - 1;
lat_rnd = lat_rnd * 1103515245; lat_rnd = lat_rnd + 12345;
j = lat_rnd >> 8; j = j % i;
ai = i*4; ai = ai + buf;
aj = j*4; aj = aj + buf;
t = ESDWORD[ai]; ESDWORD[ai] = ESDWORD[aj]; ESDWORD[aj] = t;
}
}
//--- Integer: 4 independent ALU chains, ~1e6 ops per unit ---
// Interleaved so superscalar CPUs (P5+) can overlap them; a strictly
// serial chain would only measure latency and hide pipelining.
dword t_cpu_int()
{
dword c=0, i, x1, x2, x3, x4;
BenchBegin();
do {
x1 = 0x12345678; x2 = 0x9E3779B9; x3 = 0x01234567; x4 = 0xABCDEF01;
for (i=0; i<50000; i++) {
x1 = x1 + i; x2 = x2 + i; x3 = x3 + i; x4 = x4 + i;
x1 = x1 ^ 0x55AA55AA; x2 = x2 ^ 0x33CC33CC; x3 = x3 ^ 0x0F0F0F0F; x4 = x4 ^ 0x5A5A5A5A;
x1 = x1 * 3; x2 = x2 * 3; x3 = x3 * 3; x4 = x4 * 3;
x1 = x1 - i; x2 = x2 - i; x3 = x3 - i; x4 = x4 - i;
x1 = x1 + x1; x2 = x2 + x2; x3 = x3 + x3; x4 = x4 + x4;
}
cpu_sink = x1; cpu_sink = cpu_sink ^ x2;
cpu_sink = cpu_sink ^ x3; cpu_sink = cpu_sink ^ x4;
c++; // 50000 * 20 = 1e6 ops
} while (BenchTicks() < 100);
return PerSecX100(c); // MOps/s
}
//--- Floating point: 4 independent mul/add chains, ~1e6 flops per unit ---
// No division: one fdiv (15-40+ cycles) would dominate the whole chain
// and the test would just measure fdiv latency.
dword t_cpu_float()
{
dword c=0, i;
float a1, a2, a3, a4, b, k;
BenchBegin();
do {
a1 = 1.0; a2 = 1.1; a3 = 1.2; a4 = 1.3;
b = 1.0000151; k = 0.9999847;
for (i=0; i<62500; i++) {
a1 = a1 * b; a2 = a2 * b; a3 = a3 * b; a4 = a4 * b;
a1 = a1 + k; a2 = a2 + k; a3 = a3 + k; a4 = a4 + k;
a1 = a1 * k; a2 = a2 * k; a3 = a3 * k; a4 = a4 * k;
a1 = a1 - k; a2 = a2 - k; a3 = a3 - k; a4 = a4 - k;
}
cpu_fsink = a1; cpu_fsink = cpu_fsink + a2;
cpu_fsink = cpu_fsink + a3; cpu_fsink = cpu_fsink + a4;
c++; // 62500 * 16 = 1e6 flops
} while (BenchTicks() < 100);
return PerSecX100(c); // MFLOP/s
}
//--- Memory bandwidth: 2 MB forward copy per unit ---
dword t_cpu_mem()
{
dword c=0;
BenchBegin();
do { copy_fwd(buf_b, buf_a, CPY_BYTES); c += CPY_MB; } while (BenchTicks() < 100);
return PerSecX100(c); // MB/s
}
//--- Memory latency: serial pointer chase over 2 MB, 1e6 hops per unit ---
dword t_cpu_latency()
{
dword c=0, k, p;
lat_build(buf_b);
BenchBegin();
do {
p = 0;
for (k=0; k<250000; k++) {
p = ESDWORD[buf_b+p];
p = ESDWORD[buf_b+p];
p = ESDWORD[buf_b+p];
p = ESDWORD[buf_b+p];
}
cpu_sink = p;
c++; // 250000 * 4 = 1e6 dependent loads
} while (BenchTicks() < 100);
return PerSecX100(c); // Macc/s (100/result = ns per access)
}
//--- Prime sieve: 1e6 cells per unit ---
dword t_cpu_sieve()
{
dword c=0;
BenchBegin();
do { sieve1m(buf_b); c++; } while (BenchTicks() < 100);
return PerSecX100(c); // Mcell/s
}
//--- Hash throughput (FNV-1a): 1 MB per unit ---
dword t_cpu_hash()
{
dword c=0;
BenchBegin();
do { cpu_sink = hash1mb(buf_a); c++; } while (BenchTicks() < 100);
return PerSecX100(c); // MB/s
}
void Register_CPU()
{
RegisterTest(SECT_CPU, "Integer", "MOps/s", REF_INT, #t_cpu_int);
RegisterTest(SECT_CPU, "Floating Point", "MFLOP/s", REF_FLOAT, #t_cpu_float);
RegisterTest(SECT_CPU, "Memory Copy", "MB/s", REF_MEM, #t_cpu_mem);
RegisterTest(SECT_CPU, "Memory Latency", "Macc/s", REF_LAT, #t_cpu_latency);
RegisterTest(SECT_CPU, "Prime Sieve", "Mcell/s", REF_SIEVE, #t_cpu_sieve);
RegisterTest(SECT_CPU, "Hash FNV-1a", "MB/s", REF_HASH, #t_cpu_hash);
}
#endif
+311
View File
@@ -0,0 +1,311 @@
#ifndef INCLUDE_TESTS_DISK_H
#define INCLUDE_TESTS_DISK_H
//========================================================//
// Disk test module. //
// Writes/reads a temp file via system fn 70. //
// Target directory = disk_dir (set from the UI disk //
// selector); falls back to the RAM disk /tmp0/1. //
// NB: sequential rewrite of one file mostly measures the //
// FS/cache throughput, not raw platter speed. //
//========================================================//
#define REF_DWRITE 5000 // 50.00 MB/s
#define REF_DREAD 5000
#define REF_RWRITE 1000 // 10.00 MB/s (random 4K is much slower)
#define REF_RREAD 1000
#define REF_FS 50000 // 500.00 op/s (create/delete metadata ops)
#define FS_N 10 // files + folders created per unit
#define DISK_CHUNK CPY_BYTES // 2 MB per op (reuses buf_a - content irrelevant)
#define DISK_MB 2
#define RND_FILE 8388608 // 8 MB test file for random ops
#define RND_BLK 4096 // 4 KB random block
#define RND_NBLK 2048 // RND_FILE / RND_BLK
dword buf_disk = 0;
dword disk_dir = 0; // -> selected target dir (set by the UI)
dword rnd_state; // LCG state for random offsets
char dk_op[32];
char dk_file[80];
dword FileWrite(dword name, data, size)
{
ESDWORD[#dk_op+0] = 2; // subfn 2 = create/rewrite
ESDWORD[#dk_op+4] = 0;
ESDWORD[#dk_op+8] = 0;
ESDWORD[#dk_op+12] = size;
ESDWORD[#dk_op+16] = data;
DSBYTE [#dk_op+20] = 0;
ESDWORD[#dk_op+21] = name;
EAX = 70;
EBX = #dk_op;
$int 0x40;
return EAX; // 0 = ok
}
dword FileRead(dword name, data, size)
{
ESDWORD[#dk_op+0] = 0; // subfn 0 = read
ESDWORD[#dk_op+4] = 0;
ESDWORD[#dk_op+8] = 0;
ESDWORD[#dk_op+12] = size;
ESDWORD[#dk_op+16] = data;
DSBYTE [#dk_op+20] = 0;
ESDWORD[#dk_op+21] = name;
EAX = 70;
EBX = #dk_op;
$int 0x40;
return EAX;
}
dword dk_delete(dword name)
{
ESDWORD[#dk_op+0] = 8; // subfn 8 = delete
ESDWORD[#dk_op+4] = 0;
ESDWORD[#dk_op+8] = 0;
ESDWORD[#dk_op+12] = 0;
ESDWORD[#dk_op+16] = 0;
DSBYTE [#dk_op+20] = 0;
ESDWORD[#dk_op+21] = name;
EAX = 70;
EBX = #dk_op;
$int 0x40;
return EAX;
}
dword dk_mkdir(dword name)
{
ESDWORD[#dk_op+0] = 9; // subfn 9 = create folder
ESDWORD[#dk_op+4] = 0;
ESDWORD[#dk_op+8] = 0;
ESDWORD[#dk_op+12] = 0;
ESDWORD[#dk_op+16] = 0;
DSBYTE [#dk_op+20] = 0;
ESDWORD[#dk_op+21] = name;
EAX = 70;
EBX = #dk_op;
$int 0x40;
return EAX;
}
dword FileWriteAt(dword name, data, size, pos) // fn70.3 = write at position
{
ESDWORD[#dk_op+0] = 3;
ESDWORD[#dk_op+4] = pos;
ESDWORD[#dk_op+8] = 0;
ESDWORD[#dk_op+12] = size;
ESDWORD[#dk_op+16] = data;
DSBYTE [#dk_op+20] = 0;
ESDWORD[#dk_op+21] = name;
EAX = 70;
EBX = #dk_op;
$int 0x40;
return EAX;
}
dword FileReadAt(dword name, data, size, pos) // fn70.0 = read at position
{
ESDWORD[#dk_op+0] = 0;
ESDWORD[#dk_op+4] = pos;
ESDWORD[#dk_op+8] = 0;
ESDWORD[#dk_op+12] = size;
ESDWORD[#dk_op+16] = data;
DSBYTE [#dk_op+20] = 0;
ESDWORD[#dk_op+21] = name;
EAX = 70;
EBX = #dk_op;
$int 0x40;
return EAX;
}
void DiskPath() // dk_file = <disk_dir>/bbtst.tmp
{
if (disk_dir) strcpy(#dk_file, disk_dir);
else strcpy(#dk_file, "/tmp0/1");
strcat(#dk_file, "/bbtst.tmp");
}
// create/fill an 8 MB file so random 4K offsets are valid and physically written
void EnsureRandFile()
{
dword pos;
FileWrite(#dk_file, buf_disk, DISK_CHUNK); // create
pos = DISK_CHUNK;
while (pos < RND_FILE) {
FileWriteAt(#dk_file, buf_disk, DISK_CHUNK, pos);
pos += DISK_CHUNK;
}
}
// remove the temp file (called by the UI when a run finishes)
void Disk_Cleanup()
{
DiskPath();
dk_delete(#dk_file);
}
dword t_disk_write()
{
dword c=0, r;
if (!buf_disk) buf_disk = buf_a;
DiskPath();
BenchBegin();
do {
r = FileWrite(#dk_file, buf_disk, DISK_CHUNK);
if (r) return BB_SKIP; // write error: no result, not a slow one
c += DISK_MB;
} while (BenchTicks() < 100);
return PerSecX100(c); // MB/s
}
dword t_disk_read()
{
dword c=0, r;
if (!buf_disk) buf_disk = buf_a;
DiskPath();
// Seq Write may be unticked - create the test file if it is missing
r = FileRead(#dk_file, buf_disk, RND_BLK);
if (r!=0) && (r!=6) FileWrite(#dk_file, buf_disk, DISK_CHUNK);
BenchBegin();
do {
r = FileRead(#dk_file, buf_disk, DISK_CHUNK);
if (r!=0) && (r!=6) return BB_SKIP; // 6 = EOF (still ok)
c += DISK_MB;
} while (BenchTicks() < 100);
return PerSecX100(c); // MB/s
}
// random 4K write at random offsets in the 8 MB file
dword t_disk_rwrite()
{
dword c=0, pos, r;
if (!buf_disk) buf_disk = buf_a;
DiskPath();
EnsureRandFile();
rnd_state = 0x12345678; // fixed seed: same offsets on every run
BenchBegin();
do {
rnd_state = rnd_state * 1103515245; rnd_state = rnd_state + 12345;
r = rnd_state >> 8; r = r % RND_NBLK;
pos = r * RND_BLK;
if (FileWriteAt(#dk_file, buf_disk, RND_BLK, pos)) return BB_SKIP;
c++;
} while (BenchTicks() < 100);
r = muldiv(c, 40000, BenchTicks()); // KiB/s x100 (c * 4 KiB)
return r / 1024; // -> MB/s x100 (MiB, same as Seq)
}
// random 4K read at random offsets (NB: hits the FS cache - can't be dropped)
dword t_disk_rread()
{
dword c=0, pos, r, res;
if (!buf_disk) buf_disk = buf_a;
DiskPath();
EnsureRandFile();
rnd_state = 0x12345678; // fixed seed: same offsets on every run
BenchBegin();
do {
rnd_state = rnd_state * 1103515245; rnd_state = rnd_state + 12345;
r = rnd_state >> 8; r = r % RND_NBLK;
pos = r * RND_BLK;
res = FileReadAt(#dk_file, buf_disk, RND_BLK, pos);
if (res!=0) && (res!=6) return BB_SKIP;
c++;
} while (BenchTicks() < 100);
r = muldiv(c, 40000, BenchTicks()); // KiB/s x100 (c * 4 KiB)
return r / 1024; // -> MB/s x100 (MiB, same as Seq)
}
//---- File system: create/delete files and folders (metadata ops) ----
char fs_fn[FS_N*64]; // pre-built file names (no sprintf in the loop)
char fs_dn[FS_N*64]; // pre-built folder names
// names via strcpy/strcat only - the same primitives DiskWritable/DiskPath
// use successfully on every disk (no sprintf/itoa in the path building).
// No trailing slash anywhere: folder path is "<dir>/bbd_0".
void fs_names_build()
{
dword i, p, dir;
char dig[2];
dir = disk_dir;
if (!dir) dir = "/tmp0/1";
dig[1] = 0;
for (i=0; i<FS_N; i++) {
dig[0] = i + '0';
p = i*64; p = p + #fs_fn;
strcpy(p, dir); strcat(p, "/bbf_"); strcat(p, #dig); strcat(p, ".tmp");
p = i*64; p = p + #fs_dn;
strcpy(p, dir); strcat(p, "/bbd_"); strcat(p, #dig);
}
}
void fs_wipe() // best-effort removal (also leftovers of a crash)
{
dword i, p;
for (i=0; i<FS_N; i++) {
p = i*64; p = p + #fs_fn; dk_delete(p);
p = i*64; p = p + #fs_dn; dk_delete(p);
}
}
// one unit = create FS_N files (512 B) + FS_N folders, then delete them all.
// BOTH phases are probed before timing and degrade independently: not every
// writable FS driver implements every fn70 op (e.g. exFAT has no
// CreateFolder at all). 0 only when neither files nor folders work.
dword t_disk_fs()
{
dword c=0, i, p, r;
byte files_ok, dirs_ok, ok;
if (!buf_disk) buf_disk = buf_a;
fs_names_build();
fs_wipe();
files_ok = 0; // probe file create+delete
r = FileWrite(#fs_fn, buf_disk, 512);
if (r==0) {
r = dk_delete(#fs_fn);
if (r==0) files_ok = 1;
}
dirs_ok = 0; // probe folder create+delete
r = dk_mkdir(#fs_dn);
if (r==0) {
r = dk_delete(#fs_dn);
if (r==0) dirs_ok = 1;
}
if (!files_ok) && (!dirs_ok) return BB_SKIP;
BenchBegin();
do {
if (files_ok) {
ok = 1;
for (i=0; i<FS_N; i++) {
p = i*64; p = p + #fs_fn;
if (FileWrite(p, buf_disk, 512)) ok = 0;
}
for (i=0; i<FS_N; i++) { p = i*64; p = p + #fs_fn; dk_delete(p); }
if (ok) c += 20; // 2 * FS_N file operations
else files_ok = 0; // driver refused mid-run - stop trying
}
if (dirs_ok) {
ok = 1;
for (i=0; i<FS_N; i++) {
p = i*64; p = p + #fs_dn;
if (dk_mkdir(p)) ok = 0;
}
for (i=0; i<FS_N; i++) { p = i*64; p = p + #fs_dn; dk_delete(p); }
if (ok) c += 20; // 2 * FS_N folder operations
else dirs_ok = 0;
}
if (!files_ok) && (!dirs_ok) break;
} while (BenchTicks() < 100);
fs_wipe(); // belt and braces: no leftovers
return PerSecX100(c); // op/s
}
void Register_DISK()
{
RegisterTest(SECT_DISK, "Seq Write", "MB/s", REF_DWRITE, #t_disk_write);
RegisterTest(SECT_DISK, "Seq Read", "MB/s", REF_DREAD, #t_disk_read);
RegisterTest(SECT_DISK, "Rand Write", "MB/s", REF_RWRITE, #t_disk_rwrite);
RegisterTest(SECT_DISK, "Rand Read", "MB/s", REF_RREAD, #t_disk_rread);
RegisterTest(SECT_DISK, "File System", "op/s", REF_FS, #t_disk_fs);
}
#endif
+204
View File
@@ -0,0 +1,204 @@
#ifndef INCLUDE_TESTS_GPU_H
#define INCLUDE_TESTS_GPU_H
//========================================================//
// Graphics (2D) test module. //
// Tests draw into the window canvas (GCV_* region). //
//========================================================//
#define REF_FILL 20000 // 200.00 MPix/s
#define REF_BLIT 10000 // 100.00 MPix/s
#define REF_READ 5000 // 50.00 MPix/s (VRAM read is slow)
#define REF_VWRITE 100000 // 1000.00 MB/s (direct LFB write, WC-dependent)
#define REF_LINE 50000 // 500.00 Kline/s
#define REF_TEXT 30000 // 300.00 Kchar/s
char gpu_text_str[] = "BirdBench benchmark 0123456789ab";
void DrawLine38(dword x1,y1,x2,y2,color)
{
EAX = 38;
EBX = x1<<16+x2;
ECX = y1<<16+y2;
EDX = color;
$int 0x40;
}
//--- Fill rate: solid bars (fn 13) ---
dword t_gpu_fill()
{
dword c=0, col=0, mp;
BenchBegin();
do {
DrawBar(GCV_X, GCV_Y, GCV_W, GCV_H, col);
col += 0x0A0C0E;
col = col & 0xFFFFFF; // bit 31 = gradient-fill flag (fn13 0x80RRGGBB)!
c++;
} while (BenchTicks() < 100);
mp = muldiv(c, GCV_W*GCV_H, 1000000); // total mega-pixels
return PerSecX100(mp); // MPix/s
}
//--- Blit rate: 24bpp image (fn 7) ---
dword t_gpu_blit()
{
dword c=0, mp;
BenchBegin();
do { PutImage(GCV_X, GCV_Y, GCV_W, GCV_H, buf_canvas); c++; }
while (BenchTicks() < 100);
mp = muldiv(c, GCV_W*GCV_H, 1000000);
return PerSecX100(mp); // MPix/s
}
//--- Line rate: 1000 lines (fn 38) per unit ---
dword t_gpu_lines()
{
dword c=0, i, col=0x3388CC;
dword x1,y1,x2,y2;
BenchBegin();
do {
for (i=0; i<1000; i++) {
x1 = i*7; x1 = x1 % GCV_W; x1 = x1 + GCV_X;
y1 = i*13; y1 = y1 % GCV_H; y1 = y1 + GCV_Y;
x2 = i*29; x2 = x2 % GCV_W; x2 = x2 + GCV_X;
y2 = i*23; y2 = y2 % GCV_H; y2 = y2 + GCV_Y;
DrawLine38(x1,y1,x2,y2,col);
col += 0x0A1420;
col = col & 0xFFFFFF; // bit 24 = inversed-line flag (fn38 0x01xxxxxx)!
}
c++; // 1000 lines = 1 Kline
} while (BenchTicks() < 100);
return PerSecX100(c); // Kline/s
}
//--- Text rate: WriteText (fn 4), both kernel fonts (6x9 + 8x16) ---
// Subpixel smoothing is forced for the duration (the heaviest glyph
// path), then the user's setting is restored.
dword t_gpu_text()
{
dword c=0, i, y, len, kc, sm;
len = strlen(#gpu_text_str);
EAX = 48; EBX = 9; $int 0x40; // SSF_GET_FONT_SMOOTH
sm = EAX;
EAX = 48; EBX = 10; ECX = 2; $int 0x40; // SSF_SET_FONT_SMOOTH: subpixel
BenchBegin();
do {
for (i=0; i<25; i++) {
y = i*11; y = y % GCV_H; y = y + GCV_Y;
WriteText(GCV_X, y, 0x80, 0x101010, #gpu_text_str); // 6x9
}
for (i=0; i<25; i++) {
y = i*17; y = y % GCV_H; y = y + GCV_Y;
WriteText(GCV_X, y, 0x90, 0x101010, #gpu_text_str); // 8x16
}
c++; // 50 * len chars
} while (BenchTicks() < 100);
EAX = 48; EBX = 10; ECX = sm; $int 0x40; // restore user's smoothing
kc = muldiv(c, 50*len, 1000); // total kilo-chars
return PerSecX100(kc); // Kchar/s
}
//--- VRAM read-back: read screen area to RAM (fn 36) ---
dword t_gpu_read()
{
dword c=0, mp, sx, sy;
proc_info p;
GetProcessInfo(#p, SelfInfo); // fn 36 uses absolute screen coords
sx = p.left; sx = sx + 5; sx = sx + GCV_X;
sy = p.top; sy = sy + skin_h; sy = sy + GCV_Y;
BenchBegin();
do {
CopyScreen(buf_canvas, sx, sy, GCV_W, GCV_H); // 24bpp -> buf
c++;
} while (BenchTicks() < 100);
mp = muldiv(c, GCV_W*GCV_H, 1000000);
return PerSecX100(mp); // MPix/s
}
//--- direct VRAM write: rep stosd into the LFB via the GS selector ---
// The kernel gives apps a writable GS whose base = LFBAddress (see kernel.asm:
// "Set base of graphic segment to linear address of LFB"). rep stosd needs
// ES:EDI, so we copy GS into ES for the duration, then restore it. No window
// clipping here - we fill only our own on-screen rectangle, scanline by scanline.
// GS segment limit via LSL (unprivileged). If the video mode has no LFB
// (banked VGA), the selector window is tiny and writing past it would #GP.
dword gs_limit()
{
asm {
XOR EAX, EAX
MOV AX, GS
LSL EAX, EAX
}
}
inline fastcall vram_fill_gs(EDI, ESI, EDX, ECX) // ofs, dwords/row, row-advance, rows
{
asm {
push es
push ebx
mov ebx, ecx // ebx = row count
mov ax, gs
mov es, ax // es -> LFB
cld
kvw1:
mov ecx, esi // dwords per scanline
mov eax, 0x00336699 // fill pattern
rep stosd
add edi, edx // -> next scanline
dec ebx
jnz kvw1
pop ebx
pop es
}
}
dword t_gpu_vramwrite()
{
dword c=0, sx, sy, cols, rows, pitch, bpp, bpp_b, off, rowbytes, rowdw, rowadv, bpf, mb, t, lim;
proc_info p;
GetProcessInfo(#p, SelfInfo);
EAX = 61; EBX = 3; $int 0x40; pitch = EAX; // bytes per scanline
EAX = 61; EBX = 2; $int 0x40; bpp = EAX; // bits per pixel
bpp_b = bpp + 7; bpp_b = bpp_b / 8;
if (bpp_b < 1) bpp_b = 4;
sx = p.left; sx = sx + 5; sx = sx + GCV_X;
sy = p.top; sy = sy + skin_h; sy = sy + GCV_Y;
// clamp to the screen - writing past the LFB would #GP (small screens!)
if (sx >= screen.w) return BB_SKIP;
if (sy >= screen.h) return BB_SKIP;
cols = GCV_W; t = sx + cols; if (t > screen.w) cols = screen.w - sx;
rows = GCV_H; t = sy + rows; if (t > screen.h) rows = screen.h - sy;
rowbytes = cols * bpp_b;
if (pitch < rowbytes) pitch = rowbytes;
// stosd moves whole dwords: trim the row to a multiple of 4 so rowadv and
// the reported byte count match what was actually written (at 24/16 bpp
// rowbytes is not divisible by 4 and every row would drift by 1-2 bytes)
rowdw = rowbytes / 4;
rowbytes = rowdw * 4;
rowadv = pitch - rowbytes;
t = sx * bpp_b;
off = sy * pitch; off = off + t;
// last byte we will touch must be inside the GS segment (no-LFB safety)
t = rows - 1; t = t * pitch; t = t + off; t = t + rowbytes; t = t - 1;
lim = gs_limit();
if (t > lim) return BB_SKIP;
BenchBegin();
do {
vram_fill_gs(off, rowdw, rowadv, rows);
c++;
} while (BenchTicks() < 100);
bpf = rowbytes * rows; // bytes per frame
mb = muldiv(c, bpf, 1048576); // total MB written (MiB, like the disk tests)
return PerSecX100(mb); // MB/s
}
void Register_GPU()
{
RegisterTest(SECT_GPU, "Fill Rate", "MPix/s", REF_FILL, #t_gpu_fill);
RegisterTest(SECT_GPU, "Blit (f7)", "MPix/s", REF_BLIT, #t_gpu_blit);
RegisterTest(SECT_GPU, "VRAM Read (f36)","MPix/s", REF_READ, #t_gpu_read);
RegisterTest(SECT_GPU, "VRAM Write (gs)","MB/s", REF_VWRITE, #t_gpu_vramwrite);
RegisterTest(SECT_GPU, "Lines (f38)", "Kline/s", REF_LINE, #t_gpu_lines);
RegisterTest(SECT_GPU, "Text (f4)", "Kchar/s", REF_TEXT, #t_gpu_text);
}
#endif