├── .gitignore ├── README.md ├── code ├── build.bat ├── q_client.cpp ├── q_client.h ├── q_common.cpp ├── q_common.h ├── q_game.cpp ├── q_input.cpp ├── q_input.h ├── q_lightmap.cpp ├── q_lightmap.h ├── q_math.h ├── q_model.cpp ├── q_model.h ├── q_platform.h ├── q_render.cpp ├── q_render.h ├── q_sky.cpp ├── sys_win32.cpp ├── sys_win32.h ├── vcenv.bat └── vs.bat ├── docs ├── cache_alloc_0.PNG ├── memory_management.md.html ├── pak_file.md.html ├── pixel_and_gamma.md.html ├── quake_example_00.PNG ├── quake_example_01.PNG ├── quake_example_02.PNG └── quake_example_03.PNG └── tests ├── build.bat └── tests.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *~ 3 | 4 | /assets/ 5 | /build/ 6 | 7 | /tests/* 8 | !/tests/*.h 9 | !/tests/*.cpp 10 | !/tests/*.bat 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Quake Zero is a project out of curiosity about how 3D renderer was "hande made" 2 | in early days. To fully understand what's actually going on under the hood, I 3 | built a world renderer for Quake 1. Here is the result. 4 | 5 | Thank you to [Casey Muratori](https://handmadehero.org/) and 6 | [Philip Buuck](http://philipbuuck.com/handmadequake) whose "handmade" projects 7 | inspired me to start this project, and James Gregory for putting together the 8 | ebook version of Michael Abrash's 9 | [Graphics Programming Black Book](https://github.com/jagregory/abrash-black-book) 10 | which is the main resource for studying Quake 1 rendering code. 11 | 12 | 13 | ## To Build 14 | 15 | - Create a folder with the name "assets" under the project root directory, and 16 | put the PAK file of the original Quake 1 in it. 17 | 18 | - Set up Visual C++ environment by typing the following line into your command 19 | prompt. 20 | 21 | "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64 22 | 23 | If you intalled the Visual Studio somewhere else, you need to navigate there 24 | and run "vcvarsall.bat amd64". It has to be 64-bit environment otherwise the 25 | unit tests will fail. 26 | 27 | - In command prompt, go into folder "code", run "build.bat". You might get 28 | various warnings if you are not using Visual Studio Community 2015. 29 | 30 | - Then run "vs.bat" to debug. 31 | 32 | ## Tests 33 | 34 | If you run buil.bat under the "tests" folder, and run tests.exe, you can see, 35 | after unit tests finish, a demo program illustrating how Quake's Cache Allocator 36 | works. 37 | 38 | ## Demo 39 | 40 | [Youtube Video](https://youtu.be/Y_GQc0QeKPU) 41 | 42 | ![quake example 1](docs/quake_example_00.PNG) 43 | ![quake example 2](docs/quake_example_01.PNG) 44 | ![quake example 3](docs/quake_example_02.PNG) 45 | ![quake example 4](docs/quake_example_03.PNG) 46 | -------------------------------------------------------------------------------- /code/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem -MTd : instead of using dynamic library, build static library into our executable, d for debug 4 | rem -nologo : remove MSVC compiler about infomation 5 | rem -W4 : enable the layer 4 warning 6 | rem -wd4201 : disable warning nameless struct/union 7 | rem -wd4100 : disable warning unreferenced formal parameter 8 | rem -wd4189 : disable warning local variable is initialized but not referenced 9 | rem -wd4505 : disable waring about unreferenced local functions 10 | rem -WX : treats all compiler warnings as errors 11 | rem -Gm- : disable minimal rebuild. We use unity build, entire code base will be rebuild for every change. 12 | rem -GR- : disable c++ run-time type information 13 | rem -Od : disable all compiler optimization 14 | rem -Oi : replace function calls with intrinsic whenever possible 15 | rem -Z7 : pobably works better with multi-core processer than -Zi 16 | rem -EHa- : return off exception handling 17 | 18 | set CommonCompilerFlags=-MTd -nologo -Gm- -GR- -EHa- -Od -Oi -WX -W4 -wd4100 -wd4201 -wd4189 -wd4505 ^ 19 | -DQUAKEREMAKE_INTERNAL=1 -DQUAKEREMAKE_SLOW=1 -DQUAKEREMAKE_WIN32=1 -FC -Z7 20 | 21 | rem -opt:ref : something about including minimal libs. Handmade Hero Day 016(45:08) 22 | set CommonLinkerFlags= -incremental:no -opt:ref user32.lib gdi32.lib winmm.lib 23 | 24 | set DateTime=%date:~4,2%%date:~7,2%%date:~12,2%%time:~0,2%%time:~3,2%%time:~6,2% 25 | 26 | IF NOT EXIST ..\build mkdir ..\build 27 | pushd ..\build 28 | 29 | rem -subsystem:windows : or -subsystem:console 30 | rem 32-bit build 31 | rem cl %CommonCompilerFlags% ..\code\sys_win32.cpp /link -subsystem:windows,5.1 %CommonLinkerFlags% 32 | 33 | if exist *.pdb del *.pdb 34 | 35 | rem DLL will be loaded immediately after it's been generated at which time pdb 36 | rem file has probably not been generated. We don't load new dll until lock.tmp 37 | rem has been deleted 38 | echo WAITING FOR PDB > lock.tmp 39 | 40 | rem 64-bit build 41 | 42 | rem compile to DLL 43 | cl %CommonCompilerFlags% ..\code\q_game.cpp -Fmq_game.map -LD /link -incremental:no -opt:ref -PDB:q_%Random%.pdb -EXPORT:GameInit -EXPORT:GameUpdateAndRender 44 | del lock.tmp 45 | 46 | cl %CommonCompilerFlags% ..\code\sys_win32.cpp -Fmsys_win32.map /link %CommonLinkerFlags% 47 | 48 | popd 49 | -------------------------------------------------------------------------------- /code/q_client.cpp: -------------------------------------------------------------------------------- 1 | #include "q_client.h" 2 | 3 | 4 | -------------------------------------------------------------------------------- /code/q_client.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "q_model.h" 4 | 5 | // per-level limits 6 | #define MAX_MODELS 256 7 | #define MAX_SOUNDS 256 8 | 9 | struct ClientState 10 | { 11 | Model *precachedModels[MAX_MODELS]; 12 | }; 13 | -------------------------------------------------------------------------------- /code/q_common.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "q_platform.h" 4 | #include "q_common.h" 5 | 6 | //================================= 7 | // String related operations 8 | //================================= 9 | 10 | /* 11 | * str is null terminated string 12 | */ 13 | int StringLength(const char *str) 14 | { 15 | int length = 0; 16 | 17 | while (*str != '\0') 18 | { 19 | length++; 20 | str++; 21 | } 22 | return length; 23 | } 24 | 25 | /* 26 | * dest: points to the memory that will store the string, and will always be 27 | * appended a char of '\0' 28 | * destSize: is the total memory size in byte, including '\0' 29 | * src: is null terminated string 30 | * count: is the amount of characters to copy, not including '\0' 31 | * if count is zero, try to copy all characters from src 32 | * return: The amount of characters copied, not including '\0'. The actual 33 | * amount of characters copied might not be "count", if '\0' * is met 34 | * ealier or destSize - 1 is smaller than count. Otherwise return the 35 | * amount of characters copied 36 | */ 37 | int StringCopy(char *dest, int destSize, const char *src, int count = 0) 38 | { 39 | ASSERT(destSize > 0); 40 | ASSERT(count >= 0); 41 | 42 | int amount = 0; 43 | 44 | if (count == 0) 45 | { 46 | count = destSize - 1; 47 | } 48 | 49 | for (;;) 50 | { 51 | if (destSize == 1 || amount == count) 52 | { 53 | *dest = '\0'; 54 | return amount; 55 | } 56 | 57 | *dest = *src; 58 | amount++; 59 | 60 | if (*dest == '\0') 61 | { 62 | return amount - 1; 63 | } 64 | 65 | destSize--; 66 | 67 | dest++; 68 | src++; 69 | } 70 | } 71 | 72 | /* 73 | * lhs, rhs: are null terminated strings 74 | * count: number of characters to compare. 75 | * return: 0 is strings are equal 76 | */ 77 | int StringNCompare(const char *lhs, const char *rhs, int count) 78 | { 79 | ASSERT(count >= 0); 80 | 81 | for (int i = 0; i < count; ++i) 82 | { 83 | if (lhs[i] != rhs[i]) 84 | { 85 | return lhs[i] - rhs[i]; 86 | } 87 | else if (lhs[i] == '\0') 88 | { 89 | return 0; 90 | } 91 | } 92 | 93 | return 0; 94 | } 95 | 96 | int StringCompare(const char *lhs, const char *rhs) 97 | { 98 | for (;;) 99 | { 100 | if(*lhs != *rhs) 101 | { 102 | return -1; 103 | } 104 | else if (*lhs == '\0') 105 | { 106 | return 0; 107 | } 108 | 109 | lhs++; 110 | rhs++; 111 | } 112 | } 113 | 114 | void CatString(char *src0, int src0Count, 115 | char *src1, int src1Count, 116 | char *dest, int destCount) 117 | { 118 | int destIndex = 0; 119 | 120 | for (int i = 0; i < src0Count; ++i) 121 | { 122 | if (destIndex == destCount - 1 || src0[i] == '\0') 123 | { 124 | dest[destIndex] = '\0'; 125 | return ; 126 | } 127 | else 128 | { 129 | dest[destIndex++] = src0[i]; 130 | } 131 | } 132 | 133 | for (int i = 0; i < src1Count; ++i) 134 | { 135 | if (destIndex == destCount - 1 || src1[i] == '\0') 136 | { 137 | dest[destIndex] = '\0'; 138 | return ; 139 | } 140 | else 141 | { 142 | dest[destIndex++] = src1[i]; 143 | } 144 | } 145 | 146 | dest[destIndex] = '\0'; 147 | } 148 | 149 | /* 150 | * str is null terminated string 151 | */ 152 | void IntToString(int number, char *str, int size) 153 | { 154 | int sign = 1; 155 | int index = 0; 156 | 157 | if (number < 0) 158 | { 159 | sign = -1; 160 | str[index++] = '-'; 161 | number *= -1; 162 | } 163 | 164 | int digit; 165 | 166 | for (;;) 167 | { 168 | digit = number % 10; 169 | str[index++] = (char)digit + '0'; 170 | number /= 10; 171 | 172 | if (index >= size - 1 || number == 0) 173 | { 174 | str[index] = '\0'; 175 | break ; 176 | } 177 | } 178 | 179 | // rotate digits 180 | int start_i = sign == 1 ? 0 : 1; 181 | int end_i = index - 1; 182 | while (start_i < end_i) 183 | { 184 | char tmp = str[start_i]; 185 | str[start_i] = str[end_i]; 186 | str[end_i] = tmp; 187 | 188 | start_i++; 189 | end_i--; 190 | } 191 | } 192 | 193 | /* 194 | * str is null terminated string 195 | * only handle decimal number for the moment 196 | */ 197 | int StringToInt(char *str) 198 | { 199 | int sign = 1; 200 | int number = 0; 201 | int index = 0; 202 | 203 | char c = str[index]; 204 | if (c == '-') 205 | { 206 | sign = -1; 207 | index++; 208 | } 209 | else if (c == '+') 210 | { 211 | index++; 212 | } 213 | 214 | for (;;) 215 | { 216 | c = str[index++]; 217 | if (c == '0' && number == 0) 218 | { 219 | continue; 220 | } 221 | else if (c >= '0' && c <= '9') 222 | { 223 | number *= 10; 224 | number += c - '0'; 225 | } 226 | else 227 | { 228 | break ; 229 | } 230 | } 231 | 232 | number *= sign; 233 | 234 | return number; 235 | } 236 | 237 | //========================== 238 | // Memory Operations 239 | //========================== 240 | 241 | void MemSet(void *dest, U8 value, int count) 242 | { 243 | // if dest is 4-byte aligned and count is multiple of 4 244 | if ((((size_t)dest | count) & 3) == 0) 245 | { 246 | int fill = value; 247 | fill = fill | (fill << 8) | (fill << 16) | (fill << 24); 248 | count = count >> 2; // divided by 4 249 | for (int i = 0; i < count; ++i) 250 | { 251 | ((U32 *)dest)[i] = fill; 252 | } 253 | } 254 | else 255 | { 256 | for (int i = 0; i < count; ++i) 257 | { 258 | ((U8 *)dest)[i] = value; 259 | } 260 | } 261 | } 262 | 263 | void MemCpy(void *dest, void *src, int count) 264 | { 265 | // if both dest and src are 4-byte aligned and count if multiple of 4 266 | if ((((size_t)dest | (size_t)src | count) & 3) == 0) 267 | { 268 | count = count >> 2; 269 | for (int i = 0; i < count; ++i) 270 | { 271 | ((U32 *)dest)[i] = ((U32 *)src)[i]; 272 | } 273 | } 274 | else 275 | { 276 | for (int i = 0; i < count; ++i) 277 | { 278 | ((U8 *)dest)[i] = ((U8 *)src)[i]; 279 | } 280 | } 281 | } 282 | 283 | //================================= 284 | // Memory Management 285 | //================================= 286 | 287 | /* 288 | * Zone Memory 289 | * 290 | * Zone memory serves as heap memory, is mainly used for small, dynamic 291 | * allocations like strings. All big objects are allocated on Hunk. 292 | * 293 | * Zone memory is consisted of memory blocks, free or being used. There won't be 294 | * any 2 consecutive free memory blocks 295 | * 296 | * Memory blocks are 8-byte aligned. 297 | */ 298 | 299 | #define DYNAMIC_ZONE_SIZE 128 * 1024 300 | #define ZONE_ID 0x1d4a11 301 | #define MIN_FRAGMENT 64 302 | 303 | struct MemoryBlock 304 | { 305 | // including sizeof(MemoryBlock) 306 | I32 size; 307 | // a tag of 0 is a free block 308 | I32 tag; 309 | // memory guard, should always be ZONE_ID 310 | I32 id; 311 | 312 | /* 313 | * If padding was at the end of this struct, the size of this struct would 314 | * have been 40 instead of 32. The reason is self-alignment for typed data 315 | * except char. For example, by default 2-byte short will sit on memory 316 | * address that's multiple of 2, 4-byte int on memory address multiple of 4, 317 | * and pointer on x64 machine on memory address multiple of 8. 318 | * Self-alignment facilitates memory access. 319 | * TODO lw: figure out why self-alignment helps memory access 320 | * */ 321 | int padding; 322 | 323 | MemoryBlock *next, *prev; 324 | }; 325 | 326 | struct MemoryZone 327 | { 328 | // points to a free memory block most of time, could potentially speed up 329 | // searching of free memory block 330 | MemoryBlock *rover; 331 | // the same as g_cache_head, 332 | // serves as reference node in the circular linked list 333 | MemoryBlock tailhead; 334 | // total bytes allocated including sizoef(MemoryZone) 335 | I32 size; 336 | // structure needs to be aligned to the size of the largest primitive type 337 | // data in the structure, in this case it's be 8-byte pointer "rover" 338 | I32 padding; 339 | }; 340 | 341 | MemoryZone *g_main_zone; 342 | 343 | void ZoneCheckHeap() 344 | { 345 | MemoryBlock *block = g_main_zone->tailhead.next; 346 | 347 | while (block->next != &g_main_zone->tailhead) 348 | { 349 | if ((U8 *)block + block->size != (U8 *)block->next) 350 | { 351 | g_platformAPI.SysError("ZoneCheckHeap: block size is erroneous"); 352 | } 353 | 354 | if (block->next->prev != block) 355 | { 356 | g_platformAPI.SysError("ZoneCheckHeap: memory block linked list is broken"); 357 | } 358 | 359 | if (block->tag == 0 && block->next->tag == 0) 360 | { 361 | g_platformAPI.SysError("2 consecutive free memory blocks!"); 362 | } 363 | 364 | block = block->next; 365 | } 366 | } 367 | 368 | // set all memory blocks as one free block 369 | void ZoneClearAll(MemoryZone *zone) 370 | { 371 | zone->tailhead.size = 0; // so it won't store any actual data 372 | zone->tailhead.tag = 1; // not a free block, a reference node 373 | zone->tailhead.id = ZONE_ID; 374 | 375 | zone->rover = (MemoryBlock *)((U8 *)zone + sizeof(*zone)); 376 | zone->rover->size = zone->size - sizeof(*zone); 377 | zone->rover->tag = 0; 378 | zone->rover->id = ZONE_ID; 379 | zone->rover->next = &zone->tailhead; 380 | zone->rover->prev = &zone->tailhead; 381 | 382 | zone->tailhead.next = zone->rover; 383 | zone->tailhead.prev = zone->rover; 384 | } 385 | 386 | void ZoneFree(void *ptr) 387 | { 388 | if (!ptr) 389 | { 390 | g_platformAPI.SysError("ZoneFree: free NULL pointer"); 391 | } 392 | 393 | MemoryBlock *block = (MemoryBlock *)((U8 *)ptr - sizeof(MemoryBlock)); 394 | 395 | if (block->id != ZONE_ID) 396 | { 397 | g_platformAPI.SysError("ZoneFree: free memory block without zone id"); 398 | } 399 | 400 | if (block->tag == 0) 401 | { 402 | g_platformAPI.SysError("ZoneFree: free a free memory block"); 403 | } 404 | 405 | block->tag = 0; 406 | 407 | MemoryBlock *other = block->prev; 408 | if (other->tag == 0) 409 | { 410 | // merge with previous free block 411 | other->next = block->next; 412 | block->next->prev = other; 413 | other->size = other->size + block->size; 414 | 415 | if (g_main_zone->rover == block) 416 | { 417 | g_main_zone->rover = other; 418 | } 419 | 420 | block = other; 421 | } 422 | 423 | other = block->next; 424 | if (other->tag == 0) 425 | { 426 | // merge with next free block 427 | block->next = other->next; 428 | other->prev = block; 429 | block->size = block->size + other->size; 430 | 431 | if (g_main_zone->rover == other) 432 | { 433 | g_main_zone->rover = block; 434 | } 435 | } 436 | } 437 | 438 | void *ZoneTagMalloc(int size, int tag) 439 | { 440 | if (tag == 0) 441 | { 442 | g_platformAPI.SysError("ZoneTagAlloc: using a 0 tag"); 443 | } 444 | 445 | size += sizeof(MemoryBlock); 446 | size += 4; // space at the end of memory block for trash tester 447 | size = (size + 7) & ~7; 448 | 449 | MemoryBlock *candidate = g_main_zone->rover; 450 | 451 | // Walk through all memory blocks and try to find one that's free and have 452 | // enough space. 453 | for (;;) 454 | { 455 | if (candidate == g_main_zone->rover->prev) 456 | { 457 | return NULL; // scanned all memory block, couldn't find one 458 | } 459 | 460 | if (candidate->tag == 0 && candidate->size >= size) 461 | { 462 | break ; 463 | } 464 | else 465 | { 466 | candidate = candidate->next; 467 | } 468 | } 469 | 470 | // if the extra space is large that MIN_FRAGMENT, merge it back into free 471 | // memory block 472 | int extra = candidate->size - size; 473 | if (extra > MIN_FRAGMENT) 474 | { 475 | MemoryBlock *newBlock = (MemoryBlock *)((U8 *)candidate + size); 476 | newBlock->size = extra; 477 | newBlock->tag = 0; 478 | newBlock->id = ZONE_ID; 479 | 480 | // insert new block into linked list 481 | candidate->next->prev = newBlock; 482 | newBlock->next = candidate->next; 483 | candidate->next = newBlock; 484 | newBlock->prev = candidate; 485 | 486 | candidate->size = size; 487 | } 488 | 489 | candidate->tag = tag; 490 | candidate->id = ZONE_ID; 491 | 492 | // next allocation will start looking here 493 | g_main_zone->rover = candidate->next; 494 | 495 | // marker for memory trash testing 496 | // TODO lw: ???? 497 | *(int *)((U8 *)candidate + (candidate->size - 4)) = ZONE_ID; 498 | 499 | void *result = (U8 *)candidate + sizeof(*candidate); 500 | 501 | return result; 502 | } 503 | 504 | void *ZoneMalloc(int size) 505 | { 506 | #ifdef QUAKEREMAKE_SLOW 507 | ZoneCheckHeap(); 508 | #endif 509 | 510 | void *result = ZoneTagMalloc(size, 1); 511 | 512 | if (!result) 513 | { 514 | g_platformAPI.SysError("ZoneMalloc: failed on allocation of size bytes"); 515 | } 516 | 517 | // TODO lw: zero out result 518 | 519 | return result; 520 | } 521 | 522 | 523 | 524 | /* 525 | * Hunk memory 526 | * 527 | * Hunk memory is the continuous memory block pre-allocated for the entire game. 528 | * Memory can be allocated at both end in stack fashion. 529 | * Hunk allocation is 16-byte aligned. 530 | * 531 | * Zone memory is allocated at the botton of the Hunk. 532 | * 533 | * Cache Memory is allocated inbetween low hunk and high hunk. 534 | * 535 | */ 536 | 537 | #define HUNK_SENTINEL 0x1df001ed 538 | 539 | struct HunkHeader 540 | { 541 | int sentinel; 542 | int size; // including sizeof(HunkHeader) 543 | char name[16]; // at most 15 characters, and a '\0' 544 | }; 545 | 546 | U8 *g_hunk_base; 547 | int g_hunk_total_size; 548 | 549 | int g_hunk_low_used; 550 | int g_hunk_high_used; 551 | int g_hunk_temp_used; 552 | bool g_hunk_temp_active; 553 | 554 | inline int Align16(int v) 555 | { 556 | int result = (v + 15) & ~15; 557 | return result; 558 | } 559 | 560 | inline int Align8(int v) 561 | { 562 | int result = (v + 7) & ~7; 563 | return result; 564 | } 565 | 566 | void *HunkLowAlloc(int size, char *name) 567 | { 568 | if (size < 0) 569 | { 570 | g_platformAPI.SysError("HunkLowAlloc: negative size"); 571 | } 572 | 573 | // TODO lw: find out what's the benefit of 16-byte alignment. cache line coherent? 574 | // align to 16 bytes 575 | size = Align16(size + sizeof(HunkHeader)); 576 | 577 | if (g_hunk_total_size - g_hunk_low_used - g_hunk_high_used < size) 578 | { 579 | g_platformAPI.SysError("HunkLowAlloc: out of memory"); 580 | } 581 | 582 | HunkHeader *hheader = (HunkHeader *)(g_hunk_base + g_hunk_low_used); 583 | g_hunk_low_used += size; 584 | 585 | // TODO lw: free cache memory if necessary 586 | 587 | MemSet(hheader, 0, size); 588 | 589 | hheader->sentinel = HUNK_SENTINEL; 590 | hheader->size = size; 591 | StringCopy(hheader->name, 16, name); 592 | 593 | return (void *)(hheader + 1); 594 | } 595 | 596 | void *HunkLowAlloc(int size) 597 | { 598 | void *result = HunkLowAlloc(size, "unknown"); 599 | return result; 600 | } 601 | 602 | void *HunkHighAlloc(int size, char *name) 603 | { 604 | if (size < 0) 605 | { 606 | g_platformAPI.SysError("HunkHighAlloc: negative size"); 607 | } 608 | 609 | // free temp hunk 610 | 611 | size = Align16(size + sizeof(HunkHeader)); 612 | 613 | if (g_hunk_total_size - g_hunk_low_used - g_hunk_high_used < size) 614 | { 615 | g_platformAPI.SysError("HunkHighAlloc: out of memory"); 616 | } 617 | 618 | g_hunk_high_used += size; 619 | HunkHeader *hh = (HunkHeader *)(g_hunk_base + g_hunk_total_size - g_hunk_high_used); 620 | 621 | // TODO lw: free cache memory if necessary 622 | 623 | MemSet(hh, 0, size); 624 | 625 | hh->sentinel = HUNK_SENTINEL; 626 | hh->size = size; 627 | StringCopy(hh->name, 16, name); 628 | 629 | return (void *)(hh + 1); 630 | } 631 | 632 | void HunkFreeTemp() 633 | { 634 | g_hunk_temp_active = false; 635 | g_hunk_high_used -= g_hunk_temp_used; 636 | g_hunk_temp_used = 0; 637 | } 638 | 639 | // allocating on high stack, used when loading asset files from disk 640 | void *HunkTempAlloc(int size) 641 | { 642 | // the second temp allocation removes the first temp data 643 | if (g_hunk_temp_active) 644 | { 645 | HunkFreeTemp(); 646 | } 647 | g_hunk_temp_active = true; 648 | int old_high_used = g_hunk_high_used; 649 | void *result = HunkHighAlloc(size, "temp"); 650 | g_hunk_temp_used = g_hunk_high_used - old_high_used; 651 | return result; 652 | } 653 | 654 | void *HunkHighAlloc(int size) 655 | { 656 | void *result = HunkHighAlloc(size, "unknown"); 657 | return result; 658 | } 659 | 660 | /* 661 | * Cache Memory 662 | * 663 | * Cache memory is used for dynamic loading objects. Caches are allocated 664 | * inbetween low stack and high stack, and can be removed if necessary for hunk 665 | * allocation. A circular linked list is used, in which the 666 | * g_cache_head->lru_prev is the Least Recent Used cache. 667 | * 668 | * Another circular linked list is used to keep cache pointers. Free memory is 669 | * searched by looking from the top of low hunk and move linearly along the 670 | * linked list. 671 | * 672 | */ 673 | 674 | struct CacheHeader 675 | { 676 | char name[16]; 677 | 678 | CacheUser *user; 679 | 680 | CacheHeader *prev, *next; 681 | CacheHeader *lru_prev, *lru_next; 682 | 683 | int size; 684 | int padding; 685 | }; 686 | 687 | // the g_cache_head doesn't store any real data, it serves as a reference point 688 | // in the cache circular link. 689 | CacheHeader g_cache_head; 690 | 691 | void CacheUnlinkLRU(CacheHeader *ch) 692 | { 693 | if (ch->lru_next == NULL || ch->lru_prev == NULL) 694 | { 695 | g_platformAPI.SysError("CacheUnlinkLRU: NULL link"); 696 | } 697 | 698 | ch->lru_next->lru_prev = ch->lru_prev; 699 | ch->lru_prev->lru_next = ch->lru_next; 700 | ch->lru_next = NULL; 701 | ch->lru_prev = NULL; 702 | } 703 | 704 | // mark cache as most recent used 705 | void CacheMarkMRU(CacheHeader *ch) 706 | { 707 | if (ch->lru_next || ch->lru_prev) 708 | { 709 | CacheUnlinkLRU(ch); 710 | } 711 | 712 | g_cache_head.lru_next->lru_prev = ch; 713 | ch->lru_next = g_cache_head.lru_next; 714 | g_cache_head.lru_next = ch; 715 | ch->lru_prev = &g_cache_head; 716 | } 717 | 718 | // if the cache has been loaded move it to the top of LRU list 719 | void *CacheCheck(CacheUser *user) 720 | { 721 | if (user->data == NULL) 722 | { 723 | return NULL; 724 | } 725 | 726 | CacheHeader *ch = (CacheHeader *)user->data - 1; 727 | CacheUnlinkLRU(ch); 728 | CacheMarkMRU(ch); 729 | 730 | return user->data; 731 | } 732 | 733 | void CacheFree(CacheUser *cu) 734 | { 735 | if (!cu->data) 736 | { 737 | g_platformAPI.SysError("CacheFree: not allocated"); 738 | } 739 | 740 | CacheHeader *ch = (CacheHeader *)cu->data - 1; 741 | 742 | ch->next->prev = ch->prev; 743 | ch->prev->next = ch->next; 744 | ch->prev = NULL; 745 | ch->next = NULL; 746 | 747 | cu->data = NULL; 748 | 749 | CacheUnlinkLRU(ch); 750 | } 751 | 752 | void CacheFlushAll() 753 | { 754 | CacheHeader *ch = g_cache_head.next; 755 | while(ch != &g_cache_head) 756 | { 757 | ch->user->data = NULL; 758 | } 759 | 760 | g_cache_head.next = &g_cache_head; 761 | g_cache_head.prev = &g_cache_head; 762 | g_cache_head.lru_next = &g_cache_head; 763 | g_cache_head.lru_prev = &g_cache_head; 764 | } 765 | 766 | /* 767 | * Search free memory hole from bottom. If found, insert it into cache linked 768 | * list so that the order in the linked list is the same as cache blocks in 769 | * memory. 770 | * */ 771 | CacheHeader *CacheTryAlloc(int size) 772 | { 773 | CacheHeader *new_cache = NULL; 774 | CacheHeader *old_cache = NULL; 775 | 776 | // cache list is empty 777 | if (g_cache_head.next == &g_cache_head) 778 | { 779 | if (g_hunk_total_size - g_hunk_low_used - g_hunk_high_used < size) 780 | { 781 | g_platformAPI.SysError("CacheTryAlloc: size is greater than free hunk\n"); 782 | } 783 | new_cache = (CacheHeader *)(g_hunk_base + g_hunk_low_used); 784 | MemSet(new_cache, 0, sizeof(*new_cache)); 785 | new_cache->size = size; 786 | 787 | g_cache_head.next = new_cache; 788 | g_cache_head.prev = new_cache; 789 | new_cache->next = &g_cache_head; 790 | new_cache->prev = &g_cache_head; 791 | 792 | CacheMarkMRU(new_cache); 793 | 794 | return new_cache; 795 | } 796 | 797 | new_cache = (CacheHeader *)(g_hunk_base + g_hunk_low_used); 798 | old_cache = g_cache_head.next; 799 | 800 | // linearly go through linked list, try to find a hole inbetween caches 801 | do 802 | { 803 | if ((U8 *)old_cache - (U8 *)new_cache >= size) 804 | { 805 | MemSet(new_cache, 0, sizeof(*new_cache)); 806 | new_cache->size = size; 807 | 808 | // insert new_cache before the old_cache in the linked list 809 | old_cache->prev->next = new_cache; 810 | new_cache->prev = old_cache->prev; 811 | new_cache->next = old_cache; 812 | old_cache->prev = new_cache; 813 | 814 | CacheMarkMRU(new_cache); 815 | 816 | return new_cache; 817 | } 818 | 819 | new_cache = (CacheHeader *)((U8 *)old_cache + old_cache->size); 820 | old_cache = old_cache->next; 821 | } while (old_cache != &g_cache_head); 822 | 823 | // no hole big enough, allocate at the end, between the last cache and high stack 824 | if (g_hunk_base + g_hunk_total_size - g_hunk_high_used - (U8 *)new_cache >= size) 825 | { 826 | MemSet(new_cache, 0, sizeof(*new_cache)); 827 | new_cache->size = size; 828 | 829 | // insert new_cache to the end, between g_cache_head->prev and g_cache_head 830 | g_cache_head.prev->next = new_cache; 831 | new_cache->prev = g_cache_head.prev; 832 | new_cache->next = &g_cache_head; 833 | g_cache_head.prev = new_cache; 834 | 835 | CacheMarkMRU(new_cache); 836 | 837 | return new_cache; 838 | } 839 | 840 | return NULL; 841 | } 842 | 843 | void *CacheAlloc(CacheUser *cu, int size, char *name) 844 | { 845 | if (cu->data) 846 | { 847 | g_platformAPI.SysError("CacheAlloc: already allocated"); 848 | } 849 | 850 | if (size <= 0) 851 | { 852 | g_platformAPI.SysError("CacheAlloc: bad size"); 853 | } 854 | 855 | CacheHeader *ch = NULL; 856 | 857 | size = Align16(size + sizeof(*ch)); 858 | 859 | for (;;) 860 | { 861 | ch = CacheTryAlloc(size); 862 | 863 | if (ch) 864 | { 865 | ch->user = cu; 866 | ch->user->data = (void *)(ch + 1); 867 | StringCopy(ch->name, 15, name, 15); 868 | 869 | break ; 870 | } 871 | 872 | if (g_cache_head.next == &g_cache_head) 873 | { 874 | g_platformAPI.SysError("CacheAlloc: out of memory"); 875 | } 876 | 877 | CacheFree(g_cache_head.lru_prev->user); 878 | } 879 | 880 | return ch->user->data; 881 | } 882 | 883 | void CacheInit() 884 | { 885 | g_cache_head.next = &g_cache_head; 886 | g_cache_head.prev = &g_cache_head; 887 | g_cache_head.lru_next = &g_cache_head; 888 | g_cache_head.lru_prev = &g_cache_head; 889 | g_cache_head.size = 0; 890 | 891 | // TODO lw: add flushall command 892 | } 893 | 894 | void MemoryInit(void *buf, int size) 895 | { 896 | g_hunk_base = (U8 *)buf; 897 | g_hunk_total_size = size; 898 | g_hunk_low_used = 0; 899 | g_hunk_high_used = 0; 900 | 901 | CacheInit(); 902 | 903 | int zoneSize = DYNAMIC_ZONE_SIZE; 904 | // TODO lw: set zone size from command line 905 | g_main_zone = (MemoryZone *)HunkLowAlloc(zoneSize, "zone"); 906 | g_main_zone->size = zoneSize; 907 | ZoneClearAll(g_main_zone); 908 | } 909 | 910 | //================================= 911 | // Dynamic variable tracking 912 | //================================= 913 | 914 | #define MAX_CVARS 512 915 | #define CVAR_HASH_MASK (MAX_CVARS - 1) 916 | 917 | struct CvarSystem 918 | { 919 | I32 count; 920 | I32 hash[MAX_CVARS]; 921 | Cvar cvars[MAX_CVARS]; 922 | }; 923 | 924 | CvarSystem g_cvar_pool; 925 | 926 | I32 GetCvarHashKey(const char *name) 927 | { 928 | // credit: http://www.cse.yorku.ca/~oz/hash.html 929 | I32 hash = 5381; 930 | char *c = (char *)name; 931 | while (*c) 932 | { 933 | hash = (hash << 5) + hash + *c; 934 | c++; 935 | } 936 | I32 hash_key = hash & CVAR_HASH_MASK; 937 | if (hash_key == 0) 938 | { 939 | hash_key = 1; 940 | } 941 | return 22; 942 | } 943 | 944 | // if cvar is not found, create one with default value 0 945 | Cvar *CvarGet(const char *name) 946 | { 947 | Cvar *result = NULL; 948 | 949 | I32 hash_key = GetCvarHashKey(name); 950 | I32 hash_index = hash_key; 951 | while (g_cvar_pool.hash[hash_index]) 952 | { 953 | if (g_cvar_pool.hash[hash_index] == hash_key) 954 | { 955 | if (StringCompare(g_cvar_pool.cvars[hash_index].name, name) == 0) 956 | { 957 | result = &g_cvar_pool.cvars[hash_index]; 958 | } 959 | } 960 | hash_index = (hash_index++) & CVAR_HASH_MASK; 961 | } 962 | 963 | if (!result) 964 | { 965 | if (g_cvar_pool.count >= MAX_CVARS) 966 | { 967 | g_platformAPI.SysError("CvarGet: cvar count exceeds the maximum!"); 968 | } 969 | g_cvar_pool.count++; 970 | g_cvar_pool.hash[hash_index] = hash_key; 971 | result = &g_cvar_pool.cvars[hash_index]; 972 | 973 | I32 length = StringLength(name) + 1; 974 | result->name = (char *)ZoneMalloc(length); 975 | StringCopy(result->name, length, name); 976 | } 977 | 978 | return result; 979 | } 980 | 981 | Cvar *CvarSet(const char *name, float val) 982 | { 983 | Cvar *result = CvarGet(name); 984 | result->val = val; 985 | return result; 986 | } 987 | 988 | 989 | //================================= 990 | // File system 991 | //================================= 992 | 993 | #define MAX_FILE_HANDLES 10 994 | 995 | FILE *g_file_handles[MAX_FILE_HANDLES]; 996 | 997 | SearchPath *g_search_path; 998 | 999 | int FileGetAvailableHande() 1000 | { 1001 | for (int i = 0; i < MAX_FILE_HANDLES; ++i) 1002 | { 1003 | if (!g_file_handles[i]) 1004 | { 1005 | return i; 1006 | } 1007 | } 1008 | 1009 | g_platformAPI.SysError("out of file handle"); 1010 | 1011 | return -1; 1012 | } 1013 | 1014 | int FileLength(FILE *file) 1015 | { 1016 | // backup of current position indicator 1017 | int pos = ftell(file); 1018 | 1019 | // set position indicator to the end of the file 1020 | fseek(file, 0, SEEK_END); 1021 | 1022 | // get number of bytes from beginning to position indicator 1023 | int end = ftell(file); 1024 | 1025 | // set position indicator to its previous position 1026 | fseek(file, pos, SEEK_SET); 1027 | 1028 | return end; 1029 | } 1030 | 1031 | /* 1032 | * Open file for read, and add file pointer to global file handle array. 1033 | * 1034 | */ 1035 | int FileOpenForRead(char *path, int *handle_out) 1036 | { 1037 | int handle = FileGetAvailableHande(); 1038 | 1039 | // open as binary file for read 1040 | FILE *f = NULL; 1041 | fopen_s(&f, path, "rb"); 1042 | 1043 | int result = 0; 1044 | 1045 | if (f) 1046 | { 1047 | g_file_handles[handle] = f; 1048 | *handle_out = handle; 1049 | result = FileLength(f); 1050 | } 1051 | else 1052 | { 1053 | *handle_out = -1; 1054 | result = -1; 1055 | } 1056 | 1057 | return result; 1058 | } 1059 | 1060 | void FileClose(int handle) 1061 | { 1062 | // if it's a file in PAK, don't close it 1063 | for (SearchPath *sp = g_search_path; sp != NULL; ++sp) 1064 | { 1065 | if (sp->pack != NULL && sp->pack->handle == handle) 1066 | { 1067 | return ; 1068 | } 1069 | } 1070 | 1071 | // TODO lw: close file such as config.cfg 1072 | } 1073 | 1074 | inline size_t 1075 | FileRead(int filehandle, void *dest, int count) 1076 | { 1077 | ASSERT(count >= 0); 1078 | size_t result = fread(dest, 1, count, g_file_handles[filehandle]); 1079 | return result; 1080 | } 1081 | 1082 | inline void 1083 | FileSeek(int filehandle, int position) 1084 | { 1085 | fseek(g_file_handles[filehandle], position, SEEK_SET); 1086 | } 1087 | 1088 | 1089 | // on disk 1090 | struct PackFileDisk 1091 | { 1092 | char name[56]; 1093 | int filePosition; 1094 | int fileLength; 1095 | }; 1096 | 1097 | struct PackHeaderDisk 1098 | { 1099 | char magic[4]; 1100 | // byte offset of the pack file directories 1101 | // All file directories are stored at the end of the pack file. It's easier 1102 | // to append new files and directories without modifying the existing ones. 1103 | int directoryOffset; 1104 | int directoryLength; 1105 | }; 1106 | 1107 | bool g_packModified = false; 1108 | 1109 | char g_cacheDir[MAX_OS_PATH_LENGTH]; 1110 | char g_gameDir[MAX_OS_PATH_LENGTH]; 1111 | 1112 | void GetFileNameFromPath(const char *path, char *dest, int destSize) 1113 | { 1114 | const char *onePastLastSlash = path; 1115 | const char *scan = path; 1116 | for (; *scan; ++scan) 1117 | { 1118 | if (*scan == '\\' || *scan == '/') 1119 | { 1120 | onePastLastSlash = scan + 1; 1121 | } 1122 | } 1123 | 1124 | StringCopy(dest, destSize, onePastLastSlash, (int)(scan - onePastLastSlash)); 1125 | } 1126 | 1127 | int FileFind(const char *filepath, int *handle) 1128 | { 1129 | if (*handle >= 0) 1130 | { 1131 | g_platformAPI.SysError("handle is already set"); 1132 | } 1133 | 1134 | SearchPath *searchPath = g_search_path; 1135 | for ( ; searchPath != NULL; searchPath = searchPath->next) 1136 | { 1137 | // search in PAK file 1138 | if (searchPath->pack != NULL) 1139 | { 1140 | PackHeader *pack = searchPath->pack; 1141 | for (int i = 0; i < pack->numfiles; ++i) 1142 | { 1143 | if (StringCompare(pack->files[i].name, filepath) == 0) 1144 | { 1145 | *handle = pack->handle; 1146 | FileSeek(*handle, pack->files[i].filePosition); 1147 | return pack->files[i].fileLength; 1148 | } 1149 | } 1150 | } 1151 | else 1152 | { 1153 | // TODO lw: files not in PAK such as config.cfg 1154 | } 1155 | } 1156 | 1157 | *handle = -1; 1158 | return 0; 1159 | } 1160 | 1161 | U8 *FileLoad(const char *filepath, ALLocType allocType) 1162 | { 1163 | int handle = -1; 1164 | int fileLength = FileFind(filepath, &handle); 1165 | 1166 | if (handle == -1) 1167 | { 1168 | return NULL; 1169 | } 1170 | 1171 | char baseName[16]; 1172 | GetFileNameFromPath(filepath, baseName, 16); 1173 | 1174 | U8 *buffer = NULL; 1175 | 1176 | switch (allocType) 1177 | { 1178 | case ALLocType::LOWHUNK: 1179 | { 1180 | buffer = (U8 *)HunkLowAlloc(fileLength + 1, baseName); 1181 | } break; 1182 | 1183 | case ALLocType::TEMPHUNK: 1184 | { 1185 | buffer = (U8 *)HunkTempAlloc(fileLength + 1); 1186 | } break; 1187 | 1188 | case ALLocType::ZONE: 1189 | { 1190 | buffer = (U8 *)ZoneTagMalloc(fileLength + 1, 1); 1191 | } break; 1192 | 1193 | case ALLocType::CACHE: 1194 | { 1195 | // buffer = (U8 *)CacheAlloc(loadcache, fileLength + 1, baseName); 1196 | } break; 1197 | 1198 | case ALLocType::TEMPSTACK: 1199 | { 1200 | 1201 | } break; 1202 | 1203 | default: 1204 | { 1205 | g_platformAPI.SysError("bad alloc type"); 1206 | } 1207 | } 1208 | 1209 | if (buffer == NULL) 1210 | { 1211 | g_platformAPI.SysError("not enough space for %s", filepath); 1212 | } 1213 | // TODO lw: why allocate one extra byte and set it to 0??? 1214 | buffer[fileLength] = 0; 1215 | 1216 | FileRead(handle, buffer, fileLength); 1217 | FileClose(handle); 1218 | 1219 | return buffer; 1220 | } 1221 | 1222 | U8 *FileLoadToLowHunk(const char *filepath) 1223 | { 1224 | U8 *result = FileLoad(filepath, ALLocType::LOWHUNK); 1225 | return result; 1226 | } 1227 | 1228 | PackHeader *FileLoadPack(char *packpath) 1229 | { 1230 | int packHandle = 0; 1231 | 1232 | if (FileOpenForRead(packpath, &packHandle) == -1) 1233 | { 1234 | return NULL; 1235 | } 1236 | 1237 | PackHeaderDisk packHeaderDisk; 1238 | FileRead(packHandle, (void *)&packHeaderDisk, sizeof(packHeaderDisk)); 1239 | 1240 | if (packHeaderDisk.magic[0] != 'P' 1241 | || packHeaderDisk.magic[1] != 'A' 1242 | || packHeaderDisk.magic[2] != 'C' 1243 | || packHeaderDisk.magic[3] != 'K') 1244 | { 1245 | g_platformAPI.SysError("%s is not a packfile", packpath); 1246 | } 1247 | 1248 | int packfileNum = packHeaderDisk.directoryLength / sizeof(PackFileDisk); 1249 | 1250 | if (packfileNum > MAX_FILES_IN_PACK) 1251 | { 1252 | g_platformAPI.SysError("%s has %i files, too many", packpath, packfileNum); 1253 | } 1254 | 1255 | if (packfileNum != PAK0_FILE_NUM) 1256 | { 1257 | g_packModified = true; 1258 | } 1259 | 1260 | PackFileDisk diskPackFiles[MAX_FILES_IN_PACK]; 1261 | 1262 | FileSeek(packHandle, packHeaderDisk.directoryOffset); 1263 | FileRead(packHandle, (void *)diskPackFiles, packHeaderDisk.directoryLength); 1264 | 1265 | PackFile *packFiles = (PackFile *)HunkLowAlloc(packfileNum * sizeof(PackFile), "packfiles"); 1266 | 1267 | for (int i = 0; i < packfileNum; ++i) 1268 | { 1269 | StringCopy(packFiles[i].name, MAX_PACK_FILE_PATH, diskPackFiles[i].name, 55); 1270 | packFiles[i].filePosition = diskPackFiles[i].filePosition; 1271 | packFiles[i].fileLength = diskPackFiles[i].fileLength; 1272 | } 1273 | 1274 | PackHeader *packHeader = (PackHeader *)HunkLowAlloc(sizeof(PackHeader), "packheader"); 1275 | //StringCopy(packHeader->filepath, MAX_OS_PATH_LENGTH, packHeaderDisk); 1276 | packHeader->handle = packHandle; 1277 | packHeader->numfiles = packfileNum; 1278 | packHeader->files = packFiles; 1279 | 1280 | return packHeader; 1281 | } 1282 | 1283 | /* 1284 | * Add main game directory to the path search list 1285 | * Add pack files as directory to the path search list 1286 | */ 1287 | void FileAddGameDiectory(char *dir) 1288 | { 1289 | StringCopy(g_gameDir, MAX_OS_PATH_LENGTH, dir); 1290 | 1291 | SearchPath *search = (SearchPath *)HunkLowAlloc(sizeof(SearchPath), "searchpath"); 1292 | StringCopy(search->filename, MAX_OS_PATH_LENGTH, dir); 1293 | // insert game directory in front of search path list 1294 | search->next = g_search_path; 1295 | g_search_path = search; 1296 | 1297 | char packfile[MAX_OS_PATH_LENGTH]; 1298 | PackHeader *pack; 1299 | 1300 | for (int i = 0; ; ++i) 1301 | { 1302 | // add any pack files in the format of pak[0-9].pak 1303 | sprintf_s(packfile, MAX_OS_PATH_LENGTH, "%spak%i.pak", dir, i); 1304 | pack = FileLoadPack(packfile); 1305 | if (!pack) 1306 | { 1307 | break ; 1308 | } 1309 | 1310 | search = (SearchPath *)HunkLowAlloc(sizeof(SearchPath), "packpath"); 1311 | search->pack = pack; 1312 | // insert in front of search path list 1313 | search->next = g_search_path; 1314 | g_search_path = search; 1315 | } 1316 | } 1317 | 1318 | int FileSystemInit(char *assetDir) 1319 | { 1320 | // If it's a little endian system, a 32-bit integer B0B1B2B3 is stored in 1321 | // memory from address x to x + 3 as B3 | B2 | B1 | B0 1322 | // quake's pack files were created in little-endian system 1323 | U8 endian[4] {1, 0, 0, 0}; 1324 | if (*(I32 *)endian != 1) 1325 | { 1326 | g_platformAPI.SysError("Not a Little Endian system"); 1327 | } 1328 | 1329 | FileAddGameDiectory(assetDir); 1330 | return 0; 1331 | } 1332 | 1333 | -------------------------------------------------------------------------------- /code/q_common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | //================================= 6 | // String related operations 7 | //================================= 8 | 9 | 10 | //================================= 11 | // Memory Management 12 | //================================= 13 | 14 | // a wrapper of pointer to provide type safety 15 | struct CacheUser 16 | { 17 | void *data; 18 | }; 19 | 20 | enum ALLocType 21 | { 22 | ZONE, 23 | LOWHUNK, 24 | TEMPHUNK, 25 | CACHE, 26 | TEMPSTACK 27 | }; 28 | 29 | //================================= 30 | // Dynamic variable tracking 31 | //================================= 32 | 33 | 34 | struct Cvar 35 | { 36 | char *name; 37 | float val; 38 | // set true to save the variable before quitting 39 | I32 archive; 40 | }; 41 | 42 | 43 | //================================= 44 | // File system 45 | //================================= 46 | 47 | #define MAX_PACK_FILE_PATH 64 48 | #define MAX_FILES_IN_PACK 2048 49 | #define PAK0_FILE_NUM 339 50 | 51 | /* 52 | PAK file in memory 53 | pack_header | file_data0 | file_data1 | ... | packfile_header0 | packfile_header1 | ... 54 | */ 55 | struct PackFile 56 | { 57 | char name[MAX_PACK_FILE_PATH]; 58 | int filePosition; 59 | int fileLength; 60 | }; 61 | 62 | struct PackHeader 63 | { 64 | char filepath[MAX_OS_PATH_LENGTH]; 65 | int handle; 66 | int numfiles; 67 | PackFile *files; // array of pack files 68 | }; 69 | 70 | struct SearchPath 71 | { 72 | char filename[MAX_OS_PATH_LENGTH]; 73 | PackHeader *pack; 74 | SearchPath *next; 75 | }; 76 | 77 | -------------------------------------------------------------------------------- /code/q_game.cpp: -------------------------------------------------------------------------------- 1 | #include "q_platform.h" 2 | #include "q_common.cpp" 3 | #include "q_sky.cpp" 4 | #include "q_model.cpp" 5 | #include "q_render.cpp" 6 | 7 | float g_target_dt; // target seconds per frame 8 | 9 | struct MapInfo 10 | { 11 | Model *model; 12 | Vec3f spawn_pos; 13 | LightStyle light_styles[MAX_LIGHT_STYLE_NUM]; 14 | }; 15 | 16 | MapInfo g_mapinfos[20]; 17 | 18 | void FillLightStyles(MapInfo *mapinfo, I32 index, const char *wave) 19 | { 20 | I32 len = StringCopy(mapinfo->light_styles[index].wave, 64, wave); 21 | mapinfo->light_styles[index].length = len; 22 | } 23 | 24 | void FillMapInfos() 25 | { 26 | MapInfo *mapinfo = g_mapinfos + 0; 27 | mapinfo->model = ModelLoadForName("maps/start.bsp"); 28 | mapinfo->spawn_pos = {544.6f, 290.0f, 50.0f}; 29 | FillLightStyles(mapinfo, 0, "m"); 30 | FillLightStyles(mapinfo, 1, "mmnmmommommnonmmonqnmmo"); 31 | FillLightStyles(mapinfo, 2, "mmnmmommommnonmmonqnmmo"); 32 | FillLightStyles(mapinfo, 3, "abcdefghijklmnopqrstuvwxyzyxwvutsrqponmlkjihgfedcba"); 33 | FillLightStyles(mapinfo, 4, "mmmmmaaaaammmmmaaaaaabcdefgabcdefg"); 34 | FillLightStyles(mapinfo, 5, "mamamamamama"); 35 | FillLightStyles(mapinfo, 7, "jklmnopqrstuvwxyzyxwvutsrqponmlkj"); 36 | FillLightStyles(mapinfo, 8, "nmonqnmomnmomomno"); 37 | FillLightStyles(mapinfo, 9, "mmmaaaabcdefgmmmmaaaammmaamm"); 38 | FillLightStyles(mapinfo, 10, "mmmaaammmaaammmabcdefaaaammmmabcdefmmmaaaa"); 39 | FillLightStyles(mapinfo, 11, "aaaaaaaazzzzzzzz"); 40 | FillLightStyles(mapinfo, 12, "mmamammmmammamamaaamammma"); 41 | FillLightStyles(mapinfo, 13, "abcdefghijklmnopqrrqponmlkjihgfedcba"); 42 | FillLightStyles(mapinfo, 32, "m"); 43 | FillLightStyles(mapinfo, 33, "a"); 44 | FillLightStyles(mapinfo, 34, "a"); 45 | FillLightStyles(mapinfo, 35, "a"); 46 | FillLightStyles(mapinfo, 36, "a"); 47 | FillLightStyles(mapinfo, 63, "a"); 48 | 49 | mapinfo = g_mapinfos + 1; 50 | mapinfo->model = ModelLoadForName("maps/e1m1.bsp"); 51 | mapinfo->spawn_pos = {472.281250, -352.218750, 110.031250}; 52 | 53 | mapinfo = g_mapinfos + 2; 54 | mapinfo->model = ModelLoadForName("maps/e1m3.bsp"); 55 | mapinfo->spawn_pos = {-735.968750f, -1591.96875f, 110.031250f}; 56 | } 57 | 58 | void SetLightStyle(LightStyle dest[MAX_LIGHT_STYLE_NUM], LightStyle src[MAX_LIGHT_STYLE_NUM]) 59 | { 60 | for (I32 i = 0; i < MAX_LIGHT_STYLE_NUM; ++i) 61 | { 62 | if (src[i].length) 63 | { 64 | dest[i].length = src[i].length; 65 | StringCopy(dest[i].wave, 64, src[i].wave); 66 | } 67 | } 68 | } 69 | 70 | void SetMapInfo(MapInfo *mapinfo) 71 | { 72 | g_renderdata.worldModel = mapinfo->model; 73 | g_camera.position = mapinfo->spawn_pos; 74 | g_camera.angles = {0, 0.0f, -90.0f}; 75 | 76 | SetLightStyle(g_lightsystem.styles, g_mapinfos[0].light_styles); 77 | 78 | ModelInit(); 79 | } 80 | 81 | void AllocRenderBuffer(RenderBuffer *renderBuffer, GameOffScreenBuffer *offscreenBuffer) 82 | { 83 | renderBuffer->width = offscreenBuffer->width; 84 | renderBuffer->height = offscreenBuffer->height; 85 | renderBuffer->bytesPerPixel = offscreenBuffer->bytesPerPixel; 86 | renderBuffer->bytes_per_row = offscreenBuffer->bytesPerRow; 87 | 88 | I32 pixel_buffer_size = renderBuffer->bytes_per_row * renderBuffer->height; 89 | 90 | I32 zbuffer_size = renderBuffer->width * sizeof(*renderBuffer->zbuffer) * renderBuffer->height; 91 | 92 | renderBuffer->backbuffer = (U8 *)HunkHighAlloc(pixel_buffer_size, "renderbuffer"); 93 | renderBuffer->zbuffer = (float *)HunkHighAlloc(zbuffer_size, "zbuffer"); 94 | 95 | offscreenBuffer->memory = renderBuffer->backbuffer; 96 | 97 | // allocate for surface caching 98 | I32 surface_cache_size = SurfaceCacheGetSizeForResolution(renderBuffer->width, renderBuffer->height); 99 | void *surface_cache = HunkHighAlloc(surface_cache_size, "surfacecache"); 100 | SurfaceCacheInit(surface_cache, surface_cache_size); 101 | } 102 | 103 | extern "C" GAME_INIT(GameInit) 104 | { 105 | g_platformAPI = memory->platformAPI; 106 | 107 | MemoryInit(memory->gameMemory, memory->gameMemorySize); 108 | 109 | FileSystemInit(memory->gameAssetDir); 110 | 111 | AllocRenderBuffer(&g_renderbuffer, &memory->offscreenBuffer); 112 | 113 | g_renderbuffer.colorPalette = FileLoadToLowHunk("gfx/palette.lmp"); 114 | g_renderbuffer.colormap = FileLoadToLowHunk("gfx/colormap.lmp"); 115 | RemapColorMap(g_renderbuffer.colorPalette, g_renderbuffer.colormap); 116 | 117 | { 118 | U8 new_palette[256 * 3]; 119 | U8 gamma_table[256]; 120 | 121 | BuildGammaTable(gamma_table, 1); 122 | GammaCorrect(gamma_table, new_palette, g_renderbuffer.colorPalette); 123 | g_platformAPI.SysSetPalette(new_palette); 124 | } 125 | 126 | g_defaultTexture = TextureCreateDefault(); 127 | 128 | FillMapInfos(); 129 | 130 | SetMapInfo(g_mapinfos + 2); 131 | 132 | // x right, y forward, z up 133 | AngleVectors(g_camera.angles, &g_camera.rotx, &g_camera.roty, &g_camera.rotz); 134 | 135 | Recti screenRect = {0, 0, 136 | memory->offscreenBuffer.width, 137 | memory->offscreenBuffer.height}; 138 | float fovx = 90.0f; 139 | 140 | ResetCamera(&g_camera, screenRect, fovx); 141 | 142 | RenderInit(); 143 | 144 | g_target_dt = memory->targetSecondsPerFrame; 145 | } 146 | 147 | extern "C" GAME_UPDATE_AND_RENDER(GameUpdateAndRender) 148 | { 149 | Vec3f forward = g_camera.roty; 150 | //forward.z = 0; 151 | forward = Vec3Normalize(forward); 152 | Vec3f right = g_camera.rotx; 153 | //right.z = 0; 154 | right = Vec3Normalize(right); 155 | 156 | float move_speed = 7.0f; 157 | 158 | for (I32 i = 0; i < game_input->kevt_count; ++i) 159 | { 160 | KeyState key = game_input->key_events[i]; 161 | if (key.key == 'w' && key.is_down) 162 | { 163 | g_camera.position += forward * move_speed; 164 | } 165 | else if (key.key == 's' && key.is_down) 166 | { 167 | g_camera.position -= forward * move_speed; 168 | } 169 | 170 | if (key.key == 'a' && key.is_down) 171 | { 172 | g_camera.position -= right * move_speed; 173 | } 174 | else if (key.key == 'd' && key.is_down) 175 | { 176 | g_camera.position += right * move_speed; 177 | } 178 | } 179 | 180 | const float ROTATE_EPSILON = 1; 181 | 182 | float degree = g_camera.angles.z; 183 | float delta = game_input->mouse.delta_x * 0.05f; 184 | 185 | degree += delta; 186 | if (degree > 360.0f) 187 | { 188 | degree = degree - 360.0f; 189 | } 190 | if (degree < -360.0f) 191 | { 192 | degree = degree + 360.0f; 193 | } 194 | g_camera.angles.z = degree; 195 | 196 | degree = g_camera.angles.x; 197 | delta = game_input->mouse.delta_y * 0.05f; 198 | 199 | degree -= delta; 200 | if (degree > 85.0f) 201 | { 202 | degree = 85.0f; 203 | } 204 | if (degree < -85.0f) 205 | { 206 | degree = -85.0f; 207 | } 208 | g_camera.angles.x = degree; 209 | 210 | AngleVectors(g_camera.angles, &g_camera.rotx, &g_camera.roty, &g_camera.rotz); 211 | 212 | RenderView(g_target_dt); 213 | } 214 | -------------------------------------------------------------------------------- /code/q_input.cpp: -------------------------------------------------------------------------------- 1 | #include "q_input.h" 2 | 3 | InputSystem g_inputs; 4 | 5 | void ButtonDown(Button *button) 6 | { 7 | I32 is_down = button->state & BUTTON_DOWN; 8 | if (is_down) 9 | { 10 | return ; 11 | } 12 | button->state |= BUTTON_DOWN | BUTTON_PRESS; 13 | } 14 | 15 | void ButtonUp(Button *button) 16 | { 17 | I32 is_down = button->state & BUTTON_DOWN; 18 | if (!is_down) 19 | { 20 | return ; 21 | } 22 | button->state &= ~BUTTON_DOWN; 23 | button->state |= BUTTON_RELEASE; 24 | } 25 | 26 | float EvaluateButtonState(Button *button) 27 | { 28 | I32 action_press = button->state & BUTTON_PRESS; 29 | I32 action_release = button->state & BUTTON_RELEASE; 30 | I32 is_down = button->state & BUTTON_DOWN; 31 | 32 | float val = 0; 33 | 34 | // pressed the button this frame 35 | if (action_press && !action_release) 36 | { 37 | ASSERT(is_down); 38 | val = 0.5f; 39 | } 40 | // released the button this frame 41 | else if (!action_press && action_release) 42 | { 43 | ASSERT(!is_down); 44 | val = 0.0f; 45 | } 46 | else if (action_press && action_release) 47 | { 48 | if (is_down) // pressed then released 49 | { 50 | val = 0.25f; 51 | } 52 | else // released then pressed 53 | { 54 | val = .075f; 55 | } 56 | } 57 | else if (!action_press && !action_release) 58 | { 59 | if (is_down) 60 | { 61 | val = 1.0f 62 | } 63 | } 64 | 65 | return val; 66 | } 67 | -------------------------------------------------------------------------------- /code/q_input.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define BUTTON_DOWN 1 4 | #define BUTTION_PRESS 2 5 | #define BUTTION_RELEASE 4 6 | 7 | struct Button 8 | { 9 | I32 state; 10 | }; 11 | 12 | struct InputSystem 13 | { 14 | Button forward; 15 | Button side; 16 | }; 17 | -------------------------------------------------------------------------------- /code/q_lightmap.cpp: -------------------------------------------------------------------------------- 1 | #include "q_model.h" 2 | #include "q_lightmap.h" 3 | 4 | #define SURFACE_CACHE_GUARD 0x4c575343 // 'LWSC' 5 | #define SURFACE_CACHE_GUARD_SIZE 4 6 | 7 | #define COLOR_SHADE_BITS 6 8 | #define COLOR_SHADE_GRADE (1 << COLOR_SHADE_BITS) 9 | 10 | LightSystem g_lightsystem; 11 | 12 | //====================================== 13 | // Memory management for surface caching 14 | //====================================== 15 | 16 | struct SurfaceCacheMemory 17 | { 18 | SurfaceCache *base; 19 | SurfaceCache *rover; 20 | I32 size; 21 | }; 22 | 23 | SurfaceCacheMemory g_surfcache_memory; 24 | 25 | I32 SurfaceCacheGetSizeForResolution(I32 width, I32 height) 26 | { 27 | I32 size = width * height * 16; 28 | return size; 29 | } 30 | 31 | void SurfaceCacheCheckCacheGuard() 32 | { 33 | U8 *s = (U8 *)g_surfcache_memory.base + g_surfcache_memory.size; 34 | if (*((I32 *)s) != SURFACE_CACHE_GUARD) 35 | { 36 | g_platformAPI.SysError("Surface cache is corrupted!"); 37 | } 38 | } 39 | 40 | void SurfaceCacheSetCacheGuard() 41 | { 42 | U8 *s = (U8 *)g_surfcache_memory.base + g_surfcache_memory.size; 43 | *((I32 *)s) = SURFACE_CACHE_GUARD; 44 | } 45 | 46 | void SurfaceCacheFlush() 47 | { 48 | if (!g_surfcache_memory.base) 49 | { 50 | return ; 51 | } 52 | for (SurfaceCache *sc = g_surfcache_memory.base; sc != NULL; sc = sc->next) 53 | { 54 | if (sc->owner) 55 | { 56 | *(sc->owner) = NULL; 57 | } 58 | } 59 | g_surfcache_memory.base->next = NULL; 60 | g_surfcache_memory.base->owner = NULL; 61 | g_surfcache_memory.base->size = g_surfcache_memory.size; 62 | g_surfcache_memory.rover = g_surfcache_memory.base; 63 | } 64 | 65 | void SurfaceCacheInit(void *buffer, I32 size) 66 | { 67 | g_surfcache_memory.size = size - SURFACE_CACHE_GUARD_SIZE; 68 | g_surfcache_memory.base = (SurfaceCache *)buffer; 69 | g_surfcache_memory.rover = g_surfcache_memory.base; 70 | 71 | g_surfcache_memory.base->next = NULL; 72 | g_surfcache_memory.base->owner = NULL; 73 | g_surfcache_memory.base->size = g_surfcache_memory.size; 74 | 75 | SurfaceCacheSetCacheGuard(); 76 | } 77 | 78 | SurfaceCache *SurfaceCacheAlloc(I32 width, I32 size) 79 | { 80 | if (width < 0 || width > 256) 81 | { 82 | g_platformAPI.SysError("SurfaceCacheAlloc: bad width"); 83 | } 84 | if (size <= 0 || size > 0x10000) 85 | { 86 | g_platformAPI.SysError("SurfaceCacheAlloc: bad size"); 87 | } 88 | // SurfaceCache *tempcache = 0; 89 | //size_t total_size = (size_t)(&tempcache->data[size]); 90 | I32 total_size = size + sizeof(SurfaceCache) - 4; 91 | total_size = (total_size + 3) & ~3; 92 | if (total_size > g_surfcache_memory.size) 93 | { 94 | g_platformAPI.SysError("SurfaceCacheAlloc: %d > surface cache size", total_size); 95 | } 96 | 97 | // if there is not enough memory left, we go back to base 98 | if (!g_surfcache_memory.rover 99 | || (U8 *)g_surfcache_memory.rover - (U8 *)g_surfcache_memory.base > g_surfcache_memory.size - total_size) 100 | { 101 | g_surfcache_memory.rover = g_surfcache_memory.base; 102 | } 103 | 104 | // find a memory block large enough 105 | SurfaceCache *new_cache = g_surfcache_memory.rover; 106 | if (new_cache->owner) 107 | { 108 | // unlink (surface->cachespots) from this surfacecache 109 | *(new_cache->owner) = NULL; 110 | } 111 | while (new_cache->size < total_size) 112 | { 113 | g_surfcache_memory.rover = g_surfcache_memory.rover->next; 114 | 115 | if (!g_surfcache_memory.rover) 116 | { 117 | g_platformAPI.SysError("SurfaceCacheAlloc: not enough memory!"); 118 | } 119 | 120 | if (g_surfcache_memory.rover->owner) 121 | { 122 | *(g_surfcache_memory.rover->owner) = NULL; 123 | } 124 | 125 | new_cache->next = g_surfcache_memory.rover->next; 126 | new_cache->size += g_surfcache_memory.rover->size; 127 | } 128 | 129 | // if new_cache is too big, carve out the rest 130 | if (new_cache->size - total_size > sizeof(SurfaceCache) + 256) 131 | { 132 | g_surfcache_memory.rover = (SurfaceCache *)((U8 *)new_cache + total_size); 133 | g_surfcache_memory.rover->size = new_cache->size - total_size; 134 | g_surfcache_memory.rover->next = new_cache->next; 135 | g_surfcache_memory.rover->owner = NULL; 136 | new_cache->next = g_surfcache_memory.rover; 137 | new_cache->size = total_size; 138 | } 139 | else 140 | { 141 | g_surfcache_memory.rover = new_cache->next; 142 | } 143 | 144 | new_cache->width = width; 145 | 146 | SurfaceCacheCheckCacheGuard(); 147 | 148 | return new_cache; 149 | } 150 | 151 | struct LightSurface 152 | { 153 | Surface *surface; 154 | Texture *texture; 155 | 156 | U8 *surface_cache_data; 157 | Fixed8 bright_adjusts[MAX_LIGHT_MAPS]; 158 | 159 | I32 mip_level; 160 | I32 mip_width_in_texel; 161 | I32 mip_height_in_texel; 162 | 163 | I32 lightblocks_width; 164 | I32 lightblocks_height; 165 | }; 166 | 167 | void AddDynamicLights(LightSurface *lightsurf, LightSystem *lightsystem) 168 | { 169 | Surface *surface = lightsurf->surface; 170 | TextureInfo *texinfo = surface->tex_info; 171 | 172 | for (I32 light_i = 0; light_i < MAX_LIGHT_NUM; ++light_i) 173 | { 174 | if ((surface->lightbits & (1 << light_i)) == 0) 175 | { 176 | continue; // not lit by this light 177 | } 178 | 179 | Light *light = lightsystem->lights + light_i; 180 | 181 | float light_to_surf_dist = Vec3Dot(light->position, surface->plane->normal) 182 | - surface->plane->distance; 183 | 184 | // TODO lw: ??? 185 | float dist_delta = light->radius - Absf(light_to_surf_dist); 186 | if (dist_delta < light->minlight) 187 | { 188 | continue; 189 | } 190 | float minlight = dist_delta - light->minlight; 191 | 192 | Vec3f light_on_surface_pos = light->position 193 | - surface->plane->normal * light_to_surf_dist; 194 | 195 | float light_u = Vec3Dot(light_on_surface_pos, texinfo->u_axis) 196 | + texinfo->u_offset - surface->uv_min[0]; 197 | 198 | float light_v = Vec3Dot(light_on_surface_pos, texinfo->v_axis) 199 | + texinfo->v_offset - surface->uv_min[1]; 200 | 201 | I32 u_delta = 0, v_delta = 0; 202 | float dist = 0; 203 | // uv starts at surface->uv_min 204 | for (I32 v_i = 0; v_i < lightsurf->lightblocks_height; ++v_i) 205 | { 206 | v_delta = (I32)(light_v - v_i * 16); 207 | if (v_delta < 0) 208 | { 209 | v_delta = -v_delta; 210 | } 211 | 212 | for (I32 u_i = 0; u_i < lightsurf->lightblocks_width; ++u_i) 213 | { 214 | u_delta = (I32)(light_u - u_i * 16); 215 | if (u_delta < 0) 216 | { 217 | u_delta = -u_delta; 218 | } 219 | // distance approximation? 220 | if (u_delta < v_delta) 221 | { 222 | dist = (float)(u_delta + (v_delta >> 1)); 223 | } 224 | else 225 | { 226 | dist = (float)(v_delta + (u_delta >> 1)); 227 | } 228 | 229 | if (dist < minlight) 230 | { 231 | // TODO lw: ? 232 | ((Fixed8 *)lightsystem->blocklights)[v_i * lightsurf->lightblocks_width + u_i] = 233 | (Fixed8)((dist_delta - dist) * 256); 234 | } 235 | } 236 | } 237 | } 238 | } 239 | 240 | // add calculate both static and dynamic lights 241 | void BuildLightMap(LightSurface *lightsurf, LightSystem *lightsystem, I32 framecount) 242 | { 243 | Surface *surface = lightsurf->surface; 244 | 245 | I32 lightsample_size = lightsurf->lightblocks_width * lightsurf->lightblocks_height; 246 | 247 | U8 *lightsamples = surface->samples; 248 | 249 | // Cvar *ambient_light = CvarGet("ambientlight"); 250 | 251 | Fixed8 *blocklights = (Fixed8 *)lightsystem->blocklights; 252 | 253 | // clear to ambient 254 | for (I32 i = 0; i < lightsample_size; ++i) 255 | { 256 | // blocklights[i] = (U32)ambient_light->val; 257 | blocklights[i] = 2 << 8; 258 | } 259 | if (lightsamples) 260 | { 261 | for (I32 lightmap = 0; 262 | lightmap < MAX_LIGHT_MAPS && surface->light_styles[lightmap] != 255; 263 | ++lightmap) 264 | { 265 | Fixed8 bright_adjust = lightsurf->bright_adjusts[lightmap]; 266 | 267 | // aggregate all lightmaps's values into blocklights 268 | for (I32 i = 0; i < lightsample_size; ++i) 269 | { 270 | blocklights[i] += lightsamples[i] * bright_adjust; 271 | // blocklights[i] += lightsamples[i] << 8; 272 | } 273 | lightsamples += lightsample_size; 274 | } 275 | } 276 | if (surface->lightframe == framecount) 277 | { 278 | AddDynamicLights(lightsurf, lightsystem); 279 | } 280 | 281 | for (I32 i = 0; i < lightsample_size; ++i) 282 | { 283 | /* 284 | The value of blocklight ranges from 0.0 to 255.0, but we only have 6 285 | bits for brightness. We need to scale it by multiplying (2^6 / 2^8). 286 | We also need to invert the value because in our colormap 63 is pitch 287 | black and 0 is brightest. 288 | */ 289 | I32 t = ((255 << 8) - blocklights[i]) >> (8 - COLOR_SHADE_BITS); 290 | if (t < (1 << COLOR_SHADE_BITS)) 291 | { 292 | t = 1 << COLOR_SHADE_BITS; 293 | } 294 | blocklights[i] = t; 295 | } 296 | } 297 | 298 | void LightTextureSurface(LightSurface *lightsurf, LightSystem *lightsystem, 299 | I32 framecount, U8 *colormap) 300 | { 301 | BuildLightMap(lightsurf, lightsystem, framecount); 302 | 303 | Texture *texture = lightsurf->texture; 304 | U8 *miptex = (U8 *)texture + texture->offsets[lightsurf->mip_level]; 305 | 306 | // sample at every 16 texel 307 | I32 lightsample_width = lightsurf->lightblocks_width; 308 | 309 | // width equals height, each lightblock spans over several texels 310 | I32 lightblocksize_in_texel = 16 >> lightsurf->mip_level; 311 | 312 | I32 mip_divshift = 4 - lightsurf->mip_level; 313 | 314 | // number of lightblocks 315 | I32 lightblock_num_h = lightsurf->mip_width_in_texel >> mip_divshift; 316 | I32 lightblock_num_v = lightsurf->mip_height_in_texel >> mip_divshift; 317 | 318 | // dimensions of mipmapped texture 319 | I32 miptex_width = texture->width >> lightsurf->mip_level; 320 | I32 miptex_height = texture->height >> lightsurf->mip_level; 321 | I32 miptex_size = miptex_width * miptex_height; 322 | 323 | I32 tex_offset_u = lightsurf->surface->uv_min[0]; 324 | I32 tex_offset_v = lightsurf->surface->uv_min[1]; 325 | 326 | // wrap tex_offset_u around texture boundary 327 | // (miptex_width << 16) guarantees positive value for % 328 | tex_offset_u = ((tex_offset_u >> lightsurf->mip_level) + (miptex_width << 16)) % miptex_width; 329 | tex_offset_v = ((tex_offset_v >> lightsurf->mip_level) + (miptex_height << 16)) % miptex_height; 330 | 331 | U8 *tex_src = &miptex[tex_offset_v * miptex_width]; 332 | I32 tex_src_stepback = miptex_width * miptex_height; 333 | U8 *tex_src_max = miptex + tex_src_stepback; 334 | 335 | U8 *surfcache_row = lightsurf->surface_cache_data; 336 | 337 | Fixed8 *blocklights = (Fixed8 *)lightsystem->blocklights; 338 | 339 | for (I32 u = 0; u < lightblock_num_h; ++u) 340 | { 341 | Fixed8 *lightsample_row = blocklights + u; 342 | U8 *surfcache_dest = surfcache_row; 343 | U8 *tex_src_row = tex_src + tex_offset_u; 344 | 345 | for (I32 v = 0; v < lightblock_num_v; ++v) 346 | { 347 | Fixed8 lightsample_left = lightsample_row[0]; 348 | Fixed8 lightsample_right = lightsample_row[1]; 349 | lightsample_row += lightsample_width; 350 | 351 | I32 lightsample_left_vstep = (lightsample_row[0] - lightsample_left) >> mip_divshift; 352 | I32 lightsample_right_vstep = (lightsample_row[1] - lightsample_right) >> mip_divshift; 353 | 354 | // bilinearly interpolate texels within the lightblock 355 | for (I32 y = 0; y < lightblocksize_in_texel; ++y) 356 | { 357 | I32 light_step = (lightsample_right - lightsample_left) >> mip_divshift; 358 | Fixed8 light_val = lightsample_left; 359 | 360 | for (I32 x = 0; x < lightblocksize_in_texel; ++x) 361 | { 362 | U8 texel = tex_src_row[x]; 363 | // first 0-255 bits determines the color of the pixel, and 364 | //(light_val & 0xff00) detemines the shade. 365 | I32 color_index = (light_val & 0xff00) + texel; 366 | surfcache_dest[x] = colormap[color_index]; 367 | light_val += light_step; 368 | } 369 | 370 | lightsample_left += lightsample_left_vstep; 371 | lightsample_right += lightsample_right_vstep; 372 | tex_src_row += miptex_width; // move up one row 373 | surfcache_dest += lightsurf->mip_width_in_texel; // move up one row 374 | } 375 | 376 | if (tex_src_row >= tex_src_max) 377 | { 378 | tex_src_row -= tex_src_stepback; 379 | } 380 | } 381 | 382 | tex_offset_u += lightblocksize_in_texel; 383 | if (tex_offset_u > miptex_width) 384 | { 385 | tex_offset_u = 0; 386 | } 387 | 388 | surfcache_row += lightblocksize_in_texel; 389 | } 390 | } 391 | 392 | Texture *AnimateTexture(Texture *base_tex) 393 | { 394 | return base_tex; 395 | } 396 | 397 | SurfaceCache *CacheSurface(Surface *surface, I32 miplevel, LightSystem *lightsystem, 398 | I32 framecount, U8 *colormap) 399 | { 400 | LightSurface lightsurf; 401 | 402 | // if the surface is animating or flashing, flush the code 403 | lightsurf.texture = AnimateTexture(surface->tex_info->texture); 404 | 405 | lightsurf.bright_adjusts[0] = lightsystem->styles[surface->light_styles[0]].cur_value; 406 | lightsurf.bright_adjusts[1] = lightsystem->styles[surface->light_styles[1]].cur_value; 407 | lightsurf.bright_adjusts[2] = lightsystem->styles[surface->light_styles[2]].cur_value; 408 | lightsurf.bright_adjusts[3] = lightsystem->styles[surface->light_styles[3]].cur_value; 409 | 410 | // light sample at every 16 texel 411 | lightsurf.lightblocks_width = (surface->uv_extents[0] >> 4) + 1; 412 | lightsurf.lightblocks_height = (surface->uv_extents[1] >> 4) + 1; 413 | 414 | SurfaceCache *surface_cache = surface->cachespots[miplevel]; 415 | 416 | // check if cache is still valid 417 | // TODO lw: why surface->lightframe != frameount? 418 | if (surface_cache && !surface_cache->dlight && surface->lightframe != framecount 419 | && surface_cache->bright_adjusts[0] == lightsurf.bright_adjusts[0] 420 | && surface_cache->bright_adjusts[1] == lightsurf.bright_adjusts[1] 421 | && surface_cache->bright_adjusts[2] == lightsurf.bright_adjusts[2] 422 | && surface_cache->bright_adjusts[3] == lightsurf.bright_adjusts[3]) 423 | { 424 | return surface_cache; 425 | } 426 | 427 | float surf_scale = 1.0f / (1 << miplevel); 428 | lightsurf.mip_level = miplevel; 429 | // surface cache's width and height are mip adjusted and cropped to surface size 430 | lightsurf.mip_width_in_texel = surface->uv_extents[0] >> miplevel; 431 | lightsurf.mip_height_in_texel = surface->uv_extents[1] >> miplevel; 432 | 433 | if (!surface_cache) 434 | { 435 | I32 total_size = lightsurf.mip_width_in_texel * lightsurf.mip_height_in_texel; 436 | surface_cache = SurfaceCacheAlloc(lightsurf.mip_width_in_texel, total_size); 437 | 438 | surface_cache->height = lightsurf.mip_height_in_texel; 439 | surface->cachespots[miplevel] = surface_cache; 440 | surface_cache->owner = &(surface->cachespots[miplevel]); 441 | // surface_cache->mipscale = surf_scale; 442 | } 443 | 444 | surface_cache->dlight = surface->lightframe == framecount ? 1 : 0; // TODO lw: ? 445 | lightsurf.surface_cache_data = surface_cache->data; 446 | 447 | surface_cache->bright_adjusts[0] = lightsurf.bright_adjusts[0]; 448 | surface_cache->bright_adjusts[1] = lightsurf.bright_adjusts[1]; 449 | surface_cache->bright_adjusts[2] = lightsurf.bright_adjusts[2]; 450 | surface_cache->bright_adjusts[3] = lightsurf.bright_adjusts[3]; 451 | 452 | lightsurf.surface = surface; 453 | LightTextureSurface(&lightsurf, lightsystem, framecount, colormap); 454 | 455 | return surface_cache; 456 | } 457 | 458 | void AnimateLights(LightSystem *lightsystem, I32 framecount) 459 | { 460 | // scale ('a'-'m') to (0-255) 461 | const I32 bright_scale = (I32)(255.0f / 12.0f); 462 | 463 | I32 t = (I32)(framecount * 0.7f); 464 | for (I32 i = 0; i < MAX_LIGHT_STYLE_NUM; ++i) 465 | { 466 | LightStyle *style = lightsystem->styles + i; 467 | if (style->length) 468 | { 469 | I32 k = t % style->length; 470 | k = style->wave[k] - 'a'; 471 | k *= bright_scale; 472 | style->cur_value = k; 473 | } 474 | else 475 | { 476 | style->cur_value = 256; 477 | } 478 | } 479 | } 480 | 481 | void MarkLight(Light *light, I32 lightbit, Node *node, Surface *allsurfaces, I32 light_framecount) 482 | { 483 | // skip if contents is a leaf ? 484 | if (node->contents < 0) 485 | { 486 | return ; 487 | } 488 | 489 | Plane *splitplane = node->plane; 490 | float d = Vec3Dot(light->position, splitplane->normal) - splitplane->distance; 491 | 492 | // light is at normal side, but too far fram the plane 493 | if (d > light->radius) 494 | { 495 | MarkLight(light, lightbit, node->children[1], allsurfaces, light_framecount); 496 | return ; 497 | } 498 | // light is at opposite normal side, but too far from the plane 499 | if (d < -light->radius) 500 | { 501 | MarkLight(light, lightbit, node->children[0], allsurfaces, light_framecount); 502 | return ; 503 | } 504 | 505 | // mark all surfaces on this plane as being affected by the light 506 | Surface *surface = allsurfaces + node->firstsurface; 507 | for (I32 i = 0; i < node->numsurface; ++i) 508 | { 509 | // Clear lightbits from previous frame first 510 | if (surface->lightframe != light_framecount) 511 | { 512 | surface->lightbits = 0; 513 | surface->lightframe = light_framecount; 514 | } 515 | surface->lightbits |= lightbit; 516 | } 517 | 518 | MarkLight(light, lightbit, node->children[1], allsurfaces, light_framecount); 519 | MarkLight(light, lightbit, node->children[0], allsurfaces, light_framecount); 520 | } 521 | 522 | void PushLights(LightSystem *lightsystem, float dt, Node *world_nodes, 523 | I32 frame_count, Surface *allsurfaces) 524 | { 525 | lightsystem->light_framecount = frame_count; 526 | Light *lights = lightsystem->lights; 527 | 528 | for (I32 i = 0; i < MAX_LIGHT_NUM; ++i) 529 | { 530 | Light *light = lights + i; 531 | 532 | light->time_passed += dt; 533 | if ((light->time_passed < light->duration) && light->radius != 0) 534 | { 535 | MarkLight(light, 1 << i, world_nodes, allsurfaces, lightsystem->light_framecount); 536 | } 537 | } 538 | } 539 | -------------------------------------------------------------------------------- /code/q_lightmap.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MAX_LIGHT_STYLE_NUM 64 4 | #define MAX_LIGHT_NUM 32 5 | 6 | struct LightStyle 7 | { 8 | I32 length; 9 | // Each char represents the brightness for every 0.1 second, 'a' means no 10 | // light, 'm' normal bright, 'z' double birght. 11 | char wave[64]; 12 | Fixed8 cur_value; 13 | }; 14 | 15 | struct Light 16 | { 17 | Vec3f position; 18 | float radius; 19 | float decay; 20 | float minlight; 21 | I32 key; 22 | float duration; 23 | float time_passed; 24 | }; 25 | 26 | struct LightSystem 27 | { 28 | I32 light_framecount; 29 | 30 | LightStyle styles[MAX_LIGHT_STYLE_NUM]; 31 | Light lights[MAX_LIGHT_NUM]; 32 | 33 | // Store light brightness, quake allots 6 bit for different brightness. 34 | Fixed8 blocklights[18 * 18]; 35 | }; 36 | -------------------------------------------------------------------------------- /code/q_math.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Remove this after implementing math functions ourselves. 4 | #include 5 | 6 | #define PI32 3.1415926535897932f 7 | 8 | inline float Clamp(float min, float max, float x) 9 | { 10 | if (x < min) 11 | x = min; 12 | if (x > max) 13 | x = max; 14 | return x; 15 | } 16 | 17 | inline Fixed16 Clamp(Fixed16 min, Fixed16 max, Fixed16 x) 18 | { 19 | if (x < min) 20 | x = min; 21 | if (x > max) 22 | x = max; 23 | return x; 24 | } 25 | 26 | inline float Absf(float x) 27 | { 28 | float result = fabsf(x); 29 | return result; 30 | } 31 | 32 | inline float DegreeToRadian(float angle) 33 | { 34 | float result = angle / 180.0f * PI32; 35 | return result; 36 | } 37 | 38 | inline float SquareRoot(float x) 39 | { 40 | float result = sqrtf(x); 41 | return result; 42 | } 43 | 44 | inline float Power(float base, float exponent) 45 | { 46 | float result = powf(base, exponent); 47 | return result; 48 | } 49 | 50 | inline float InvSquareRoot(float x) 51 | { 52 | float result = 1.0f / SquareRoot(x); 53 | return result; 54 | } 55 | 56 | inline float Sine(float x) 57 | { 58 | float result = sinf(x); 59 | return result; 60 | } 61 | 62 | inline float Cosine(float x) 63 | { 64 | float result = cosf(x); 65 | return result; 66 | } 67 | 68 | inline float Tangent(float x) 69 | { 70 | float result = tanf(x); 71 | return result; 72 | } 73 | 74 | void BuildSineTable(I32 *sine_table, I32 table_size, I32 sample_size, 75 | float offset_y, float amplifier) 76 | { 77 | for (I32 i = 0; i < table_size; ++i) 78 | { 79 | float sample_step = 2.0f * PI32 * i / sample_size; 80 | sine_table[i] = (I32)(amplifier * (Sine(sample_step) + offset_y)); 81 | } 82 | } 83 | 84 | //====================================================== 85 | // Linear Algebra 86 | //====================================================== 87 | 88 | struct Vec2f 89 | { 90 | float & operator[](int index) 91 | { 92 | ASSERT(index >= 0); 93 | ASSERT(index < 2); 94 | return ((float *)this)[index]; 95 | } 96 | 97 | union 98 | { 99 | struct {float x, y;}; 100 | struct {float s, t;}; 101 | struct {float u, v;}; 102 | }; 103 | }; 104 | 105 | struct Vec2i 106 | { 107 | union 108 | { 109 | struct {I32 x, y;}; 110 | struct {I32 s, t;}; 111 | struct {I32 u, v;}; 112 | }; 113 | }; 114 | 115 | struct Vec3f 116 | { 117 | float & operator[](int index) 118 | { 119 | ASSERT(index >= 0); 120 | ASSERT(index < 3); 121 | return ((float *)this)[index]; 122 | } 123 | 124 | void operator+=(const Vec3f &rhv) 125 | { 126 | x += rhv.x; 127 | y += rhv.y; 128 | z += rhv.z; 129 | } 130 | 131 | void operator-=(const Vec3f &rhv) 132 | { 133 | x -= rhv.x; 134 | y -= rhv.y; 135 | z -= rhv.z; 136 | } 137 | 138 | union 139 | { 140 | struct {float x, y, z;}; 141 | struct {float r, g, b;}; 142 | }; 143 | }; 144 | 145 | struct Vec4f 146 | { 147 | float & operator[](int index) 148 | { 149 | ASSERT(index >= 0); 150 | ASSERT(index < 4); 151 | return ((float *)this)[index]; 152 | } 153 | 154 | union 155 | { 156 | struct {float x, y, z, w;}; 157 | struct {float r, g, b, a;}; 158 | }; 159 | }; 160 | 161 | inline float Vec3Dot(const Vec3f &lhv, const Vec3f &rhv) 162 | { 163 | float result = lhv.x * rhv.x + lhv.y * rhv.y + lhv.z * rhv.z; 164 | return result; 165 | } 166 | 167 | inline float Vec3Length(const Vec3f& v) 168 | { 169 | float result = SquareRoot(Vec3Dot(v, v)); 170 | return result; 171 | } 172 | 173 | inline Vec3f Vec3Normalize(const Vec3f &v) 174 | { 175 | float invlen = InvSquareRoot(Vec3Dot(v, v)); 176 | Vec3f result = {v.x * invlen, v.y * invlen, v.z * invlen}; 177 | return result; 178 | } 179 | 180 | B32 operator==(const Vec3f &lhv, const Vec3f &rhv) 181 | { 182 | B32 result = (lhv.x == rhv.x && lhv.y == rhv.y && lhv.z == rhv.z); 183 | return result; 184 | } 185 | 186 | inline Vec3f operator+(const Vec3f &lhv, const Vec3f &rhv) 187 | { 188 | Vec3f result = {lhv.x + rhv.x, lhv.y + rhv.y, lhv.z + rhv.z}; 189 | return result; 190 | } 191 | 192 | inline Vec3f operator-(const Vec3f &lhv, const Vec3f &rhv) 193 | { 194 | Vec3f result = {lhv.x - rhv.x, lhv.y - rhv.y, lhv.z - rhv.z}; 195 | return result; 196 | } 197 | 198 | inline Vec3f operator*(float scalar, const Vec3f &rhv) 199 | { 200 | Vec3f result = {scalar * rhv.x, scalar * rhv.y, scalar * rhv.z}; 201 | return result; 202 | } 203 | 204 | inline Vec3f operator*(const Vec3f &lhv, float scalar) 205 | { 206 | Vec3f result = scalar * lhv; 207 | return result; 208 | } 209 | 210 | /* 211 | TODO lw: better explanation of matrix 212 | 213 | Matrix is laid out in memory as row major, meaning 214 | matrix[0-3] is the first row, 215 | matrix[4-7] is the second row, 216 | matrix[8-11] is the third row, 217 | matrix[12-15] is the fourth row. 218 | 219 | Note, memory layout has nothing to do with the way matrices multiply with 220 | vectors. It depends on your coordinate system. 221 | */ 222 | 223 | struct Mat3 224 | { 225 | const float *operator[](int index) const 226 | { 227 | ASSERT(index >= 0); 228 | ASSERT(index < 3); 229 | return &(values[index][0]); 230 | } 231 | 232 | float *operator[](int index) 233 | { 234 | ASSERT(index >= 0); 235 | ASSERT(index < 3); 236 | return &(values[index][0]); 237 | } 238 | 239 | float values[3][3]; 240 | }; 241 | 242 | struct Mat4 243 | { 244 | float values[4][4]; 245 | }; 246 | 247 | inline Vec3f operator*(const Mat3 m3, const Vec3f v3) 248 | { 249 | Vec3f result = { 250 | m3[0][0] * v3.x + m3[0][1] * v3.y + m3[0][2] * v3.z, 251 | m3[1][0] * v3.x + m3[1][1] * v3.y + m3[1][2] * v3.z, 252 | m3[2][0] * v3.x + m3[2][1] * v3.y + m3[2][2] * v3.z 253 | }; 254 | return result; 255 | } 256 | 257 | void AngleVectors(Vec3f angles, Vec3f *vx, Vec3f *vy, Vec3f *vz) 258 | { 259 | float radian = 0; 260 | radian = DegreeToRadian(angles[0]); 261 | float sinx = Sine(radian); 262 | float cosx = Cosine(radian); 263 | radian = DegreeToRadian(angles[1]); 264 | float siny = Sine(radian); 265 | float cosy = Cosine(radian); 266 | radian = DegreeToRadian(angles[2]); 267 | float sinz = Sine(radian); 268 | float cosz = Cosine(radian); 269 | 270 | /* 271 | Simple trigonometry could be used to build rotation matrices around the X, 272 | Y, Z axes in world space. And since we are always rotating around the world 273 | Z axis, we could rotate around the local x axis first which is coincident 274 | with world x axis initially, then rotate around world axis. 275 | */ 276 | *vx = {cosz, -sinz, 0}; 277 | *vy = {sinz * cosx, cosz * cosx, sinx}; 278 | *vz = {-sinz * sinx, -cosz * sinx, cosx}; 279 | } 280 | -------------------------------------------------------------------------------- /code/q_model.cpp: -------------------------------------------------------------------------------- 1 | #include "q_platform.h" 2 | #include "q_model.h" 3 | 4 | /* 5 | * floor, ceil 6 | */ 7 | #include 8 | 9 | #define BSPVERSION 29 10 | 11 | #define IDSPRITEHEADER (('P'<<24)+('S'<<16)+('D'<<8)+'I') 12 | #define IDPOLYHEADER (('O'<<24)+('P'<<16)+('D'<<8)+'I') 13 | 14 | #define MAX_MAP_LEAVES 8192 15 | 16 | struct VertexDisk 17 | { 18 | Vec3f position; 19 | }; 20 | 21 | struct EdgeDisk 22 | { 23 | U16 vertIndex[2]; 24 | }; 25 | 26 | struct PlaneDisk 27 | { 28 | Vec3f normal; 29 | float distance; 30 | int type; 31 | }; 32 | 33 | struct MipTexLump 34 | { 35 | int numMipTex; 36 | U32 dataOffsets[1]; // [numMipTex], serve as starting pointer 37 | }; 38 | 39 | struct TextureInfoDisk 40 | { 41 | float vecs[2][4]; 42 | int mipTexIndex; 43 | int flags; 44 | }; 45 | 46 | struct MipTexture 47 | { 48 | char name[16]; 49 | U32 width; 50 | U32 height; 51 | U32 offsets[MIP_LEVELS]; 52 | }; 53 | 54 | struct FaceDisk 55 | { 56 | I16 planeOffset; 57 | I16 side; 58 | 59 | int firstEdge; 60 | I16 numEdge; 61 | I16 texInfoOffset; 62 | 63 | U8 light_styles[MAX_LIGHT_MAPS]; 64 | int lightOffset; 65 | }; 66 | 67 | struct LeafDisk 68 | { 69 | int contents; 70 | int visibilityOffset; // -1 = no visibility info 71 | 72 | // for frustum culling 73 | I16 mins[3]; 74 | I16 maxs[3]; 75 | 76 | U16 firstMarksurface; 77 | U16 numMarksurface; 78 | 79 | U8 ambientLevel[NUM_AMBIENT_SOUND]; 80 | }; 81 | 82 | struct NodeDisk 83 | { 84 | I32 planeOffset; 85 | I16 childOffsets[2]; // negative numbers are -(leaves + 1), not node 86 | I16 mins[3]; 87 | I16 maxs[3]; 88 | U16 firstFace; 89 | U16 numFace; // counting both sides 90 | }; 91 | 92 | // used to reference data in files in pack 93 | struct Lump 94 | { 95 | int offset; 96 | int length; 97 | }; 98 | 99 | enum ModelLump 100 | { 101 | ENTITY = 0, 102 | PLANE, 103 | TEXTURE, 104 | VERTEX, 105 | VISIBILITY, 106 | NODE, 107 | TEXTUREINFO, 108 | FACE, 109 | LIGHTING, 110 | CLIPNODE, 111 | LEAF, 112 | MARKSURFACE, 113 | EDGE, 114 | SURFACEEDGE, 115 | SUBMODEL, 116 | COUNT 117 | }; 118 | 119 | struct ModelHeaderDisk 120 | { 121 | int version; 122 | Lump lumps[ModelLump::COUNT]; 123 | }; 124 | 125 | Texture *g_defaultTexture; 126 | 127 | Texture *TextureCreateDefault() 128 | { 129 | // create a simple checkerboard texture 130 | 131 | Texture *tx = NULL; 132 | int size = sizeof(*tx) + 64 * 64 + 16 * 16 + 8 * 8 + 4 * 4; 133 | tx = (Texture *)HunkLowAlloc(size, "defaulttexture"); 134 | StringCopy(tx->name, 16, "default"); 135 | 136 | tx->width = 64; 137 | tx->height = 64; 138 | tx->offsets[0] = sizeof(*tx); 139 | tx->offsets[1] = sizeof(*tx) + 64 * 64; 140 | tx->offsets[2] = sizeof(*tx) + 64 * 64 + 16 * 16; 141 | tx->offsets[3] = sizeof(*tx) + 64 * 64 + 16 * 16 + 8 * 8; 142 | 143 | // 4 mipmaps 144 | for (int i = 0; i < 4; ++i) 145 | { 146 | U8 *dest = (U8 *)tx + tx->offsets[i]; 147 | 148 | int height = tx->height >> i; 149 | int width = tx->width >> i; 150 | 151 | for (int y = 0; y < height; ++y) 152 | { 153 | for (int x = 0; x < width; ++x) 154 | { 155 | int cy = (y >> (4 - i)) & 0x1; 156 | int cx = (x >> (4 - i)) & 0x1; 157 | if (cy ^ cx) 158 | { 159 | *dest = 0; 160 | } 161 | else 162 | { 163 | *dest = 0xff; 164 | } 165 | dest++; 166 | } 167 | } 168 | } 169 | 170 | return tx; 171 | } 172 | 173 | void 174 | ModelLoadVertices(Model *model, U8 *base, Lump lump) 175 | { 176 | VertexDisk *vertDisk = (VertexDisk *)(base + lump.offset); 177 | if (lump.length % sizeof(*vertDisk)) 178 | { 179 | g_platformAPI.SysError("incorrect lump size for vertex"); 180 | } 181 | 182 | int vertCount = lump.length / sizeof(*vertDisk); 183 | Vertex *vert = NULL; 184 | vert = (Vertex *)HunkLowAlloc(vertCount * sizeof(*vert), model->name); 185 | 186 | model->vertices = vert; 187 | model->numVert = vertCount; 188 | 189 | for (int i = 0; i < vertCount; ++i, ++vert, ++vertDisk) 190 | { 191 | vert->position = vertDisk->position; 192 | } 193 | } 194 | 195 | void 196 | ModelLoadEdges(Model *model, U8 *base, Lump lump) 197 | { 198 | EdgeDisk *edgeDisk = (EdgeDisk *)(base + lump.offset); 199 | if (lump.length % sizeof(*edgeDisk)) 200 | { 201 | g_platformAPI.SysError("incorrect lump size for edges"); 202 | } 203 | 204 | int edgeCount = lump.length / sizeof(*edgeDisk); 205 | Edge *edge = NULL; 206 | edge = (Edge *)HunkLowAlloc(edgeCount * sizeof(*edge), model->name); 207 | MemSet(edge, 0, edgeCount * sizeof(*edge)); 208 | 209 | model->edges = edge; 210 | model->numEdge = edgeCount; 211 | 212 | for (int i = 0; i < edgeCount; ++i, ++edge, ++edgeDisk) 213 | { 214 | edge->vertIndex[0] = edgeDisk->vertIndex[0]; 215 | edge->vertIndex[1] = edgeDisk->vertIndex[1]; 216 | } 217 | } 218 | 219 | void 220 | ModelLoadSurfaceEdges(Model *model, U8 *base, Lump lump) 221 | { 222 | int *surfaceEdgeDisk = (int *)(base + lump.offset); 223 | if (lump.length % sizeof(*surfaceEdgeDisk)) 224 | { 225 | g_platformAPI.SysError("incorrect lump size for surface edge"); 226 | } 227 | 228 | int surfaceEdgeCount = lump.length / sizeof(*surfaceEdgeDisk); 229 | int *surfaceEdge = NULL; 230 | surfaceEdge = (int *)HunkLowAlloc(surfaceEdgeCount * sizeof(*surfaceEdge), model->name); 231 | 232 | model->surfaceEdges = surfaceEdge; 233 | model->numSurfaceEdge = surfaceEdgeCount; 234 | 235 | for (int i = 0; i < surfaceEdgeCount; ++i) 236 | { 237 | surfaceEdge[i] = surfaceEdgeDisk[i]; 238 | } 239 | } 240 | 241 | void 242 | ModelLoadPlanes(Model *model, U8 *base, Lump lump) 243 | { 244 | PlaneDisk *planeDisk = (PlaneDisk *)(base + lump.offset); 245 | if (lump.length % sizeof(*planeDisk)) 246 | { 247 | g_platformAPI.SysError("incorrect lump size for planes"); 248 | } 249 | 250 | I32 planeCount = lump.length / sizeof(*planeDisk); 251 | Plane *plane = NULL; 252 | 253 | // TODO lw: why allocated twice amount of memory? 254 | plane = (Plane *)HunkLowAlloc(planeCount * 2 * sizeof(*plane), model->name); 255 | 256 | model->planes = plane; 257 | model->numPlane = planeCount; 258 | 259 | for (I32 i = 0; i < planeCount; ++i, ++plane, ++planeDisk) 260 | { 261 | U8 bits = 0; 262 | for (I32 j = 0; j < 3; ++j) 263 | { 264 | plane->normal[j] = planeDisk->normal[j]; 265 | if (plane->normal[j] < 0) 266 | { 267 | bits |= 1 << j; 268 | } 269 | } 270 | 271 | plane->distance = planeDisk->distance; 272 | plane->type = (U8)planeDisk->type; 273 | plane->signBits = bits; 274 | } 275 | } 276 | 277 | void 278 | ModelLoadTextures(Model *model, U8 *base, Lump lump) 279 | { 280 | if (!lump.length) 281 | { 282 | model->textures = NULL; 283 | } 284 | 285 | MipTexLump *miptexLump = (MipTexLump *)(base + lump.offset); 286 | MipTexture *mipTex = NULL; 287 | 288 | model->numTexture = miptexLump->numMipTex; 289 | model->textures = (Texture **)HunkLowAlloc( 290 | miptexLump->numMipTex * sizeof(*model->textures), model->name); 291 | 292 | if ((miptexLump->numMipTex + 1) * sizeof(int) != miptexLump->dataOffsets[0]) 293 | { 294 | g_platformAPI.SysError("texture data is corrupted!"); 295 | } 296 | 297 | Texture *tx = NULL; 298 | for (int i = 0; i < miptexLump->numMipTex; ++i) 299 | { 300 | if (miptexLump->dataOffsets[i] == -1) 301 | { 302 | continue; 303 | } 304 | 305 | mipTex = (MipTexture *)((I8 *)miptexLump + miptexLump->dataOffsets[i]); 306 | 307 | if (mipTex->width & 15 || mipTex->height & 15) 308 | { 309 | g_platformAPI.SysError("Texture %s is 16-byte aligned"); 310 | } 311 | 312 | // w*h + w/2*h/2 + w/4*h/4 + w/8*h/8 313 | int pixelCount = mipTex->width * mipTex->height * 85 / 64; 314 | tx = (Texture *)HunkLowAlloc(sizeof(*tx) + pixelCount, model->name); 315 | model->textures[i] = tx; 316 | 317 | StringCopy(tx->name, sizeof(tx->name), mipTex->name); 318 | tx->width = mipTex->width; 319 | tx->height = mipTex->height; 320 | for (int j = 0; j < MIP_LEVELS; ++j) 321 | { 322 | tx->offsets[j] = mipTex->offsets[j] - sizeof(*mipTex) + sizeof(*tx); 323 | } 324 | MemCpy(tx + 1, mipTex + 1, pixelCount); 325 | 326 | if (StringNCompare(tx->name, "sky", 3) == 0) 327 | { 328 | SkyInit(&g_skycanvas, tx); 329 | } 330 | } 331 | } 332 | 333 | void 334 | ModelLoadLighting(Model *model, U8 *base, Lump lump) 335 | { 336 | if (lump.length == 0) 337 | { 338 | model->light_data = NULL; 339 | } 340 | else 341 | { 342 | model->light_data = (U8 *)HunkLowAlloc(lump.length, model->name); 343 | MemCpy(model->light_data, base + lump.offset, lump.length); 344 | } 345 | } 346 | 347 | void 348 | ModelLoadTextureInfo(Model *model, U8 *base, Lump lump) 349 | { 350 | TextureInfoDisk *texInfoDisk = (TextureInfoDisk *)(base + lump.offset); 351 | if (lump.length % sizeof(*texInfoDisk)) 352 | { 353 | g_platformAPI.SysError("incorrect lump size for texture info"); 354 | } 355 | 356 | int count = lump.length / sizeof(*texInfoDisk); 357 | TextureInfo *tex_info = NULL; 358 | tex_info = (TextureInfo *)HunkLowAlloc(count * sizeof(*tex_info), model->name); 359 | 360 | model->numTexInfo = count; 361 | model->tex_info = tex_info; 362 | 363 | for (int i = 0; i < count; ++i, ++texInfoDisk, ++tex_info) 364 | { 365 | MemCpy(tex_info, texInfoDisk, 8 * sizeof(float)); 366 | 367 | float u_length = Vec3Length(tex_info->u_axis); 368 | float v_length = Vec3Length(tex_info->v_axis); 369 | float length = (u_length + v_length) / 2.0f; 370 | 371 | // decide the mipmap level 372 | // TODO lw: why use length as as reference 373 | if (length < 0.32f) 374 | { 375 | tex_info->mip_adjust = 4; 376 | } 377 | else if (length < 0.49f) 378 | { 379 | tex_info->mip_adjust = 3; 380 | } 381 | else if (length < 0.99f) 382 | { 383 | tex_info->mip_adjust = 2; 384 | } 385 | else 386 | { 387 | tex_info->mip_adjust = 1; 388 | } 389 | 390 | tex_info->flags = texInfoDisk->flags; 391 | 392 | if (!model->textures) 393 | { 394 | tex_info->texture = g_defaultTexture; 395 | tex_info->flags = 0; 396 | } 397 | else 398 | { 399 | if (texInfoDisk->mipTexIndex >= model->numTexture) 400 | { 401 | g_platformAPI.SysError("mip texture index too big"); 402 | } 403 | 404 | tex_info->texture = model->textures[texInfoDisk->mipTexIndex]; 405 | if (!tex_info->texture) 406 | { 407 | tex_info->texture = g_defaultTexture; 408 | tex_info->flags = 0; 409 | } 410 | } 411 | } 412 | } 413 | 414 | void 415 | CalcTexCoordExtents(Model *model, Surface *surface) 416 | { 417 | Vec2f min = {9999, 9999}; 418 | Vec2f max = {-9999, -9999}; 419 | 420 | for (int i = 0; i < surface->numEdge; ++i) 421 | { 422 | int edgeIndex = model->surfaceEdges[surface->firstEdge + i]; 423 | 424 | Vertex vert; 425 | if (edgeIndex >= 0) 426 | { 427 | vert = model->vertices[model->edges[edgeIndex].vertIndex[0]]; 428 | } 429 | else 430 | { 431 | vert = model->vertices[model->edges[-edgeIndex].vertIndex[1]]; 432 | } 433 | 434 | float u = Vec3Dot(surface->tex_info->u_axis, vert.position) + surface->tex_info->u_offset; 435 | float v = Vec3Dot(surface->tex_info->v_axis, vert.position) + surface->tex_info->v_offset; 436 | 437 | if (u < min.u) 438 | { 439 | min.u = u; 440 | } 441 | if (u > max.u) 442 | { 443 | max.u = u; 444 | } 445 | 446 | if (v < min.v) 447 | { 448 | min.v = v; 449 | } 450 | if (v > max.v) 451 | { 452 | max.v = v; 453 | } 454 | } 455 | 456 | int bmins[2]; 457 | int bmaxs[2]; 458 | for (int i = 0; i < 2; ++i) 459 | { 460 | // make bmins and bmaxs discrete at multiples of 16 461 | bmins[i] = (int)floor(min[i] / 16); 462 | bmaxs[i] = (int)ceil(max[i] / 16); 463 | 464 | surface->uv_min[i] = (I16)(bmins[i] * 16); 465 | surface->uv_extents[i] = (I16)((bmaxs[i] - bmins[i]) * 16); 466 | 467 | ASSERT(surface->uv_extents[i] > 0); 468 | 469 | if (!(surface->tex_info->flags & TEX_SPECIAL) && surface->uv_extents[i] > 256) 470 | { 471 | g_platformAPI.SysError("bad surface uv_extents"); 472 | } 473 | } 474 | } 475 | 476 | void 477 | ModelLoadFaces(Model *model, U8 *base, Lump lump) 478 | { 479 | FaceDisk * faceDisk= (FaceDisk *)(base + lump.offset); 480 | if (lump.length % sizeof(*faceDisk)) 481 | { 482 | g_platformAPI.SysError("incorrect lump size for surface"); 483 | } 484 | 485 | I32 count = lump.length / sizeof(*faceDisk); 486 | 487 | Surface *surface = NULL; 488 | surface = (Surface *)HunkLowAlloc(count * sizeof(*surface), model->name); 489 | 490 | MemSet(surface, 0, count * sizeof(*surface)); 491 | 492 | model->surfaces = surface; 493 | model->numSurface = count; 494 | 495 | for (I32 i = 0; i < count; ++i, ++faceDisk, ++surface) 496 | { 497 | surface->firstEdge = faceDisk->firstEdge; 498 | surface->numEdge = faceDisk->numEdge; 499 | surface->flags = 0; 500 | 501 | if (faceDisk->side) 502 | { 503 | surface->flags |= SURF_PLANE_BACK; 504 | } 505 | 506 | surface->plane = model->planes + faceDisk->planeOffset; 507 | surface->tex_info = model->tex_info + faceDisk->texInfoOffset; 508 | 509 | CalcTexCoordExtents(model, surface); 510 | 511 | // load lighting info 512 | for (I32 j = 0; j < MAX_LIGHT_MAPS; ++j) 513 | { 514 | surface->light_styles[j] = faceDisk->light_styles[j]; 515 | } 516 | 517 | if (faceDisk->lightOffset == -1) 518 | { 519 | surface->samples = NULL; 520 | } 521 | else 522 | { 523 | surface->samples = model->light_data + faceDisk->lightOffset; 524 | } 525 | 526 | // set drawing flags 527 | if (StringNCompare(surface->tex_info->texture->name, "sky", 3) == 0) 528 | { 529 | surface->flags |= SURF_DRAW_SKY | SURF_DRAW_TILED; 530 | } 531 | else if (StringNCompare(surface->tex_info->texture->name, "*", 1) == 0) 532 | { 533 | surface->flags |= SURF_DRAW_TURB | SURF_DRAW_TILED; 534 | for (I32 j = 0; j < 2; ++j) 535 | { 536 | // TODO wl: ? 537 | surface->uv_min[j] = -8192; 538 | surface->uv_extents[j] = 16384; 539 | } 540 | } 541 | } 542 | } 543 | 544 | void 545 | ModelLoadMarkSurfaces(Model *model, U8 *base, Lump lump) 546 | { 547 | I16 *markSurfOffset = (I16 *)(base + lump.offset); 548 | if (lump.length % sizeof(*markSurfOffset)) 549 | { 550 | g_platformAPI.SysError("incorrect lump size for mark surface offsets"); 551 | } 552 | 553 | int count = lump.length / sizeof(*markSurfOffset); 554 | Surface **marksurface = NULL; 555 | marksurface = (Surface **)HunkLowAlloc(count * sizeof(*marksurface), model->name); 556 | 557 | model->marksurfaces = marksurface; 558 | model->numMarksurface = count; 559 | 560 | for (int i = 0; i < count; ++i) 561 | { 562 | if (markSurfOffset[i] >= model->numSurface) 563 | { 564 | g_platformAPI.SysError("ModelLoadMarkSurfaces: bad marksurface"); 565 | } 566 | marksurface[i] = model->surfaces + markSurfOffset[i]; 567 | } 568 | } 569 | 570 | void 571 | ModelLoadVisibility(Model *model, U8 *base, Lump lump) 572 | { 573 | if (!lump.length) 574 | { 575 | model->visibility = NULL; 576 | return ; 577 | } 578 | 579 | model->visibility = (U8 *)HunkLowAlloc(lump.length, model->name); 580 | MemCpy(model->visibility, base + lump.offset, lump.length); 581 | } 582 | 583 | void 584 | ModelLoadLeaves(Model *model, U8 *base, Lump lump) 585 | { 586 | LeafDisk *leafDisk = (LeafDisk *)(base + lump.offset); 587 | if (lump.length % sizeof(*leafDisk)) 588 | { 589 | g_platformAPI.SysError("incorrect lump size for leaf"); 590 | } 591 | 592 | int count = lump.length / sizeof(*leafDisk); 593 | 594 | Leaf *leaf = NULL; 595 | leaf = (Leaf *)HunkLowAlloc(count * sizeof(*leaf), model->name); 596 | MemSet(leaf, 0, count * sizeof(*leaf)); 597 | 598 | model->leaves = leaf; 599 | model->numLeaf = count; 600 | 601 | for (int i = 0; i < count; ++i, ++leafDisk, ++leaf) 602 | { 603 | for (int j = 0; j < 3; ++j) 604 | { 605 | leaf->minmax[j] = leafDisk->mins[j]; 606 | leaf->minmax[3 + j] = leafDisk->maxs[j]; 607 | } 608 | 609 | int temp = leafDisk->contents; 610 | leaf->contents = temp; 611 | 612 | leaf->firstMarksurface = model->marksurfaces + leafDisk->firstMarksurface; 613 | leaf->numMarksurface = leafDisk->numMarksurface; 614 | 615 | temp = leafDisk->visibilityOffset; 616 | if (temp == -1) 617 | { 618 | leaf->visibilityCompressed = NULL; 619 | } 620 | else 621 | { 622 | leaf->visibilityCompressed = model->visibility + temp; 623 | } 624 | 625 | leaf->efrags = NULL; 626 | 627 | for (int j = 0; j < 4; ++j) 628 | { 629 | leaf->ambientSoundLevel[j] = leafDisk->ambientLevel[j]; 630 | } 631 | } 632 | } 633 | 634 | void ModelSetNodeParent(Node *node, Node *parent) 635 | { 636 | node->parent = parent; 637 | if (node->contents >= 0) 638 | { 639 | ModelSetNodeParent(node->children[0], node); 640 | ModelSetNodeParent(node->children[1], node); 641 | } 642 | } 643 | 644 | void ModelLoadNodes(Model *model, U8 *base, Lump lump) 645 | { 646 | NodeDisk *nodeDisk = (NodeDisk *)(base + lump.offset); 647 | if (lump.length % sizeof(*nodeDisk)) 648 | { 649 | g_platformAPI.SysError("incorrect lump size for nodes"); 650 | } 651 | 652 | int count = lump.length / sizeof(*nodeDisk); 653 | Node *node = NULL; 654 | node = (Node *)HunkLowAlloc(count * sizeof(*node), model->name); 655 | MemSet(node, 0, count * sizeof(*node)); 656 | 657 | model->nodes = node; 658 | model->numNode = count; 659 | 660 | for (int i = 0; i < count; ++i, ++nodeDisk, ++node) 661 | { 662 | for (int j = 0; j < 3; ++j) 663 | { 664 | node->minmax[j] = nodeDisk->mins[j]; 665 | node->minmax[j + 3] = nodeDisk->maxs[j]; 666 | } 667 | node->plane = model->planes + nodeDisk->planeOffset; 668 | node->firstsurface = nodeDisk->firstFace; 669 | node->numsurface = nodeDisk->numFace; 670 | node->contents = 0; 671 | 672 | for (int j = 0; j < 2; ++j) 673 | { 674 | int p = nodeDisk->childOffsets[j]; 675 | if (p >= 0) 676 | { 677 | node->children[j] = model->nodes + p; 678 | } 679 | else 680 | { 681 | node->children[j] = (Node *)(model->leaves + (-1 - p)); 682 | } 683 | } 684 | } 685 | 686 | ModelSetNodeParent(model->nodes, NULL); 687 | } 688 | 689 | void ModelLoadClipNodes(Model *model, U8 *base, Lump lump) 690 | { 691 | ClipNode *clipNodeDisk = (ClipNode *)(base + lump.offset); 692 | ClipNode *clipNode = NULL; 693 | 694 | if (lump.length % sizeof(*clipNodeDisk)) 695 | { 696 | g_platformAPI.SysError("incorrect lump size for clip node"); 697 | } 698 | int count = lump.length / sizeof(*clipNodeDisk); 699 | clipNode = (ClipNode *)HunkLowAlloc(count * sizeof(*clipNode), model->name); 700 | 701 | model->clipNodes = clipNode; 702 | model->numClipNode = count; 703 | 704 | Hull *hull = &model->hulls[1]; // TODO lw: why 1 not 0? 705 | hull->clipNodes = clipNode; 706 | hull->firstClipNode = 0; 707 | hull->lastClipNode = count - 1; 708 | hull->planes = model->planes; 709 | hull->clipMin = {-16, -16, -24}; 710 | hull->clipMax = {16, 16, 32}; 711 | 712 | hull = &model->hulls[2]; 713 | hull->clipNodes = clipNode; 714 | hull->firstClipNode = 0; 715 | hull->lastClipNode = count - 1; 716 | hull->planes = model->planes; 717 | hull->clipMin = {-32, -32, -24}; 718 | hull->clipMax = {32, 32, 64}; 719 | 720 | for (int i = 0; i < count; ++i, ++clipNode, ++clipNodeDisk) 721 | { 722 | clipNode->planeOffset = clipNodeDisk->planeOffset; 723 | clipNode->children[0] = clipNodeDisk->children[0]; 724 | clipNode->children[1] = clipNodeDisk->children[1]; 725 | } 726 | } 727 | 728 | void ModelLoadEntities(Model *model, U8 *base, Lump lump) 729 | { 730 | if (lump.length == 0) 731 | { 732 | model->entities = NULL; 733 | } 734 | 735 | model->entities = (char *)HunkLowAlloc(lump.length, model->name); 736 | MemCpy(model->entities, base + lump.offset, lump.length); 737 | } 738 | 739 | void ModelLoadSubmodels(Model *model, U8 *base, Lump lump) 740 | { 741 | Submodel *submodelDisk = (Submodel *)(base + lump.offset); 742 | if (lump.length % sizeof(*submodelDisk)) 743 | { 744 | g_platformAPI.SysError("incorrect lump size for submodels"); 745 | } 746 | int count = lump.length / sizeof(*submodelDisk); 747 | Submodel *submodel = NULL; 748 | submodel = (Submodel *)HunkLowAlloc(count * sizeof(*submodel), model->name); 749 | 750 | model->submodels = submodel; 751 | model->numSubmodel = count; 752 | 753 | for (int i = 0; i < count; ++i, ++submodelDisk, ++submodel) 754 | { 755 | Vec3f one = {1, 1, 1}; 756 | submodel->min = submodelDisk->min - one; 757 | submodel->max = submodelDisk->max + one; 758 | submodel->origin = submodelDisk->origin; 759 | 760 | for (int j = 0; j < MAX_MAP_HULLS; ++j) 761 | { 762 | submodel->headNodes[j] = submodelDisk->headNodes[j]; 763 | } 764 | submodel->visibleLeaves = submodelDisk->visibleLeaves; 765 | submodel->firstFace = submodelDisk->firstFace; 766 | submodel->numFace = submodelDisk->numFace; 767 | } 768 | } 769 | 770 | void ModelMakeHull(Model *model) 771 | { 772 | Hull *hull = &model->hulls[0]; 773 | 774 | Node *node = model->nodes; 775 | ClipNode *clipNode = (ClipNode *)HunkLowAlloc( 776 | model->numNode * sizeof(ClipNode), model->name); 777 | 778 | hull->clipNodes = clipNode; 779 | hull->planes = model->planes; 780 | hull->firstClipNode = 0; 781 | hull->lastClipNode = model->numNode - 1; 782 | 783 | for (int i = 0; i < model->numNode; ++i, node++, clipNode++) 784 | { 785 | clipNode->planeOffset = (I32)(node->plane - model->planes); 786 | for (int j = 0; j < 2; ++j) 787 | { 788 | Node *child = node->children[j]; 789 | if (child->contents < 0) 790 | { 791 | clipNode->children[j] = (I16)child->contents; 792 | } 793 | else 794 | { 795 | clipNode->children[j] = (I16)(child - model->nodes); 796 | } 797 | } 798 | } 799 | } 800 | 801 | #define MAX_KNOWN_MODEL 256 802 | Model g_knownModels[MAX_KNOWN_MODEL]; 803 | int g_numKnownModel; 804 | 805 | // Try to find a loaded model that's matching the name, else return an unused 806 | // mode in g_knownModels[MAX_KNOWN_MODEL] 807 | Model *ModelFindForName(char *name) 808 | { 809 | if (name == NULL || name[0] == '\0') 810 | { 811 | g_platformAPI.SysError("no model name"); 812 | } 813 | 814 | Model *unusedModel = NULL; 815 | Model *model = g_knownModels; 816 | int modelIndex = 0; 817 | for (modelIndex = 0; modelIndex < g_numKnownModel; ++modelIndex, ++model) 818 | { 819 | if (StringCompare(model->name, name) == 0) 820 | { 821 | break ; 822 | } 823 | if (model->loadStatus == ModelLoadStatus::UNUSED) 824 | { 825 | if (unusedModel == NULL || model->type != ModelType::ALIAS) 826 | { 827 | unusedModel = model; 828 | } 829 | } 830 | } 831 | 832 | // no existing model matches the name, need to load one 833 | if (modelIndex == g_numKnownModel) 834 | { 835 | if (unusedModel) 836 | { 837 | model = unusedModel; 838 | // if cache is still in memory, eject it 839 | if (model->type == ModelType::ALIAS) 840 | { 841 | if (CacheCheck(&model->cache)) 842 | { 843 | CacheFree(&model->cache); 844 | } 845 | } 846 | } 847 | else if (g_numKnownModel == MAX_KNOWN_MODEL) 848 | { 849 | g_platformAPI.SysError("Exceeds maximum model number!"); 850 | } 851 | else 852 | { 853 | g_numKnownModel++; 854 | } 855 | 856 | StringCopy(model->name, sizeof(model->name), name); 857 | model->loadStatus = ModelLoadStatus::NEEDLOAD; 858 | } 859 | 860 | return model; 861 | } 862 | 863 | float ModelRadiusFromBounds(Vec3f min, Vec3f max) 864 | { 865 | Vec3f extreme = {0}; 866 | for (int i = 0; i < 3; ++i) 867 | { 868 | extreme[i] = Absf(min[i]) > Absf(max[i]) ? Absf(min[i]) : Absf(max[i]); 869 | } 870 | 871 | float result = Vec3Length(extreme); 872 | return result; 873 | } 874 | 875 | /* 876 | Type Submodel is used for describing data, the real data of each submodel are 877 | stored as an instance of type of Model too. Submodel includes the room model as 878 | well as torche and other small models. 879 | */ 880 | void ModelSetupSubmodel(Model *model) 881 | { 882 | Model *submodel = model; 883 | for (int i = 0; i < model->numSubmodel; ++i) 884 | { 885 | Submodel *spec = &model->submodels[i]; 886 | submodel->hulls[0].firstClipNode = spec->headNodes[0]; 887 | for (int j = 0; j < MAX_MAP_HULLS; ++j) 888 | { 889 | submodel->hulls[j].firstClipNode = spec->headNodes[j]; 890 | submodel->hulls[j].lastClipNode = model->numClipNode - 1; 891 | } 892 | 893 | submodel->firstModelSurface = spec->firstFace; 894 | submodel->numModelSurface = spec->numFace; 895 | 896 | submodel->min = spec->min; 897 | submodel->max = spec->max; 898 | submodel->radius = ModelRadiusFromBounds(submodel->min, submodel->max); 899 | 900 | submodel->numLeaf = spec->visibleLeaves; 901 | 902 | if (i < model->numSubmodel - 1) 903 | { 904 | char submodelname[MAX_PACK_FILE_PATH]; 905 | snprintf(submodelname, MAX_PACK_FILE_PATH, "%s*%d", model->name, i); 906 | Model *nextSubmodel = ModelFindForName(submodelname); 907 | // duplicate the basic information 908 | *nextSubmodel = *model; 909 | StringCopy(nextSubmodel->name, MAX_PACK_FILE_PATH, submodelname, 0); 910 | submodel = nextSubmodel; 911 | } 912 | } 913 | 914 | } 915 | 916 | void ModelLoadBrushModel(Model *model, void *buffer) 917 | { 918 | model->type = ModelType::BRUSH; 919 | 920 | ModelHeaderDisk *headerDisk = (ModelHeaderDisk *)buffer; 921 | 922 | if (headerDisk->version != BSPVERSION) 923 | { 924 | g_platformAPI.SysError("ModelLoadBrushModel: %s has wrong version number", model->name); 925 | } 926 | 927 | // load into low hunk 928 | U8 *base = (U8 *)buffer; 929 | ModelLoadVertices(model, base, headerDisk->lumps[ModelLump::VERTEX]); 930 | ModelLoadEdges(model, base, headerDisk->lumps[ModelLump::EDGE]); 931 | ModelLoadSurfaceEdges(model, base, headerDisk->lumps[ModelLump::SURFACEEDGE]); 932 | ModelLoadTextures(model, base, headerDisk->lumps[ModelLump::TEXTURE]); 933 | ModelLoadLighting(model, base, headerDisk->lumps[ModelLump::LIGHTING]); 934 | ModelLoadPlanes(model, base, headerDisk->lumps[ModelLump::PLANE]); 935 | ModelLoadTextureInfo(model, base, headerDisk->lumps[ModelLump::TEXTUREINFO]); 936 | ModelLoadFaces(model, base, headerDisk->lumps[ModelLump::FACE]); 937 | ModelLoadMarkSurfaces(model, base, headerDisk->lumps[ModelLump::MARKSURFACE]); 938 | ModelLoadVisibility(model, base, headerDisk->lumps[ModelLump::VISIBILITY]); 939 | ModelLoadLeaves(model, base, headerDisk->lumps[ModelLump::LEAF]); 940 | ModelLoadNodes(model, base, headerDisk->lumps[ModelLump::NODE]); 941 | ModelLoadClipNodes(model, base, headerDisk->lumps[ModelLump::CLIPNODE]); 942 | ModelLoadEntities(model, base, headerDisk->lumps[ModelLump::ENTITY]); 943 | ModelLoadSubmodels(model, base, headerDisk->lumps[ModelLump::SUBMODEL]); 944 | 945 | ModelMakeHull(model); 946 | 947 | model->numFrame = 2; // regular and alternate animation TODO lw: ??? 948 | model->flags = 0; 949 | 950 | ModelSetupSubmodel(model); 951 | } 952 | 953 | void ModelLoad(Model *model) 954 | { 955 | void *buffer = FileLoad(model->name, ALLocType::TEMPHUNK); 956 | 957 | model->loadStatus = ModelLoadStatus::PRESENT; 958 | 959 | switch (*((U32 *)buffer)) 960 | { 961 | case IDPOLYHEADER: 962 | { 963 | 964 | } break; 965 | 966 | case IDSPRITEHEADER: 967 | { 968 | 969 | } break; 970 | 971 | default: 972 | { 973 | ModelLoadBrushModel(model, buffer); 974 | } break; 975 | } 976 | } 977 | 978 | Model *ModelLoadForName(char *name) 979 | { 980 | // find a model. if it's an existing one, load it. 981 | Model *result = ModelFindForName(name); 982 | 983 | if (result->loadStatus == ModelLoadStatus::PRESENT) 984 | { 985 | if (result->type == ModelType::ALIAS) 986 | { 987 | // make sure cache has not been evicted 988 | if (CacheCheck(&result->cache)) 989 | { 990 | return result; 991 | } 992 | } 993 | else 994 | { 995 | return result; 996 | } 997 | } 998 | 999 | ModelLoad(result); 1000 | 1001 | return result; 1002 | } 1003 | 1004 | U8 g_allVisible[MAX_MAP_LEAVES / 8]; 1005 | 1006 | U8 *ModelDecompressVisibility(U8 *visibility, int numLeaf) 1007 | { 1008 | // the n-th bit is set if the n-th leaf if visible 1009 | // MAX_MAP_LEAVES is defined to be multiple of 8, no need to ceil 1010 | static U8 decompressed[MAX_MAP_LEAVES / 8]; 1011 | 1012 | // ensure to have enough bits 1013 | int numBytes = (numLeaf + 7) >> 3; 1014 | 1015 | U8 *result = decompressed; 1016 | 1017 | // no visibility info, make all leaves visible 1018 | if (visibility == NULL) 1019 | { 1020 | result = g_allVisible; 1021 | } 1022 | else 1023 | { 1024 | do 1025 | { 1026 | if (*visibility) 1027 | { // if the byte is not zero, write it 1028 | *result++ = *visibility++; 1029 | continue; 1030 | } 1031 | else 1032 | { // if the byte is zero, get the next byte to see how many zeroes 1033 | // following it, then write all zeroes. 1034 | int count = visibility[1]; 1035 | visibility += 2; 1036 | while (count) 1037 | { 1038 | *result++ = 0; 1039 | count--; 1040 | } 1041 | } 1042 | } while (result - decompressed < numBytes); 1043 | } 1044 | 1045 | 1046 | return decompressed; 1047 | } 1048 | 1049 | U8 *ModelGetDecompressedPVS(Leaf *leaf, Model *model) 1050 | { 1051 | if (leaf == model->leaves) 1052 | { 1053 | return g_allVisible; 1054 | } 1055 | else 1056 | { 1057 | U8 *result = ModelDecompressVisibility( 1058 | leaf->visibilityCompressed, model->numLeaf); 1059 | 1060 | return result; 1061 | } 1062 | } 1063 | 1064 | Leaf *ModelFindViewLeaf(Vec3f pos, Model *worldModel) 1065 | { 1066 | if (worldModel == NULL || worldModel->nodes == NULL) 1067 | { 1068 | g_platformAPI.SysError("ModelFindViewingLeaf: bad model!"); 1069 | } 1070 | 1071 | Node *node = worldModel->nodes; 1072 | for (;;) 1073 | { 1074 | if (node->contents < 0) 1075 | { 1076 | return (Leaf *)node; 1077 | } 1078 | // (P - O) * N - (Q - O) * N = (P - Q) * N 1079 | // P is pos, N is normal, Q is any point on the plane, O is the origin 1080 | float d = Vec3Dot(pos, node->plane->normal) - node->plane->distance; 1081 | if (d > 0) 1082 | { // same side the normal points to 1083 | node = node->children[0]; 1084 | } 1085 | else 1086 | { // opposite side the normal points to, or on the plane 1087 | // TODO lw: why on the plane is considered back facing??? 1088 | node = node->children[1]; 1089 | } 1090 | } 1091 | } 1092 | 1093 | void ModelInit() 1094 | { 1095 | MemSet(g_allVisible, 0xff, sizeof(g_allVisible)); 1096 | } 1097 | -------------------------------------------------------------------------------- /code/q_model.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "q_math.h" 4 | 5 | #define MAX_MAP_HULLS 4 6 | 7 | #define MIP_LEVELS 4 8 | #define MAX_LIGHT_MAPS 4 9 | 10 | #define TEX_SPECIAL 1 // sky or slime, no lightmap or 256 subdivision 11 | 12 | #define SURF_PLANE_BACK 2 13 | #define SURF_DRAW_SKY 4 14 | #define SURF_DRAW_SPRITE 8 15 | #define SURF_DRAW_TURB 0x10 16 | #define SURF_DRAW_TILED 0x20 17 | #define SURF_DRAW_BACKGROUND 0x40 18 | 19 | #define NUM_AMBIENT_SOUND 4 // automatic ambient sound 20 | 21 | 22 | struct Vertex 23 | { 24 | Vec3f position; 25 | }; 26 | 27 | struct Edge 28 | { 29 | U16 vertIndex[2]; 30 | U32 iedge_cache_state; 31 | }; 32 | 33 | struct Texture 34 | { 35 | char name[16]; 36 | U32 width; 37 | U32 height; 38 | I32 animTotal; // total tenths in sequence (0 = no) 39 | Texture *animNext; // in the animation sequence 40 | Texture *alternateAnims; // bmodels in frame 1 use this 41 | U32 offsets[MIP_LEVELS]; // 4 mip maps stored 42 | }; 43 | 44 | struct TextureInfo 45 | { 46 | Vec3f u_axis; 47 | float u_offset; 48 | Vec3f v_axis; 49 | float v_offset; 50 | 51 | Texture *texture; 52 | float mip_adjust; 53 | I32 flags; 54 | }; 55 | 56 | struct Plane 57 | { 58 | Vec3f normal; 59 | float distance; 60 | U8 type; 61 | 62 | // the signness of z, y, z component of the normal precalculated while 63 | // loading from disk. Help speed up BOX_ON_PLANE_SIDE routine. 64 | // sign_x + sign_y << 1 + sign_z << 2 65 | U8 signBits; 66 | 67 | U8 padding[2]; 68 | }; 69 | 70 | struct SurfaceCache 71 | { 72 | SurfaceCache *next; 73 | SurfaceCache **owner; 74 | Texture *texture; 75 | I32 bright_adjusts[MAX_LIGHT_MAPS]; 76 | I32 dlight; // ? 77 | I32 size; // including the header 78 | U32 width; 79 | U32 height; // debug 80 | float mipscale; 81 | U8 data[4]; // &data[0] is the starting address of cache data 82 | }; 83 | 84 | struct Surface 85 | { 86 | Plane *plane; 87 | TextureInfo *tex_info; 88 | 89 | SurfaceCache *cachespots[MIP_LEVELS]; 90 | 91 | I32 visibleframe; 92 | 93 | I32 lightframe; 94 | // every 1-bit represents a light affectting this surface 95 | I32 lightbits; 96 | 97 | I32 flags; 98 | 99 | I32 firstEdge; 100 | I32 numEdge; 101 | 102 | I16 uv_min[2]; 103 | I16 uv_extents[2]; 104 | 105 | 106 | U32 light_styles[MAX_LIGHT_MAPS]; 107 | U8 *samples; 108 | }; 109 | 110 | // BSP node 111 | struct Node 112 | { 113 | // common with leaf 114 | Node *parent; 115 | I32 contents; // 0, to differentiate from leaves 116 | I32 visibleframe; 117 | I16 minmax[6]; // for bounding box culling 118 | 119 | // node specific 120 | U16 firstsurface; 121 | U16 numsurface; 122 | Plane *plane; 123 | Node *children[2]; 124 | }; 125 | 126 | struct ClipNode 127 | { 128 | I32 planeOffset; 129 | // negative means leaf content, positive means node offset 130 | I16 children[2]; 131 | }; 132 | 133 | struct Leaf 134 | { 135 | // common with node 136 | Node *parent; 137 | I32 contents; // negative value means leaf 138 | I32 visibleFrame; // not used for leaf 139 | I16 minmax[6]; // not used for leaf 140 | 141 | // leaf specific 142 | I32 numMarksurface; // TODO lw: what is this? 143 | I32 key; // BSP sequence number for leaf's content 144 | U8 ambientSoundLevel[NUM_AMBIENT_SOUND]; 145 | 146 | U8 *visibilityCompressed; // run-length compressed 147 | struct EFrag *efrags; 148 | 149 | Surface **firstMarksurface; // surfaces that this leaf contains 150 | }; 151 | 152 | struct Hull 153 | { 154 | ClipNode *clipNodes; 155 | Plane *planes; 156 | I32 firstClipNode; 157 | I32 lastClipNode; 158 | Vec3f clipMin; 159 | Vec3f clipMax; 160 | }; 161 | 162 | // TODO lw: Entity Fragment??? 163 | struct EFrag 164 | { 165 | Leaf *leaf; 166 | EFrag *nextLeaf; 167 | 168 | struct Entity *entity; 169 | EFrag *nextEntity; 170 | }; 171 | 172 | struct Submodel 173 | { 174 | Vec3f min; 175 | Vec3f max; 176 | Vec3f origin; 177 | I32 headNodes[MAX_MAP_HULLS]; 178 | I32 visibleLeaves; // not including the solid leaf 0 179 | I32 firstFace; // offset 180 | I32 numFace; 181 | }; 182 | 183 | enum ModelType 184 | { 185 | BRUSH, // wall, building, etc. 186 | SPRITE, // particle effect etc. 187 | ALIAS // monster, weapon, player etc. 188 | }; 189 | 190 | enum ModelLoadStatus 191 | { 192 | PRESENT, 193 | NEEDLOAD, 194 | UNUSED 195 | }; 196 | 197 | struct Model 198 | { 199 | char name[MAX_PACK_FILE_PATH]; 200 | 201 | // brush model data 202 | 203 | I32 firstModelSurface; 204 | I32 numModelSurface; 205 | 206 | Submodel *submodels; 207 | Vertex *vertices; 208 | Edge *edges; 209 | Node *nodes; 210 | Plane *planes; 211 | // edge indices stored sequentially for each surface, 212 | // queried by firstEdge and numEdge in Surface struct 213 | I32 *surfaceEdges; 214 | Texture **textures; 215 | TextureInfo *tex_info; 216 | Surface *surfaces; 217 | ClipNode *clipNodes; 218 | Surface **marksurfaces; 219 | // leaf 0 is the generic SOLID leaf used for all solid area, all other 220 | // leaves need visibility info 221 | Leaf *leaves; 222 | 223 | // run-length encoded visibility data for all leaves 224 | U8 *visibility; 225 | 226 | U8 *light_data; 227 | char *entities; 228 | 229 | // additional model data, only access through Mod_Extradata 230 | CacheUser cache; 231 | 232 | Hull hulls[MAX_MAP_HULLS]; 233 | 234 | // bounding volume 235 | Vec3f max; 236 | Vec3f min; 237 | float radius; 238 | 239 | I32 numSubmodel; 240 | I32 numVert; 241 | I32 numEdge; 242 | I32 numNode; 243 | I32 numPlane; 244 | I32 numSurfaceEdge; 245 | I32 numTexture; 246 | I32 numTexInfo; 247 | I32 numSurface; 248 | I32 numClipNode; 249 | I32 numMarksurface; 250 | I32 numLeaf; 251 | 252 | I32 numFrame; 253 | I32 flags; 254 | 255 | ModelType type; 256 | ModelLoadStatus loadStatus; 257 | }; 258 | -------------------------------------------------------------------------------- /code/q_platform.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef int8_t I8; 6 | typedef int16_t I16; 7 | typedef int32_t I32; 8 | typedef int64_t I64; 9 | typedef int B32; 10 | 11 | typedef uint8_t U8; 12 | typedef uint16_t U16; 13 | typedef uint32_t U32; 14 | typedef uint64_t U64; 15 | 16 | typedef I32 Fixed8; // 8 bits for mantissa 17 | typedef I32 Fixed16; // 16 bits for mantissa 18 | typedef I32 Fixed20; // 20 bits for mantissa 19 | 20 | #define INTERNAL_LINKAGE static 21 | 22 | #define MAX_OS_PATH_LENGTH 256 23 | 24 | #if QUAKEREMAKE_SLOW 25 | #define ASSERT(expression) if (!(expression)) { *((int *)0) = 0; } 26 | #else 27 | #define ASSERT(expression) 28 | #endif 29 | 30 | #define ARRAY_COUNT(array) sizeof(array) / sizeof(array[0]) 31 | 32 | #define KILO_BYTES(val) ((val) * 1024LL) 33 | #define MEGA_BYTES(val) (KILO_BYTES(val) * 1024LL) 34 | #define GIGA_BYTES(val) (MEGA_BYTES(val) * 1024LL) 35 | #define TERA_BYTES(val) (GIGA_BYTES(val) * 1024LL) 36 | 37 | inline Fixed20 FloatToFixed20(float v) 38 | { 39 | // 0x100000 = 2^20 40 | Fixed20 result = (I32)(v * 0x100000); 41 | return result; 42 | } 43 | 44 | inline float Fixed20ToFloat(Fixed20 f) 45 | { 46 | float r = (float)f / 0x100000; 47 | return r; 48 | } 49 | 50 | inline Fixed16 FloatToFixed16(float v) 51 | { 52 | Fixed16 r = (I32)(v * 0x10000); 53 | return r; 54 | } 55 | 56 | inline float Fixed16ToFloat(Fixed16 v) 57 | { 58 | float r = (float)v / 0x10000; 59 | return r; 60 | } 61 | 62 | struct ThreadContext 63 | { 64 | int placeHolder; 65 | }; 66 | 67 | //=== Services the platform layer provides to the game === 68 | 69 | #if QUAKEREMAKE_INTERNAL 70 | 71 | struct DebugReadFileResult 72 | { 73 | int contentSize; 74 | void *content; 75 | }; 76 | 77 | #define DEBUG_PLATFORM_FREE_FILE_MEMORY(name) void name(ThreadContext *thread, DebugReadFileResult *file) 78 | typedef DEBUG_PLATFORM_FREE_FILE_MEMORY(DebugPlatformFreeFileMemory_t); 79 | 80 | #define DEBUG_PLATFORM_READ_WHOLE_FILE(name) DebugReadFileResult name(ThreadContext *thread, const char *filename) 81 | 82 | typedef DEBUG_PLATFORM_READ_WHOLE_FILE(DebugPlatformReadWholeFile_t); 83 | 84 | #define DEBUG_PLATFORM_WRITE_WHOLE_FILE(name) bool name(ThreadContext *thread, const char *filename, void *memory, U32 memorySize) 85 | 86 | typedef DEBUG_PLATFORM_WRITE_WHOLE_FILE(DebugPlatformWriteWholeFile_t); 87 | 88 | #endif 89 | 90 | 91 | //======================================================== 92 | 93 | //=== Services that the game provides to the platform layer 94 | struct GameOffScreenBuffer 95 | { 96 | I32 width; 97 | I32 height; 98 | I32 bytesPerPixel; 99 | I32 bytesPerRow; 100 | void *memory; 101 | U8 *palette; 102 | }; 103 | 104 | struct GameSoundOutputBuffer 105 | { 106 | I32 samplesPerSecond; 107 | I32 sampleCount; 108 | I16 *samples; 109 | }; 110 | 111 | // 112 | // these are the key numbers that should be passed to Key_Event 113 | // 114 | #define K_TAB 9 115 | #define K_ENTER 13 116 | #define K_ESCAPE 27 117 | #define K_SPACE 32 118 | 119 | // normal keys should be passed as lowercased ascii 120 | 121 | #define K_BACKSPACE 127 122 | #define K_UPARROW 128 123 | #define K_DOWNARROW 129 124 | #define K_LEFTARROW 130 125 | #define K_RIGHTARROW 131 126 | 127 | #define K_ALT 132 128 | #define K_CTRL 133 129 | #define K_SHIFT 134 130 | #define K_F1 135 131 | #define K_F2 136 132 | #define K_F3 137 133 | #define K_F4 138 134 | #define K_F5 139 135 | #define K_F6 140 136 | #define K_F7 141 137 | #define K_F8 142 138 | #define K_F9 143 139 | #define K_F10 144 140 | #define K_F11 145 141 | #define K_F12 146 142 | #define K_INS 147 143 | #define K_DEL 148 144 | #define K_PGDN 149 145 | #define K_PGUP 150 146 | #define K_HOME 151 147 | #define K_END 152 148 | 149 | #define K_PAUSE 255 150 | 151 | struct KeyState 152 | { 153 | U32 key : 8; 154 | U32 is_down: 8; 155 | }; 156 | 157 | struct MouseState 158 | { 159 | I32 delta_x; 160 | I32 delta_y; 161 | I32 old_x; 162 | I32 old_y; 163 | }; 164 | 165 | // | player makes input | we process input | inputs take effect 166 | // frame0 frame1 frame2 167 | // so, input is always one frame behind 168 | struct GameInput 169 | { 170 | // bits 0-8: key code, bits 9-15: 1 is key down, 0 is key up 171 | KeyState key_events[32]; 172 | I32 kevt_count; 173 | 174 | MouseState mouse; 175 | }; 176 | 177 | #define SYS_ERROR(name) void name(char *format, ...) 178 | typedef SYS_ERROR(SysError_t); 179 | 180 | #define SYS_SET_PALETTE(name) void name(U8 *palette) 181 | typedef SYS_SET_PALETTE(SysSetPalette_t); 182 | 183 | struct PlatformAPI 184 | { 185 | SysError_t *SysError; 186 | SysSetPalette_t *SysSetPalette; 187 | }; 188 | 189 | PlatformAPI g_platformAPI; 190 | 191 | struct GameMemory 192 | { 193 | void *gameMemory; 194 | I32 gameMemorySize; 195 | 196 | PlatformAPI platformAPI; 197 | 198 | char gameAssetDir[MAX_OS_PATH_LENGTH]; 199 | 200 | GameOffScreenBuffer offscreenBuffer; 201 | float targetSecondsPerFrame; 202 | }; 203 | 204 | #define GAME_INIT(name) void name(GameMemory *memory) 205 | typedef GAME_INIT(GameInit_t); 206 | 207 | GAME_INIT(GameInit_stub) { } 208 | 209 | #define GAME_UPDATE_AND_RENDER(name) \ 210 | void name(GameInput *game_input) 211 | 212 | typedef GAME_UPDATE_AND_RENDER(GameUpdateAndRender_t); 213 | 214 | GAME_UPDATE_AND_RENDER(GameUpdateAndRender_stub) { } 215 | 216 | #define GAME_GET_SOUND_SAMPLES(name) \ 217 | void name(ThreadContext *thread, GameMemory *gameMemory, GameSoundOutputBuffer *soundBuffer) 218 | 219 | typedef GAME_GET_SOUND_SAMPLES(GameGetSoundSamples_t); 220 | 221 | // GAME_GET_SOUND_SAMPLES(GameGetSoundSamples_stub) { } 222 | -------------------------------------------------------------------------------- /code/q_render.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "q_math.h" 4 | 5 | struct RenderBuffer 6 | { 7 | I32 width; 8 | I32 height; 9 | I32 bytesPerPixel; 10 | I32 bytes_per_row; 11 | U8 *colorPalette; 12 | /* 13 | 0 ... 255 -->color 14 | 1 ... 1 15 | . ... . ^ 16 | . ... . | shades 17 | 63 ... 63 | 18 | */ 19 | U8 *colormap; 20 | U8 *backbuffer; 21 | float *zbuffer; 22 | }; 23 | 24 | struct ClipPlane 25 | { 26 | ClipPlane *next; 27 | Vec3f normal; 28 | float distance; 29 | U8 leftEdge; 30 | U8 rightEdge; 31 | U8 reserved[2]; 32 | }; 33 | 34 | struct Recti 35 | { 36 | I32 x, y; // top-left corner 37 | I32 width, height; 38 | }; 39 | 40 | struct Camera 41 | { 42 | Recti screen_rect; 43 | Vec2f screen_center; 44 | Vec2f screen_clamp_min; 45 | Vec2f screen_clamp_max; 46 | 47 | float near_z; 48 | // float farZ; 49 | // float half_fovy; // half of field of view in y axis, in redian 50 | // float aspect; // width / height 51 | 52 | /* 53 | x_w / scale_z = tan(fovx * 0.5); 54 | x_w equals the width of the screen and scale_z is the corresponding z value. 55 | x_s / x_v = scaleZ / z_v => x_s = scale_z / z_v * x_v 56 | x_v and z_v are the values in view space, x_s is the projected value of x_v 57 | on the plane whose width is x_w, essentially x_s is the value in screen space. 58 | */ 59 | float scale_z; 60 | float scale_invz; 61 | 62 | Vec3f position; // in world space 63 | Vec3f angles; 64 | 65 | /* 66 | T * R is camera transform matrix, 67 | Pv, Pw are points in view and world space respectively 68 | 69 | (T*R) * Pv = Pw 70 | --> inverse(T*R) * (T*R) * Pv = inverse(T*R) * Pw 71 | --> Pv = inverse(T*R) * Pw 72 | --> Pv = inverse(R) * inverse(T) * Pw 73 | 74 | R = |transpose(rotx), transpose(roty), transpose(rotz)| 75 | */ 76 | Vec3f rotx, roty, rotz; 77 | 78 | // left, right, top, bottom 79 | Plane frustumPlanes[4]; 80 | // frustum planes transformed in world space 81 | ClipPlane worldFrustumPlanes[4]; 82 | I32 frustumIndices[4 * 6]; 83 | 84 | U32 dirty; 85 | }; 86 | 87 | struct ESpan 88 | { 89 | ESpan *next; 90 | I32 x_start, y; 91 | I32 count; // pixel count 92 | I32 padding; 93 | }; 94 | 95 | // intermediate edge data for span drawing 96 | struct IEdge 97 | { 98 | IEdge *prev; 99 | IEdge *next; 100 | IEdge *nextRemove; 101 | Edge *owner; 102 | 103 | Fixed20 x_start; // in screen space 104 | Fixed20 x_step; 105 | // isurfaceOffsets[0] is set for trailing(right) edge, 106 | // isurfaceOffsets[1] is set for leading(left) edge 107 | U32 isurfaceOffsets[2]; 108 | float nearInvZ; 109 | }; 110 | 111 | // intermediate surface data for span drawing 112 | struct ISurface 113 | { 114 | ISurface *next; 115 | ISurface *prev; 116 | ESpan *spans; 117 | 118 | void *data; 119 | Entity *entity; 120 | 121 | // We are using span-base drawing, no need to walk bsp tree from back to 122 | // front. It's actually being walked from front to back, and therefore 123 | // smaller keys are in front. 124 | I32 key; 125 | I32 x_last; 126 | // safe guard to ensure that trailing edge only comes after leading edge 127 | I32 spanState; 128 | I32 flags; 129 | float nearest_invz; 130 | B32 in_submodel; 131 | // used for calculating 1/z in screen space 132 | float zi_stepx, zi_stepy, zi_start; 133 | }; 134 | 135 | #define MAX_PIXEL_HEIGHT 1024 136 | #define MIP_NUM 4 137 | 138 | #define SINE_SAMPLE_SIZE 128 139 | #define SINE_TABLE_SIZE (1280 + SINE_SAMPLE_SIZE) 140 | 141 | // imtermediate data for drawing 142 | struct RenderData 143 | { 144 | IEdge *newIEdges[MAX_PIXEL_HEIGHT]; 145 | IEdge *removeIEdges[MAX_PIXEL_HEIGHT]; 146 | 147 | IEdge *iedges; 148 | IEdge *currentIEdge; 149 | IEdge *endIEdge; 150 | 151 | ISurface *isurfaces; 152 | ISurface *currentISurface; 153 | ISurface *endISurface; 154 | 155 | Leaf *oldViewLeaf; 156 | Leaf *currentViewLeaf; 157 | 158 | Model *worldModel; 159 | 160 | B32 in_water; 161 | 162 | float nearest_invz; // for surface 163 | 164 | I32 currentKey; 165 | 166 | I32 framecount; 167 | I32 updateCountPVS; 168 | 169 | I32 outOfIEdges; 170 | I32 surfaceCount; 171 | 172 | float scaled_mip[MIP_NUM - 1]; 173 | I32 mip_min; 174 | 175 | I32 sine_table[SINE_TABLE_SIZE]; 176 | }; 177 | -------------------------------------------------------------------------------- /code/q_sky.cpp: -------------------------------------------------------------------------------- 1 | #include "q_platform.h" 2 | #include "q_model.h" 3 | #include "q_render.h" 4 | 5 | #define SKY_SIZE 128 6 | #define SKY_SIZE_MASK SKY_SIZE - 1 7 | #define SKY_TEXTURE_WIDTH SKY_SIZE * 2 8 | #define SKY_WEIRD_NUMBER 131 9 | 10 | struct SkyCanvas 11 | { 12 | /* 13 | newsky and topsky both pack in here, 128 bytes of newsky on the left of 14 | each scan, 128 bytes of topsky on the right, because the low-level drawers 15 | need 256-byte scan widths 16 | */ 17 | U8 new_sky[SKY_SIZE * SKY_TEXTURE_WIDTH]; 18 | 19 | /* 20 | Sky texture must be size of 256*128, and stores 2 skies of 128*128. In 21 | front is the right sky where the black pixel represent transparency. 22 | */ 23 | U8 left_sky[SKY_SIZE * SKY_WEIRD_NUMBER]; 24 | U8 left_mask[SKY_SIZE * SKY_WEIRD_NUMBER]; 25 | 26 | float sky_shift; // sky moving speed 27 | }; 28 | 29 | SkyCanvas g_skycanvas; 30 | 31 | void SkyInit(SkyCanvas *sky, Texture *texture) 32 | { 33 | U8 *newsky = sky->new_sky + SKY_SIZE; 34 | U8 *tex_src = (U8 *)texture + texture->offsets[0] + SKY_SIZE; 35 | 36 | for (I32 y = 0; y < SKY_SIZE; ++y) 37 | { 38 | U8 *sky_pixel = newsky; 39 | U8 *texel = tex_src; 40 | for (I32 x = 0; x < SKY_SIZE; ++x) 41 | { 42 | *sky_pixel++ = *texel++; 43 | } 44 | newsky += SKY_TEXTURE_WIDTH; 45 | tex_src += SKY_TEXTURE_WIDTH; 46 | } 47 | 48 | newsky = sky->new_sky; 49 | tex_src = (U8 *)texture + texture->offsets[0]; 50 | 51 | for (I32 y = 0; y < SKY_SIZE; ++y) 52 | { 53 | for (I32 x = 0; x < SKY_WEIRD_NUMBER; ++x) 54 | { 55 | U8 color = tex_src[y * SKY_TEXTURE_WIDTH + (x & SKY_SIZE_MASK)]; 56 | sky->left_sky[y * SKY_WEIRD_NUMBER + x] = color; 57 | sky->left_mask[y * SKY_WEIRD_NUMBER + x] = color ? 0 : 0xff; 58 | } 59 | } 60 | } 61 | 62 | void SkyAnimate(SkyCanvas *sky) 63 | { 64 | I32 right_sky_row = 0; 65 | I32 right_sky_offset = 0; 66 | U8 *sky_texel = sky->new_sky; 67 | 68 | for (I32 y = 0; y < SKY_SIZE; ++y) 69 | { 70 | right_sky_row = ((y + (I32)sky->sky_shift) & SKY_SIZE_MASK) * SKY_WEIRD_NUMBER; 71 | for (I32 x = 0; x < SKY_SIZE; ++x) 72 | { 73 | right_sky_offset = right_sky_row + ((x + (I32)sky->sky_shift) & SKY_SIZE_MASK); 74 | 75 | U8 left_sky_texel = *(sky_texel + SKY_SIZE); 76 | U8 right_sky_texel = sky->left_sky[right_sky_offset]; 77 | U8 right_sky_mask = sky->left_mask[right_sky_offset]; 78 | *sky_texel++ = (left_sky_texel & right_sky_mask) | right_sky_texel; 79 | } 80 | sky_texel += SKY_SIZE; 81 | } 82 | } 83 | 84 | void SkySetupFrame(SkyCanvas *sky) 85 | { 86 | // TODO lw: floating-point, especially 0.5f, makes sky movement jerky. 87 | sky->sky_shift += 0.6f; 88 | } 89 | 90 | Vec2i SkyGetTextureUV(I32 x, I32 y, I32 screen_half_width, I32 screen_half_height, 91 | float sky_shift, Camera *camera) 92 | { 93 | // Find the direction from origin to the pixel on the screen plane in view 94 | // space 95 | float wu = (float)(x - screen_half_width); 96 | float wv = (float)(screen_half_height - y); 97 | float wz = camera->scale_z; 98 | 99 | Vec3f dir_world = {0}; 100 | dir_world.x = wz * camera->roty[0] + wu * camera->rotx[0] + wv * camera->rotz[0]; 101 | dir_world.y = wz * camera->roty[1] + wu * camera->rotx[1] + wv * camera->rotz[1]; 102 | dir_world.z = wz * camera->roty[2] + wu * camera->rotx[2] + wv * camera->rotz[2]; 103 | dir_world.z *= 3; 104 | dir_world = Vec3Normalize(dir_world); 105 | 106 | float sky_multiplier = 320.0f; 107 | 108 | Vec2i result = { 109 | (I32)((sky_shift + sky_multiplier * dir_world.x) * 0x10000), 110 | (I32)((sky_shift + sky_multiplier * dir_world.y) * 0x10000) 111 | }; 112 | 113 | return result; 114 | } 115 | -------------------------------------------------------------------------------- /code/sys_win32.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "q_platform.h" 7 | #include "sys_win32.h" 8 | 9 | #define GLOBAL_VARIABLE static 10 | #define INTERNAL_LINKAGE static 11 | 12 | INTERNAL_LINKAGE bool g_running; 13 | INTERNAL_LINKAGE Win32State g_win32_state; 14 | INTERNAL_LINKAGE Win32ScreenBuffer g_screenBuffer; 15 | INTERNAL_LINKAGE GameInput g_game_input; 16 | 17 | SYS_ERROR(Win32SysError) 18 | { 19 | char error[1024]; 20 | va_list vl; 21 | va_start(vl, format); 22 | vsprintf_s(error, 1024, format, vl); 23 | va_end(vl); 24 | 25 | MessageBox(NULL, error, "Quake Error", MB_OK | MB_SETFOREGROUND | MB_ICONSTOP); 26 | } 27 | 28 | SYS_SET_PALETTE(Win32SetPalette) 29 | { 30 | for (int i = 0; i < 256; ++i) 31 | { 32 | g_screenBuffer.bitmapInfo.bmiColors[i].rgbRed = palette[i * 3 + 0]; 33 | g_screenBuffer.bitmapInfo.bmiColors[i].rgbGreen = palette[i * 3 + 1]; 34 | g_screenBuffer.bitmapInfo.bmiColors[i].rgbBlue = palette[i * 3 + 2]; 35 | g_screenBuffer.bitmapInfo.bmiColors[i].rgbReserved = 0; // must be zero 36 | } 37 | } 38 | 39 | inline U64 40 | Win32GetWallClock() 41 | { 42 | LARGE_INTEGER counter; 43 | QueryPerformanceCounter(&counter); 44 | return counter.QuadPart; 45 | } 46 | 47 | inline float 48 | Win32GetSecondsElapsed(U64 startCounter, U64 endCounter, U64 counterFrequency) 49 | { 50 | float result = (float)(endCounter - startCounter) / (float)counterFrequency; 51 | return result; 52 | } 53 | 54 | INTERNAL_LINKAGE void 55 | Win32GetExeFileName(Win32State *state) 56 | { 57 | // TODO lw: why using MAX_PATH in user facing code is a bad idea 58 | DWORD filePathLength = GetModuleFileNameA(0, state->exeFilePath, sizeof(state->exeFilePath)); 59 | state->onePastLastExeFilePathSlash = state->exeFilePath; 60 | for (char *scan = state->exeFilePath; *scan != '\0'; ++scan) 61 | { 62 | if (*scan == '\\') 63 | { 64 | state->onePastLastExeFilePathSlash = scan + 1; 65 | } 66 | } 67 | } 68 | 69 | INTERNAL_LINKAGE void 70 | Win32CatString(char *src0, int src0Count, 71 | char *src1, int src1Count, 72 | char *dest, int destCount) 73 | { 74 | int destIndex = 0; 75 | 76 | for (int i = 0; i < src0Count; ++i) 77 | { 78 | if (destIndex == destCount - 1 || src0[i] == '\0') 79 | { 80 | dest[destIndex] = '\0'; 81 | return ; 82 | } 83 | else 84 | { 85 | dest[destIndex++] = src0[i]; 86 | } 87 | } 88 | 89 | for (int i = 0; i < src1Count; ++i) 90 | { 91 | if (destIndex == destCount - 1 || src1[i] == '\0') 92 | { 93 | dest[destIndex] = '\0'; 94 | return ; 95 | } 96 | else 97 | { 98 | dest[destIndex++] = src1[i]; 99 | } 100 | } 101 | 102 | dest[destIndex] = '\0'; 103 | } 104 | 105 | INTERNAL_LINKAGE int 106 | StringLength(char *src) 107 | { 108 | int length = 0; 109 | char *scan = src; 110 | while (*scan != '\0') 111 | { 112 | length++; 113 | scan++; 114 | } 115 | return length; 116 | } 117 | 118 | INTERNAL_LINKAGE void 119 | Win32BuildGameFilePath(Win32State *state, char *filename, 120 | char *dest, int destSize) 121 | { 122 | Win32CatString(state->exeFilePath, 123 | (unsigned int)(state->onePastLastExeFilePathSlash - state->exeFilePath), 124 | filename, StringLength(filename), 125 | dest, destSize); 126 | } 127 | 128 | INTERNAL_LINKAGE Win32WindowSize 129 | Win32GetWindowSize(HWND window) 130 | { 131 | RECT clientRect; 132 | GetClientRect(window, &clientRect); 133 | Win32WindowSize result = 134 | {clientRect.right -clientRect.left, 135 | clientRect.bottom -clientRect.top}; 136 | return result; 137 | } 138 | 139 | inline FILETIME 140 | Win32GetLastWriteTime(char *filename) 141 | { 142 | FILETIME lastTime = { }; 143 | WIN32_FILE_ATTRIBUTE_DATA data; 144 | if (GetFileAttributesEx(filename, GetFileExInfoStandard, &data)) 145 | { 146 | lastTime = data.ftLastWriteTime; 147 | } 148 | 149 | return lastTime; 150 | } 151 | 152 | INTERNAL_LINKAGE Win32GameCode 153 | Win32LoadGameCode(char *sourceDLLName, char *tempDLLName, char *lockFileName) 154 | { 155 | Win32GameCode result = { }; 156 | 157 | WIN32_FILE_ATTRIBUTE_DATA dummy; 158 | if (!GetFileAttributesEx(lockFileName, GetFileExInfoStandard, &dummy)) 159 | { 160 | result.lastWriteTime = Win32GetLastWriteTime(sourceDLLName); 161 | 162 | if (!CopyFile(sourceDLLName, tempDLLName, FALSE)) 163 | { 164 | Win32SysError("Copy DLL failed!"); 165 | } 166 | 167 | result.gameCodeDLL = LoadLibraryA(tempDLLName); 168 | if (result.gameCodeDLL) 169 | { 170 | result.GameInit = (GameInit_t *) 171 | GetProcAddress(result.gameCodeDLL, "GameInit"); 172 | 173 | result.GameUpdateAndRender = (GameUpdateAndRender_t *) 174 | GetProcAddress(result.gameCodeDLL, "GameUpdateAndRender"); 175 | 176 | result.isValid = (result.GameInit && result.GameUpdateAndRender); 177 | } 178 | else 179 | { 180 | Win32SysError("Can't Load Game DLL!"); 181 | } 182 | 183 | if (!result.isValid) 184 | { 185 | result.GameInit = GameInit_stub; 186 | result.GameUpdateAndRender = GameUpdateAndRender_stub; 187 | 188 | Win32SysError("Can't Load Game Functions!"); 189 | } 190 | } 191 | 192 | return result; 193 | } 194 | 195 | INTERNAL_LINKAGE void 196 | Win32UnloadGameCode(Win32GameCode *gameCode) 197 | { 198 | if (gameCode->gameCodeDLL) 199 | { 200 | FreeLibrary(gameCode->gameCodeDLL); 201 | gameCode->gameCodeDLL = NULL; 202 | } 203 | 204 | gameCode->isValid = false; 205 | gameCode->GameInit = NULL; 206 | gameCode->GameUpdateAndRender = NULL; 207 | } 208 | 209 | INTERNAL_LINKAGE int 210 | Win32DisplayBufferInWindow(HDC deviceContext, Win32ScreenBuffer *screenBuffer, Win32WindowSize window_size) 211 | { 212 | int result = 0; 213 | 214 | int offsetX = 10; 215 | int offsetY = 10; 216 | 217 | #if 0 218 | // top 219 | PatBlt(deviceContext, 0, 0, window_size.width, offsetY, BLACKNESS); 220 | // left 221 | PatBlt(deviceContext, 0, 0, offsetX, window_size.height, BLACKNESS); 222 | // bottom 223 | PatBlt(deviceContext, 0, screenBuffer->height + offsetY, 224 | windowSize.width, windowSize.height, BLACKNESS); 225 | // right 226 | PatBlt(deviceContext, screenBuffer->width + offsetX, 0, 227 | windowSize.width, windowSize.height, BLACKNESS); 228 | 229 | #endif 230 | result = StretchDIBits(deviceContext, 231 | 0, 0, window_size.width, window_size.height, 232 | 0, 0, screenBuffer->width, screenBuffer->height, 233 | screenBuffer->memory, 234 | (BITMAPINFO *)&screenBuffer->bitmapInfo, 235 | DIB_RGB_COLORS, SRCCOPY); 236 | 237 | return result; 238 | } 239 | 240 | INTERNAL_LINKAGE void 241 | Win32ResizeDIBSection(Win32ScreenBuffer *screenBuffer, 242 | GameOffScreenBuffer *offscreenBuffer) 243 | { 244 | screenBuffer->width = offscreenBuffer->width; 245 | screenBuffer->height = offscreenBuffer->height; 246 | screenBuffer->pixelBytes = offscreenBuffer->bytesPerPixel; 247 | screenBuffer->rowBytes = offscreenBuffer->bytesPerRow; 248 | 249 | screenBuffer->bitmapInfo.bmiHeader = {0}; 250 | 251 | screenBuffer->bitmapInfo.bmiHeader.biSize = sizeof(screenBuffer->bitmapInfo.bmiHeader); 252 | screenBuffer->bitmapInfo.bmiHeader.biWidth = screenBuffer->width; 253 | // Negative height means top-left corner is the origin 254 | screenBuffer->bitmapInfo.bmiHeader.biHeight = -screenBuffer->height; 255 | screenBuffer->bitmapInfo.bmiHeader.biPlanes = 1; 256 | screenBuffer->bitmapInfo.bmiHeader.biBitCount = (WORD)(offscreenBuffer->bytesPerPixel * 8); 257 | screenBuffer->bitmapInfo.bmiHeader.biCompression = BI_RGB; 258 | 259 | screenBuffer->memory = offscreenBuffer->memory; 260 | } 261 | 262 | INTERNAL_LINKAGE U32 263 | Win32MapKey(LPARAM lparam) 264 | { 265 | U32 key = (lparam >> 16) & 255; 266 | if (key > 127) 267 | { 268 | return 0; 269 | } 270 | key = g_scantokey[key]; 271 | return key; 272 | } 273 | 274 | INTERNAL_LINKAGE void 275 | Win32PostKeyEvent(U32 key, U32 is_down) 276 | { 277 | if (key == K_ESCAPE) 278 | { 279 | g_running = false; 280 | } 281 | g_game_input.key_events[g_game_input.kevt_count].key = (U8)key; 282 | g_game_input.key_events[g_game_input.kevt_count].is_down = (U8)is_down; 283 | g_game_input.kevt_count++; 284 | } 285 | 286 | INTERNAL_LINKAGE void 287 | Win32ProcessPendingMessages() 288 | { 289 | g_game_input.kevt_count = 0; 290 | 291 | MSG msg; 292 | while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE)) 293 | { 294 | switch (msg.message) 295 | { 296 | case WM_QUIT: 297 | { 298 | g_running = false; 299 | } break; 300 | 301 | case WM_SYSKEYUP: 302 | case WM_KEYUP: 303 | { 304 | Win32PostKeyEvent(Win32MapKey(msg.lParam), 0); 305 | } break; 306 | case WM_SYSKEYDOWN: 307 | case WM_KEYDOWN: 308 | { 309 | Win32PostKeyEvent(Win32MapKey(msg.lParam), 1); 310 | } break; 311 | 312 | default: 313 | { 314 | TranslateMessage(&msg); 315 | DispatchMessage(&msg); 316 | } 317 | } 318 | } 319 | } 320 | 321 | INTERNAL_LINKAGE void 322 | Win32ProcessMouseMove(bool has_focus, RECT win_rect, MouseState *mouse) 323 | { 324 | if (has_focus) 325 | { 326 | POINT mouse_point = {0}; 327 | GetCursorPos(&mouse_point); 328 | 329 | int win_center_x = (win_rect.left + win_rect.right) / 2; 330 | int win_center_y = (win_rect.top + win_rect.bottom) / 2; 331 | 332 | mouse->delta_x = mouse_point.x - win_center_x; 333 | mouse->delta_y = mouse_point.y - win_center_y; 334 | 335 | // TODO lw: this is a hack because somehow calling GetCursorPos 336 | // immediately after SetCursorPos returns a positions that's one pixel 337 | // off on either x or y from what's been set. 338 | if (mouse->delta_x ==1 || mouse->delta_x == -1) 339 | { 340 | mouse->delta_x = 0; 341 | } 342 | if (mouse->delta_y ==1 || mouse->delta_y == -1) 343 | { 344 | mouse->delta_y = 0; 345 | } 346 | 347 | SetCursorPos(win_center_x, win_center_y); 348 | // GetCursorPos(&mouse_point); // one pixel off 349 | 350 | mouse->old_x = mouse_point.x; 351 | mouse->old_y = mouse_point.y; 352 | } 353 | else 354 | { 355 | mouse->delta_x = 0; 356 | mouse->delta_y = 0; 357 | } 358 | } 359 | 360 | INTERNAL_LINKAGE LRESULT CALLBACK 361 | Win32MainWindowCallback(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 362 | { 363 | LRESULT result = 0; 364 | 365 | switch (message) 366 | { 367 | case WM_CLOSE: 368 | { 369 | g_running = false; 370 | } break; 371 | 372 | case WM_ACTIVATEAPP: 373 | { 374 | // TODO 375 | } break; 376 | 377 | case WM_SIZE: 378 | { 379 | switch (wParam) 380 | { 381 | case SIZE_MINIMIZED: 382 | { 383 | g_win32_state.has_focus = 1; 384 | 385 | } break; 386 | 387 | case SIZE_MAXIMIZED: 388 | { 389 | g_win32_state.has_focus = 0; 390 | } break; 391 | } 392 | } break; 393 | 394 | case WM_SETFOCUS: 395 | { 396 | g_win32_state.has_focus = 1; 397 | } break; 398 | 399 | case WM_KILLFOCUS: 400 | { 401 | g_win32_state.has_focus = 0; 402 | } break; 403 | 404 | case WM_DESTROY: 405 | { 406 | g_running = false; 407 | } break; 408 | 409 | case WM_SETCURSOR: 410 | { 411 | result = DefWindowProcA(hwnd, message, wParam, lParam); 412 | } break; 413 | 414 | case WM_SYSKEYDOWN: 415 | case WM_SYSKEYUP: 416 | case WM_KEYDOWN: 417 | case WM_KEYUP: 418 | { 419 | ASSERT(!"Keyboard input come in through a non-dispatch messsage"); 420 | } break; 421 | 422 | // PeekMessage does not remve WM_PAINT itself, have to handle this. 423 | // see MSDN page on PeekMessage 424 | case WM_PAINT: 425 | { 426 | PAINTSTRUCT paint; 427 | HDC dc = BeginPaint(hwnd, &paint); 428 | Win32WindowSize size = Win32GetWindowSize(hwnd); 429 | Win32DisplayBufferInWindow(dc, &g_screenBuffer, size); 430 | EndPaint(hwnd, &paint); 431 | } break; 432 | 433 | default: 434 | { 435 | result = DefWindowProcA(hwnd, message, wParam, lParam); 436 | } break; 437 | } 438 | 439 | return result; 440 | } 441 | 442 | int CALLBACK 443 | WinMain(HINSTANCE instance, HINSTANCE preInstance, LPSTR cmdline, int showCode) 444 | { 445 | WNDCLASSA windowClass = { }; 446 | 447 | /* 448 | For applications like games which need to draw frequently, we can specify 449 | CS_OWNDC to create a private Device Context only used by this application 450 | window, which could avoid the overhead of retrieving DC every draw. 451 | 452 | alse see: 453 | MSDN: GetDC / ReleaseDC 454 | Raymond Chen: What does the CS_OWNDC class style do 455 | */ 456 | 457 | windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; 458 | windowClass.lpfnWndProc = Win32MainWindowCallback; 459 | // windowClass.cbClsExtra; 460 | // windowClass.cbWndExtra; 461 | windowClass.hInstance = instance; 462 | // windowClass.hIcon; 463 | windowClass.hCursor = LoadCursor(0, IDC_ARROW); 464 | // windowClass.hbrBackground; 465 | // windowClass.lpszMenuName; 466 | windowClass.lpszClassName = "Quake Zero"; 467 | 468 | HWND hwnd; 469 | if (RegisterClassA(&windowClass)) 470 | { 471 | hwnd = CreateWindowExA(0, 472 | windowClass.lpszClassName, 473 | "Quake Zero", 474 | WS_OVERLAPPEDWINDOW, 475 | CW_USEDEFAULT, 476 | CW_USEDEFAULT, 477 | CW_USEDEFAULT, 478 | CW_USEDEFAULT, 479 | 0, 480 | 0, 481 | instance, 482 | 0); 483 | } 484 | else 485 | { 486 | // TODO lw: logging 487 | return 0; 488 | } 489 | 490 | if (!hwnd) 491 | { 492 | // TODO lw: logging 493 | return 0; 494 | } 495 | 496 | U64 counterFrequency; 497 | 498 | { 499 | LARGE_INTEGER cf; 500 | QueryPerformanceFrequency(&cf); 501 | counterFrequency = cf.QuadPart; 502 | } 503 | 504 | // TODO find out pros and cons of high system schedule resolution 505 | UINT timerResolution = 1; // in millisecond 506 | bool isSleepGranular = (timeBeginPeriod(timerResolution) == TIMERR_NOERROR); 507 | 508 | HDC privateDC = GetDC(hwnd); 509 | 510 | float targetSecondsPerFrame = 0; 511 | 512 | // get pfs of the screen 513 | { 514 | int refreshRate = GetDeviceCaps(privateDC, VREFRESH); 515 | // default to 60fps 516 | refreshRate = (refreshRate > 0) ? refreshRate : 60; 517 | 518 | // set the target frame rate to half of the screen frame rate 519 | targetSecondsPerFrame = 1.0f / (float)refreshRate * 2.0f; 520 | } 521 | 522 | 523 | Win32GetExeFileName(&g_win32_state); 524 | 525 | char sourceGameDLLPath[WIN32_MAX_FILE_PATH_LENGTH]; 526 | Win32BuildGameFilePath(&g_win32_state, "q_game.dll", 527 | sourceGameDLLPath, sizeof(sourceGameDLLPath)); 528 | 529 | char tempGameDLLPath[WIN32_MAX_FILE_PATH_LENGTH]; 530 | Win32BuildGameFilePath(&g_win32_state, "q_temp_game.dll", 531 | tempGameDLLPath, sizeof(tempGameDLLPath)); 532 | 533 | char gameCodeLockPath[WIN32_MAX_FILE_PATH_LENGTH]; 534 | Win32BuildGameFilePath(&g_win32_state, "lock.tmp", 535 | gameCodeLockPath, sizeof(gameCodeLockPath)); 536 | 537 | GameMemory gameMemory = {}; 538 | 539 | gameMemory.targetSecondsPerFrame = targetSecondsPerFrame; 540 | 541 | gameMemory.gameMemory = malloc(MEGA_BYTES(64)); 542 | gameMemory.gameMemorySize = MEGA_BYTES(64); 543 | 544 | gameMemory.platformAPI.SysError = Win32SysError; 545 | gameMemory.platformAPI.SysSetPalette = Win32SetPalette; 546 | 547 | Win32BuildGameFilePath(&g_win32_state, "..\\assets\\", 548 | gameMemory.gameAssetDir, sizeof(gameMemory.gameAssetDir)); 549 | 550 | Win32GameCode gameCode = Win32LoadGameCode(sourceGameDLLPath, 551 | tempGameDLLPath, 552 | gameCodeLockPath); 553 | 554 | GameInput gameInput[2] = {0}; 555 | 556 | U64 startCounter = Win32GetWallClock(); 557 | 558 | // set offscreen buffer size 559 | gameMemory.offscreenBuffer.width = 320; 560 | gameMemory.offscreenBuffer.height = 240; 561 | gameMemory.offscreenBuffer.bytesPerPixel = 1; 562 | int widthbytes = gameMemory.offscreenBuffer.width * gameMemory.offscreenBuffer.bytesPerPixel; 563 | gameMemory.offscreenBuffer.bytesPerRow = (widthbytes + sizeof(LONG) - 1) & ~(sizeof(LONG) -1); 564 | 565 | gameCode.GameInit(&gameMemory); 566 | 567 | Win32ResizeDIBSection(&g_screenBuffer, &gameMemory.offscreenBuffer); 568 | 569 | // move the window to the center of the screen 570 | { 571 | int window_width = 640; 572 | int window_height = 480; 573 | RECT rect = {0}; 574 | const HWND hDesktop = GetDesktopWindow(); 575 | GetWindowRect(hDesktop, &rect); 576 | int left = (rect.right - window_width) / 2; 577 | int top = (rect.bottom - window_height) / 2; 578 | rect.left = left; 579 | rect.top = top; 580 | rect.right = window_width + rect.left - 1; 581 | rect.bottom = window_height + rect.top - 1; 582 | AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); 583 | 584 | // TODO lw: We could make the window resizable, because the frame buffer 585 | // and the z-buffer are allocated at high hunk. Resize them won't affect 586 | // other memory. 587 | SetWindowPos(hwnd, HWND_TOP, rect.left, rect.top, 588 | rect.right - rect.left, rect.bottom - rect.top, 589 | SWP_SHOWWINDOW); 590 | 591 | RECT window_rect = {0}; 592 | 593 | GetClientRect(hwnd, &rect); 594 | POINT point = {0}; 595 | ClientToScreen(hwnd, &point); 596 | 597 | window_rect.left = point.x; 598 | window_rect.top = point.y; 599 | 600 | point = {rect.right, rect.bottom}; 601 | ClientToScreen(hwnd, &point); 602 | 603 | window_rect.right = point.x; 604 | window_rect.bottom = point.y; 605 | 606 | ShowCursor(FALSE); 607 | // TODO lw: after switching windows, cursor won't be clipped 608 | ClipCursor(&window_rect); 609 | SetCursorPos((window_rect.left + window_rect.right) / 2, 610 | (window_rect.top + window_rect.bottom) / 2); 611 | 612 | g_game_input.mouse.old_x = (window_rect.left + window_rect.right) / 2; 613 | g_game_input.mouse.old_y = (window_rect.top + window_rect.bottom) / 2; 614 | 615 | g_win32_state.window_size = window_rect; 616 | } 617 | 618 | g_win32_state.has_focus = 1; 619 | 620 | g_running = true; 621 | while (g_running) 622 | { 623 | Win32ProcessPendingMessages(); 624 | Win32ProcessMouseMove(g_win32_state.has_focus, g_win32_state.window_size, &g_game_input.mouse); 625 | 626 | U64 counter = Win32GetWallClock(); 627 | 628 | gameCode.GameUpdateAndRender(&g_game_input); 629 | 630 | float secondsElapsed = Win32GetSecondsElapsed(startCounter, counter, counterFrequency); 631 | #ifdef QUAKEREMAKE_INTERNAL 632 | 633 | // TODO lw: Game DLL stores many global variables which will be zeroed 634 | // out after reloading the DLL. So hot reloading cpp won't work properly 635 | // for now. 636 | FILETIME newFileTime = Win32GetLastWriteTime(sourceGameDLLPath); 637 | if (CompareFileTime(&newFileTime, &gameCode.lastWriteTime) != 0) 638 | { 639 | Win32UnloadGameCode(&gameCode); 640 | for (int loadTryIndex = 0; !gameCode.isValid && (loadTryIndex < 100); ++loadTryIndex) 641 | { 642 | gameCode = Win32LoadGameCode(sourceGameDLLPath, tempGameDLLPath, gameCodeLockPath); 643 | Sleep(100); 644 | } 645 | } 646 | 647 | #endif 648 | 649 | if (secondsElapsed < targetSecondsPerFrame) 650 | { 651 | DWORD sleepMilliseconds = (DWORD)(1000.0f * (targetSecondsPerFrame - secondsElapsed)); 652 | 653 | if (sleepMilliseconds) 654 | { 655 | static char tempbuffer[64]; 656 | sprintf_s(tempbuffer, 64, "Sleep for %u milliseconds\n", sleepMilliseconds); 657 | OutputDebugStringA(tempbuffer); 658 | 659 | // TODO lw: find a better way to do this 660 | Sleep(sleepMilliseconds); 661 | } 662 | OutputDebugStringA("Wake up\n"); 663 | 664 | U64 testCounter = Win32GetWallClock(); 665 | float testSecondsElapsed = Win32GetSecondsElapsed(startCounter, testCounter, counterFrequency); 666 | 667 | if (testSecondsElapsed > targetSecondsPerFrame) 668 | { 669 | OutputDebugStringA("Slept over one frame!\n"); 670 | } 671 | 672 | while (testSecondsElapsed < targetSecondsPerFrame) 673 | { 674 | testCounter = Win32GetWallClock(); 675 | testSecondsElapsed = Win32GetSecondsElapsed(startCounter, testCounter, counterFrequency); 676 | } 677 | } 678 | 679 | U64 endCounter = Win32GetWallClock(); 680 | float millisecPerFrame = 1000.0f * Win32GetSecondsElapsed(startCounter, endCounter, counterFrequency); 681 | startCounter = endCounter; 682 | 683 | Win32WindowSize windowSize = Win32GetWindowSize(hwnd); 684 | Win32DisplayBufferInWindow(privateDC, &g_screenBuffer, windowSize); 685 | } 686 | 687 | return 0; 688 | } 689 | -------------------------------------------------------------------------------- /code/sys_win32.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Win32BitmapInfo 4 | { 5 | BITMAPINFOHEADER bmiHeader; 6 | RGBQUAD bmiColors[256]; 7 | }; 8 | 9 | struct Win32ScreenBuffer 10 | { 11 | Win32BitmapInfo bitmapInfo; 12 | int width; 13 | int height; 14 | int pixelBytes; 15 | int rowBytes; 16 | void *memory; 17 | }; 18 | 19 | struct Win32WindowSize 20 | { 21 | int width; 22 | int height; 23 | }; 24 | 25 | #define WIN32_MAX_FILE_PATH_LENGTH 256 26 | struct Win32State 27 | { 28 | char exeFilePath[WIN32_MAX_FILE_PATH_LENGTH]; 29 | char *onePastLastExeFilePathSlash; 30 | RECT window_size; 31 | bool has_focus; 32 | }; 33 | 34 | struct Win32GameCode 35 | { 36 | HMODULE gameCodeDLL; 37 | FILETIME lastWriteTime; 38 | 39 | GameInit_t *GameInit; 40 | GameUpdateAndRender_t *GameUpdateAndRender; 41 | 42 | bool isValid; 43 | }; 44 | 45 | INTERNAL_LINKAGE 46 | U8 g_scantokey[128] = { 47 | 0 , K_ESCAPE, '1', '2', '3', '4', '5', '6', 48 | '7', '8', '9', '0', '-', '=', K_BACKSPACE, 9, // 0 49 | 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 50 | 'o', 'p', '[', ']', 13 , K_CTRL,'a', 's', // 1 51 | 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', 52 | '\'' , '`', K_SHIFT,'\\', 'z', 'x', 'c', 'v', // 2 53 | 'b', 'n', 'm', ',', '.', '/', K_SHIFT,'*', 54 | K_ALT,' ', 0 , K_F1, K_F2, K_F3, K_F4, K_F5, // 3 55 | K_F6, K_F7, K_F8, K_F9, K_F10, K_PAUSE, 0 , K_HOME, 56 | K_UPARROW,K_PGUP,'-',K_LEFTARROW,'5',K_RIGHTARROW,'+',K_END, //4 57 | K_DOWNARROW,K_PGDN,K_INS,K_DEL,0,0, 0, K_F11, 58 | K_F12,0 , 0 , 0 , 0 , 0 , 0 , 0, // 5 59 | 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 60 | 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, // 6 61 | 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0, 62 | 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 // 7 63 | }; 64 | -------------------------------------------------------------------------------- /code/vcenv.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x64 4 | -------------------------------------------------------------------------------- /code/vs.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | IF EXIST ..\build\sys_win32.sln ( 4 | devenv ..\build\sys_win32.sln 5 | ) ELSE ( 6 | devenv ..\build\sys_win32.exe 7 | ) 8 | -------------------------------------------------------------------------------- /docs/cache_alloc_0.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linw789/quake-zero/8ada482c819498ca2550104a2c5822f208fe8ac9/docs/cache_alloc_0.PNG -------------------------------------------------------------------------------- /docs/memory_management.md.html: -------------------------------------------------------------------------------- 1 | **Memory Management** 2 | 3 | 4 | (#) Memory Management 5 | 6 | Quake's memory scheme is to allocate up front all the memory it potentially 7 | needs for running the game in a continuous memory block, within which all the 8 | game data are managed by three different allocators, namely, Hunk Allocator, 9 | Zone Allocator and Cache Allocator. Each allocator serves a dfferent purpose and 10 | takes care of certain types of data. 11 | 12 | (##) Hunk Allocator 13 | 14 | Hunk is the main allocator that manages the entire memory block given to Quake. 15 | As a double-ended [stack][wikipedia_stack] allocator, hunk can grow either from 16 | the lowest memory address up to high memory address or from the highest memory 17 | address down to low memory address, as long as they don't collide. The 18 | volatility of data determines where they will be allocated. 19 | 20 | ********************************************************************************* 21 | * whole memory for game * 22 | * .-----------------------------+-----------------------------------. * 23 | * | | * 24 | * .----+---+----+-----------------------+---------+-------+-----------. * 25 | * |zone|bsp|hunk|-----> free memory <---|temporary|zbuffer|videobuffer| * 26 | * '----+---+----+-----------------------+---------+-------+-----------' * 27 | * ^ ^ * 28 | * | | * 29 | * lowest memory address highest memory address * 30 | ********************************************************************************* 31 | [Figure : Hunk Allocator] 32 | 33 | The low hunks are primarily used to store static data that don't change in a 34 | level or even during the entire game session, like BSP which includes vertices, 35 | surfaces, textures and ec cetera. In Quake the first hunk allocated at low hunk 36 | memory is used as a memory pool for Zone Allocator (discussed in the next 37 | section). Contrary to what's showed in the above diagram the data in low hunks 38 | are much larger than that in high hunks. 39 | 40 | A drawback of stack based allocator is that items can only be popped out 41 | (deleted) in the reverse order they are pushed onto the stack, which is known as 42 | FILO for First In Last Out. That means zone hunk cannot be popped until bsp hunk 43 | is popped because zone hunk are pushed onto the stack before bsp hunk. Although, 44 | the data in each item can still be modified, the size cannot change without 45 | popping the item pushed after it. Quake chooses to allocate video buffer, z 46 | buffer together in high hunks because their sizes can change at any time during 47 | the game which would disturb the static data pushed after them if they were in 48 | low hunks. And the fact that their sizes should always change at the same time 49 | counters the drawback mentioned before. 50 | 51 | Another use for high hunks is to allocate temporary data. A major application is 52 | when loading BSP from the PAK file at which time data are loaded into the 53 | temporary hunk and transformed and moved to low hunks. Unliked normal high 54 | hunks, there is only one temporary hunk and it's always on the top of the high 55 | hunk stack. 56 | 57 | Each hunk comes with a header in front of the actual data. And it contains a 58 | char array of name, an integer of size as well as an integer for memory 59 | corruption detection. 60 | 61 | ~~~~~~~~~~~~ 62 | struct 63 | { 64 | int sentinel; /* this value is always 0x1df001ed */ 65 | int size; /* hunk size including the header itself */ 66 | char name[8]; 67 | }; 68 | ~~~~~~~~~~~~ 69 | 70 | The value of the first four bytes of each hunk will always be 0x1df001ed 71 | (read "id fooled"). If it's not then anything comes after it might no longer be 72 | valid. The size of every hunk is 16 bytes aligned for more efficient memory 73 | access. 74 | 75 | 76 | (##) Zone Allocator 77 | 78 | Zone Allocator manages, within the first low hunk, small, dynamic memory 79 | allocations. In contrast to the stack based allocator it is implemented as a 80 | [doubly linked list][wikipedia_doubly_linked_lsst] which facilitates easier 81 | deletion and insertion. 82 | 83 | Each node in the linked list is a memory block that is either occupied or free. 84 | Initially Zone Allocator uses its entire provisioned memory pool as one free 85 | memory block and inserts it as the first node into the linked list. Later for 86 | each allocation, the Zone Allocator linearly searches through the linked list 87 | and find a free memory node whose size is larger than requested then breaks it 88 | into two memory nodes one with requested size and the rest as free then relink 89 | the two. 90 | 91 | ********************************************************************************* 92 | * N: node, O: occupied, F: free * 93 | * before insertion * 94 | * .------. .-------------------------. .-----------. .---------------. * 95 | * | N1(O)|<-->| N2(F) |<-->| N3(O) |<-->| N4(F) | * 96 | * '------' '-------------------------' '-----------' '---------------' * 97 | * after insertion * 98 | * .------. .-------------. .------. .-----------. .---------------. * 99 | * | N1(O)|<-->| N5(O) |<-->| N6(F)|<-->| N3(O) |<-->| N4(F) | * 100 | * '------' '-------------' '------' '-----------' '---------------' * 101 | ********************************************************************************* 102 | [Figure : Zone Allocator Insertion] 103 | 104 | After deleting a node from the linked list, the allocator checks nodes on 105 | both sides of the deleted one and merge them into one free node if they are 106 | both free. This guarantees that there will never be two consecutive free 107 | memory nodes in the linked list, hence less 108 | [fragmentation][stackoverflow_fragmentation]. 109 | 110 | ********************************************************************************* 111 | * N: node, O: occupied, F: free * 112 | * before deletion * 113 | * .-----. .--------------. .-------. .----------. .---------------. * 114 | * |N1(O)|<-->| N2(F) |<-->| N3(O) |<-->| N4(F) |<-->| N5(O) | * 115 | * '-----' '--------------' '-------' '----------' '---------------' * 116 | * after deletion of N3 * 117 | * .-----. .-------------------------------------------. .---------------. * 118 | * |N1(O)|<-->| N6(F) |<-->| N5(O) | * 119 | * '-----' '-------------------------------------------' '---------------' * 120 | ********************************************************************************* 121 | [Figure : Zone Allocator Deletion] 122 | 123 | An important implementation detail here is that Quake implements the doubly 124 | linked list as circular list with a dummy node designated as both the head and 125 | the tail. The dummy node links the first real node as its next node and the 126 | last real node links the dummy node as its next one. At the cost of small extra 127 | space of a dummy node, the circular list eliminates branches in inertion and 128 | deletion operations, making them both simpler and faster. All doubly linked 129 | lists are circular lists in Quake. 130 | 131 | ~~~~~~~~~~~~~~~~ 132 | /* 133 | untested code snippet demonstrating circular linked list 134 | */ 135 | 136 | struct Node 137 | { 138 | Node *prev; /* pointer to the previous node */ 139 | Node *next; /* pointer to the next node */ 140 | int val; 141 | }; 142 | 143 | /* 144 | insert and delete operations on non-circular doubly linked list 145 | */ 146 | 147 | // the first node in the non-circular linked list, NULL means the list is empty. 148 | Node *head; 149 | 150 | // insert operation on non-circular doubly linked list 151 | void InsertAfter(Node *node, Node *node_to_insert) 152 | { 153 | if (node == NULL) 154 | { 155 | head = node_to_insert; 156 | head->next = NULL; 157 | return ; 158 | } 159 | 160 | if (node->next != NULL) 161 | { 162 | node->next->prev = node_to_insert; 163 | node_to_insert->next = node->next; 164 | node->next = node_to_insert; 165 | node_to_insert->prev = node; 166 | } 167 | else 168 | { 169 | node->next = node_to_insert; 170 | node_to_insert->prev = node; 171 | node_to_insert->next = NULL; 172 | } 173 | } 174 | 175 | // delete operation on non-circular doubly linked list 176 | void Delete(Node *node_to_delete) 177 | { 178 | if (node_to_delete == NULL) 179 | return ; 180 | 181 | if (node_to_delete->next == NULL) 182 | { 183 | if (node_to_delete->prev == NULL) 184 | { 185 | head = NULL; 186 | } 187 | else 188 | { 189 | node_to_delete->prev->next = NULL; 190 | } 191 | } 192 | else if (node_to_delete->prev == NULL) 193 | { 194 | head = node_to_delete->next; 195 | } 196 | else 197 | { 198 | node_to_delete->next->prev = node_to_delete->prev; 199 | node_to_delete->prev->next = node_to_delete->next; 200 | } 201 | } 202 | 203 | /* 204 | insert and delete operations on circular doubly linked list 205 | */ 206 | 207 | // the dummy node in the circular linked list, when the list is empty both 208 | // dummy's next and prev point to itself. 209 | Node *dummy 210 | 211 | // insert operation on circular doubly linked list 212 | void InsertAfter(Node *node, Node *node_to_insert) 213 | { 214 | // Here we don't need to worry if the node is the last one in the linked 215 | // list. Even if it is, its next node is the dummy node instead of NULL. 216 | node->next->prev = node_to_insert; 217 | node_to_insert->next = node->next; 218 | node->next = node_to_insert; 219 | node_to_insert->prev = node; 220 | } 221 | 222 | // delete operation on circular doubly linked list 223 | void Delete(Node *node_to_delete) 224 | { 225 | node_to_delete->next->prev = node_to_delete->prev; 226 | node_to_delete->prev->next = node_to_delete->next; 227 | } 228 | ~~~~~~~~~~~~~~~~ 229 | 230 | 231 | (##) Cache Allocator 232 | 233 | Cache Allocator is the most complicated of all three. Its purpose is to save 234 | data loading time by caching them on memory and also, more importantly, 235 | circumvents the memory size limit by ejecting the 236 | [Least Recently Used][lru_caching] data to make room for new ones. 237 | 238 | Let's take as an example loading monster models in each level. Because many 239 | levels use the same monster models, it would be less efficient to reload them 240 | every time entering a different level. Instead, a monster model could be reused 241 | if it has already been loaded into a cache that still exists in the memory. Due 242 | to the limited memory space, however, it is infeasible to load all monster 243 | models into caches. When memory is full, the Least Recently Used caches, which 244 | most likely hosting monster models that have been killed for a while, will be 245 | deleted to accommodate new monster models. 246 | 247 | Cache Allocator operates dinamically in the free memory region between the low 248 | hunks and the high hunks, meaning that the region could expand or shrink at both 249 | ends at the needs of Hunk Allocator. For example, if Hunk Allocator needs more 250 | space at the low stack it can iteratively delete caches closest to the low stack 251 | until enough room is made. And if Hunk Allocator pops several items from the 252 | top of the low stack, Cache Allocator can allocate new data at those free space. 253 | 254 | Cache Allocator is implemented using two circular doubly linked lists, one is to 255 | link caches and the other is to keep track of the Least Recently Used cache. It 256 | starts allocation at the top of the low hunk stack and linearly searches upwards 257 | to find a hole inbetween caches. If failed, the allocator will try the free 258 | memory betwen the last cache and the top of the high hunk stack. And if that 259 | memory block is not big enough it then ejects the Least Recently Used cache, 260 | consequently creating a hole, and starts over the search again. 261 | 262 | To better demonstrate the mechanism of Cache Allocator, I created a demo program 263 | that prints out the two links after every allocation. The following is a 264 | snapshot of the program after four allocations. 265 | ![Demo of Cache Allocator](cache_alloc_0.png) 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | [wikipedia_stack]: https://en.wikipedia.org/wiki/Stack_(abstract_data_type) 275 | [wikipedia_doubly_linked_lsst]: https://en.wikipedia.org/wiki/Doubly_linked_list 276 | [stackoverflow_fragmentation]: http://stackoverflow.com/questions/3770457/what-is-memory-fragmentation 277 | [lru_caching]: http://mcicpc.cs.atu.edu/archives/2012/mcpc2012/lru/lru.html 278 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /docs/pak_file.md.html: -------------------------------------------------------------------------------- 1 | **PAK File** 2 | 3 | 4 | (#) Format 5 | 6 | (#) Little Endian 7 | 8 | (#) Data Alignment 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/pixel_and_gamma.md.html: -------------------------------------------------------------------------------- 1 | 2 | gamma correction references: 3 | http://beautifulpixels.blogspot.com/2009/10/gamma-correct-lighting-on-moon.html 4 | http://www.poynton.com/notes/colour_and_gamma/GammaFAQ.html 5 | http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html 6 | http://filmicgames.com/archives/299 7 | http://filmicgames.com/archives/6 8 | http://www.cambridgeincolour.com/tutorials/gamma-correction.htm 9 | -------------------------------------------------------------------------------- /docs/quake_example_00.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linw789/quake-zero/8ada482c819498ca2550104a2c5822f208fe8ac9/docs/quake_example_00.PNG -------------------------------------------------------------------------------- /docs/quake_example_01.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linw789/quake-zero/8ada482c819498ca2550104a2c5822f208fe8ac9/docs/quake_example_01.PNG -------------------------------------------------------------------------------- /docs/quake_example_02.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linw789/quake-zero/8ada482c819498ca2550104a2c5822f208fe8ac9/docs/quake_example_02.PNG -------------------------------------------------------------------------------- /docs/quake_example_03.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linw789/quake-zero/8ada482c819498ca2550104a2c5822f208fe8ac9/docs/quake_example_03.PNG -------------------------------------------------------------------------------- /tests/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem -MTd : instead of using dynamic library, build static library into our executable, d for debug 4 | rem -nologo : remove MSVC compiler about infomation 5 | rem -W4 : enable the layer 4 warning 6 | rem -wd4201 : disable warning nameless struct/union 7 | rem -wd4100 : disable warning unreferenced formal parameter 8 | rem -wd4189 : disable warning local variable is initialized but not referenced 9 | rem -wd4505 : disable warning about unreferenced local functions 10 | rem -wd4127 : disable warning about conditional expression is constant 11 | rem -WX : treats all compiler warnings as errors 12 | rem -Gm- : disable minimal rebuild. We use unity build, entire code base will be rebuild for every change. 13 | rem -GR- : diable c++ run-time type information 14 | rem -Od : disable all compiler optimization 15 | rem -Oi : replace function calls with intrinsic whenever possible 16 | rem -Z7 : pobably works better with multi-core processer than -Zi 17 | rem -EHa- : return off exception handling 18 | 19 | set CommonCompilerFlags=-MTd -nologo -Gm- -GR- -EHa- -Od -Oi -WX -W4 ^ 20 | -wd4127 -wd4100 -wd4201 -wd4189 -wd4505 ^ 21 | -DQUAKEREMAKE_INTERNAL=1 -DQUAKEREMAKE_SLOW=1 -DQUAKEREMAKE_WIN32=0 -FC -Z7 22 | 23 | rem -opt:ref : something about including minimal libs. Handmade Hero Day 016(45:08) 24 | set CommonLinkerFlags= -incremental:no -opt:ref 25 | 26 | cl %CommonCompilerFlags% tests.cpp -Fmtests.map /link %CommonLinkerFlags% 27 | -------------------------------------------------------------------------------- /tests/tests.cpp: -------------------------------------------------------------------------------- 1 | #include "..\code\q_platform.h" 2 | #include "..\code\q_common.cpp" 3 | #include "..\code\q_math.h" 4 | 5 | #include 6 | #include 7 | 8 | SYS_ERROR(CmdError) 9 | { 10 | char error[1024]; 11 | va_list vl; 12 | va_start(vl, format); 13 | vsprintf_s(error, 1024, format, vl); 14 | va_end(vl); 15 | 16 | printf("Error: "); 17 | printf(error); 18 | } 19 | 20 | I32 g_errorCount = 0; 21 | #define ERROR(condition) if (!(condition)) { \ 22 | ++g_errorCount; \ 23 | printf("!!! Error at line %d\n", __LINE__); \ 24 | } 25 | 26 | void test_StringLength() 27 | { 28 | char *testStr = ""; 29 | I32 length = StringLength(testStr); 30 | ERROR(length == 0); 31 | 32 | // error: testStr = '\0'; assigning 0 to Str 33 | testStr = "\0"; 34 | length = StringLength(testStr); 35 | ERROR(length == 0); 36 | 37 | testStr = "quake remake\n \r"; 38 | length = StringLength(testStr); 39 | ERROR(length == 15); 40 | } 41 | 42 | void test_StringCompare() 43 | { 44 | char *str1 = "test string 1"; 45 | char *str2 = "test string 2"; 46 | 47 | I32 result = StringCompare(str1, str2); 48 | ERROR(result == -1); 49 | 50 | result = StringNCompare(str1, str2, 12); 51 | ERROR(result == 0); 52 | 53 | result = StringNCompare(str1, str2, 0); 54 | ERROR(result == 0); 55 | 56 | str1 = ""; 57 | 58 | result = StringCompare(str1, str2); 59 | ERROR(result == -1); 60 | 61 | str2 = ""; 62 | 63 | result = StringCompare(str1, str2); 64 | ERROR(result == 0); 65 | 66 | str1 = "I am, indeed, a king,"; 67 | 68 | result = StringCompare(str1, "I am, indeed, a king,"); 69 | ERROR(result == 0); 70 | } 71 | 72 | void test_StringCopy() 73 | { 74 | char *src = "normal string"; 75 | char dest[128]; 76 | 77 | I32 result = StringCopy(dest, 128, src, 0); 78 | ERROR(result == 13); 79 | ERROR(StringCompare(dest, src) == 0); 80 | 81 | result = StringCopy(dest, 128, src, 6); 82 | ERROR(result == 6); 83 | ERROR(StringCompare(dest, "normal") == 0); 84 | 85 | result = StringCopy(dest, 3, src, 128); 86 | ERROR(result == 2); 87 | ERROR(StringCompare(dest, "no") == 0); 88 | 89 | result = StringCopy(dest, 128, "", 128); 90 | ERROR(result == 0); 91 | ERROR(StringCompare(dest, "") == 0); 92 | } 93 | 94 | void test_IntToString() 95 | { 96 | char str[128]; 97 | IntToString(1234, str, 128); 98 | ERROR(StringCompare(str, "1234") == 0); 99 | 100 | IntToString(123456, str, 3); 101 | ERROR(StringCompare(str, "56") == 0); 102 | 103 | IntToString(-12223, str, 128); 104 | ERROR(StringCompare(str, "-12223") == 0); 105 | 106 | IntToString(0, str, 128); 107 | ERROR(StringCompare(str, "0") == 0); 108 | } 109 | 110 | void test_StringToInt() 111 | { 112 | I32 result = StringToInt("9999"); 113 | ERROR(result == 9999); 114 | 115 | result = StringToInt("-12345"); 116 | ERROR(result == -12345); 117 | 118 | result = StringToInt("+9900"); 119 | ERROR(result == 9900); 120 | 121 | result = StringToInt("d12345"); 122 | ERROR(result == 0); 123 | 124 | result = StringToInt("324dx34"); 125 | ERROR(result == 324); 126 | 127 | result = StringToInt("00320"); 128 | ERROR(result == 320); 129 | } 130 | 131 | void test_MemSet() 132 | { 133 | // U8 dest[128] = {7}; will only initialize the dest[0] to 7, the rest 134 | // will still be 0 135 | U8 dest[128] = {0}; 136 | for (I32 i = 0; i < 128; ++i) 137 | { 138 | dest[i] = 7; 139 | } 140 | dest[0] = 5; 141 | dest[1] = 11; 142 | dest[2] = 13; 143 | dest[3] = 17; 144 | 145 | ERROR(((size_t)dest & 3) == 0); 146 | 147 | MemSet(dest, 2, 1); 148 | ERROR(dest[0] == 2); 149 | 150 | dest[0] = 5; 151 | MemSet(dest + 1, 2, 4); 152 | for (I32 i = 1; i < 5; ++i) 153 | { 154 | dest[i] = 2; 155 | } 156 | 157 | MemSet(dest, 2, 128); 158 | for (I32 i = 0; i < 128; ++i) 159 | { 160 | ERROR(dest[i] == 2); 161 | } 162 | } 163 | 164 | void test_MemCpy() 165 | { 166 | U8 dest[128] = {0}; 167 | for (I32 i = 0; i < 128; ++i) 168 | { 169 | dest[i] = 3; 170 | } 171 | 172 | U8 src[128] = {0}; 173 | for (I32 i = 0; i < 128; ++i) 174 | { 175 | src[i] = 5; 176 | } 177 | 178 | MemCpy(dest + 1, src, 4); 179 | ERROR(dest[0] == 3); 180 | for (I32 i = 1; i < 5; ++i) 181 | { 182 | ERROR(dest[i] == 5); 183 | } 184 | 185 | for (I32 i = 1; i < 5; ++i) 186 | { 187 | src[i] = 7; 188 | } 189 | MemCpy(dest, src + 1, 4); 190 | for (I32 i = 0; i < 4; ++i) 191 | { 192 | src[i] = 7; 193 | } 194 | 195 | for (I32 i = 0; i < 4; ++i) 196 | { 197 | src[i] = 11; 198 | } 199 | MemCpy(dest, src, 128); 200 | for (I32 i = 0; i < 4; ++i) 201 | { 202 | ERROR(dest[i] == 11); 203 | } 204 | } 205 | 206 | // re-arrange data order in struct to reduce padding size 207 | void test_DataSize() 208 | { 209 | ERROR(sizeof(MemoryBlock) == 32); 210 | ERROR(sizeof(MemoryZone) == 48); 211 | ERROR(sizeof(HunkHeader) == 24); 212 | ERROR(sizeof(CacheHeader) == 64); 213 | ERROR(sizeof(PackFile) == MAX_PACK_FILE_PATH + 8); 214 | ERROR(sizeof(PackHeader) == MAX_OS_PATH_LENGTH + 16); 215 | ERROR(sizeof(PackFileDisk) == 64); 216 | ERROR(sizeof(PackHeaderDisk) == 12); 217 | ERROR(sizeof(SearchPath) == MAX_OS_PATH_LENGTH + 16); 218 | } 219 | 220 | #define POOL_SIZE 16 * 1204 * 1024 221 | U8 pool[POOL_SIZE]; 222 | 223 | void test_MemoryAlloc() 224 | { 225 | MemoryInit((void *)pool, POOL_SIZE); 226 | 227 | ERROR(g_hunk_base == pool); 228 | ERROR(g_hunk_total_size == POOL_SIZE); 229 | I32 size = Align16(DYNAMIC_ZONE_SIZE + sizeof(HunkHeader)); 230 | ERROR(g_hunk_low_used == size); 231 | ERROR(g_hunk_high_used == 0); 232 | 233 | ERROR((U8 *)g_main_zone == pool + sizeof(HunkHeader)); 234 | ERROR(g_main_zone->rover == (MemoryBlock *)(g_main_zone + 1)); 235 | ERROR(g_main_zone->size == DYNAMIC_ZONE_SIZE); 236 | 237 | // ==================== 238 | // test hunk allocation 239 | // ==================== 240 | 241 | void *hunk0 = HunkLowAlloc(1231, "hunk0"); 242 | HunkHeader *hunk0Header = (HunkHeader *)hunk0 - 1; 243 | I32 hunk0Size = Align16(1231 + sizeof(HunkHeader)); 244 | ERROR(hunk0Header->size == hunk0Size); 245 | ERROR(hunk0Header->sentinel == HUNK_SENTINEL); 246 | ERROR(StringCompare(hunk0Header->name, "hunk0") == 0); 247 | ERROR(g_hunk_low_used == size + hunk0Size); 248 | 249 | void *hunk1 = HunkHighAlloc(3211, "hunk1"); 250 | HunkHeader *hunk1Header = (HunkHeader *)hunk1 - 1; 251 | I32 hunk1Size = Align16(3211 + sizeof(HunkHeader)); 252 | ERROR(hunk0Header->size == hunk0Size); 253 | ERROR(hunk0Header->sentinel == HUNK_SENTINEL); 254 | ERROR(StringCompare(hunk1Header->name, "hunk1") == 0); 255 | ERROR(g_hunk_high_used == hunk1Size); 256 | 257 | // ==================== 258 | // test zone allocation 259 | // ==================== 260 | 261 | void *zone0 = ZoneTagMalloc(234, 8); 262 | MemoryBlock *zone0Block = (MemoryBlock *)zone0 - 1; 263 | I32 zone0Size = Align8(234 + sizeof(MemoryBlock) + 4); 264 | ERROR(zone0Block->size == zone0Size); 265 | ERROR(zone0Block->tag == 8); 266 | ERROR(zone0Block->id == ZONE_ID); 267 | ERROR(zone0Block->prev == &g_main_zone->tailhead); 268 | MemoryBlock *freeBlock = zone0Block->next; 269 | ERROR(freeBlock->tag == 0); 270 | 271 | void *zone1 = ZoneTagMalloc(324, 1); 272 | void *zone2 = ZoneTagMalloc(432, 2); 273 | void *zone3 = ZoneTagMalloc(223, 3); 274 | void *zone4 = ZoneTagMalloc(333, 4); 275 | 276 | ZoneFree(zone1); 277 | MemoryBlock *zone1Block = (MemoryBlock *)zone1 - 1; 278 | I32 zone1Size = Align8(324 + sizeof(MemoryBlock) + 4); 279 | ERROR(zone1Block->tag == 0); 280 | ERROR(zone1Block->size == zone1Size); 281 | ERROR(zone1Block->id == ZONE_ID); 282 | ERROR(zone1Block->prev == (MemoryBlock *)zone0 - 1); 283 | ERROR(zone1Block->next == (MemoryBlock *)zone2 - 1); 284 | 285 | ZoneFree(zone3); 286 | MemoryBlock *zone3Block = (MemoryBlock *)zone3 - 1; 287 | I32 zone3Size = Align8(223 + sizeof(MemoryBlock) + 4); 288 | ERROR(zone3Block->tag == 0); 289 | ERROR(zone3Block->size == zone3Size); 290 | ERROR(zone3Block->id == ZONE_ID); 291 | ERROR(zone3Block->prev == (MemoryBlock *)zone2 - 1); 292 | ERROR(zone3Block->next == (MemoryBlock *)zone4 - 1); 293 | 294 | ZoneFree(zone2); 295 | I32 zone2Size = Align8(432 + sizeof(MemoryBlock) + 4); 296 | ERROR(zone1Block->tag == 0); 297 | ERROR(zone1Block->size == zone1Size + zone2Size + zone3Size); 298 | ERROR(zone1Block->prev == (MemoryBlock *)zone0 - 1); 299 | ERROR(zone1Block->next == (MemoryBlock *)zone4 - 1); 300 | 301 | void *tooBig = ZoneTagMalloc(DYNAMIC_ZONE_SIZE * 2, 22); 302 | ERROR(tooBig == NULL); 303 | 304 | ZoneClearAll(g_main_zone); 305 | ERROR(g_main_zone->tailhead.next == g_main_zone->rover); 306 | ERROR(g_main_zone->tailhead.prev == g_main_zone->rover); 307 | ERROR(g_main_zone->rover->next == &g_main_zone->tailhead); 308 | ERROR(g_main_zone->rover->next == &g_main_zone->tailhead); 309 | } 310 | 311 | void tests() 312 | { 313 | test_StringLength(); 314 | test_StringCompare(); 315 | test_StringCopy(); 316 | // TODO lw: test_CatString(); 317 | test_IntToString(); 318 | test_StringToInt(); 319 | 320 | test_MemSet(); 321 | test_MemCpy(); 322 | 323 | test_DataSize(); 324 | 325 | test_MemoryAlloc(); 326 | 327 | if (g_errorCount == 0) 328 | { 329 | printf("All tests succeeded.\n"); 330 | } 331 | } 332 | 333 | void demo_ParseCommand(char *line, char *verb, char *noun) 334 | { 335 | if (!line) 336 | { 337 | return ; 338 | } 339 | 340 | while (*line == ' ') 341 | { 342 | line++; 343 | continue; 344 | } 345 | 346 | while (*line != '\n' && *line != '\0') 347 | { 348 | if (*line == ' ') 349 | { 350 | break; 351 | } 352 | else 353 | { 354 | *verb = *line; 355 | line++; 356 | verb++; 357 | } 358 | } 359 | *verb = '\0'; 360 | 361 | while (*line != '\n' && *line != '\0') 362 | { 363 | if (*line == ' ') 364 | { 365 | line++; 366 | continue; 367 | } 368 | else 369 | { 370 | *noun = *line; 371 | line++; 372 | noun++; 373 | } 374 | } 375 | *noun = '\0'; 376 | 377 | return; 378 | } 379 | 380 | I32 demo_EndFreeSizeForCache() 381 | { 382 | I32 size = g_hunk_total_size - g_hunk_low_used - g_hunk_high_used; 383 | CacheHeader *first = g_cache_head.next; 384 | CacheHeader *last = g_cache_head.prev; 385 | I32 cacheUsed = (I32)((U8 *)last - (U8 *)first) + last->size; 386 | size = size - cacheUsed; 387 | return size; 388 | } 389 | 390 | void demo_drawCacheList() 391 | { 392 | CacheHeader *ch = g_cache_head.next; 393 | 394 | if (ch == &g_cache_head) 395 | { 396 | I32 endFreeSize = demo_EndFreeSizeForCache(); 397 | printf("[end:%d]", endFreeSize); 398 | return ; 399 | } 400 | 401 | while (ch != &g_cache_head) 402 | { 403 | printf("[%s:%d]->", ch->name, ch->size); 404 | 405 | if (ch->next != &g_cache_head) 406 | { 407 | I32 holeSize = (I32)((U8 *)ch->next - (U8 *)ch - ch->size); 408 | if (holeSize > 0) 409 | { 410 | printf("[hole:%d]->", holeSize); 411 | } 412 | } 413 | else 414 | { 415 | I32 endFreeSize = demo_EndFreeSizeForCache(); 416 | printf("[end:%d]", endFreeSize); 417 | } 418 | 419 | ch = ch->next; 420 | } 421 | 422 | printf("\n"); 423 | } 424 | 425 | void demo_DrawLRUList() 426 | { 427 | printf("LRU list: "); 428 | CacheHeader *ch = g_cache_head.lru_next; 429 | while (ch != &g_cache_head) 430 | { 431 | printf("%s", ch->name); 432 | if (ch->lru_next != &g_cache_head) 433 | { 434 | printf("->"); 435 | } 436 | ch = ch->lru_next; 437 | } 438 | printf("\n\n"); 439 | } 440 | 441 | void demo_CacheAlloc() 442 | { 443 | // leave 1024 bytes for cache allocation 444 | g_hunk_low_used = g_hunk_total_size - g_hunk_high_used - 1024; 445 | 446 | char cmdLine[32]; 447 | char arg0[16]; 448 | char arg1[16]; 449 | char userCountStr[16]; 450 | CacheUser cacheUser[128] = {0}; 451 | I32 cacheUserCount = 0; 452 | 453 | printf("Cache Alloc Demo Starts ... \n\n"); 454 | printf("Available cache size: %d bytes\n", demo_EndFreeSizeForCache()); 455 | printf("Allocation size includes the size of cache header which is 64 bytes and will be 16-byte aligned\n"); 456 | printf("[allocation count | hole | end: memory size]\n"); 457 | printf("hole and end are free memory blocks\n"); 458 | printf("LRU list: most recent used -> least recent used\n"); 459 | printf("type \"alloc\" and a number to allocate cache or \"exit\" to end the demo\n\n"); 460 | 461 | bool running = true; 462 | while (running) 463 | { 464 | if (fgets(cmdLine, 32, stdin) != cmdLine) 465 | { 466 | continue ; 467 | } 468 | 469 | demo_ParseCommand(cmdLine, arg0, arg1); 470 | 471 | if (StringCompare(arg0, "alloc") == 0) 472 | { 473 | cacheUserCount++; 474 | 475 | if (cacheUserCount >= 128) 476 | { 477 | printf("Exceed maximum times of allocation!"); 478 | break ; 479 | } 480 | 481 | I32 size = StringToInt(arg1); 482 | IntToString(cacheUserCount, userCountStr, 16); 483 | 484 | CacheAlloc(cacheUser + cacheUserCount, size, userCountStr); 485 | 486 | demo_drawCacheList(); 487 | demo_DrawLRUList(); 488 | } 489 | else if (StringCompare(arg0, "exit") == 0) 490 | { 491 | running = false; 492 | } 493 | else 494 | { 495 | printf("unrecognized command\n"); 496 | } 497 | } 498 | 499 | printf("Cache Alloc Demo Ended."); 500 | } 501 | 502 | I32 main(I32 argc, char *argv[]) 503 | { 504 | g_platformAPI.SysError = CmdError; 505 | 506 | printf("Unit Tests Begin ... \n\n"); 507 | 508 | tests(); 509 | 510 | printf("\nUnit Tests End.\n\n\n"); 511 | 512 | demo_CacheAlloc(); 513 | } 514 | --------------------------------------------------------------------------------