NV3 Getting Started: Difference between revisions
No edit summary |
No edit summary |
||
| (One intermediate revision by the same user not shown) | |||
| Line 3: | Line 3: | ||
== Introduction == | == Introduction == | ||
Most of the pages on this website are for people interested in how Nvidia's hardware works. You don't need to know everything about how the RIVA 128 works in order to actually program the hardware; you just need to be able to understand the interfaces to it and some of the registers. This is not for the faint of heart. You are directly programming complex graphics hardware (old does not mean simple - the RIVA 128 has 3.5 million transistors, far more than any human can individually comprehend) that is not yet fully understood, but advances are being made all the time, so check back to this page for new updates. Eventually versions of this page will be made for the NV1 and RIVA TNT, and hopefully the Riva TNT2 and maybe, one day, even the GeForce 256 and its Celsius/NV1x brethren, such as the GeForce 2 and the GeForce 4 MX. If you want to write a game or some other piece of software in this way, you will be effectively writing a mini-GPU driver to manage the GPU hardware resources combined with whatever software you may wish to develop. If you want to write raw-NV software for Windows, Linux or any other operating system you will need to write a kernel driver that exposes the GPU's raw interface to usermode before you can write anything at all (this driver may be done in the future). You can get around this by using something like [https://www.delorie.com/djgpp/ DJGPP] in order to write a DOS application using a 32-bit memory model, without any of DOS's 16-bit segmented cruft. This is the recommended approach as it has the minimum amount of overhead and boilerplate code, plus working under a DOS box such as the Windows 9x DOS box. | Most of the pages on this website are for people interested in how Nvidia's hardware works. You don't need to know everything about how the RIVA 128 works in order to actually program the hardware; you just need to be able to understand the interfaces to it and some of the registers. This is not for the faint of heart. You are directly programming complex graphics hardware (old does not mean simple - the RIVA 128 has 3.5 million transistors, far more than any human can individually comprehend) that is not yet fully understood, but advances are being made all the time, so check back to this page for new updates. Eventually versions of this page will be made for the NV1 and RIVA TNT, and hopefully the Riva TNT2 and maybe, one day, even the GeForce 256 and its Celsius/NV1x brethren, such as the GeForce 2 and the GeForce 4 MX. If you want to write a game or some other piece of software in this way, you will be effectively writing a mini-GPU driver to manage the GPU hardware resources combined with whatever software you may wish to develop. If you want to write raw-NV software for Windows, Linux or any other operating system you will need to write a kernel driver that exposes the GPU's raw interface to usermode before you can write anything at all (this driver may be done in the future). You can get around this by using something like [https://www.delorie.com/djgpp/ DJGPP] in order to write a DOS application using a 32-bit memory model, without any of DOS's 16-bit segmented cruft. This is the recommended approach as it has the minimum amount of overhead and boilerplate code, plus working under a DOS box such as the Windows 9x DOS box. | ||
== Concepts == | |||
Before we start programming, there are many concepts, some that are more general to raw-level GPU development and other very specific to Nvidia, that need to be explained. | |||
== Pre-initialisation == | == Pre-initialisation == | ||
| Line 54: | Line 57: | ||
Then the two addresses returned must have their lower 24 bits masked off, so that they are aligned to a 16 MB boundary, as the lower byte is used for determining if prefetching is enabled (which it is) rather than store the lower byte of the address. Additionally, the RIVA 128 only decodes the upper 8 bits of its PCI BAR values, so if these values are not aligned, MMIO addresses will be misaligned, which is fatal for graphics programming. This is especially important if you want to change the PCI BARs later. Since these addresses are not mapped within your CPU's memory management unit (MMU), they now need to be mapped in order to actually perform memory-mapped I/O. This is done, on x86 architectures, by adding additional segment descriptors to the local descriptor table (LDT) of your CPU, with a base address for each BAR and a "limit" (MMU parlance for size) of 16 MB. In DJGPP, this is done by <code>#including</code> the file <code>dpmi.h</code> allocating two LDT descriptors with the function <code>__dpmi_allocate_ldt_descriptors(1);</code>. The parameter 1 is the number of descriptors to allocate, as it is is easier syntactically to allocate them one at a time. The function returns an integer that is later used to actually interface with the MMIO space. After this, the functions <code>__dpmi_set_segment_base_address</code> and <code>__dpmi_set_segment_limit</code> are done to actually assign the base and limit values to the LDT descriptors. | Then the two addresses returned must have their lower 24 bits masked off, so that they are aligned to a 16 MB boundary, as the lower byte is used for determining if prefetching is enabled (which it is) rather than store the lower byte of the address. Additionally, the RIVA 128 only decodes the upper 8 bits of its PCI BAR values, so if these values are not aligned, MMIO addresses will be misaligned, which is fatal for graphics programming. This is especially important if you want to change the PCI BARs later. Since these addresses are not mapped within your CPU's memory management unit (MMU), they now need to be mapped in order to actually perform memory-mapped I/O. This is done, on x86 architectures, by adding additional segment descriptors to the local descriptor table (LDT) of your CPU, with a base address for each BAR and a "limit" (MMU parlance for size) of 16 MB. In DJGPP, this is done by <code>#including</code> the file <code>dpmi.h</code> allocating two LDT descriptors with the function <code>__dpmi_allocate_ldt_descriptors(1);</code>. The parameter 1 is the number of descriptors to allocate, as it is is easier syntactically to allocate them one at a time. The function returns an integer that is later used to actually interface with the MMIO space. After this, the functions <code>__dpmi_set_segment_base_address</code> and <code>__dpmi_set_segment_limit</code> are done to actually assign the base and limit values to the LDT descriptors. | ||
It is highly recommended to write utility functions to make it easier to read and write to MMIO space. Most Nvidia registers are 32-bit, but 8-bit versions of the functions are also required in order to write to 8-bit legacy VGA registers. In DJGPP, these functions would be written like this; | It is highly recommended to write utility functions to make it easier to read and write to MMIO space. Most Nvidia registers are 32-bit, but 8-bit versions of the functions are also required in order to write to 8-bit legacy VGA registers. In DJGPP, these functions would be written like this, with <code>bar0_selector</code> being the selector that you allocated earlier; these functions are very easy to port to any other 32-bit flat DOS environment of your choosing: | ||
<pre> | <pre> | ||
| Line 69: | Line 72: | ||
} | } | ||
/* Write 8-bit value to the MMIO */ | |||
void NV_WriteMMIO8(uint32_t offset, uint8_t val) | void NV_WriteMMIO8(uint32_t offset, uint8_t val) | ||
{ | { | ||
| Line 74: | Line 78: | ||
} | } | ||
/* Write 32-bit value to the MMIO */ | |||
void NV_WriteMMIO32(uint32_t offset, uint32_t val) | void NV_WriteMMIO32(uint32_t offset, uint32_t val) | ||
{ | { | ||
| Line 81: | Line 86: | ||
== Initialisation == | == Initialisation == | ||
After this the actual initialisation of the GPU can commence. But before that, ''what do you actually get?'' from those two descriptors? | After this the actual initialisation of the GPU can commence. But before that, ''what do you actually get?'' from those two descriptors? | ||
BAR0 is a pointer into the GPU's register space. Registers are very sparsely grouped together; the vast majority of addresses decode to open bus (often, but not entirely consistent, garbage, seemingly related to what the GPU is currently writing into Video RAM). A header file with the addresses of the registers can be obtained here: ((TODO : UPLOAD UNOFFICIAL NV3_REF.H)); see [[NV3 memory mapping]] for an explanation of which regions correspond to which functional blocks within the chip. It also contains (at offset <code>0x110000</code>) a mirror of the Video BIOS, if one is installed (an integrated chip may not have one). | |||
'''Warning: ''' If the "mediaport" region (at <code>0x200000-0x200FFF</code>) is written to or read from without something being plugged into the mediaport, or certain other regions like the object registers, '''the entire PCI bus will lock up''', and the system will hard freeze, with the only option for recovering the system being using the rocker switch (the switch at the back). | |||
'''Note: ''' For some reason, at least under Windows 9x, if you open a DOS box, the top left corner of the window will be mirrored in the legacy VGA space (BAR0 <code>0xA0000-0xBFFFF</code>), in whatever graphics format the GPU is set up to use, but interlaced. This is, quite frankly, bizarre. | |||
BAR1 has two primary purposes on NV3. The first chunk of the BAR1 space is | |||
Revision as of 01:05, 11 November 2026
You can program your completely obsolete GPU the way that the company intended, using boring APIs such as Direct3D and OpenGL. Or you can do it in the cool way. This page details how to be cool within extremely niche circles. The choice is yours.
Introduction
Most of the pages on this website are for people interested in how Nvidia's hardware works. You don't need to know everything about how the RIVA 128 works in order to actually program the hardware; you just need to be able to understand the interfaces to it and some of the registers. This is not for the faint of heart. You are directly programming complex graphics hardware (old does not mean simple - the RIVA 128 has 3.5 million transistors, far more than any human can individually comprehend) that is not yet fully understood, but advances are being made all the time, so check back to this page for new updates. Eventually versions of this page will be made for the NV1 and RIVA TNT, and hopefully the Riva TNT2 and maybe, one day, even the GeForce 256 and its Celsius/NV1x brethren, such as the GeForce 2 and the GeForce 4 MX. If you want to write a game or some other piece of software in this way, you will be effectively writing a mini-GPU driver to manage the GPU hardware resources combined with whatever software you may wish to develop. If you want to write raw-NV software for Windows, Linux or any other operating system you will need to write a kernel driver that exposes the GPU's raw interface to usermode before you can write anything at all (this driver may be done in the future). You can get around this by using something like DJGPP in order to write a DOS application using a 32-bit memory model, without any of DOS's 16-bit segmented cruft. This is the recommended approach as it has the minimum amount of overhead and boilerplate code, plus working under a DOS box such as the Windows 9x DOS box.
Concepts
Before we start programming, there are many concepts, some that are more general to raw-level GPU development and other very specific to Nvidia, that need to be explained.
Pre-initialisation
If you are programming a game against a graphics card, the graphics card initialisation has already been done for you by the vendor-provided drivers. But if you are doing raw-level programming of a GPU, you will have to do this step manualy; this is done by writing directly to memory-mapped I/O addresses decoded via PCI BARs (Base Address Registers); in NV3, there are two, that are both 16 MB. But these are not mapped to your CPU's MMU by default, and for some cards (like NV1) the card will be completely dead when you start to interact with it - it will not even respond to MMIO accesses. However, in most cases for the RIVA 128, this is not actually required (if your card was integrated of a motherboard, this may not be done), as the RIVA 128's PCI configuration (such as its bus mastering capability and addresses for both BAR0 and BAR1) are pre-configured by the Video BIOS. This means that all you have to do is map the 32 MB of MMIO the NV3 uses, which can be done using code like this in DJGPP: (example from nvplay).
// only top 8 bits actually matter
uint32_t bar0_base = PCI_ReadConfig32(current_device.bus_info.bus_number, current_device.bus_info.function_number, PCI_CFG_OFFSET_BAR0);
uint32_t bar1_base = PCI_ReadConfig32(current_device.bus_info.bus_number, current_device.bus_info.function_number, PCI_CFG_OFFSET_BAR1);
/* According to the datasheet only the most significant 8 bits matter */
bar0_base &= 0xFF000000;
bar1_base &= 0xFF000000;
Logging_Write(LOG_LEVEL_DEBUG, "NV3 - PCI BAR0 0x%08lX\n", bar0_base);
Logging_Write(LOG_LEVEL_DEBUG, "NV3 - PCI BAR1 0x%08lX\n", bar1_base);
/* We need to allocate an LDT for this */
/* So start by allocating a physical mapping. */
__dpmi_meminfo meminfo_bar0 = {0};
__dpmi_meminfo meminfo_bar1 = {0};
meminfo_bar0.address = bar0_base;
meminfo_bar0.size = NV_MMIO_SIZE;
meminfo_bar1.address = bar1_base;
meminfo_bar1.size = NV_MMIO_SIZE; //this will change
current_device.ramin_start = bar1_base + NV3_RAMIN_START;
__dpmi_physical_address_mapping(&meminfo_bar0);
__dpmi_physical_address_mapping(&meminfo_bar1);
Logging_Write(LOG_LEVEL_DEBUG, "NV3 Init: Mapping BAR0 MMIO...\n");
/* Set up two LDTs, we don't need one for ramin, because, it's just a part of bar1 we map differently */
current_device.bus_info.bar0_selector = __dpmi_allocate_ldt_descriptors(1);
__dpmi_set_segment_base_address(current_device.bus_info.bar0_selector, meminfo_bar0.address);
__dpmi_set_segment_limit(current_device.bus_info.bar0_selector, NV_MMIO_SIZE - 1);
Logging_Write(LOG_LEVEL_DEBUG, "NV3 Init: Mapping BAR1 (DFB / RAMIN)...\n");
current_device.bus_info.bar1_selector = __dpmi_allocate_ldt_descriptors(1);
__dpmi_set_segment_base_address(current_device.bus_info.bar1_selector, meminfo_bar1.address);
__dpmi_set_segment_limit(current_device.bus_info.bar1_selector, NV_MMIO_SIZE - 1); // ultimately the same size
The first step of this code is using the PCI BIOS; the PCI BIOS is used from real mode, or a DPMI-compliant 32-bit environment, by invoking interrupt 1A with AH set to 0xB1 and AL set to the function number. The function number for ReadConfig32 is hence B10Ah, or Read Config Dword) in order to read the BAR 0 and BAR 1 addresses from the chip's PCI configuration. In order to use all of the required PCI BIOS services, the version of the PCI specification imlemented by your motherboard must be version 2.1 or beter; you should test for this by using INT 1A, function B101h, and checking if the value in the BX register is at least 0x0201, or PCI version 2.1. Nearly all PCI compliant devices should be compliant with version 2.1 of the PCI specification, but some of the very earliest PCI chipsets were not, so still use this function to check for compliance.
Then the two addresses returned must have their lower 24 bits masked off, so that they are aligned to a 16 MB boundary, as the lower byte is used for determining if prefetching is enabled (which it is) rather than store the lower byte of the address. Additionally, the RIVA 128 only decodes the upper 8 bits of its PCI BAR values, so if these values are not aligned, MMIO addresses will be misaligned, which is fatal for graphics programming. This is especially important if you want to change the PCI BARs later. Since these addresses are not mapped within your CPU's memory management unit (MMU), they now need to be mapped in order to actually perform memory-mapped I/O. This is done, on x86 architectures, by adding additional segment descriptors to the local descriptor table (LDT) of your CPU, with a base address for each BAR and a "limit" (MMU parlance for size) of 16 MB. In DJGPP, this is done by #including the file dpmi.h allocating two LDT descriptors with the function __dpmi_allocate_ldt_descriptors(1);. The parameter 1 is the number of descriptors to allocate, as it is is easier syntactically to allocate them one at a time. The function returns an integer that is later used to actually interface with the MMIO space. After this, the functions __dpmi_set_segment_base_address and __dpmi_set_segment_limit are done to actually assign the base and limit values to the LDT descriptors.
It is highly recommended to write utility functions to make it easier to read and write to MMIO space. Most Nvidia registers are 32-bit, but 8-bit versions of the functions are also required in order to write to 8-bit legacy VGA registers. In DJGPP, these functions would be written like this, with bar0_selector being the selector that you allocated earlier; these functions are very easy to port to any other 32-bit flat DOS environment of your choosing:
/* Read 8-bit value from the MMIO */
uint8_t NV_ReadMMIO8(uint32_t offset)
{
return _farpeekb(current_device.bus_info.bar0_selector, offset);
}
/* Read 32-bit value from the MMIO */
uint32_t NV_ReadMMIO32(uint32_t offset)
{
return _farpeekl(current_device.bus_info.bar0_selector, offset);
}
/* Write 8-bit value to the MMIO */
void NV_WriteMMIO8(uint32_t offset, uint8_t val)
{
_farpokeb(current_device.bus_info.bar0_selector, offset, val);
}
/* Write 32-bit value to the MMIO */
void NV_WriteMMIO32(uint32_t offset, uint32_t val)
{
_farpokel(current_device.bus_info.bar0_selector, offset, val);
}
Initialisation
After this the actual initialisation of the GPU can commence. But before that, what do you actually get? from those two descriptors?
BAR0 is a pointer into the GPU's register space. Registers are very sparsely grouped together; the vast majority of addresses decode to open bus (often, but not entirely consistent, garbage, seemingly related to what the GPU is currently writing into Video RAM). A header file with the addresses of the registers can be obtained here: ((TODO : UPLOAD UNOFFICIAL NV3_REF.H)); see NV3 memory mapping for an explanation of which regions correspond to which functional blocks within the chip. It also contains (at offset 0x110000) a mirror of the Video BIOS, if one is installed (an integrated chip may not have one).
Warning: If the "mediaport" region (at 0x200000-0x200FFF) is written to or read from without something being plugged into the mediaport, or certain other regions like the object registers, the entire PCI bus will lock up, and the system will hard freeze, with the only option for recovering the system being using the rocker switch (the switch at the back).
Note: For some reason, at least under Windows 9x, if you open a DOS box, the top left corner of the window will be mirrored in the legacy VGA space (BAR0 0xA0000-0xBFFFF), in whatever graphics format the GPU is set up to use, but interlaced. This is, quite frankly, bizarre.
BAR1 has two primary purposes on NV3. The first chunk of the BAR1 space is