#include "platform.h" #include "os.h" #include "cgdoom.h" #include #include "libprof.h" #include "doomtype.h" #include "m_misc.h" #include "d_main.h" #include "v_video.h" #include "g_game.h" #include "cgdoom-alloc.h" #include "cgdoom-kbd.h" #include "cgdoom-ui.h" void *CGD_malloc(int size) { void *p = malloc(size); if (!p) I_Error ("CGD_malloc(%d) failure", size); return p; } void *CGD_calloc(int size) { void *p = CGD_malloc(size); if(p) memset(p, 0, size); return p; } void *CGD_realloc(void *p, int size) { if(p == NULL) return malloc(size); else return realloc(p, size); } uint16_t *VRAM; uint16_t *SecondaryVRAM; unsigned char *SystemStack; int strnicmp(const char *s1,const char *s2,int count) { for(int i = 0; i < count; i++) { int c1 = (s1[i] >= 'a' && s1[i] <= 'z') ? s1[i] & ~0x20 : s1[i]; int c2 = (s2[i] >= 'a' && s2[i] <= 'z') ? s2[i] & ~0x20 : s2[i]; if(c1 != c2 || !c1 || !c2) return c1 - c2; } return 0; } /////////////////////////////////////////////////////////////////////////////// // WAD file access mechanism // // Since BFile is too slow to access the WAD file in real-time, the fragments // are searched in ROM and their physical addresses are stored for direct // access later. This is similar to mmap() except that a manual translation is // used instead of the MMU. As far as I know, the first version of this system // was implemented by Martin Poupe. // // The file is obviously fragmented and Yatis reverse-engineered Fugue enough // to determine that storage units are sectors of 512 bytes. While clusters of // 4 kiB are used in general, a file might not start on the first sector of a // cluster, and some sectors might also simply be dead. // // Although all 65536 Flash sectors are searched when needed, several // heuristics are used: // * The region between FLASH_FS_HINT and FLASH_END is searched first, since // this is roughly where the filesystem is located. // * All sectors aligned on 4-kiB boundaries between FLASH_FS_HINT and // FLASH_END (of which there are 4096) are indexed by their first 4 bytes and // binary searched for matches before anything else. // // See for Flash traversal parameters. /////////////////////////////////////////////////////////////////////////////// /* Index of most likely ROM sectors. */ typedef struct { const void *sector; uint32_t start_bytes; } SectorIndexInfo; /* Description of a fragment of a WAD file, in Flash. This is placed in PRAM0 to save RAM elsewhere. PRAM0 only supports 32-bit accesses, so we make this structure a volatile bitfield where each entry has a 32-bit supporting type, and build with -fstrict-volatile-bitfields which instructs GCC to use accesses of the size of the supporting type without compromising on the naming of the fields. */ typedef volatile struct { /* Index of first sector in Flash (0...64k) */ uint32_t flash_address :16; /* Corresponding sector in the WAD file */ uint32_t file_address :16; } FileMappingItem; /* Maximum number of fragments in the file map. Fragment information is stored in PRAM0, so up to ~40k entries can be stored; this limit is mostly to catch failures in fragment detection. (I have witnessed Ultimate Doom WADs with up to 500 fragments.)*/ #define MAX_FRAGMENTS 2048 typedef struct { /* Table of fragments */ FileMappingItem *mTable; int miItemCount; /* Length of the file */ int miTotalLength; } FileMapping; /* File descriptor to WAD, used in Flash_ReadFile calls. (CGDOOM_WAD_BFILE) */ static int gWADfd = -1; /* Memory map of WAD file. (CGDOOM_WAD_MMAP) */ static FileMapping gWADMap; /* Index of most likely sectors for fragment search. (CGDOOM_WAD_MMAP) */ static SectorIndexInfo *gIndex = NULL; CGDoom_Options CGD_Options; CGDoom_Perf CGD_Perf; CGDoom_Stats CGD_Stats; /* Global options */ int CGD_SingleEpisodeUltimate = 0; int CGD_2MBLineMemory = 0; int CGD_Frameskip = 1; const char *CGD_WADFileName = NULL; int CGD_RecordDemoSlot = -1; int CGD_PlayDemoOnly = 0; /* Delayed file accesses */ CGDoom_DelayedFileWrite CGD_DelayedSaves[CGD_DELAYEDSAVES_COUNT] = { 0 }; #ifndef CG_EMULATOR //The whole sound doesn't fir onto the RAM. //Reading per partes is not possible as this is synchronnous player (there would be silences when reading). /* Fast memcmp() for 512-byte sectors. */ int CGD_sector_memcmp(const void *fast_ram, const void *rom, size_t _512); /* Caching structure to read WAD files by larger chunks than Flash sectors. */ typedef struct { int fd; int size, offset; char *data; /* of size FLASH_BFILE_UNIT */ } FileAccessCache; /* Read next sector from file, while caching into a buffer. */ const void *ReadNextSector(FileAccessCache *fc, int *size) { if(fc->size == 0) { fc->size = Bfile_ReadFile_OS(fc->fd, fc->data, FLASH_BFILE_UNIT, -1); fc->offset = 0; } if(fc->size <= 0) { *size = -1; return NULL; } *size = (fc->size < FLASH_PAGE_SIZE) ? fc->size : FLASH_PAGE_SIZE; fc->size -= *size; const void *sector = fc->data + fc->offset; fc->offset += *size; return sector; } /* Compare two sectors in ROM for the index. */ int IndexCompareSectors(const void *p1, const void *p2) { const SectorIndexInfo *i1 = p1; const SectorIndexInfo *i2 = p2; return i1->start_bytes - i2->start_bytes; } /* Find all matching sectors in index (returns in-out interval). */ void IndexSearchSector(SectorIndexInfo *index, const void *buf, int *lo_ptr, int *hi_ptr) { uint32_t needle = *(const uint32_t *)buf; *lo_ptr = *hi_ptr = -1; /* Find the first occurrence, set it in *lo_ptr */ int lo=0, hi=FLASH_INDEX_SIZE; while(lo < hi) { int m = (lo + hi) / 2; if(index[m].start_bytes < needle) lo = m + 1; else hi = m; } if(lo >= FLASH_INDEX_SIZE || index[lo].start_bytes != needle) return; *lo_ptr = hi = lo; /* Store last occurrence in *hi_ptr */ do hi++; while(hi < FLASH_INDEX_SIZE && index[hi].start_bytes == needle); *hi_ptr = hi; } /* Find a flash sector which contains the same data as buf. */ int FindSectorInFlash(const void *buf, int size) { typeof(&memcmp) memcmp_fun = &memcmp; if(size == FLASH_PAGE_SIZE) memcmp_fun = &CGD_sector_memcmp; CGD_Stats.WADFragments++; #ifdef FLASH_INDEX /* If an index has been built, search in it */ int lo, hi; IndexSearchSector(gIndex, buf, &lo, &hi); for(int i = lo; i < hi; i++) { if(!memcmp_fun(buf, gIndex[i].sector, size)) { CGD_Stats.WADIndexHits++; return (gIndex[i].sector - FLASH_START) / FLASH_PAGE_SIZE; } } #endif const void *sector = FLASH_FS_HINT; do { if(!memcmp_fun(buf, sector, size)) return (sector - FLASH_START) / FLASH_PAGE_SIZE; sector += FLASH_PAGE_SIZE; if(sector == FLASH_END) sector = FLASH_START; } while(sector != FLASH_FS_HINT); return -1; } int CreateFileMapping(int fd, FileMapping *pMap) { /* Cache accesses through a larger buffer */ FileAccessCache fc = { .data = (void *)0xe5007000, /* XRAM */ .fd = fd }; int iLength = 0; int iFileSize = Bfile_GetFileSize_OS(fd); pMap->miItemCount = 0; pMap->miTotalLength = 0; int iLastProgress = 0; const void *pFileData = ReadNextSector(&fc, &iLength); while(iLength > 0) { /* Don't show this too often, or it will eat several seconds */ if((pMap->miTotalLength - iLastProgress) * 20 > iFileSize) { UI_ProgressBar(HEIGHT-10, pMap->miTotalLength, iFileSize); iLastProgress = pMap->miTotalLength; } int iSectorID = FindSectorInFlash(pFileData, iLength); if(iSectorID == -1) return -2; // Page not found! pMap->miItemCount++; if(pMap->miItemCount >= MAX_FRAGMENTS) return -3; // File too fragmented! pMap->mTable[pMap->miItemCount-1].flash_address = iSectorID; pMap->mTable[pMap->miItemCount-1].file_address = pMap->miTotalLength / FLASH_PAGE_SIZE; /* Look for consecutive sectors in the same fragment */ const void *pFragment = FLASH_START + (iSectorID * FLASH_PAGE_SIZE); if(!CGD_Stats.WADLowestFragment || pFragmentmiTotalLength += iLength; iSectorID++; pFragment += FLASH_PAGE_SIZE; if(iLength < FLASH_PAGE_SIZE) { //this was the last page return pMap->miTotalLength; } pFileData = ReadNextSector(&fc, &iLength); if(iLength <= 0) break; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wstringop-overread" if((iLength == FLASH_PAGE_SIZE) ? CGD_sector_memcmp(pFileData, pFragment, iLength) : memcmp(pFileData, pFragment, iLength)) break; #pragma GCC diagnostic pop } } if(iLength < 0) return -1; return pMap->miTotalLength; } /* Find a fragment in the file map by binary search. */ int FindFragmentInMap(FileMapping *map, int sector_number) { int lo=0, hi=map->miItemCount; while (lo < hi) { int m = (lo + hi) / 2; if (map->mTable[m].file_address > sector_number) hi = m; else lo = m + 1; } return hi - 1; } int FindInFlash(const void **buf, int size, int readpos) { if(CGD_Options.WADMethod == CGDOOM_WAD_BFILE) return 0; ASSERT(readpos >= 0); if (readpos + size > gWADMap.miTotalLength) return -1; int iFragIndx = FindFragmentInMap(&gWADMap, readpos / FLASH_PAGE_SIZE); int iFragOffset = gWADMap.mTable[iFragIndx].file_address * FLASH_PAGE_SIZE; int iSubOffset = readpos - iFragOffset; int iFragEnd; if (iFragIndx+1 < gWADMap.miItemCount) iFragEnd = gWADMap.mTable[iFragIndx+1].file_address * FLASH_PAGE_SIZE; else iFragEnd = gWADMap.miTotalLength; *buf = FLASH_CACHED_START + (gWADMap.mTable[iFragIndx].flash_address * FLASH_PAGE_SIZE) + iSubOffset; /* Return how many bytes can be read off the fragment (up to size). */ int iAvailableLen = (iFragEnd-readpos < size) ? iFragEnd-readpos : size; ASSERT(iAvailableLen > 0); return iAvailableLen; } static int FindZeroedMemory(void *start) { /* Look for zero-longwords every 16 bytes */ int size = 0; /* Limit to 6 MB since the fx-CG 50 doesn't have any more memory and anything after the RAM is likely to be zero non-writable */ while(size < CGDOOM_2MBLINEMEMORY_MAX && !*(uint32_t *)(start + size)) size += 16; /* Round down to a multiple of 4096 */ return size & ~0xfff; } void abort(void){ int x=0,y=160; PrintMini(&x,&y,"Abort called",0,0xFFFFFFFF,0,0,0xFFFF,0,1,0); int key; for(;;) GetKey(&key); } #endif /* CG_EMULATOR */ int Flash_ReadFile(void *buf, int size, int readpos) { if(CGD_Options.WADMethod == CGDOOM_WAD_BFILE) return Bfile_ReadFile_OS(gWADfd, buf, size, readpos); const void *pSrc; int iRet = 0; while(size >0) { int i = FindInFlash(&pSrc,size, readpos); if(i<0) { I_Error ("Flash_ReadFile: cannot find position %d (size %d)", readpos, size); return i; } memcpy(buf,pSrc,i); buf = ((char*)buf)+i; readpos +=i; size -=i; iRet +=i; } return iRet; } /* Find files in the filesystem based on a pattern. */ static int FindFiles(const uint16_t *wildcard, CGDoom_FileInfo *files, int max) { uint16_t path[32]; Bfile_FileInfo info; int sd, rc, total=0; rc = Bfile_FindFirst(wildcard, &sd, path, &info); while(rc != -16 && total < max) { memcpy(files[total].path, u"\\\\fls0\\", 14); memcpy(files[total].path+7, path, 32*2); Bfile_NameToStr_ncpy(files[total].name, path, 32); files[total].size = info.fsize; total++; rc = Bfile_FindNext(sd, path, &info); } Bfile_FindClose(sd); return total; } static void DelayedWriteFile(int i) { CGDoom_DelayedFileWrite const *dfw = &CGD_DelayedSaves[i]; uint16_t fc_path[100] = u"\\\\fls0\\"; int j=7, rc, fd; for (int i = 0; dfw->filename[i]; i++) fc_path[j++] = dfw->filename[i]; fc_path[j++] = 0x0000; UI_DelayedWrites(CGD_DelayedSaves, CGD_DELAYEDSAVES_COUNT, i, -1); Bfile_DeleteEntry(fc_path); rc = Bfile_CreateEntry_OS(fc_path, BFILE_CREATEMODE_FILE, (size_t *)&dfw->size); if (rc < 0) { I_Error("Bfile_CreateEntry_OS(%s, %d bytes): %d", dfw->filename, dfw->size, rc); return; } fd = Bfile_OpenFile_OS(fc_path, BFILE_WRITE, 0); if (fd < 0) { I_Error("Bfile_OpenFile_OS(%s): %d", dfw->filename, fd); return; } /* Write chunks of 4096 bytes and show progress in-between chunks */ int size = dfw->size; const void *source = dfw->data; while(size > 0) { int chunk_size = (size >= 4096) ? 4096 : size; Bfile_WriteFile_OS(fd, source, chunk_size); source += chunk_size; size -= chunk_size; UI_DelayedWrites(CGD_DelayedSaves, CGD_DELAYEDSAVES_COUNT, i, dfw->size - size); } Bfile_CloseFile_OS(fd); } //--- // Setting save/load //--- static void SaveSettings(const CGDoom_Options *o) { char *out_start = (char *)CGDOOM_SCREENS_BASE; char *out = out_start; /* Version identifcation (just in case something goes wrong) */ out += sprintf(out, "Version=%d\n", CGDOOM_SETTINGS_FILE_VERSION); /* Settings */ out += sprintf(out, "WADMethod=%s\n", o->WADMethod == CGDOOM_WAD_BFILE ? "BFILE" : o->WADMethod == CGDOOM_WAD_MMAP ? "MMAP" : "?"); out += sprintf(out, "DeveloperInfo=%d\n", o->DeveloperInfo); out += sprintf(out, "TrustUnalignedLumps=%d\n", o->TrustUnalignedLumps); out += sprintf(out, "EnableDemos=%d\n", o->EnableDemos); out += sprintf(out, "Autostart=%d\n", o->AutoStart); out += sprintf(out, "ExperimentalMemory=%d\n", o->EnableExperimentalMemory); /* Keyboard layout */ for(int i = 0; i < CGDOOM_KEYMAP_SIZE; i++) { out += sprintf(out, "Keymap.%s=%s\n", CGD_DoomKey_TechnicalName(CGD_KeymapEntries[i]), CGD_PhysicalKey_TechnicalName(o->Keymap[i])); } M_WriteFile("CGDoom.cfg", out_start, out - out_start); memset(out_start, 0xff, out - out_start); } static void LoadSettings(CGDoom_Options *o) { char *buffer = (char *)CGDOOM_SCREENS_BASE; int length = M_ReadFile("CGDoom.cfg", (byte **)&buffer, 0); if(length <= 0) return; /* Parse every line on the form "=\n" */ const char *line, *nextline=NULL; for(line = buffer; line < buffer + length; line = nextline) { /* Start of next line, or end of string */ nextline = strchr(line, '\n'); nextline = nextline ? nextline + 1 : line + strlen(line); if(line[0] == '#' || line[0] == '\n') continue; const char *eq = strchr(line, '='); const char *value = eq+1; if(!eq || eq >= nextline) continue; if(!strncmp(line, "Version", eq-line)) { int version = atoi(value); if(version != CGDOOM_SETTINGS_FILE_VERSION) return; } else if(!strncmp(line, "WADMethod", eq-line)) { if(!strncmp(value, "BFILE", 5)) o->WADMethod = CGDOOM_WAD_BFILE; else if(!strncmp(value, "MMAP", 4)) o->WADMethod = CGDOOM_WAD_MMAP; } else if(!strncmp(line, "Keymap.", 7)) { int k1 = CGD_DoomKey_FromTechnicalName(line+7, eq-line-7); int k2 = CGD_PhysicalKey_FromTechnicalName(value,nextline-value-1); if(k1 > 0 && k2 >= 0) { for(int i = 0; i < CGDOOM_KEYMAP_SIZE; i++) { if(CGD_KeymapEntries[i] == k1) { o->Keymap[i] = k2; break; } } } } else if(!strncmp(line, "DeveloperInfo", eq-line)) o->DeveloperInfo = atoi(value); else if(!strncmp(line, "TrustUnalignedLumps", eq-line)) o->TrustUnalignedLumps = atoi(value); else if(!strncmp(line, "EnableDemos", eq-line)) o->EnableDemos = atoi(value); else if(!strncmp(line, "Autostart", eq-line)) o->AutoStart = atoi(value); else if(!strncmp(line, "ExperimentalMemory", eq-line)) o->EnableExperimentalMemory = atoi(value); } } static int SettingsChanged(const CGDoom_Options *before, const CGDoom_Options *after) { return memcmp(before, after, sizeof *before) != 0; } //--- // Main function //--- int main(void) { //--- // First configuration step //--- VRAM = (void *)GetVRAMAddress(); uintptr_t vram2 = ((uintptr_t)GetSecondaryVRAMAddress() + 3) & -4; SecondaryVRAM = (void *)vram2; EnableColor(1); EnableStatusArea(3); Bdisp_FrameAndColor(3, 16); Bdisp_FrameAndColor(1, 0); prof_init(); /* Allow the user to use memory past the 2 MB line on tested OS versions */ int allow_experimental_RAM = 0; char const *osv = GetOSVersion(); if(!strncmp(osv, "03.", 3) && osv[3] <= '6') // 3.60 or earlier allow_experimental_RAM = 1; else CGD_Options.EnableExperimentalMemory = 0; //--- // Load default and saved options //--- CGDoom_Options o = { .DeveloperInfo = 0, .WADMethod = CGDOOM_WAD_MMAP, .TrustUnalignedLumps = 1, .AutoStart = 0, .EnableDemos = 0, .EnableExperimentalMemory = 0, }; CGD_CopyKeymap(o.Keymap, CGD_Keymap_ThumbsOnly); /* Override with settings read from file if there is one */ LoadSettings(&o); /* Keep o so we can later look for changes and resave them */ memcpy(&CGD_Options, &o, sizeof o); /* Offer to start at E1M1 if skipping the title screen */ extern int startmap; extern int startepisode; startmap = 1; startepisode = 1; //--- // Run main menu and apply settings //--- /* Look for WAD files at the root of the filesystem */ CGDoom_FileInfo wads[16]; int wad_count = FindFiles(u"\\\\fls0\\*.wad", wads, 16); /* Look for demo files to replay, also at the root */ CGDoom_FileInfo demos[32]; int demo_count = FindFiles(u"\\\\fls0\\*_demo*.lmp", demos, 32); CGDoom_FileInfo *selection = UI_Main(wads, wad_count, demos, demo_count, &CGD_Options, &startmap, &startepisode, &CGD_RecordDemoSlot, allow_experimental_RAM); int choice = -1; if(selection >= demos && selection < demos + demo_count) { /* Find the WAD file with the correct name */ for(int i = 0; i < wad_count; i++) { int len = strlen(wads[i].name) - 4; /* omit ".wad" */ if(!strncmp(selection->name, wads[i].name, len) && !strncmp(selection->name + len, "_demo", 5)) { choice = i; break; } } if(choice == -1) { UI_Error("The WAD file for %s has been removed or renamed!", selection->name); return 1; } G_DeferedPlayDemoFile(selection->name); CGD_PlayDemoOnly = 1; } else if(selection >= wads && selection < wads + wad_count) { choice = selection - wads; } else return 1; /* Override parameters unavailable on the SDL2 build */ #ifdef CG_EMULATOR CGD_Options.WADMethod = CGDOOM_WAD_BFILE; CGD_Options.TrustUnalignedLumps = 0; CGD_Options.EnableExperimentalMemory = 0; #endif /* Apply settings to Doom variables */ extern boolean autostart; autostart = CGD_Options.AutoStart; /* Determine how much RAM is zeroed out at 2 MB */ if(CGD_Options.EnableExperimentalMemory) CGD_2MBLineMemory = FindZeroedMemory((void *)0xac200000); else CGD_2MBLineMemory = 0; /* Save settings for the next load */ if(SettingsChanged(&o, &CGD_Options)) SaveSettings(&CGD_Options); //--- // Second configuration step //--- void *PRAM0_alloc_start = PRAM0_START; /* Override version detection for single-episode Ultimate Doom WADs */ if (!strcmp(wads[choice].name, "doomu1.wad")) CGD_SingleEpisodeUltimate = 1; if (!strcmp(wads[choice].name, "doomu2.wad")) CGD_SingleEpisodeUltimate = 2; if (!strcmp(wads[choice].name, "doomu3.wad")) CGD_SingleEpisodeUltimate = 3; if (!strcmp(wads[choice].name, "doomu4.wad")) CGD_SingleEpisodeUltimate = 4; /* Remember WAD file name for saves and loads */ static char wad_name[32] = { 0 }; for (int i = 0; wads[choice].name[i] != '.'; i++) wad_name[i] = wads[choice].name[i]; CGD_WADFileName = wad_name; /* fx-CG 50 / Graph 90+E: RAM starts at 0x0c000000 in physical memory */ SystemStack = (void *)0xac0f0000; //--- // Setup access to the WAD file //--- int time, ms_index=0, ms_mmap=0; if(CGD_Options.WADMethod == CGDOOM_WAD_BFILE) { gWADfd = Bfile_OpenFile_OS(wads[choice].path, 0, 0); } else { #ifdef FLASH_INDEX /* Index most likely flash sectors into a sorted array, so that sectors can be hit quickly. The index contains every sector on a 4-kiB boundary (where fragments are most likely to start) between FLASH_FS_HINT and FLASH_END. */ time = RTC_GetTicks(); gIndex = (void *)SystemStack; for(int i = 0; i < FLASH_INDEX_SIZE; i++) { SectorIndexInfo *info = &gIndex[i]; info->sector = FLASH_FS_HINT + (i * 4096); info->start_bytes = *(const uint32_t *)info->sector; } qsort(gIndex, FLASH_INDEX_SIZE, sizeof *gIndex, IndexCompareSectors); ms_index = (RTC_GetTicks() - time) * 8; #endif /* FLASH_INDEX */ time = RTC_GetTicks(); gWADMap.mTable = PRAM0_START; int fd = Bfile_OpenFile_OS(wads[choice].path, 0, 0); int size = CreateFileMapping(fd, &gWADMap); Bfile_CloseFile_OS(fd); UI_ProgressBar(HEIGHT-10, 1, 1); ms_mmap = (RTC_GetTicks() - time) * 8; if(size == -1) { I_Error ("File read error"); return 1; } else if(size == -2) { I_Error ("Page not found"); return 1; } else if(size == -3) { I_Error ("File too fragmented"); return 1; } else if(CGD_Options.DeveloperInfo) { Layout l; Layout_Init(&l); Bdisp_AllClr_VRAM(); Layout_CenteredText(&l, "Developer info"); Layout_Spacing(&l, 12); Layout_Text(&l, "Fragments:", "%d", CGD_Stats.WADFragments); Layout_Text(&l, "Index hits:", "%d (%d%%)", CGD_Stats.WADIndexHits, (CGD_Stats.WADIndexHits * 100 / CGD_Stats.WADFragments)); Layout_Text(&l, "Lowest fragment:", "%p", CGD_Stats.WADLowestFragment); Layout_Spacing(&l, 12); Layout_Text(&l, "Index build time:", "%d ms", ms_index); Layout_Text(&l, "File mapping time:", "%d ms", ms_mmap); Layout_Spacing(&l, 12); Layout_Text(&l, "Memory beyond the 2MB line:", "%d kB", CGD_2MBLineMemory >> 10); Bdisp_PutDisp_DD(); int key; GetKey(&key); } PRAM0_alloc_start += gWADMap.miItemCount * sizeof(FileMappingItem); } /* Initialize the PRAM allocator */ CGD_PRAM_Init(PRAM0_alloc_start, PRAM0_END); memset(VRAM, 0, WIDTH*HEIGHT*2); D_DoomMain(); if(gWADfd >= 0) Bfile_CloseFile_OS(gWADfd); for(int i = 0; i < CGD_DELAYEDSAVES_COUNT; i++) { CGDoom_DelayedFileWrite *dfw = &CGD_DelayedSaves[i]; if(dfw->data != NULL) DelayedWriteFile(i); } if(CGD_Options.DeveloperInfo) { Layout l; Layout_Init(&l); Bdisp_AllClr_VRAM(); Layout_CenteredText(&l, "Developer info"); Layout_Spacing(&l, 12); Layout_Text(&l, "Memory allocated:", "%d kB", CGD_Stats.MemoryAllocated >> 10); Layout_Spacing(&l, 12); Layout_Text(&l, "Lumps loaded:", "%d (%d kB)", CGD_Stats.LumpsLoaded, (int)(CGD_Stats.LumpsLoadedTotal >> 10)); Layout_Text(&l, "... of which unaligned:", "%d (%d kB)", CGD_Stats.UnalignedLumpsLoaded, (int)(CGD_Stats.UnalignedLumpsLoadedTotal >> 10)); Layout_Text(&l, "Lumps referenced:", "%d (%d kB)", CGD_Stats.LumpsReferenced, (int)(CGD_Stats.LumpsReferencedTotal >> 10)); Bdisp_FrameAndColor(3, 16); Bdisp_FrameAndColor(1, 0); Bdisp_PutDisp_DD(); while(PRGM_GetKey() != 0) {} int key; GetKey(&key); } prof_quit(); return EXIT_SUCCESS; }