1 module dcpu16.emulator.devices.lem1802; 2 3 import dcpu16.emulator.idevice; 4 import dcpu16.emulator; 5 6 enum CHAR_SIZE_X = 4; 7 enum CHAR_SIZE_Y = 8; 8 enum X_RESOLUTION = 32; 9 enum Y_RESOLUTION = 12; 10 enum X_PIXELS = X_RESOLUTION * CHAR_SIZE_X; 11 enum Y_PIXELS = Y_RESOLUTION * CHAR_SIZE_Y; 12 enum PIXELS_NUM = X_PIXELS * Y_PIXELS; 13 static assert(PIXELS_NUM == 128 * 96); 14 15 class LEM1802 : IDevice 16 { 17 override uint id() const pure { return 0x7349f615; }; 18 override uint manufacturer() const pure { return 0x1c6c8b36; }; 19 override ushort ver() const pure { return 0x1802; }; 20 21 private const(ushort)* screen; 22 private const(ushort)* font; 23 private const(ushort)* palette; 24 private ubyte borderColor; 25 bool isBlinkingVisible = true; 26 void delegate(InterruptAction) onInterruptAction; 27 private long splashTimeRemaining; /// in hnsecs (hectoseconds) 28 29 bool isDisconnected() const { return screen is null; } 30 31 this(Computer comp) 32 { 33 screen = &comp.mem[0x8000]; // de facto standard 34 35 reset(); 36 } 37 38 override void reset() 39 { 40 font = defaultFont.ptr; 41 palette = defaultPalette.ptr; 42 borderColor = 0; 43 splashTimeRemaining = 25_000_000; // 2.5 seconds 44 } 45 46 override void handleHardwareInterrupt(Computer comp) 47 { 48 auto action = cast(InterruptAction) comp.cpu.regs.A; 49 50 with(InterruptAction) 51 with(comp) 52 with(cpu.regs) 53 switch(action) 54 { 55 case MEM_MAP_SCREEN: 56 if(B != 0) 57 screen = &mem[B]; 58 break; 59 60 case MEM_MAP_FONT: 61 font = (B == 0) ? defaultFont.ptr : &mem[B]; 62 break; 63 64 case MEM_MAP_PALETTE: 65 palette = (B == 0) ? defaultPalette.ptr : &mem[B]; 66 break; 67 68 case SET_BORDER_COLOR: 69 borderColor = B & 0xF; 70 break; 71 72 case MEM_DUMP_FONT: 73 dump(mem, font[0 .. defaultFont.length], B); 74 return; 75 76 case MEM_DUMP_PALETTE: 77 dump(mem, palette[0 .. defaultPalette.length], B); 78 return; 79 80 default: 81 break; 82 } 83 84 if(onInterruptAction !is null) 85 onInterruptAction(action); 86 } 87 88 private void dump(Memory mem, const(ushort)[] from, ushort to) pure 89 { 90 // cutt to fit into memory, if necessary 91 if(to + from.length > mem.length) 92 from.length = mem.length - to; 93 94 mem[to .. to + from.length] = from[]; 95 } 96 97 const(Symbol) getSymbol(size_t idx) const 98 { 99 assert(idx < 386); 100 assert(!isSplashDisplayed); 101 102 return Symbol(screen[idx]); 103 } 104 105 const(Symbol) getSymbol(uint x, uint y) const 106 { 107 return getSymbol(x + y * X_RESOLUTION); 108 } 109 110 alias SymbolBitmap = ushort[2]; 111 112 bool getPixel(uint x, uint y) const 113 { 114 assert(!isSplashDisplayed); 115 116 auto symbolX = x / CHAR_SIZE_X; 117 auto symbolY = y / CHAR_SIZE_Y; 118 119 auto s = getSymbol(symbolX, symbolY); 120 SymbolBitmap bitmap = getSymbolBitmap(s.character); 121 122 auto relativeX = x % CHAR_SIZE_X; 123 auto relativeY = y % CHAR_SIZE_Y; 124 125 return getPixelOfSymbol(bitmap, relativeX, relativeY); 126 } 127 128 static bool getPixelOfSymbol(SymbolBitmap symbolBitmap, uint relativeX, uint relativeY) pure 129 { 130 assert(relativeX < CHAR_SIZE_X); 131 assert(relativeY < CHAR_SIZE_Y); 132 133 union CharBitArray 134 { 135 ushort[2] for_ctor; 136 ubyte[4] ub_arr; 137 138 this(ushort[2] from) pure 139 { 140 import std.bitmanip: swapEndian; 141 142 for_ctor[0] = swapEndian(from[0]); 143 for_ctor[1] = swapEndian(from[1]); 144 } 145 } 146 147 auto bitArray = CharBitArray(symbolBitmap); 148 relativeY %= CHAR_SIZE_Y; 149 150 assert(relativeY < 8); 151 152 ubyte ul = bitArray.ub_arr[relativeX]; 153 return bt(ul, cast(ubyte) relativeY) != 0; 154 } 155 unittest 156 { 157 /// 'F' 158 ushort[2] f_img = 159 [ 160 0b11111111_00001001, 161 0b00001001_00000000 162 ]; 163 164 immutable bool[][] pending = 165 [ 166 [1,1,1,0], 167 [1,0,0,0], 168 [1,0,0,0], 169 [1,1,1,0], 170 [1,0,0,0], 171 [1,0,0,0], 172 [1,0,0,0], 173 [1,0,0,0], 174 ]; 175 176 foreach(y; 0 .. 8) 177 foreach(x; 0 .. 4) 178 assert(pending[y][x] == getPixelOfSymbol(f_img, x, y)); 179 } 180 181 SymbolBitmap getSymbolBitmap(ubyte character) pure const 182 { 183 const ushort* ptr = &font[character * 2]; 184 185 return ptr[0 .. 2]; 186 } 187 188 void forEachPixel(void delegate(ubyte x, ubyte y, PaletteColor c) dg) const 189 { 190 assert(!isSplashDisplayed); 191 192 for(ubyte y = 0; y < Y_RESOLUTION * CHAR_SIZE_Y; y++) 193 { 194 for(ubyte x = 0; x < X_RESOLUTION * CHAR_SIZE_X; x++) 195 { 196 const s = getSymbol(x / CHAR_SIZE_X, y / CHAR_SIZE_Y); 197 198 PaletteColor c; 199 200 bool visible = isBlinkingVisible | !s.blinking; 201 202 c = (visible && getPixel(x, y)) 203 ? getColor(s.foreground) 204 : getColor(s.background); 205 206 dg(x, y, c); 207 } 208 } 209 } 210 211 // TODO: make it more faster 212 RGB[PIXELS_NUM] getRgbFrame() const 213 { 214 RGB[PIXELS_NUM] ret; 215 size_t currPixel; 216 217 forEachPixel( 218 (x, y, c) 219 { 220 ret[currPixel] = c.toRGB; 221 currPixel++; 222 } 223 ); 224 225 assert(currPixel == PIXELS_NUM); 226 227 return ret; 228 } 229 230 PaletteColor getColor(ubyte paletteIdx) const 231 { 232 ushort c = isSplashDisplayed ? defaultPalette[paletteIdx] : palette[paletteIdx]; 233 234 return PaletteColor(c); 235 } 236 237 PaletteColor getBorderColor() const 238 { 239 if(isSplashDisplayed) 240 return getColor(0x7); 241 else 242 return getColor(borderColor); 243 } 244 245 /** 246 * Turn switch state of blinking symbols 247 * 248 * Recommended calling interval: 249 * The Spectrum's 'FLASH' effect: every 16 frames (50 fps), the ink and paper of all flashing bytes is swapped 250 */ 251 void switchBlink() 252 { 253 isBlinkingVisible = !isBlinkingVisible; 254 } 255 256 void splashClock(long interval) 257 { 258 if(splashTimeRemaining > 0) 259 splashTimeRemaining -= interval; 260 } 261 262 bool isSplashDisplayed() const pure 263 { 264 return splashTimeRemaining > 0; 265 } 266 267 void forEachSplashPixel(void delegate(ubyte x, ubyte y, PaletteColor c) dg) const 268 { 269 assert(isSplashDisplayed); 270 271 foreach(ubyte y; 0 .. Y_PIXELS) 272 foreach(ubyte x; 0 .. X_PIXELS) 273 { 274 enum fakeBorderWidth = 8; 275 enum fakeBorderHeight = 7; 276 PaletteColor c; 277 278 if( 279 (x >= fakeBorderWidth && x <= X_PIXELS - fakeBorderWidth) && 280 (y >= fakeBorderHeight && y <= Y_PIXELS - fakeBorderHeight) 281 ) 282 { 283 if(splashTimeRemaining > 15_000_000) 284 { 285 enum yPixels1_3 = (Y_PIXELS - fakeBorderHeight*2)/3; // 1/3 of pixels on this "zx" screen 286 auto relY = y - fakeBorderHeight; 287 288 if 289 ( 290 splashTimeRemaining < 20_800_000 && 291 (x % 4 == 0) // red line at every column 292 ) 293 { 294 if 295 ( 296 // upper dotted lines 297 (relY % 4 == 0 && splashTimeRemaining > 20_000_000) || 298 // dotted lines at middle of the screen 299 (relY % 4 == 0 && relY >= yPixels1_3 && splashTimeRemaining > 19_700_000) || 300 // solid lines at bottom 301 (relY % 2 == 0 && relY >= yPixels1_3*2 && splashTimeRemaining > 19_300_000) 302 ) 303 { 304 c = getColor(0x4); // red lines 305 } 306 else 307 { 308 c = getColor(0x0); 309 } 310 } 311 } 312 else 313 c = getCopyrightPixels(x, y); 314 } 315 else // draw fake border 316 { 317 c = getColor(0x7); 318 } 319 320 dg(x, y, c); 321 } 322 } 323 324 private PaletteColor getCopyrightPixels(byte x, byte y) const 325 { 326 enum w = 93; 327 enum h = 3; 328 enum letterWidth = 3; 329 330 // start coords 331 enum x0 = CHAR_SIZE_X * 2; 332 enum y0 = letterWidth * 29; 333 334 x -= x0; 335 y -= y0; 336 337 bool pixel; 338 339 if( 340 (x >= 0 && x < w) && 341 (y >= 0 && y < h) 342 ) 343 { 344 pixel = nyaResearchLtd[x + y * w] != 0; 345 } 346 347 return pixel 348 ? getColor(0x0) 349 : getColor(0x7); // default ZX background color 350 } 351 } 352 353 unittest 354 { 355 auto c = new Computer; 356 auto d = new LEM1802(c); 357 d.splashTimeRemaining = 0; 358 c.attachDevice = d; 359 360 361 c.mem[0x8000] = 0b0111_1110_0_0000000 + '1'; // colored nonblinking '1' 362 c.mem[0x8001] = '2'; 363 c.mem[0x8002] = '3'; 364 c.mem[0x8000 + 32] = 'a'; 365 c.mem[0x8000 + 33] = 0b0101_0110_1_0000000 + 'b'; 366 c.mem[0x8000 + 34] = 'c'; 367 368 assert(d.getSymbol(0).character == '1'); 369 assert(d.getSymbol(1).character == '2'); 370 assert(d.getSymbol(2).character == '3'); 371 assert(d.getSymbol(32).character == 'a'); 372 assert(d.getSymbol(33).character == 'b'); 373 assert(d.getSymbol(34).character == 'c'); 374 375 assert(d.getPixel(5, 4) == true); 376 assert(d.getPixel(6, 4) == false); 377 378 auto f = d.getRgbFrame; 379 380 assert(f[0] == RGB(255, 255, 85)); 381 assert(f[1] == RGB(170, 170, 170)); 382 assert(f[4] == RGB(0, 0, 0)); 383 assert(f[1028] == RGB(170, 0, 170)); 384 assert(f[1029] == RGB(170, 85, 0)); 385 } 386 387 import std.bitmanip: bitfields; 388 389 struct Symbol 390 { 391 union 392 { 393 ushort word; 394 395 mixin(bitfields!( 396 ubyte, "character", 7, 397 bool, "blinking", 1, 398 ubyte, "background", 4, 399 ubyte, "foreground", 4, 400 )); 401 } 402 } 403 404 struct PaletteColor 405 { 406 union 407 { 408 ushort word; 409 410 mixin(bitfields!( 411 ubyte, "b", 4, 412 ubyte, "g", 4, 413 ubyte, "r", 4, 414 ubyte, "", 4, 415 )); 416 } 417 418 RGB toRGB() const 419 { 420 RGB s; 421 422 s.r += r; s.r *= 17; 423 s.g += g; s.g *= 17; 424 s.b += b; s.b *= 17; 425 426 return s; 427 } 428 } 429 430 enum InterruptAction : ushort 431 { 432 MEM_MAP_SCREEN, 433 MEM_MAP_FONT, 434 MEM_MAP_PALETTE, 435 SET_BORDER_COLOR, 436 MEM_DUMP_FONT, 437 MEM_DUMP_PALETTE, 438 } 439 440 struct RGB 441 { 442 ubyte r, g, b; 443 } 444 445 private immutable ushort[256] defaultFont = 446 [ 447 0xb79e, 0x388e, 0x722c, 0x75f4, 0x19bb, 0x7f8f, 0x85f9, 0xb158, 448 0x242e, 0x2400, 0x082a, 0x0800, 0x0008, 0x0000, 0x0808, 0x0808, 449 0x00ff, 0x0000, 0x00f8, 0x0808, 0x08f8, 0x0000, 0x080f, 0x0000, 450 0x000f, 0x0808, 0x00ff, 0x0808, 0x08f8, 0x0808, 0x08ff, 0x0000, 451 0x080f, 0x0808, 0x08ff, 0x0808, 0x6633, 0x99cc, 0x9933, 0x66cc, 452 0xfef8, 0xe080, 0x7f1f, 0x0701, 0x0107, 0x1f7f, 0x80e0, 0xf8fe, 453 0x5500, 0xaa00, 0x55aa, 0x55aa, 0xffaa, 0xff55, 0x0f0f, 0x0f0f, 454 0xf0f0, 0xf0f0, 0x0000, 0xffff, 0xffff, 0x0000, 0xffff, 0xffff, 455 0x0000, 0x0000, 0x005f, 0x0000, 0x0300, 0x0300, 0x3e14, 0x3e00, 456 0x266b, 0x3200, 0x611c, 0x4300, 0x3629, 0x7650, 0x0002, 0x0100, 457 0x1c22, 0x4100, 0x4122, 0x1c00, 0x1408, 0x1400, 0x081c, 0x0800, 458 0x4020, 0x0000, 0x0808, 0x0800, 0x0040, 0x0000, 0x601c, 0x0300, 459 0x3e49, 0x3e00, 0x427f, 0x4000, 0x6259, 0x4600, 0x2249, 0x3600, 460 0x0f08, 0x7f00, 0x2745, 0x3900, 0x3e49, 0x3200, 0x6119, 0x0700, 461 0x3649, 0x3600, 0x2649, 0x3e00, 0x0024, 0x0000, 0x4024, 0x0000, 462 0x0814, 0x2200, 0x1414, 0x1400, 0x2214, 0x0800, 0x0259, 0x0600, 463 0x3e59, 0x5e00, 0x7e09, 0x7e00, 0x7f49, 0x3600, 0x3e41, 0x2200, 464 0x7f41, 0x3e00, 0x7f49, 0x4100, 0x7f09, 0x0100, 0x3e41, 0x7a00, 465 0x7f08, 0x7f00, 0x417f, 0x4100, 0x2040, 0x3f00, 0x7f08, 0x7700, 466 0x7f40, 0x4000, 0x7f06, 0x7f00, 0x7f01, 0x7e00, 0x3e41, 0x3e00, 467 0x7f09, 0x0600, 0x3e61, 0x7e00, 0x7f09, 0x7600, 0x2649, 0x3200, 468 0x017f, 0x0100, 0x3f40, 0x7f00, 0x1f60, 0x1f00, 0x7f30, 0x7f00, 469 0x7708, 0x7700, 0x0778, 0x0700, 0x7149, 0x4700, 0x007f, 0x4100, 470 0x031c, 0x6000, 0x417f, 0x0000, 0x0201, 0x0200, 0x8080, 0x8000, 471 0x0001, 0x0200, 0x2454, 0x7800, 0x7f44, 0x3800, 0x3844, 0x2800, 472 0x3844, 0x7f00, 0x3854, 0x5800, 0x087e, 0x0900, 0x4854, 0x3c00, 473 0x7f04, 0x7800, 0x047d, 0x0000, 0x2040, 0x3d00, 0x7f10, 0x6c00, 474 0x017f, 0x0000, 0x7c18, 0x7c00, 0x7c04, 0x7800, 0x3844, 0x3800, 475 0x7c14, 0x0800, 0x0814, 0x7c00, 0x7c04, 0x0800, 0x4854, 0x2400, 476 0x043e, 0x4400, 0x3c40, 0x7c00, 0x1c60, 0x1c00, 0x7c30, 0x7c00, 477 0x6c10, 0x6c00, 0x4c50, 0x3c00, 0x6454, 0x4c00, 0x0836, 0x4100, 478 0x0077, 0x0000, 0x4136, 0x0800, 0x0201, 0x0201, 0x0205, 0x0200, 479 ]; 480 481 private immutable ushort[16] defaultPalette = 482 [ 483 0x000, 0x00a, 0x0a0, 0x0aa, 484 0xa00, 0xa0a, 0xa50, 0xaaa, 485 0x555, 0x55f, 0x5f5, 0x5ff, 486 0xf55, 0xf5f, 0xff5, 0xfff 487 ]; 488 489 private immutable nyaResearchLtd = cast(immutable ubyte[]) import("splash_string.data"); 490 491 // TODO: Phobos core.bitop.bt is glitches in release versions, so I made my own 492 /// Checks bit 493 private bool bt(ubyte value, ubyte bit) pure 494 { 495 assert(bit < 8); 496 497 auto t = value >>> bit; 498 return t % 2 != 0; 499 }