DOOM's fire algo, in zig, for 256 color terminals w/no dependencies
at main 943 lines 34 kB view raw
1// TEST YOUR (TTY) MIGHT: DOOM FIRE! 2// (c) 2022, 2023, 2024, 2025 const void* 3// 4// Please see GPL3 license at bottom of source. 5// 6const builtin = @import("builtin"); 7const std = @import("std"); 8 9const allocator = std.heap.page_allocator; 10 11var g_tty_win: win32.HANDLE = undefined; 12 13/////////////////////////////////// 14// Tested on M1 osx15.3, Windows 10 on Intel + Artix Linux. 15// fast - vs code terminal 16// slow - Terminal.app 17/////////////////////////////////// 18 19// TODO 20// - How can we better offer cross platform support for multiple bit architectures? 21// Currently memory is fixed w/x64 unsigned integers to support high res displays. 22// This should probably be a comptime setting so that the integer size can be identified 23// based on platform (x64 vs x86 vs ... ). 24// - windows kernel32 *W API expect UTF16; this is very difficult as zig uses UTF8. Converting from utf8 to uf16 seems like 25// a lot of work, so for now, windows doesn't get fancy UTF8 glyphs. 26 27// Windows Notes 28// - Bugs are my own (const void*) 29// 30// - Windows Console Info contains two key pieces of information - the size, which is not the WINDOW SIZE, but the BUFFER size. 31// For rendering graphics, we do not want to use the buffer size--while good to know, it is unhelpful. Windows Console Info 32// also includes the window coordinates, assumably the coordinates of the buffer that is displayed; this means, to get the 33// potential terminal size, we have to calculate the width and height of the view buffer. I am picturing a massive canvas, 34// the buffer, and the window size is a 3x5" notecard overlay of that canvas. We don't need more much more information, 35// but it is interesting to think how scrollbars could impact the display. 36// 37// - Windows did not seem to enjoy newline characters - \n is not good. 38// 39// When the console was allowed to treat \n as a newline character, it did not appear as if the ansi cursor movement sequences 40// were respected, with some sort of infinite scroll. This may have been due to something else entirely. 41// 42// When the console was told to ignore newline characters, now ... cursor movement worked, but now ... \n was totally 43// ignored, as advertised. 44// 45// No matter. We craft our own, ANSI equivalent of new line: 46// 1. Fill the end of the line with the current color 47// 2. Go to the first character of the next line, which we do by moving the cursor down one line, then to the first character. 48// 49// IMPACT: Never use \n in a string, always use emit(nl) 50// 51// - Windows seems to want to use UTF16 to render UTF8 glyphs; while the "Ansi" WriteConsoleA function accepts 52// utf8 strings, which zig groks natively; sadly, WriteConsoleA seems to only render Ansi characters. the "Wide" 53// WriteConsoleW function accepts utf16 strings, however zig uses utf8 strings. I am unsure what code page to use 54// for ut16 outputs...is CP_UTF8 good enough? I am shelving the complexities of this problem...for now. 55// 56 57/////////////////////////////////// 58// credits / helpful articles / inspirations 59/////////////////////////////////// 60 61// doom fire - https://github.com/filipedeschamps/doom-fire-algorithm 62// - https://github.com/fabiensanglard/DoomFirePSX/blob/master/flames.html 63// color layout - https://en.wikipedia.org/wiki/ANSI_escape_code 64// ansi codes - http://xfree86.org/current/ctlseqs.html 65// str, zig - https://www.huy.rocks/everyday/01-04-2022-zig-strings-in-5-minutes 66// emit - https://zig.news/kristoff/where-is-print-in-zig-57e9 67// term sz, zig - https://github.com/jessrud/zbox/blob/master/src/prim.zig 68// osx term sz - https://github.com/sindresorhus/macos-term-size/blob/main/term-size.c 69// px char - https://github.com/cronvel/terminal-kit 70// win - https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences 71 72/////////////////////////////////// 73// zig hints 74/////////////////////////////////// 75// `try` everywhere and !<return type> 76// 77// External Windows references are 100% contained inside win32 structure. 78// 79// cross platform constants: const CONSTANT = if (builtin.os.tag == .windows ) <windows constant> else <linux/macos constant> 80// 81// cross platform functions: OS type is function suffix 82// pub fn someFuncWin() !void { ... Windows specific logic ... } 83// pub fn someFuncLinux() !void { ... Linux/MacOS specific logic... } 84// pub fn someFunc() !void { return if (buildin.os.tag == .windows) try someFuncWin() else try someFuncLinux(); } 85// 86 87/////////////////////////////////// 88// zig helpers 89/////////////////////////////////// 90 91//// consts, vars, settings 92var rand: std.Random = undefined; 93 94//// functions 95 96// seed & prep for rng 97pub fn initRNG() void { 98 //rnd setup -- seed with timestamp for sufficient entropy 99 const instant = std.time.Instant.now() catch { 100 // Fallback to a compile-time seed if time is unavailable 101 var prng = std.Random.DefaultPrng.init(0xDEADBEEF); 102 rand = prng.random(); 103 return; 104 }; 105 // Use the timestamp's nsec component as seed 106 const seed: u64 = if (builtin.os.tag == .windows) 107 instant.timestamp 108 else 109 @as(u64, @intCast(instant.timestamp.sec)) *% std.time.ns_per_s +% @as(u64, @intCast(instant.timestamp.nsec)); 110 var prng = std.Random.DefaultPrng.init(seed); 111 rand = prng.random(); 112} 113 114// print - uses libc write for cross-platform stdout writing 115pub fn emit(s: []const u8) !void { 116 if (builtin.os.tag == .windows) { 117 var sz: win32.DWORD = 0; 118 119 const rv = win32.WriteConsoleA(g_tty_win, s.ptr, @intCast(s.len), &sz, undefined); 120 if (rv == 0) { 121 return; 122 } 123 return; 124 } else { 125 // Use libc write directly since we're linking libc 126 _ = std.c.write(std.posix.STDOUT_FILENO, s.ptr, s.len); 127 return; 128 } 129} 130 131// sleep helper - takes seconds and nanoseconds 132fn sleep(sec: isize, nsec: isize) void { 133 const ts = std.c.timespec{ .sec = sec, .nsec = nsec }; 134 _ = std.c.nanosleep(&ts, null); 135} 136 137// format a string then print 138pub fn emitFmt(comptime s: []const u8, args: anytype) !void { 139 const t = try std.fmt.allocPrint(allocator, s, args); 140 defer allocator.free(t); 141 try emit(t); 142} 143 144/////////////////////////////////// 145// TTY/Terminal Helpers 146/////////////////////////////////// 147 148//// Settings 149 150// Get this value from libc. 151const TIOCGWINSZ = std.c.T.IOCGWINSZ; // ioctl flag 152 153//term size 154const TermSz = struct { height: usize, width: usize }; 155var term_sz: TermSz = .{ .height = 0, .width = 0 }; // set via initTermSz 156 157//ansi escape codes 158const esc = "\x1B"; 159const csi = esc ++ "["; 160 161const line_prior = csi ++ "1F"; //prior line 162const line_next = csi ++ "1E"; //next line 163const line_start = csi ++ "1G"; //carriage return 164const line_new = line_clear_to_eol ++ line_next ++ line_start; //new line 165const line_up = line_prior ++ line_start; //pup one 166const nl = if (builtin.os.tag == .windows) line_new else "\n"; 167 168const cursor_save = esc ++ "7"; // or csi++s 169const cursor_load = esc ++ "8"; // or csi++u 170 171const cursor_show = csi ++ "?25h"; //h=high 172const cursor_hide = csi ++ "?25l"; //l=low 173const cursor_home = csi ++ "1;1H"; //1,1 174 175const screen_reset = csi ++ "!p"; 176const screen_clear = csi ++ "2J"; 177const screen_buf_on = csi ++ "?1049h"; //h=high 178const screen_buf_off = csi ++ "?1049l"; //l=low 179 180const char_set_ascii = esc ++ "(B"; // ascii borders 181const char_set_ansi = esc ++ "(0"; // ansi aka DEC borders 182 183const line_clear_to_eol = csi ++ "0K"; 184 185const color_reset = csi ++ "0m"; 186const color_fg = "38;5;"; 187const color_bg = "48;5;"; 188 189const color_fg_def = csi ++ color_fg ++ "15m"; // white 190const color_bg_def = csi ++ color_bg ++ "0m"; // black 191const color_def = color_bg_def ++ color_fg_def; 192const color_italic = csi ++ "3m"; 193const color_not_italic = csi ++ "23m"; 194 195const term_on = screen_buf_on ++ cursor_hide ++ cursor_home ++ color_def ++ screen_clear; 196const term_off = screen_buf_off ++ cursor_show ++ nl; 197 198const sep = if (builtin.os.tag == .windows) "|" else ""; 199 200//colors 201const MAX_COLOR = 256; 202const LAST_COLOR = MAX_COLOR - 1; 203 204var fg: [MAX_COLOR][]u8 = undefined; 205var bg: [MAX_COLOR][]u8 = undefined; 206 207//// functions 208 209// cache fg/bg ansi codes 210pub fn initColor() !void { 211 var color_idx: u32 = 0; 212 while (color_idx < MAX_COLOR) : (color_idx += 1) { 213 fg[color_idx] = try std.fmt.allocPrint(allocator, "{s}38;5;{d}m", .{ csi, color_idx }); 214 bg[color_idx] = try std.fmt.allocPrint(allocator, "{s}48;5;{d}m", .{ csi, color_idx }); 215 } 216} 217 218//todo - free bg/fg color ache 219// defer freeColor(); just too lazy right now. 220 221pub fn getTermSzWin() !TermSz { 222 //Microsoft Windows Case 223 var info: win32.CONSOLE_SCREEN_BUFFER_INFO = .{ .dwSize = .{ .X = 0, .Y = 0 }, .dwCursorPosition = .{ .X = 0, .Y = 0 }, .wAttributes = 0, .srWindow = .{ .Left = 0, .Top = 0, .Right = 0, .Bottom = 0 }, .dwMaximumWindowSize = .{ .X = 0, .Y = 0 } }; 224 225 if (0 == win32.GetConsoleScreenBufferInfo(g_tty_win, &info)) switch (std.os.windows.kernel32.GetLastError()) { 226 else => |e| return std.os.windows.unexpectedError(e), 227 }; 228 229 return TermSz{ 230 .height = @intCast(info.srWindow.Bottom - info.srWindow.Top + 1), 231 .width = @intCast(info.srWindow.Right - info.srWindow.Left + 1), 232 }; 233} 234 235pub fn getTermSzLinux() !TermSz { 236 //Linux-MacOS Case 237 238 //base case - invoked from cmd line 239 const tty_nix = std.posix.STDOUT_FILENO; 240 var winsz = std.c.winsize{ .col = 0, .row = 0, .xpixel = 0, .ypixel = 0 }; 241 const rv = std.c.ioctl(tty_nix, TIOCGWINSZ, @intFromPtr(&winsz)); 242 const err = std.posix.errno(rv); 243 244 if (rv >= 0) { 245 if (winsz.row == 0 or winsz.col == 0) { // ltty IOCTL failed ie in lldb 246 //alt case - invoked from inside lldb or terminal size unavailable 247 //Return a sensible default if we can't get terminal size 248 return TermSz{ .height = 24, .width = 80 }; 249 } else { 250 return TermSz{ .height = winsz.row, .width = winsz.col }; 251 } 252 } else { 253 std.process.exit(0); 254 //TODO this is a pretty terrible way to handle issues... 255 return std.posix.unexpectedErrno(err); 256 } 257} 258 259//get terminal size 260pub fn getTermSz() !TermSz { 261 return if (builtin.os.tag == .windows) try getTermSzWin() else try getTermSzLinux(); 262} 263 264pub fn initTermSize() !void { 265 term_sz = try getTermSz(); 266} 267 268// Windows specific terminal initialization 269pub fn initTermWin() !void { 270 // credit: n-prat ( https://github.com/const-void/DOOM-fire-zig/issues/17#issuecomment-2587481269 ) 271 var consoleMode: win32.DWORD = 0; 272 273 // pull tty from std.os.windows 274 g_tty_win = std.os.windows.GetStdHandle(win32.STD_OUTPUT_HANDLE) catch return std.os.windows.unexpectedError(std.os.windows.kernel32.GetLastError()); 275 276 //get console mode 277 if (0 == win32.GetConsoleMode(g_tty_win, &consoleMode)) switch (std.os.windows.kernel32.GetLastError()) { 278 else => |e| return std.os.windows.unexpectedError(e), 279 }; 280 281 // activate ANSI power! 282 consoleMode |= win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING | win32.DISABLE_NEWLINE_AUTO_RETURN; 283 //win32.ENABLE_PROCESSED_OUTPUT does not appear to be needed 284 285 // apply updated consoleMode to console 286 if (0 == win32.SetConsoleMode(g_tty_win, consoleMode)) switch (std.os.windows.kernel32.GetLastError()) { 287 else => |e| return std.os.windows.unexpectedError(e), 288 }; 289 290 // todo - UINT WINAPI GetConsoleCP(void); https://learn.microsoft.com/en-us/windows/console/getconsolecp 291 // g_cur_consolecp = win32.GetConsoleCP(); 292 // then, in complete, restore code page. 293 294 // enable UTF8 295 if (0 == win32.SetConsoleOutputCP(win32.CP_UTF8)) switch (std.os.windows.kernel32.GetLastError()) { 296 else => |e| return std.os.windows.unexpectedError(e), 297 }; 298} 299 300pub fn initTerm() !void { 301 try initColor(); 302 303 if (builtin.os.tag == .windows) { 304 try initTermWin(); 305 } 306 307 try initTermSize(); 308 initRNG(); 309 310 try emit(term_on); 311 if (builtin.os.tag == .windows) { 312 //try emit(csi ++ "0;0r"); 313 try emit(char_set_ascii); 314 } 315} 316 317// initTerm(); defer complete(); 318pub fn complete() !void { 319 //todo -- free colors 320 //todo if win, completeWin() -> restore code page 321 try emit(term_off); 322 try emit("Complete!" ++ nl); 323} 324 325///////////////// 326// doom-fire 327///////////////// 328 329pub fn pause() !void { 330 //todo - poll / read a keystroke w/out echo, \n etc 331 332 try emit(color_reset); 333 try emit("Press return to continue..."); 334 var buffer: [1]u8 = undefined; 335 _ = std.posix.read(std.posix.STDIN_FILENO, buffer[0..]) catch undefined; 336 const b = buffer[0]; 337 338 if (b == 'q') { 339 //exit cleanly 340 try complete(); 341 std.process.exit(0); 342 } 343} 344 345/// Part I - Terminal Size Check 346/// showTermCap() needs about 120x22; if screen is too small, give user a chance to abort and try again. 347/// 348/// no biggie if they don't. 349/// 350 351// do nothing if term sz is big enough 352pub fn checkTermSz() !void { 353 const min_w = 120; 354 const min_h = 22; 355 var w_ok = true; 356 var h_ok = true; 357 358 // chk cur < min 359 if (term_sz.width < min_w) { 360 w_ok = false; 361 } 362 if (term_sz.height < min_h) { 363 h_ok = false; 364 } 365 366 if (w_ok and h_ok) { 367 return; 368 } else { 369 //screen is too small 370 371 //red text 372 try emit(fg[9]); 373 374 //check conditions 375 if (w_ok and !h_ok) { 376 try emitFmt("Screen may be too short - height is {d} and need {d}.", .{ term_sz.height, min_h }); 377 } else if (!w_ok and h_ok) { 378 try emitFmt("Screen may be too narrow - width is {d} and need {d}.", .{ term_sz.width, min_w }); 379 } else if (term_sz.width == 0) { 380 // IOCTL succesfully failed! We believe the call to retrieve terminal dimensions succeeded, 381 // however our structure is zero. 382 try emit(bg[1]); 383 try emit(fg[15]); 384 try emitFmt("Call to retreive terminal dimensions may have failed" ++ nl ++ 385 "Width is {d} (ZERO!) and we need {d}." ++ nl ++ 386 "We will allocate 0 bytes of screen buffer, resulting in immediate failure.", .{ term_sz.width, min_w }); 387 try emit(color_reset); 388 } else if (term_sz.height == 0) { 389 // IOCTL succesfully failed! We believe the call to retrieve terminal dimensions succeeded, 390 // however our structure is zero. 391 try emit(bg[1]); 392 try emit(fg[15]); 393 try emitFmt("Call to retreive terminal dimensions may have failed" ++ nl ++ 394 "Height is {d} (ZERO!) and we need {d}." ++ nl ++ 395 "We will allocate 0 bytes of screen buffer, resulting in immediate failure.", .{ term_sz.height, min_h }); 396 try emit(color_reset); 397 } else { 398 try emitFmt("Screen is too small - have {d} x {d} and need {d} x {d}", .{ term_sz.width, term_sz.height, min_w, min_h }); 399 } 400 401 try emit(nl); 402 try emit(nl); 403 404 //warn user w/white on red 405 try emit(bg[1]); 406 try emit(fg[15]); 407 try emit("There may be rendering issues on the next screen; to correct, <q><enter>, resize and try again."); 408 try emit(line_clear_to_eol); 409 try emit(color_reset); 410 try emit(nl ++ nl ++ "Continue?" ++ nl ++ nl); 411 412 //assume ok...pause will exit for us. 413 try pause(); 414 415 //clear all the warning text and keep on trucking! 416 try emit(color_reset); 417 try emit(cursor_home); 418 try emit(screen_clear); 419 } 420} 421 422/// Part II - Show terminal capabilities 423/// 424/// Since user terminals vary in capabilities, handy to have a screen that renders ACTUAL colors 425/// and exercises various terminal commands prior to DOOM fire. 426/// 427pub fn showTermSz() !void { 428 //todo - show os, os ver, zig ver 429 try emit(color_def); // this should probably be windows only 430 try emitFmt("Screen size: {d}w x {d}h", .{ term_sz.width, term_sz.height }); 431 try emit(nl); //window doesn't appear to like \n 432 try emit(nl); 433} 434 435pub fn showLabel(label: []const u8) !void { 436 try emit(color_def); 437 try emitFmt("{s}{s}:", .{ color_def, label }); 438 try emit(nl); 439} 440 441pub fn showStdColors() !void { 442 try showLabel("Standard colors"); 443 444 //first 8 colors (standard) 445 try emit(fg[15]); 446 var color_idx: u8 = 0; 447 while (color_idx < 8) : (color_idx += 1) { 448 try emit(bg[color_idx]); 449 if (color_idx == 7) { 450 try emit(fg[0]); 451 } 452 try emitFmt("{s} {d:2} ", .{ sep, color_idx }); 453 } 454 try emit(color_def); 455 try emit(nl); 456 457 //next 8 colors ("hilight") 458 try emit(fg[15]); 459 while (color_idx < 16) : (color_idx += 1) { 460 try emit(bg[color_idx]); 461 if (color_idx == 15) { 462 try emit(fg[0]); 463 } 464 try emitFmt("{s} {d:2} ", .{ sep, color_idx }); 465 } 466 try emit(color_def); 467 try emit(nl); 468 try emit(nl); 469} 470 471pub fn show216Colors() !void { 472 try showLabel("216 colors"); 473 474 var color_addendum: u8 = 0; 475 var bg_idx: u8 = 0; 476 var fg_idx: u8 = 0; 477 478 //show remaining of colors in 6 blocks of 6x6 479 480 // 6 rows of color 481 var color_shift: u8 = 0; 482 while (color_shift < 6) : (color_shift += 1) { 483 color_addendum = color_shift * 36 + 16; 484 485 // colors are pre-organized into blocks 486 var color_idx: u8 = 0; 487 while (color_idx < 36) : (color_idx += 1) { 488 bg_idx = color_idx + color_addendum; 489 490 // invert color id for readability 491 if (color_idx > 17) { 492 fg_idx = 0; 493 } else { 494 fg_idx = 15; 495 } 496 497 // display color 498 try emit(bg[bg_idx]); 499 try emit(fg[fg_idx]); 500 try emitFmt("{d:3}", .{bg_idx}); 501 } 502 try emit(color_def); 503 try emit(nl); 504 } 505 try emit(color_def); 506 try emit(nl); 507} 508 509pub fn showGrayscale() !void { 510 try showLabel("Grayscale"); 511 512 var fg_idx: u8 = 15; 513 try emit(fg[fg_idx]); 514 515 var bg_idx: u32 = 232; 516 while (bg_idx < 256) : (bg_idx += 1) { 517 if (bg_idx > 243) { 518 fg_idx = 0; 519 try emit(fg[fg_idx]); 520 } 521 522 try emit(bg[bg_idx]); 523 try emitFmt("{s}{d} ", .{ sep, bg_idx }); 524 } 525 try emit(color_def); 526 try emit(nl); 527 528 //cleanup 529 try emit(color_def); 530 try emit(nl); 531} 532 533pub fn scrollMarquee() !void { 534 //marquee - 4 lines of yellowish background 535 const bg_idx: u8 = 222; 536 const marquee_row = if (builtin.os.tag == .windows) nl else line_clear_to_eol ++ nl; 537 const marquee_bg = marquee_row ++ marquee_row ++ marquee_row ++ marquee_row; 538 539 //init marquee background 540 try emit(cursor_save); // save character position 541 try emit(bg[bg_idx]); // set yellow background 542 try emit(marquee_bg); // paint four rows 543 544 //quotes - will confirm animations are working on current terminal 545 const txt = [_][]const u8{ " Things move along so rapidly nowadays that people saying " ++ color_italic ++ "It can't be done" ++ color_not_italic ++ " are always being interrupted", " by somebody doing it. " ++ color_italic ++ "-- Puck, 1902" ++ color_not_italic, " Test your might!", " " ++ color_italic ++ "-- Mortal Kombat" ++ color_not_italic, " How much is the fish?", " " ++ color_italic ++ "-- Scooter" ++ color_not_italic }; 546 const txt_len: u8 = txt.len / 2; // print two rows at a time 547 548 //fade txt in and out 549 const fade_seq = [_]u8{ 222, 221, 220, 215, 214, 184, 178, 130, 235, 58, 16 }; 550 const fade_len: u8 = fade_seq.len; 551 552 var fade_idx: u8 = 0; 553 var txt_idx: u8 = 0; 554 555 while (txt_idx < txt_len) : (txt_idx += 1) { 556 //fade in 557 fade_idx = 0; 558 while (fade_idx < fade_len) : (fade_idx += 1) { 559 //reset to 1,1 of marquee 560 try emit(cursor_load); 561 try emit(bg[bg_idx]); 562 try emit(nl); 563 564 //print marquee txt 565 try emit(fg[fade_seq[fade_idx]]); 566 try emit(txt[txt_idx * 2]); 567 try emit(line_clear_to_eol); 568 try emit(nl); 569 try emit(txt[txt_idx * 2 + 1]); 570 try emit(line_clear_to_eol); 571 try emit(nl); 572 573 sleep(0, 10 * std.time.ns_per_ms); 574 } 575 576 //let quote chill for a second 577 sleep(1, 0); 578 579 //fade out 580 fade_idx = fade_len - 1; 581 while (fade_idx > 0) : (fade_idx -= 1) { 582 //reset to 1,1 of marquee 583 try emit(cursor_load); 584 try emit(bg[bg_idx]); 585 try emit(nl); 586 587 //print marquee txt 588 try emit(fg[fade_seq[fade_idx]]); 589 try emit(txt[txt_idx * 2]); 590 try emit(line_clear_to_eol); 591 try emit(nl); 592 try emit(txt[txt_idx * 2 + 1]); 593 try emit(line_clear_to_eol); 594 try emit(nl); 595 sleep(0, 10 * std.time.ns_per_ms); 596 } 597 try emit(nl); 598 } 599} 600 601// prove out terminal implementation by rendering colors and some simple animations 602pub fn showTermCap() !void { 603 try showTermSz(); 604 try showStdColors(); 605 try show216Colors(); 606 try showGrayscale(); 607 try scrollMarquee(); 608 609 try pause(); 610} 611 612/// DOOM Fire 613/// Slowest - raw emit() 614/// Slower - raw emit() + \n 615/// Below - moderately faster 616 617//pixel character 618const px = ""; 619 620//bs = buffer string 621var bs: []u8 = undefined; 622var bs_idx: u64 = 0; 623var bs_len: u64 = 0; 624var bs_sz_min: u64 = 0; 625var bs_sz_max: u64 = 0; 626var bs_sz_avg: u64 = 0; 627var bs_frame_tic: u64 = 0; 628var timer: std.time.Timer = undefined; 629var t_dur: f64 = 0.0; 630var fps: f64 = 0.0; 631 632pub fn initBuf() !void { 633 //some lazy guesswork to make sure we have enough of a buffer to render DOOM fire. 634 const px_char_sz = px.len; 635 const px_color_sz = bg[LAST_COLOR].len + fg[LAST_COLOR].len; 636 const px_sz = px_color_sz + px_char_sz; 637 const screen_sz: u64 = @as(u64, px_sz * term_sz.width * term_sz.width); 638 const overflow_sz: u64 = px_char_sz * 100; 639 const bs_sz: u64 = screen_sz + overflow_sz; 640 641 bs = try allocator.alloc(u8, bs_sz * 2); 642 timer = try std.time.Timer.start(); 643 resetBuf(); 644} 645 646//reset buffer indexes to start of buffer 647pub fn resetBuf() void { 648 bs_idx = 0; 649 bs_len = 0; 650} 651 652//copy input string to buffer string 653pub fn drawBuf(s: []const u8) void { 654 for (s) |b| { 655 bs[bs_idx] = b; 656 bs_idx += 1; 657 bs_len += 1; 658 } 659} 660 661//print buffer to string...can be a decent amount of text! 662pub fn paintBuf() !void { 663 try emit(bs[0 .. bs_len - 1]); 664 const elapsed_ns = timer.read(); 665 bs_frame_tic += 1; 666 if (bs_sz_min == 0) { 667 //first frame 668 bs_sz_min = bs_len; 669 bs_sz_max = bs_len; 670 bs_sz_avg = bs_len; 671 } else { 672 if (bs_len < bs_sz_min) { 673 bs_sz_min = bs_len; 674 } 675 if (bs_len > bs_sz_max) { 676 bs_sz_max = bs_len; 677 } 678 bs_sz_avg = bs_sz_avg * (bs_frame_tic - 1) / bs_frame_tic + bs_len / bs_frame_tic; 679 } 680 681 t_dur = @as(f64, @floatFromInt(elapsed_ns)) / @as(f64, std.time.ns_per_s); 682 fps = @as(f64, @floatFromInt(bs_frame_tic)) / t_dur; 683 684 try emit(fg[0]); 685 try emitFmt("mem: {d} min / {d} avg / {d} max [ {d:.2} fps ]", .{ bs_sz_min, bs_sz_avg, bs_sz_max, fps }); 686} 687 688// initBuf(); defer freeBuf(); 689pub fn freeBuf() void { 690 allocator.free(bs); 691} 692 693pub fn showDoomFire() !void { 694 //term size => fire size 695 const FIRE_H: u64 = @as(u64, @intCast(term_sz.height)) * 2; 696 const FIRE_W: u64 = @as(u64, @intCast(term_sz.width)); 697 const FIRE_SZ: u64 = FIRE_H * FIRE_W; 698 const FIRE_LAST_ROW: u64 = (FIRE_H - 1) * FIRE_W; 699 700 //colors - tinker w/palette as needed! 701 const fire_palette = [_]u8{ 0, 233, 234, 52, 53, 88, 89, 94, 95, 96, 130, 131, 132, 133, 172, 214, 215, 220, 220, 221, 3, 226, 227, 230, 195, 230 }; 702 const fire_black: u8 = 0; 703 const fire_white: u8 = fire_palette.len - 1; 704 705 //screen buf default color is black 706 var screen_buf: []u8 = undefined; //{fire_black}**FIRE_SZ; 707 screen_buf = try allocator.alloc(u8, FIRE_SZ); 708 defer allocator.free(screen_buf); 709 710 //init buffer 711 var buf_idx: u64 = 0; 712 while (buf_idx < FIRE_SZ) : (buf_idx += 1) { 713 screen_buf[buf_idx] = fire_black; 714 } 715 716 //last row is white...white is "fire source" 717 buf_idx = 0; 718 while (buf_idx < FIRE_W) : (buf_idx += 1) { 719 screen_buf[FIRE_LAST_ROW + buf_idx] = fire_white; 720 } 721 722 //reset terminal 723 try emit(cursor_home); 724 try emit(color_reset); 725 try emit(color_def); 726 try emit(screen_clear); 727 728 //scope cache //////////////////// 729 //scope cache - update fire buf 730 var doFire_x: u64 = 0; 731 var doFire_y: u64 = 0; 732 var doFire_idx: u64 = 0; 733 734 //scope cache - spread fire 735 var spread_px: u8 = 0; 736 var spread_rnd_idx: u8 = 0; 737 var spread_dst: u64 = 0; 738 739 //scope cache - frame reset 740 const init_frame = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ cursor_home, bg[0], fg[0] }); 741 defer allocator.free(init_frame); 742 743 //scope cache - fire 2 screen buffer 744 var frame_x: u64 = 0; 745 var frame_y: u64 = 0; 746 var px_hi: u8 = fire_black; 747 var px_lo: u8 = fire_black; 748 var px_prev_hi = px_hi; 749 var px_prev_lo = px_lo; 750 751 //get to work! 752 try initBuf(); 753 defer freeBuf(); 754 755 //when there is an ez way to poll for key stroke...do that. for now, ctrl+c! 756 const ok = true; 757 while (ok) { 758 759 //update fire buf 760 doFire_x = 0; 761 while (doFire_x < FIRE_W) : (doFire_x = doFire_x + 1) { 762 doFire_y = 0; 763 while (doFire_y < FIRE_H) : (doFire_y += 1) { 764 doFire_idx = doFire_y * FIRE_W + doFire_x; 765 766 //spread fire 767 spread_px = screen_buf[doFire_idx]; 768 769 //bounds checking 770 if ((spread_px == 0) and (doFire_idx >= FIRE_W)) { 771 screen_buf[doFire_idx - FIRE_W] = 0; 772 } else { 773 spread_rnd_idx = rand.intRangeAtMost(u8, 0, 3); 774 if (doFire_idx >= (spread_rnd_idx + 1)) { 775 spread_dst = doFire_idx - spread_rnd_idx + 1; 776 } else { 777 spread_dst = doFire_idx; 778 } 779 if (spread_dst >= FIRE_W) { 780 if (spread_px > (spread_rnd_idx & 1)) { 781 screen_buf[spread_dst - FIRE_W] = spread_px - (spread_rnd_idx & 1); 782 } else { 783 screen_buf[spread_dst - FIRE_W] = 0; 784 } 785 } 786 } 787 } 788 } 789 790 //paint fire buf 791 resetBuf(); 792 drawBuf(init_frame); 793 794 // for each row 795 frame_y = 0; 796 while (frame_y < FIRE_H) : (frame_y += 2) { // 'paint' two rows at a time because of half height char 797 // for each col 798 frame_x = 0; 799 while (frame_x < FIRE_W) : (frame_x += 1) { 800 //each character rendered is actually to rows of 'pixels' 801 // - "hi" (current px row => fg char) 802 // - "low" (next row => bg color) 803 px_hi = screen_buf[frame_y * FIRE_W + frame_x]; 804 px_lo = screen_buf[(frame_y + 1) * FIRE_W + frame_x]; 805 806 // only *update* color if prior color is actually diff 807 if (px_lo != px_prev_lo) { 808 drawBuf(bg[fire_palette[px_lo]]); 809 } 810 if (px_hi != px_prev_hi) { 811 drawBuf(fg[fire_palette[px_hi]]); 812 } 813 drawBuf(px); 814 815 //cache current colors 816 px_prev_hi = px_hi; 817 px_prev_lo = px_lo; 818 } 819 drawBuf(nl); //is this needed? 820 } 821 try paintBuf(); 822 resetBuf(); 823 } 824} 825 826/////////////////////////////////// 827// main 828/////////////////////////////////// 829 830pub fn main() anyerror!void { 831 try initTerm(); 832 defer complete() catch {}; 833 834 try checkTermSz(); 835 try showTermCap(); 836 try showDoomFire(); 837} 838 839// -- NOTE -- Keep the below aligned w/Microsoft Windows (MS WIN) specification 840// Changes to the below that are unaligned will cause panics or strange bugs 841 842const win32 = struct { 843 // -------------- 844 // define MS WIN base types in zig terms 845 // part of std.os.windows 846 pub const WORD: type = std.os.windows.WORD; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/f8573df3-a44a-4a50-b070-ac4c3aa78e3c 847 pub const BOOL: type = std.os.windows.BOOL; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/9d81be47-232e-42cf-8f0d-7a3b29bf2eb2 848 pub const HANDLE: type = std.os.windows.HANDLE; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/929187f0-f25c-4b05-9497-16b066d8a912 849 pub const SHORT: type = std.os.windows.SHORT; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/47b1e7d6-b5a1-48c3-986e-b5e5eb3f06d2 850 pub const DWORD: type = std.os.windows.DWORD; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/262627d8-3418-4627-9218-4ffe110850b2 851 pub const UINT: type = std.os.windows.UINT; // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/52ddd4c3-55b9-4e03-8287-5392aac0627f 852 pub const LPVOID: type = std.os.windows.LPVOID; 853 //void https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c0b7741b-f577-4eed-aff3-2e909df10a4d 854 855 // 856 // -------------- 857 // define MS WIN constants in terms of MS WIN base types 858 // 859 // screen buffer output constants c/o https://learn.microsoft.com/en-us/windows/console/setconsolemode 860 pub const ENABLE_PROCESSED_OUTPUT: DWORD = 0x0001; // USED 861 pub const ENABLE_WRAP_AT_EOL_OUTPUT: DWORD = 0x0002; 862 pub const ENABLE_VIRTUAL_TERMINAL_PROCESSING: DWORD = 0x0004; // USED 863 pub const DISABLE_NEWLINE_AUTO_RETURN: DWORD = 0x0008; 864 pub const ENABLE_LVB_GRID_WORLDWIDE: DWORD = 0x0010; 865 866 // https://learn.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page 867 pub const CP_UTF8: UINT = 65001; 868 pub const CP_ANSI: UINT = 1252; 869 870 // https://learn.microsoft.com/en-us/windows/console/getstdhandle 871 // part of std.os.windows 872 // for use with https://ziglang.org/documentation/master/std/#std.os.windows.GetStdHandle 873 pub const STD_INPUT_HANDLE = std.os.windows.STD_INPUT_HANDLE; 874 pub const STD_OUTPUT_HANDLE = std.os.windows.STD_OUTPUT_HANDLE; 875 pub const STD_ERR_HANDLE = std.os.windows.STD_ERROR_HANDLE; 876 877 // -------------- 878 // define MS WIN structures in terms of MS WIN base types 879 // CORD = https://learn.microsoft.com/en-us/windows/console/coord-str 880 pub const COORD = extern struct { 881 X: SHORT, 882 Y: SHORT, 883 }; 884 885 //SMALL_RECT = https://learn.microsoft.com/en-us/windows/console/small-rect-str 886 pub const SMALL_RECT = extern struct { 887 Left: SHORT, 888 Top: SHORT, 889 Right: SHORT, 890 Bottom: SHORT, 891 }; 892 893 // CONSOLE_SCREEN_BUFFER_INFO = https://learn.microsoft.com/en-us/windows/console/console-screen-buffer-info-str 894 pub const CONSOLE_SCREEN_BUFFER_INFO = extern struct { 895 dwSize: COORD, //For terminal init, do not use - buffer size -- not the window size 896 dwCursorPosition: COORD, 897 wAttributes: WORD, 898 srWindow: SMALL_RECT, //For terminal init, use - window 899 dwMaximumWindowSize: COORD, 900 }; 901 902 // ----------------- 903 // define MS WIN console API in terms of MS WIN base types and MS WIN structures 904 // reference: https://learn.microsoft.com/en-us/windows/console/console-functions 905 906 // https://learn.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo 907 pub extern "kernel32" fn GetConsoleScreenBufferInfo( 908 hConsoleOutput: ?HANDLE, 909 lpConsoleScreenBufferInfo: ?*CONSOLE_SCREEN_BUFFER_INFO, 910 ) callconv(std.os.windows.WINAPI) BOOL; 911 912 // https://learn.microsoft.com/en-us/windows/console/getconsolemode 913 pub extern "kernel32" fn GetConsoleMode(hConsoleOutput: ?HANDLE, lpOutMode: ?*DWORD) callconv(std.os.windows.WINAPI) BOOL; 914 915 // https://learn.microsoft.com/en-us/windows/console/setconsolemode 916 pub extern "kernel32" fn SetConsoleMode(in_hConsoleHandle: HANDLE, in_Mode: DWORD) callconv(std.os.windows.WINAPI) BOOL; 917 918 // https://learn.microsoft.com/en-us/windows/console/setconsoleoutputcp 919 pub extern "kernel32" fn SetConsoleOutputCP(in_wCodePageID: UINT) callconv(std.os.windows.WINAPI) BOOL; 920 921 // https://learn.microsoft.com/en-us/windows/console/writeconsole 922 // W = Wide = UTF8 923 pub extern "kernel32" fn WriteConsoleW(in_hConsoleOutput: HANDLE, in_lpBuffer: [*]const u8, in_nNumberOfCharsToWrite: DWORD, out_lpNumberOfCharsWritten: ?*DWORD, lpReserved: *void) callconv(std.os.windows.WINAPI) BOOL; 924 925 // https://learn.microsoft.com/en-us/windows/console/writeconsole 926 // A = ASCII 927 pub extern "kernel32" fn WriteConsoleA(in_hConsoleOutput: HANDLE, in_lpBuffer: [*]const u8, in_nNumberOfCharsToWrite: DWORD, out_lpNumberOfCharsWritten: ?*DWORD, lpReserved: *void) callconv(std.os.windows.WINAPI) BOOL; 928}; 929 930//LICENSE -- 931//DOOM-fire-zig is free software: you can redistribute it and/or modify it under the terms of 932//the GNU General Public License as published by the Free Software Foundation, either 933//version 3 of the License, or (at your option) any later version. 934// 935//This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 936//without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 937// 938//See the GNU General Public License for more details. 939// 940//You should have received a copy of the GNU General Public License along with this program. 941// 942//If not, see https://www.gnu.org/licenses/. 943//---------