1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-14 08:54:58 +00:00
serenity/Kernel/Devices/GPU/3dfx/VoodooDisplayConnector.cpp
Edwin Rijkee 8388fe51b5 Kernel: Add a framebuffer driver for 3Dfx Voodoo 3
A bit old but a relatively uncomplicated device capable of outputting
1920x1080 video with 32-bit color. Tested with a Voodoo 3 3000 16MB
PCI card. Resolution switching from DisplaySettings also works.

If the requested mode contains timing information, it is used directly.
Otherwise, display timing values are selected from the EDID. First the
detailed timings are checked, and then standard and established
timings for which there is a matching DMT mode. The driver does not
(yet) read the actual EDID, so the generic EDID in DisplayConnector now
includes a set of common display modes to make this work.

The driver should also be compatible with the Voodoo Banshee, 4 and 5
but I don't have these cards to test this with. The PCI IDs of these
cards are included as a commented line in case someone wants to give it
a try.
2023-10-16 01:25:45 +02:00

416 lines
18 KiB
C++

/*
* Copyright (c) 2023, Edwin Rijkee <edwin@virtualparadise.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <Kernel/Devices/GPU/3dfx/VoodooDisplayConnector.h>
#include <Kernel/Devices/GPU/Console/ContiguousFramebufferConsole.h>
#include <Kernel/Devices/GPU/Management.h>
#include <Kernel/Time/TimeManagement.h>
namespace Kernel::VoodooGraphics {
NonnullLockRefPtr<VoodooDisplayConnector> VoodooDisplayConnector::must_create(PhysicalAddress framebuffer_address, size_t framebuffer_resource_size, Memory::TypedMapping<RegisterMap volatile> registers_mapping, NonnullOwnPtr<IOWindow> io_window)
{
auto device_or_error = DeviceManagement::try_create_device<VoodooDisplayConnector>(framebuffer_address, framebuffer_resource_size, move(registers_mapping), move(io_window));
VERIFY(!device_or_error.is_error());
auto connector = device_or_error.release_value();
MUST(connector->create_attached_framebuffer_console());
MUST(connector->fetch_and_initialize_edid());
return connector;
}
ErrorOr<void> VoodooDisplayConnector::fetch_and_initialize_edid()
{
// TODO: actually fetch the EDID.
return initialize_edid_for_generic_monitor({});
}
ErrorOr<void> VoodooDisplayConnector::create_attached_framebuffer_console()
{
m_framebuffer_console = Graphics::ContiguousFramebufferConsole::initialize(m_framebuffer_address.value(), 1024, 768, 1024 * sizeof(u32));
GraphicsManagement::the().set_console(*m_framebuffer_console);
return {};
}
VoodooDisplayConnector::VoodooDisplayConnector(PhysicalAddress framebuffer_address, size_t framebuffer_resource_size, Memory::TypedMapping<RegisterMap volatile> registers_mapping, NonnullOwnPtr<IOWindow> io_window)
: DisplayConnector(framebuffer_address, framebuffer_resource_size, true)
, m_registers(move(registers_mapping))
, m_io_window(move(io_window))
{
}
ErrorOr<IterationDecision> VoodooDisplayConnector::for_each_dmt_timing_in_edid(Function<IterationDecision(EDID::DMT::MonitorTiming const&)> callback) const
{
IterationDecision iteration_decision = TRY(m_edid_parser->for_each_standard_timing([&](auto& standard_timing) {
auto timing = EDID::DMT::find_timing_by_dmt_id(standard_timing.dmt_id());
if (!timing) {
return IterationDecision::Continue;
}
return callback(*timing);
}));
if (iteration_decision == IterationDecision::Break) {
return iteration_decision;
}
iteration_decision = TRY(m_edid_parser->for_each_established_timing([&](auto& established_timing) {
auto timing = EDID::DMT::find_timing_by_dmt_id(established_timing.dmt_id());
if (!timing) {
return IterationDecision::Continue;
}
return callback(*timing);
}));
return iteration_decision;
}
auto VoodooDisplayConnector::find_suitable_mode(ModeSetting const& requested_mode) const -> ErrorOr<ModeSetting>
{
u32 width = requested_mode.horizontal_active;
u32 height = requested_mode.vertical_active;
ModeSetting result = requested_mode;
if (requested_mode.horizontal_stride == 0) {
result.horizontal_stride = sizeof(u32) * width;
}
if (requested_mode.pixel_clock_in_khz != 0) {
dbgln_if(TDFX_DEBUG, "3dfx: Requested mode {}x{} includes timing information", width, height);
return result;
}
dbgln_if(TDFX_DEBUG, "3dfx: Looking for suitable mode with resolution {}x{}", width, height);
IterationDecision iteration_decision = TRY(m_edid_parser->for_each_detailed_timing([&](auto& timing, unsigned) {
dbgln_if(TDFX_DEBUG, "3dfx: Considering detailed timing {}x{} @ {}", timing.horizontal_addressable_pixels(), timing.vertical_addressable_lines(), timing.refresh_rate());
if (timing.is_interlaced() || timing.horizontal_addressable_pixels() != width || timing.vertical_addressable_lines() != height) {
return IterationDecision::Continue;
}
result.pixel_clock_in_khz = timing.pixel_clock_khz();
result.horizontal_front_porch_pixels = timing.horizontal_front_porch_pixels();
result.horizontal_sync_time_pixels = timing.horizontal_sync_pulse_width_pixels();
result.horizontal_blank_pixels = timing.horizontal_blanking_pixels();
result.vertical_front_porch_lines = timing.vertical_front_porch_lines();
result.vertical_sync_time_lines = timing.vertical_sync_pulse_width_lines();
result.vertical_blank_lines = timing.vertical_blanking_lines();
return IterationDecision::Break;
}));
if (iteration_decision == IterationDecision::Break) {
return result;
}
iteration_decision = TRY(for_each_dmt_timing_in_edid([&](auto& timing) {
dbgln_if(TDFX_DEBUG, "3dfx: Considering DMT timing {}x{} @ {}", timing.horizontal_pixels, timing.vertical_lines, timing.vertical_frequency_hz());
if (timing.scan_type != EDID::DMT::MonitorTiming::ScanType::NonInterlaced || timing.horizontal_pixels != width || timing.vertical_lines != height) {
return IterationDecision::Continue;
}
result.pixel_clock_in_khz = timing.pixel_clock_khz;
result.horizontal_front_porch_pixels = timing.horizontal_front_porch_pixels;
result.horizontal_sync_time_pixels = timing.horizontal_sync_time_pixels;
result.horizontal_blank_pixels = timing.horizontal_blank_pixels;
result.vertical_front_porch_lines = timing.vertical_front_porch_lines;
result.vertical_sync_time_lines = timing.vertical_sync_time_lines;
result.vertical_blank_lines = timing.vertical_blank_lines;
return IterationDecision::Break;
}));
if (iteration_decision == IterationDecision::Break) {
return result;
}
dbgln_if(TDFX_DEBUG, "3dfx: No timing information available for display mode {}x{}", width, height);
return EINVAL;
}
ErrorOr<void>
VoodooDisplayConnector::set_mode_setting(ModeSetting const& requested_mode_setting)
{
SpinlockLocker locker(m_modeset_lock);
VERIFY(m_framebuffer_console);
ModeSetting mode_setting = TRY(find_suitable_mode(requested_mode_setting));
dbgln_if(TDFX_DEBUG, "VoodooDisplayConnector resolution registers set to - {}x{}", mode_setting.horizontal_active, mode_setting.vertical_active);
auto regs = TRY(prepare_mode_switch(mode_setting));
TRY(perform_mode_switch(regs));
m_framebuffer_console->set_resolution(mode_setting.horizontal_active, mode_setting.vertical_active, mode_setting.horizontal_stride);
m_current_mode_setting = mode_setting;
return {};
}
ErrorOr<void> VoodooDisplayConnector::set_y_offset(size_t)
{
return ENOTIMPL;
}
ErrorOr<void> VoodooDisplayConnector::set_safe_mode_setting()
{
DisplayConnector::ModeSetting safe_mode_set {
.horizontal_stride = 1024 * sizeof(u32),
.pixel_clock_in_khz = 65000,
.horizontal_active = 1024,
.horizontal_front_porch_pixels = 24,
.horizontal_sync_time_pixels = 136,
.horizontal_blank_pixels = 320,
.vertical_active = 768,
.vertical_front_porch_lines = 3,
.vertical_sync_time_lines = 6,
.vertical_blank_lines = 38,
.horizontal_offset = 0,
.vertical_offset = 0,
};
return set_mode_setting(safe_mode_set);
}
ErrorOr<void> VoodooDisplayConnector::unblank()
{
return ENOTIMPL;
}
ErrorOr<void> VoodooDisplayConnector::flush_first_surface()
{
return ENOTSUP;
}
void VoodooDisplayConnector::enable_console()
{
VERIFY(m_control_lock.is_locked());
VERIFY(m_framebuffer_console);
m_framebuffer_console->enable();
}
void VoodooDisplayConnector::disable_console()
{
VERIFY(m_control_lock.is_locked());
VERIFY(m_framebuffer_console);
m_framebuffer_console->disable();
}
u8 VoodooDisplayConnector::read_vga(VGAPort port)
{
return m_io_window->read8(static_cast<u16>(port) - 0x300);
}
u8 VoodooDisplayConnector::read_vga_indexed(VGAPort index_port, VGAPort data_port, u8 index)
{
m_io_window->write8(static_cast<u16>(index_port) - 0x300, index);
return m_io_window->read8(static_cast<u16>(data_port) - 0x300);
}
void VoodooDisplayConnector::write_vga(VGAPort port, u8 value)
{
m_io_window->write8(static_cast<u16>(port) - 0x300, value - 0x300);
}
void VoodooDisplayConnector::write_vga_indexed(VGAPort index_port, VGAPort data_port, u8 index, u8 value)
{
m_io_window->write8(static_cast<u16>(index_port) - 0x300, index);
m_io_window->write8(static_cast<u16>(data_port) - 0x300, value);
}
ErrorOr<void> VoodooDisplayConnector::wait_for_fifo_space(u32 entries)
{
VERIFY(entries < 32);
auto deadline = TimeManagement::the().monotonic_time() + Duration::from_seconds(1);
do {
if ((m_registers->status & 0x1f) >= entries) {
return {};
}
(void)Thread::current()->sleep(Duration::from_milliseconds(1));
} while (TimeManagement::the().monotonic_time() < deadline);
dbgln_if(TDFX_DEBUG, "3dfx: timed out waiting for fifo space");
return EIO;
}
PLLSettings VoodooDisplayConnector::calculate_pll(i32 desired_frequency_in_khz)
{
VoodooGraphics::PLLSettings current;
VoodooGraphics::PLLSettings best;
i32 best_difference;
best_difference = desired_frequency_in_khz;
for (current.m = 0; current.m < 64; current.m++) {
for (current.n = 0; current.n < 256; current.n++) {
for (current.k = 0; current.k < 4; current.k++) {
auto frequency_in_khz = current.frequency_in_khz();
auto error = AK::abs(frequency_in_khz - desired_frequency_in_khz);
if (error < best_difference) {
best_difference = error;
best = current;
}
}
}
}
return best;
}
ErrorOr<ModeRegisters> VoodooDisplayConnector::prepare_mode_switch(ModeSetting const& mode_setting)
{
u32 width = mode_setting.horizontal_active;
u32 height = mode_setting.vertical_active;
ModeRegisters regs;
regs.vga_init0 = EnableVgaExtensions | WakeUpSelect3C3 | EnableAltReadback | FIFODepth8Bit | ExtendedShiftOut;
regs.vid_proc_cfg |= VideoProcessorEnable | DesktopSurfaceEnable | DesktopPixelFormat32Bit | DesktopCLUTBypass;
// We only want to touch the 2X flag of the DAC Mode register, the other flags are preserved
regs.dac_mode = m_registers->dac_mode & ~DacMode2x;
u32 const max_pixel_clock_in_khz = 270000;
if (mode_setting.pixel_clock_in_khz > max_pixel_clock_in_khz) {
return ENOTSUP;
}
u32 horizontal_divisor = 8;
if (mode_setting.pixel_clock_in_khz > max_pixel_clock_in_khz / 2) {
horizontal_divisor = 16;
regs.dac_mode |= DacMode2x;
regs.vid_proc_cfg |= TwoXMode;
}
dbgln_if(TDFX_DEBUG, "3dfx: Calculating best PLL settings for pixel clock {} KHz", mode_setting.pixel_clock_in_khz);
auto pll = calculate_pll(mode_setting.pixel_clock_in_khz);
dbgln_if(TDFX_DEBUG, "3dfx: Best matching PLL settings: m={}, n={}, k={}. Frequency: {} KHz", pll.m, pll.n, pll.k, pll.frequency_in_khz());
regs.pll_ctrl0 = pll.register_value();
regs.vid_screen_size = width | (height << 12);
regs.vid_desktop_overlay_stride = mode_setting.horizontal_stride;
regs.misc_out_reg = ClockSelectPLL | CRTCAddressColor;
if (height < 768) {
regs.misc_out_reg |= VerticalSyncPositive | HorizontalSyncPositive;
}
u32 hor_total = mode_setting.horizontal_total() / horizontal_divisor - 5;
u32 hor_disp_en_end = width / horizontal_divisor - 1;
u32 hor_sync_start = mode_setting.horizontal_sync_start() / horizontal_divisor;
u32 hor_sync_end = (mode_setting.horizontal_sync_end() / horizontal_divisor) & 0x1f;
u32 hor_blank_start = hor_disp_en_end;
u32 hor_blank_end = hor_total & 0x7f;
u32 vert_total = mode_setting.vertical_total() - 2;
u32 vert_disp_en_end = height - 1;
u32 vert_sync_start = mode_setting.vertical_sync_start();
u32 vert_sync_end = mode_setting.vertical_sync_end() & 0xf;
u32 vert_blank_start = mode_setting.vertical_blanking_start() - 1;
u32 vert_blank_end = (mode_setting.vertical_total() - 1) & 0xff;
if (hor_total > 0x1ff || // 9-bit field
hor_disp_en_end > 0x1ff || // 9-bit field
hor_sync_start > 0x1ff || // 9-bit field
hor_sync_end > 0x1f || // 5-bit field
hor_blank_start > 0x1ff || // 9-bit field
hor_blank_end > 0x7f || // 7-bit field
vert_total > 0x7ff || // 11-bit field
vert_disp_en_end > 0x7ff || // 11-bit field
vert_sync_start > 0x7ff || // 11-bit field
vert_sync_end > 0x0f || // 4-bit field
vert_blank_start > 0x7ff || // 11-bit field
vert_blank_end > 0xff // 8-bit field
) {
dbgln_if(TDFX_DEBUG, "3dfx: One of the timing values is too large to fit in its register");
return EOVERFLOW;
}
// CRT Controller Registers
regs.cr.horizontal_total = hor_total; // bit 0-7 of hor_total
regs.cr.horizontal_display_enable_end = hor_disp_en_end; // bit 0-7 of hor_disp_en_end
regs.cr.horizontal_blanking_start = hor_blank_start; // bit 0-7 of hor_blank_start
regs.cr.horizontal_blanking_end = (hor_blank_end & 0x1f) // bit 0-4 of hor_blank_end
| CompatibilityRead;
regs.cr.horizontal_sync_start = hor_sync_start; // bit 0-7 of hor_sync_start
regs.cr.horizontal_sync_end = ((hor_sync_end & 0x1f) // bit 0-4 of hor_sync_end
| ((hor_blank_end & 0x20) << 2)); // bit 5 of hor_blank_end
regs.cr.vertical_total = vert_total; // bit 0-7 of vert_total
regs.cr.overflow = (((vert_total & 0x100) >> 8) // bit 8 of vert_total
| ((vert_disp_en_end & 0x100) >> 7) // bit 8 of vert_disp_en_end
| ((vert_sync_start & 0x100) >> 6) // bit 8 of vert_sync_start
| ((vert_blank_start & 0x100) >> 5) // bit 8 of vert_blank_start
| ((vert_total & 0x200) >> 4) // bit 9 of vert_disp_en_end
| ((vert_disp_en_end & 0x200) >> 3) // bit 9 of vert_disp_en_end
| ((vert_sync_start & 0x200) >> 2)); // bit 9 of vert_sync_start
regs.cr.maximum_scan_line = ((vert_blank_start & 0x200) >> 4); // bit 9 of vert_blank_start
regs.cr.vertical_sync_start = vert_sync_start; // bit 0-7 of vert_sync_start
regs.cr.vertical_sync_end = (vert_sync_end & 0x0f) // bit 0-3 of vert_sync_end
| EnableVertInt;
regs.cr.vertical_display_enable_end = vert_disp_en_end; // bit 0-7 of vert_disp_en_end
regs.cr.vertical_blanking_start = vert_blank_start; // bit 0-7 of vert_blank_start
regs.cr.vertical_blanking_end = vert_blank_end; // bit 0-7 of vert_blank_end
regs.cr.mode_control = TimingEnable | ByteWordMode;
regs.cr.horizontal_extensions = (hor_total & 0x100) >> 8 // bit 8 of hor_total
| (hor_disp_en_end & 0x100) >> 6 // bit 8 of hor_disp_en_end
| (hor_blank_start & 0x100) >> 4 // bit 8 of hor_blank_start
| (hor_blank_end & 0x40) >> 1 // bit 6 of hor_blank_end
| (hor_sync_start & 0x100) >> 2 // bit 8 of hor_sync_start
| (hor_sync_end & 0x20) << 2; // bit 5 of hor_sync_end
regs.cr.vertical_extensions = (vert_total & 0x400) >> 10 // bit 10 of vert_total
| (vert_disp_en_end & 0x400) >> 8 // bit 10 of vert_disp_en_endx
| (vert_blank_start & 0x400) >> 6 // bit 10 of vert_blank_start
| (vert_blank_end & 0x400) >> 4 // bit 10 of vert_blank_end
| (vert_sync_start & 0x400) >> 4; // bit 10 of vert_sync_start
// Graphics Controller Registers
regs.gr.graphics_controller_miscellaneous = MemoryMapEGAVGAExtended;
// Attribute Controller Registers
regs.ar.attribute_controller_mode = GraphicsMode | PixelWidth;
// Sequencer Registers
regs.sr.sequencer_reset = AsynchronousReset | SynchronousReset;
regs.sr.sequencer_clocking_mode = DotClock8;
return regs;
}
ErrorOr<void> VoodooDisplayConnector::perform_mode_switch(ModeRegisters const& regs)
{
TRY(wait_for_fifo_space(2));
m_registers->vid_proc_cfg = 0;
m_registers->pll_ctrl0 = regs.pll_ctrl0;
write_vga(VGAPort::MiscOutputWrite, regs.misc_out_reg);
for (u8 i = 0; i < regs.sr_data.size(); i++) {
write_vga_indexed(VGAPort::SequencerIndex, VGAPort::SequencerData, i, regs.sr_data[i]);
}
// first unprotect CR0 - CR7
write_vga_indexed(VGAPort::CrtcIndex, VGAPort::CrtcData, 0x11, read_vga_indexed(VGAPort::CrtcIndex, VGAPort::CrtcData, 0x11) & ~CRTCRegsWriteProt);
for (u8 i = 0; i < regs.cr_data.size(); i++) {
write_vga_indexed(VGAPort::CrtcIndex, VGAPort::CrtcData, i, regs.cr_data[i]);
}
for (u8 i = 0; i < regs.gr_data.size(); i++) {
write_vga_indexed(VGAPort::GraphicsControllerIndex, VGAPort::GraphicsControllerData, i, regs.gr_data[i]);
}
// The AttributeController IO port flips between the index and the data register on every write.
// Reading InputStatus1 has the side effect of resetting this, this way we know it is in the state we expect
read_vga(VGAPort::InputStatus1);
for (u8 i = 0; i < regs.ar_data.size(); i++) {
write_vga_indexed(VGAPort::AttributeController, VGAPort::AttributeController, i, regs.ar_data[i]);
}
TRY(wait_for_fifo_space(6));
m_registers->vga_init0 = regs.vga_init0;
m_registers->dac_mode = regs.dac_mode;
m_registers->vid_desktop_overlay_stride = regs.vid_desktop_overlay_stride;
m_registers->vid_screen_size = regs.vid_screen_size;
m_registers->vid_desktop_start_addr = 0;
m_registers->vid_proc_cfg = regs.vid_proc_cfg;
return {};
}
}