From 6864d9339ae3211cf8cdeefd5442922d8dffa79c Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:35:48 +0200 Subject: [PATCH 1/3] Translate CPU detection from assembly to C++ No documentation update: tests/cpudetect checks the ported detection against values derived directly from CPUID, so the documented CPU-detection behavior is unchanged. --- code/detproc.asm | 301 --------------------------------- code/detproc.cpp | 164 ++++++++++++++++++ code/getcpu.cpp | 14 +- code/getcpu.h | 11 +- tests/CMakeLists.txt | 1 + tests/cpudetect/CMakeLists.txt | 30 ++++ tests/cpudetect/cpudetect.cpp | 130 ++++++++++++++ 7 files changed, 336 insertions(+), 315 deletions(-) delete mode 100644 code/detproc.asm create mode 100644 code/detproc.cpp create mode 100644 tests/cpudetect/CMakeLists.txt create mode 100644 tests/cpudetect/cpudetect.cpp diff --git a/code/detproc.asm b/code/detproc.asm deleted file mode 100644 index d223ae09..00000000 --- a/code/detproc.asm +++ /dev/null @@ -1,301 +0,0 @@ -;****************************************************************************** -;* O P E N T S -;****************************************************************************** -;* SPDX-License-Identifier: GPL-3.0-or-later -;* Copyright 2025 Electronic Arts Inc. -;* Copyright 2026 OpenTS contributors -;* -;* Contains material derived from Electronic Arts source code. -;* Modified by OpenTS contributors, 2026. -;* EA's GPLv3 Section 7 additional terms and supplemental warranty -;* disclaimers apply; see LICENSE.md. -;****************************************************************************** - -;*************************************************************************** -;** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S I N C ** -;*************************************************************************** -;* * -;* Project Name : Command & Conquer * -;* * -;* File Name : MMX.ASM * -;* * -;* Programmer : Steve Tall * -;* * -;* Start Date : May 19th, 1996 * -;* * -;* Last Update : May 19th 1996 [ST] * -;* * -;*-------------------------------------------------------------------------* -;* Functions: * -;* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * - - - ; include - - - .586 ;.686 - .model flat - - - .code - -externdef C UseCMOV:byte -externdef C HasCMOV:byte -externdef C UseMMX:byte -externdef C CPUType:byte -externdef C VendorID:byte - - - -;********************************************************************************************* -;* Detect_MMX_Availability -- Detect the presence of MMX technology. * -;* * -;* * -;* INPUT: Nothing * -;* * -;* OUTPUT: True if MMX technology is available. * -;* * -;* Warnings: * -;* * -;* Note: Based in part on CPUID32.ASM by Intel * -;* * -;* HISTORY: * -;* 05/19/96 ST : Created. * -;*===========================================================================================* - -Detect_MMX_Availability proc C uses ebx ecx - - local idflag:byte - local local_cputype:byte - -;assume processor is at least 386 -; -;check whether AC bit in eflags can be toggled. -;If not then processor is 386 - - mov [idflag],0 - - pushfd ;get Eflags in EAX - pop eax - mov ecx,eax ;save eflags - xor eax,40000h ;toggle AC bit in eflags - push eax ;new eflags on stack - popfd ;move new value into eflags - pushfd ;get new eflags back into eax - pop eax - xor eax,ecx ;if AC bit not toggled then CPU=386 - mov [local_cputype],3 - jz @@end_get_cpu ;cpu is 386 - - push ecx - popfd ;restore AC bit in eflags - - -;processor is at least 486 -; -;Check for ability to set/clear ID flag in EFLAGS -;ID flag indicates ability of processor to execute the CPUID instruction. -;486 not guaranteed to have CPUID inst? -; - mov [local_cputype],4 - mov eax,ecx ;original EFLAGS - xor eax,200000h ;toggle ID bit - push eax - popfd - pushfd - pop eax - xor eax,ecx ;check if still toggled - jz @@end_get_cpu - - -; Execute CPUID instruction to determine vendor, family, -; model and stepping. -; - - mov [idflag],1 ;flag ID is available - - xor eax,eax - cpuid - - mov dword ptr [VendorID],ebx - mov dword ptr [VendorID+4],edx - mov dword ptr [VendorID+8],ecx - mov dword ptr [VendorID+12]," " - - cmp eax,1 ;check if 1 is valid - jl @@end_get_cpu ;inp for cpuid inst. - - xor eax,eax - inc eax - - cpuid ;get stepping, model and family - - and ax,0f00H - shr ax,08H - - mov [local_cputype],al - -@@end_get_cpu: mov al,[local_cputype] - mov [CPUType],al - - -; -; We have the CPU type in al now. -; If we arent on at least a pentium then we can assume there is no MMX -; - cmp al,5 - jb @@no_mmx - - mov eax,1 - cpuid - test edx,00800000h - jz @@no_mmx - -; -; MMX detected - return true -; - mov eax,1 - mov UseMMX, 1 - ret - - -@@no_mmx: xor eax,eax - mov UseMMX, 0 - ret - - -Detect_MMX_Availability endp - - - -Detect_CMOV_Availability proc C uses ebx ecx - cmp CPUType, 5 - jb @@no_cmov - - mov eax, 1 - cpuid - test edx, 00008000h - jz @@no_cmov - - cmp CPUType, 5 - jbe @@supports_cmov - -@@use_cmov: - mov UseCMOV, 1 - mov HasCMOV, 1 - mov eax, 1 - ret - -@@supports_cmov: - mov UseCMOV, 0 - mov HasCMOV, 1 - mov eax, 1 - ret - -@@no_cmov: - mov UseCMOV, 0 - mov HasCMOV, 0 - mov eax, 0 - ret - -Detect_CMOV_Availability endp - - - -;*********************************************************************************************** -;* Get_CPU_Clock -- Fetches the current CPU clock time. * -;* * -;* This routine will return the internal Pentium clock accumulator. This accumulator is * -;* incremented every clock tick. Since this clock value can get very very large, the value * -;* returned is in 64 bits. The low half is returned directly, the high half is stored in * -;* location specified. * -;* * -;* INPUT: high -- Reference to the high value of the 64 bit clock number. * -;* * -;* OUTPUT: Returns with the low half of the CPU clock value. * -;* * -;* WARNINGS: This instruction is only available on Pentium or later processors. * -;* * -;* HISTORY: * -;* 07/17/1996 JLB : Created. * -;*=============================================================================================* - -Get_CPU_Clock proc C uses esi _high:dword - - rdtsc - mov esi, _high - mov [esi], edx - ret - -Get_CPU_Clock endp - - - -Processor proc C uses ebx - - local idflag:word - -;assume processor is at least 386 -; -;check whether AC bit in eflags can be toggled. -;If not then processor is 386 - pushfd - - mov [idflag],0 - - pushfd ;get Eflags in EAX - pop eax - mov ebx,eax ;save eflags - xor eax,40000h ;toggle AC bit in eflags - push eax ;new eflags on stack - popfd ;move new value into eflags - pushfd ;get new eflags back into eax - pop eax - xor eax,ebx ;if AC bit not toggled then CPU=386 - jz @@end_get_cpu ;cpu is 386 - - -;processor is at least 486 -; -;Check for ability to set/clear ID flag in EFLAGS -;ID flag indicates ability of processor to execute the CPUID instruction. -;486 not guaranteed to have CPUID inst? -; - mov [idflag],1 ;flag ID is available - pushfd - pop eax - mov ebx,eax ;original EFLAGS - xor eax,200000h ;toggle ID bit - push eax - popfd - pushfd - pop eax - xor eax,ebx ;check if still toggled - jz @@end_get_cpu - - -; Execute CPUID instruction to determine vendor, family, -; model and stepping. -; - mov [idflag],2 - -@@end_get_cpu: - popfd - sub eax, eax - mov ax, [idflag] - ret - -Processor endp - - - - .data - -UseCMOV db 0 -HasCMOV db 0 -UseMMX db 0 -CPUType db 0 -VendorID db "Not available",0,0,0,0,0,0 - - -end \ No newline at end of file diff --git a/code/detproc.cpp b/code/detproc.cpp new file mode 100644 index 00000000..4b00c4a3 --- /dev/null +++ b/code/detproc.cpp @@ -0,0 +1,164 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2025 Electronic Arts Inc. + * Copyright 2026 OpenTS contributors + * + * Contains material derived from Electronic Arts source code. + * Modified by OpenTS contributors, 2026. + * EA's GPLv3 Section 7 additional terms and supplemental warranty + * disclaimers apply; see LICENSE.md. + ******************************************************************************/ + +/*************************************************************************** + ** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S I N C ** + *************************************************************************** + * * + * Project Name : Command & Conquer * + * * + * File Name : MMX.ASM * + * * + * Programmer : Steve Tall * + * * + * Start Date : May 19th, 1996 * + * * + * Last Update : May 19th 1996 [ST] * + * * + *-------------------------------------------------------------------------* + * Functions: * + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ + +#include "always.h" + +#include "getcpu.h" +#include "misc.h" +#include "mpu.h" + +#include +#include + +extern "C" { + +char UseCMOV = 0; +char HasCMOV = 0; +char UseMMX = 0; +char CPUType = 0; + +/* + * Filled in by Detect_MMX_Availability from CPUID leaf 0. The buffer holds the twelve + * vendor characters, the separating space the original wrote after them, and the + * terminator Get_CPU_Type copies up to. + */ +char VendorID[20] = "Not available"; + +} + + +/// +/// Detects MMX support and records the processor family and vendor. +/// This routine leaves the family in CPUType, which Detect_CMOV_Availability then reads, so +/// it has to run first. It also fills VendorID. +/// +/// bool; Is MMX technology available? +bool __cdecl Detect_MMX_Availability(void) +{ + int regs[4]; + + /* + * The original probed the EFLAGS AC and ID bits here to separate a 386 from a 486 from a + * processor carrying CPUID. Every processor the current build supports carries CPUID, so + * the family comes straight from it and the 386 and 486 answers are unreachable. + */ + char cputype = 4; + + __cpuid(regs, 0); + int const maxleaf = regs[0]; + + std::memcpy(&VendorID[0], ®s[1], 4); + std::memcpy(&VendorID[4], ®s[3], 4); + std::memcpy(&VendorID[8], ®s[2], 4); + VendorID[12] = ' '; + VendorID[13] = '\0'; + + if (maxleaf >= 1) { + __cpuid(regs, 1); + cputype = (char)((regs[0] & 0x0F00) >> 8); + } + + CPUType = cputype; + + if (CPUType < 5) { + UseMMX = 0; + return(false); + } + + __cpuid(regs, 1); + + if ((regs[3] & 0x00800000) == 0) { + UseMMX = 0; + return(false); + } + + UseMMX = 1; + return(true); +} + + +/// +/// Detects CMOV support, distinguishing a processor that has the instruction from one the +/// original considered worth using it on. Reads the family recorded by +/// Detect_MMX_Availability, so it must be called after it. +/// +/// bool; Does the processor have CMOV? +bool __cdecl Detect_CMOV_Availability(void) +{ + if (CPUType < 5) { + UseCMOV = 0; + HasCMOV = 0; + return(false); + } + + int regs[4]; + __cpuid(regs, 1); + + if ((regs[3] & 0x00008000) == 0) { + UseCMOV = 0; + HasCMOV = 0; + return(false); + } + + /* + * A Pentium that reports CMOV still runs the non-CMOV path; only a later family takes it. + */ + HasCMOV = 1; + UseCMOV = (CPUType > 5) ? 1 : 0; + return(true); +} + + +/// +/// Fetches the processor's clock accumulator, which advances every clock tick. The value is +/// 64 bits wide; the low half is returned and the high half stored through the reference. +/// +/// Receives the high half of the 64 bit clock value. +/// unsigned int; The low half of the clock value. +unsigned int __cdecl Get_CPU_Clock(unsigned int & high) +{ + unsigned long long const stamp = __rdtsc(); + + high = (unsigned int)(stamp >> 32); + return((unsigned int)stamp); +} + + +/// +/// Reports how capable the processor is. The original distinguished a 386 that could not +/// toggle the EFLAGS AC bit, a 486 that could not toggle the ID bit, and anything carrying +/// CPUID. The supported build requires SSE2, so CPUID is always present. +/// +/// WORD; PROC_PENTIUM, meaning the processor carries CPUID. +WORD __cdecl Processor(void) +{ + return(PROC_PENTIUM); +} diff --git a/code/getcpu.cpp b/code/getcpu.cpp index f97e00f8..0bc4b9b1 100644 --- a/code/getcpu.cpp +++ b/code/getcpu.cpp @@ -43,17 +43,6 @@ #include #include -/* -** Prototypes for linkage to assembly module -*/ -extern "C" { - bool __cdecl Detect_MMX_Availability (void); - bool __cdecl Detect_CMOV_Availability (void); - - extern char CPUType; - extern char VendorID; -} - /*********************************************************************************************** * Get_CPU_Type -- Find out what kind of CPU we are running on * @@ -86,7 +75,6 @@ void Get_CPU_Type(int & cpu_type, bool & mmx, char * vendor_id, int vendor_id_le cpu_type = (int)CPUType; if (vendor_id != NULL) { - char * vendor_ptr = &VendorID; - strncpy(vendor_id, vendor_ptr, vendor_id_length); + strncpy(vendor_id, VendorID, vendor_id_length); } } diff --git a/code/getcpu.h b/code/getcpu.h index 9a8c17ab..5a1acafb 100644 --- a/code/getcpu.h +++ b/code/getcpu.h @@ -20,7 +20,16 @@ extern "C" { bool __cdecl Detect_CMOV_Availability(void); extern char CPUType; - extern char VendorID; + extern char VendorID[]; + + /* + * Written by the two detection routines above. HasCMOV records that the instruction + * exists, UseCMOV that the detection considers it worth taking; a Pentium reporting CMOV + * still runs the path that avoids it. + */ + extern char UseCMOV; + extern char HasCMOV; + extern char UseMMX; } /* diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7e8311bd..7990efef 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(logstress) +add_subdirectory(cpudetect) diff --git a/tests/cpudetect/CMakeLists.txt b/tests/cpudetect/CMakeLists.txt new file mode 100644 index 00000000..0e7d6512 --- /dev/null +++ b/tests/cpudetect/CMakeLists.txt @@ -0,0 +1,30 @@ +# The detection sources are compiled straight into the harness. They live outside tests/ so +# that the recursive glob building OpenTS cannot pick this target's entry point up. +add_executable(CpuDetect + "${CMAKE_CURRENT_SOURCE_DIR}/cpudetect.cpp" + "${CMAKE_SOURCE_DIR}/code/detproc.cpp" + "${CMAKE_SOURCE_DIR}/code/getcpu.cpp" +) + +target_compile_features(CpuDetect PRIVATE cxx_std_20) + +target_include_directories(CpuDetect PRIVATE + "${CMAKE_SOURCE_DIR}/code" +) + +target_compile_definitions(CpuDetect PRIVATE WIN32 _WINDOWS _MBCS) + +# The engine builds these sources with SSE2 and precise floating point. The harness matches +# that so a result here carries over to the engine. +target_compile_options(CpuDetect PRIVATE + $<$:/MTd /EHsc /Zc:__cplusplus /arch:SSE2 /fp:precise> + $<$:/MT /EHsc /Zc:__cplusplus /arch:SSE2 /fp:precise> +) + +target_link_libraries(CpuDetect PRIVATE kernel32 user32 shell32) + +set_target_properties(CpuDetect PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +add_test(NAME cpudetect COMMAND CpuDetect) diff --git a/tests/cpudetect/cpudetect.cpp b/tests/cpudetect/cpudetect.cpp new file mode 100644 index 00000000..f008643b --- /dev/null +++ b/tests/cpudetect/cpudetect.cpp @@ -0,0 +1,130 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Checks the processor detection in detproc.cpp against CPUID read directly here. The +// detection used to be hand-written assembly, so the point is to confirm the C++ reports the +// same family, vendor and feature flags the instruction does, on whatever machine runs it. +// Needs no game data. + +#include + +#include +#include +#include + +#include "getcpu.h" +#include "misc.h" +#include "mpu.h" + +namespace { + +int Failures = 0; + + +void Check(bool condition, char const * what) +{ + std::printf("%-52s %s\n", what, condition ? "ok" : "FAILED"); + + if (!condition) { + Failures++; + } +} + + +/* + * The detection reads the base family field only, as the assembly did. An extended family is + * deliberately not folded in, so this reference computes the value the same narrow way. + */ +int Reference_Family(void) +{ + int regs[4]; + __cpuid(regs, 1); + return((regs[0] & 0x0F00) >> 8); +} + + +int Reference_Feature_Edx(void) +{ + int regs[4]; + __cpuid(regs, 1); + return(regs[3]); +} + +} // namespace + + +int main(void) +{ + int regs[4]; + __cpuid(regs, 0); + + char vendor[16]; + std::memcpy(&vendor[0], ®s[1], 4); + std::memcpy(&vendor[4], ®s[3], 4); + std::memcpy(&vendor[8], ®s[2], 4); + vendor[12] = '\0'; + + int const maxleaf = regs[0]; + Check(maxleaf >= 1, "CPUID reports leaf 1"); + + int const family = Reference_Family(); + int const edx = Reference_Feature_Edx(); + + std::printf("Reported vendor '%s', family %d, feature EDX %08X\n\n", vendor, family, (unsigned int)edx); + + int cpu_type = -1; + bool mmx = false; + char reported[64]; + std::memset(reported, 0, sizeof(reported)); + + Get_CPU_Type(cpu_type, mmx, reported, sizeof(reported) - 1); + + Check(cpu_type == family, "Get_CPU_Type family matches CPUID"); + Check(CPUType == (char)family, "CPUType global matches CPUID"); + + /* + * Detect_MMX_Availability writes the twelve vendor characters and then a space, so the + * buffer Get_CPU_Type copies out is the vendor followed by that separator. + */ + char expected[16]; + std::memcpy(expected, vendor, 12); + expected[12] = ' '; + expected[13] = '\0'; + Check(std::strcmp(reported, expected) == 0, "Vendor string matches CPUID"); + + bool const expect_mmx = (family >= 5) && ((edx & 0x00800000) != 0); + Check(mmx == expect_mmx, "MMX result matches CPUID feature bit"); + Check((UseMMX != 0) == expect_mmx, "UseMMX global matches CPUID feature bit"); + + bool const cmov_bit = ((edx & 0x00008000) != 0); + bool const expect_has = (family >= 5) && cmov_bit; + bool const expect_use = (family > 5) && cmov_bit; + Check((HasCMOV != 0) == expect_has, "HasCMOV matches CPUID feature bit"); + Check((UseCMOV != 0) == expect_use, "UseCMOV set only above family 5"); + + Check(Processor() == PROC_PENTIUM, "Processor reports a CPUID-capable part"); + + /* + * The clock accumulator only ever counts up, so a later read cannot be the smaller of + * the two once both halves are put back together. + */ + unsigned int high1 = 0; + unsigned int const low1 = Get_CPU_Clock(high1); + unsigned int high2 = 0; + unsigned int const low2 = Get_CPU_Clock(high2); + + unsigned long long const stamp1 = ((unsigned long long)high1 << 32) | low1; + unsigned long long const stamp2 = ((unsigned long long)high2 << 32) | low2; + + Check(stamp1 != 0, "Get_CPU_Clock returns a running count"); + Check(stamp2 >= stamp1, "Get_CPU_Clock advances"); + + std::printf("\n%s\n", Failures == 0 ? "All checks passed." : "Some checks FAILED."); + return(Failures == 0 ? 0 : 1); +} From cd9aaf4d6695b1f34e738e0d38588d3eab976efe Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:39:10 +0200 Subject: [PATCH 2/3] Assume MMX and CMOV on the supported minimum hardware --- code/detproc.cpp | 164 ------------------------ code/getcpu.cpp | 60 ++++++++- code/getcpu.h | 14 +- code/init.cpp | 6 +- code/misc.h | 5 - code/mpu.cpp | 41 ++---- code/mpu.h | 6 +- manual/changes/cpu-feature-detection.md | 29 +++++ tests/cpudetect/CMakeLists.txt | 2 +- tests/cpudetect/cpudetect.cpp | 27 ++-- 10 files changed, 124 insertions(+), 230 deletions(-) delete mode 100644 code/detproc.cpp create mode 100644 manual/changes/cpu-feature-detection.md diff --git a/code/detproc.cpp b/code/detproc.cpp deleted file mode 100644 index 4b00c4a3..00000000 --- a/code/detproc.cpp +++ /dev/null @@ -1,164 +0,0 @@ -/******************************************************************************* - * O P E N T S - ******************************************************************************* - * SPDX-License-Identifier: GPL-3.0-or-later - * Copyright 2025 Electronic Arts Inc. - * Copyright 2026 OpenTS contributors - * - * Contains material derived from Electronic Arts source code. - * Modified by OpenTS contributors, 2026. - * EA's GPLv3 Section 7 additional terms and supplemental warranty - * disclaimers apply; see LICENSE.md. - ******************************************************************************/ - -/*************************************************************************** - ** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S I N C ** - *************************************************************************** - * * - * Project Name : Command & Conquer * - * * - * File Name : MMX.ASM * - * * - * Programmer : Steve Tall * - * * - * Start Date : May 19th, 1996 * - * * - * Last Update : May 19th 1996 [ST] * - * * - *-------------------------------------------------------------------------* - * Functions: * - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - -#include "always.h" - -#include "getcpu.h" -#include "misc.h" -#include "mpu.h" - -#include -#include - -extern "C" { - -char UseCMOV = 0; -char HasCMOV = 0; -char UseMMX = 0; -char CPUType = 0; - -/* - * Filled in by Detect_MMX_Availability from CPUID leaf 0. The buffer holds the twelve - * vendor characters, the separating space the original wrote after them, and the - * terminator Get_CPU_Type copies up to. - */ -char VendorID[20] = "Not available"; - -} - - -/// -/// Detects MMX support and records the processor family and vendor. -/// This routine leaves the family in CPUType, which Detect_CMOV_Availability then reads, so -/// it has to run first. It also fills VendorID. -/// -/// bool; Is MMX technology available? -bool __cdecl Detect_MMX_Availability(void) -{ - int regs[4]; - - /* - * The original probed the EFLAGS AC and ID bits here to separate a 386 from a 486 from a - * processor carrying CPUID. Every processor the current build supports carries CPUID, so - * the family comes straight from it and the 386 and 486 answers are unreachable. - */ - char cputype = 4; - - __cpuid(regs, 0); - int const maxleaf = regs[0]; - - std::memcpy(&VendorID[0], ®s[1], 4); - std::memcpy(&VendorID[4], ®s[3], 4); - std::memcpy(&VendorID[8], ®s[2], 4); - VendorID[12] = ' '; - VendorID[13] = '\0'; - - if (maxleaf >= 1) { - __cpuid(regs, 1); - cputype = (char)((regs[0] & 0x0F00) >> 8); - } - - CPUType = cputype; - - if (CPUType < 5) { - UseMMX = 0; - return(false); - } - - __cpuid(regs, 1); - - if ((regs[3] & 0x00800000) == 0) { - UseMMX = 0; - return(false); - } - - UseMMX = 1; - return(true); -} - - -/// -/// Detects CMOV support, distinguishing a processor that has the instruction from one the -/// original considered worth using it on. Reads the family recorded by -/// Detect_MMX_Availability, so it must be called after it. -/// -/// bool; Does the processor have CMOV? -bool __cdecl Detect_CMOV_Availability(void) -{ - if (CPUType < 5) { - UseCMOV = 0; - HasCMOV = 0; - return(false); - } - - int regs[4]; - __cpuid(regs, 1); - - if ((regs[3] & 0x00008000) == 0) { - UseCMOV = 0; - HasCMOV = 0; - return(false); - } - - /* - * A Pentium that reports CMOV still runs the non-CMOV path; only a later family takes it. - */ - HasCMOV = 1; - UseCMOV = (CPUType > 5) ? 1 : 0; - return(true); -} - - -/// -/// Fetches the processor's clock accumulator, which advances every clock tick. The value is -/// 64 bits wide; the low half is returned and the high half stored through the reference. -/// -/// Receives the high half of the 64 bit clock value. -/// unsigned int; The low half of the clock value. -unsigned int __cdecl Get_CPU_Clock(unsigned int & high) -{ - unsigned long long const stamp = __rdtsc(); - - high = (unsigned int)(stamp >> 32); - return((unsigned int)stamp); -} - - -/// -/// Reports how capable the processor is. The original distinguished a 386 that could not -/// toggle the EFLAGS AC bit, a 486 that could not toggle the ID bit, and anything carrying -/// CPUID. The supported build requires SSE2, so CPUID is always present. -/// -/// WORD; PROC_PENTIUM, meaning the processor carries CPUID. -WORD __cdecl Processor(void) -{ - return(PROC_PENTIUM); -} diff --git a/code/getcpu.cpp b/code/getcpu.cpp index 0bc4b9b1..486bfaea 100644 --- a/code/getcpu.cpp +++ b/code/getcpu.cpp @@ -43,6 +43,7 @@ #include #include +#include /*********************************************************************************************** * Get_CPU_Type -- Find out what kind of CPU we are running on * @@ -61,13 +62,70 @@ * HISTORY: * * 6/26/96 10:15AM ST : Created * *=============================================================================================*/ + +extern "C" { + +char UseCMOV = 1; +char HasCMOV = 1; +char UseMMX = 1; +char CPUType = 0; + +/* + * Filled in by Detect_MMX_Availability from CPUID leaf 0. The buffer holds the twelve + * vendor characters, the separating space the original wrote after them, and the + * terminator Get_CPU_Type copies up to. + */ +char VendorID[20] = "Not available"; + +} + + +/// +/// Records the processor family in CPUType and the vendor in VendorID, and reports MMX +/// support. The supported minimum hardware (SSE2, so a Pentium 4 or Athlon 64 onward) always +/// has MMX, so this always sets UseMMX and returns true rather than reading the CPUID +/// feature bit. +/// +/// bool; always true on the supported minimum hardware. + +bool __cdecl Detect_MMX_Availability(void) +{ + int regs[4]; + + /* + * The original probed the EFLAGS AC and ID bits here to separate a 386 from a 486 from a + * processor carrying CPUID. Every processor the current build supports carries CPUID, so + * the family comes straight from it and the 386 and 486 answers are unreachable. + */ + char cputype = 4; + + __cpuid(regs, 0); + int const maxleaf = regs[0]; + + std::memcpy(&VendorID[0], ®s[1], 4); + std::memcpy(&VendorID[4], ®s[3], 4); + std::memcpy(&VendorID[8], ®s[2], 4); + VendorID[12] = ' '; + VendorID[13] = '\0'; + + if (maxleaf >= 1) { + __cpuid(regs, 1); + cputype = (char)((regs[0] & 0x0F00) >> 8); + } + + CPUType = cputype; + + UseMMX = 1; + return(true); +} + + void Get_CPU_Type(int & cpu_type, bool & mmx, char * vendor_id, int vendor_id_length) { /* ** Call the asm CPU detection code */ mmx = Detect_MMX_Availability(); - Detect_CMOV_Availability(); /* ** Return the promised results diff --git a/code/getcpu.h b/code/getcpu.h index 5a1acafb..74fb28bb 100644 --- a/code/getcpu.h +++ b/code/getcpu.h @@ -17,25 +17,23 @@ void Get_CPU_Type(int & cpu_type, bool & mmx, char * vendor_id = 0, int vendor_i extern "C" { bool __cdecl Detect_MMX_Availability(void); - bool __cdecl Detect_CMOV_Availability(void); extern char CPUType; extern char VendorID[]; /* - * Written by the two detection routines above. HasCMOV records that the instruction - * exists, UseCMOV that the detection considers it worth taking; a Pentium reporting CMOV - * still runs the path that avoids it. + * Fixed true rather than probed: the supported minimum hardware (SSE2, so a Pentium 4 or + * Athlon 64 onward) always has CMOV and MMX. */ extern char UseCMOV; extern char HasCMOV; extern char UseMMX; } -/* -** The type of processor running on this system as -** returned by Processor(). -*/ +// Processor family constants. Get_CPU_Type reports the CPUID base family through its +// cpu_type parameter; callers compare it against these to scale behavior with CPU +// generation. PROC_80386 and PROC_80486 are unreachable on the supported minimum hardware +// (SSE2, so a Pentium 4 or Athlon 64 onward), which always carries CPUID. #define PROC_80386 0 #define PROC_80486 1 #define PROC_PENTIUM 2 diff --git a/code/init.cpp b/code/init.cpp index ed66b0c6..77244408 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -304,14 +304,12 @@ int Init_Game(int , char * []) } /* - ** Allocate the benchmark tracking objects only if the machine and - ** compile flags indicate. + ** Allocate the benchmark tracking objects in debug builds; no runtime capability check + ** is needed since the supported minimum hardware always qualifies. */ #ifdef _DEBUG - if (Processor() >= 2) { DebugString("Creating benchmarks\n"); Benches = new Benchmark [BENCH_COUNT]; - } #endif /* diff --git a/code/misc.h b/code/misc.h index 96cad481..8c129f86 100644 --- a/code/misc.h +++ b/code/misc.h @@ -87,11 +87,6 @@ void __cdecl Shake_Screen(int shakes); //void * Build_Fading_Table(PaletteClass const & palette, void * dest, int color, int frac); //void * __cdecl Build_Fading_Table(void const *palette, void const *dest, long int color, long int frac); -/*=========================================================================*/ -/* The following prototypes are for the file: DETPROC.ASM */ -/*=========================================================================*/ - -extern WORD __cdecl Processor(void); extern WORD __cdecl Operating_System(void); diff --git a/code/mpu.cpp b/code/mpu.cpp index 9969c735..6c562329 100644 --- a/code/mpu.cpp +++ b/code/mpu.cpp @@ -81,38 +81,21 @@ unsigned int Get_CPU_Rate(unsigned int & high) } -#if 0 -/*********************************************************************************************** - * Get_CPU_Clock -- Fetches the current CPU clock time. * - * * - * This routine will return the internal Pentium clock accumulator. This accumulator is * - * incremented every clock tick. Since this clock value can get very very large, the value * - * returned is in 64 bits. The low half is returned directly, the high half is stored in * - * location specified. * - * * - * INPUT: high -- Reference to the high value of the 64 bit clock number. * - * * - * OUTPUT: Returns with the low half of the CPU clock value. * - * * - * WARNINGS: This instruction is only available on Pentium or later processors. * - * * - * HISTORY: * - * 07/17/1996 JLB : Created. * - *=============================================================================================*/ +/// +/// Fetches the processor's time stamp counter, which increments every clock tick. The value +/// is 64 bits wide; the low half is returned and the high half stored through the reference. +/// RDTSC is available on every processor the supported minimum hardware covers (SSE2, so a +/// Pentium 4 or Athlon 64 onward). +/// +/// Receives the high half of the 64 bit clock value. +/// unsigned int; the low half of the clock value. unsigned int Get_CPU_Clock(unsigned int & high) { - int h; - int l; - __asm { - _emit 0Fh - _emit 31h - mov [h],edx - mov [l],eax - } - high = h; - return(l); + unsigned long long const stamp = __rdtsc(); + + high = (unsigned int)(stamp >> 32); + return((unsigned int)stamp); } -#endif /* diff --git a/code/mpu.h b/code/mpu.h index 2ebd443d..1cdbf6db 100644 --- a/code/mpu.h +++ b/code/mpu.h @@ -32,9 +32,9 @@ #pragma once -/* -** Prototypes for linkage to assembly module -*/ +// Get_CPU_Clock reads the time stamp counter via the RDTSC intrinsic, available on every +// processor the supported minimum hardware covers (SSE2, so a Pentium 4 or Athlon 64 +// onward). extern "C" { unsigned int __cdecl Get_CPU_Clock(unsigned int & high); } diff --git a/manual/changes/cpu-feature-detection.md b/manual/changes/cpu-feature-detection.md new file mode 100644 index 00000000..2943609f --- /dev/null +++ b/manual/changes/cpu-feature-detection.md @@ -0,0 +1,29 @@ +--- +title: Assume MMX and CMOV on the supported minimum hardware +category: internal +release: 0.2.0 +breaking: true +migration: +- Remove any code that calls `Detect_CMOV_Availability` or `Processor`; both have been removed. +- Treat `UseMMX`, `UseCMOV`, and `HasCMOV` as fixed-true constants rather than runtime-detected flags. +targets: [] +credit: [tinix0] +--- + +Runtime detection of MMX and CMOV support has been removed. `Detect_MMX_Availability`, called +through `Get_CPU_Type`, now unconditionally sets `UseMMX` and returns true instead of reading +the CPUID feature bit, and `UseCMOV` and `HasCMOV` are fixed to true at initialization rather +than written by the removed `Detect_CMOV_Availability`. The processor family CPUID reports is +still read and returned through `Get_CPU_Type`'s `cpu_type` parameter and the `CPUType` global. + +`UseMMX` and `UseCMOV` still gate the MMX and CMOV code paths in the palette-fade routines in +`winasm.asm`; those paths are now always taken rather than only on hardware CPUID actually +reports as capable. + +`Detect_CMOV_Availability` and `Processor` have been removed outright; neither had a remaining +caller once detection stopped needing them. + +This formalizes the minimum hardware OpenTS already requires (SSE2, so a Pentium 4 or Athlon +64 onward), which always carries MMX and CMOV. A machine below that minimum was never covered +by a build claim; running the engine there previously fell back to a slower code path, and now +instead executes an MMX or CMOV instruction the processor does not have. diff --git a/tests/cpudetect/CMakeLists.txt b/tests/cpudetect/CMakeLists.txt index 0e7d6512..13c2f535 100644 --- a/tests/cpudetect/CMakeLists.txt +++ b/tests/cpudetect/CMakeLists.txt @@ -2,8 +2,8 @@ # that the recursive glob building OpenTS cannot pick this target's entry point up. add_executable(CpuDetect "${CMAKE_CURRENT_SOURCE_DIR}/cpudetect.cpp" - "${CMAKE_SOURCE_DIR}/code/detproc.cpp" "${CMAKE_SOURCE_DIR}/code/getcpu.cpp" + "${CMAKE_SOURCE_DIR}/code/mpu.cpp" ) target_compile_features(CpuDetect PRIVATE cxx_std_20) diff --git a/tests/cpudetect/cpudetect.cpp b/tests/cpudetect/cpudetect.cpp index f008643b..e0f219fe 100644 --- a/tests/cpudetect/cpudetect.cpp +++ b/tests/cpudetect/cpudetect.cpp @@ -7,10 +7,10 @@ * See LICENSE.md for applicable additional terms and warranty disclaimers. ******************************************************************************/ -// Checks the processor detection in detproc.cpp against CPUID read directly here. The +// Checks the processor detection in getcpu.cpp against CPUID read directly here. The // detection used to be hand-written assembly, so the point is to confirm the C++ reports the -// same family, vendor and feature flags the instruction does, on whatever machine runs it. -// Needs no game data. +// same family and vendor the instruction does, and that MMX and CMOV are reported available +// unconditionally, as required by the supported minimum hardware. Needs no game data. #include @@ -19,7 +19,6 @@ #include #include "getcpu.h" -#include "misc.h" #include "mpu.h" namespace { @@ -98,17 +97,15 @@ int main(void) expected[13] = '\0'; Check(std::strcmp(reported, expected) == 0, "Vendor string matches CPUID"); - bool const expect_mmx = (family >= 5) && ((edx & 0x00800000) != 0); - Check(mmx == expect_mmx, "MMX result matches CPUID feature bit"); - Check((UseMMX != 0) == expect_mmx, "UseMMX global matches CPUID feature bit"); - - bool const cmov_bit = ((edx & 0x00008000) != 0); - bool const expect_has = (family >= 5) && cmov_bit; - bool const expect_use = (family > 5) && cmov_bit; - Check((HasCMOV != 0) == expect_has, "HasCMOV matches CPUID feature bit"); - Check((UseCMOV != 0) == expect_use, "UseCMOV set only above family 5"); - - Check(Processor() == PROC_PENTIUM, "Processor reports a CPUID-capable part"); + /* + * The supported minimum hardware (SSE2, so a Pentium 4 or Athlon 64 onward) always carries + * MMX and CMOV, so detection reports both available unconditionally rather than reading + * the CPUID feature bits. + */ + Check(mmx, "Get_CPU_Type reports MMX available"); + Check(UseMMX != 0, "UseMMX global reports available"); + Check(HasCMOV != 0, "HasCMOV global reports available"); + Check(UseCMOV != 0, "UseCMOV global reports available"); /* * The clock accumulator only ever counts up, so a later read cannot be the smaller of From 96ae888859484181a8ae2e8d5601b33826810394 Mon Sep 17 00:00:00 2001 From: Jakub Vesely <1251980+tinix0@users.noreply.github.com> Date: Sun, 30 Aug 2026 08:49:40 +0200 Subject: [PATCH 3/3] Formatting and comment cleanup --- code/getcpu.cpp | 6 ------ code/init.cpp | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/code/getcpu.cpp b/code/getcpu.cpp index 486bfaea..456c1b32 100644 --- a/code/getcpu.cpp +++ b/code/getcpu.cpp @@ -87,16 +87,10 @@ char VendorID[20] = "Not available"; /// feature bit. /// /// bool; always true on the supported minimum hardware. - bool __cdecl Detect_MMX_Availability(void) { int regs[4]; - /* - * The original probed the EFLAGS AC and ID bits here to separate a 386 from a 486 from a - * processor carrying CPUID. Every processor the current build supports carries CPUID, so - * the family comes straight from it and the 386 and 486 answers are unreachable. - */ char cputype = 4; __cpuid(regs, 0); diff --git a/code/init.cpp b/code/init.cpp index 77244408..90a84164 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -308,8 +308,8 @@ int Init_Game(int , char * []) ** is needed since the supported minimum hardware always qualifies. */ #ifdef _DEBUG - DebugString("Creating benchmarks\n"); - Benches = new Benchmark [BENCH_COUNT]; + DebugString("Creating benchmarks\n"); + Benches = new Benchmark [BENCH_COUNT]; #endif /*