mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 11:12:45 +00:00 
			
		
		
		
	Kernel: Add ioctl to get the EDID from a framebuffer
This commit is contained in:
		
							parent
							
								
									8184870f93
								
							
						
					
					
						commit
						03c45b1865
					
				
					 20 changed files with 265 additions and 84 deletions
				
			
		|  | @ -41,6 +41,11 @@ ALWAYS_INLINE int fb_set_resolution(int fd, FBHeadResolution* info) | ||||||
|     return ioctl(fd, FB_IOCTL_SET_HEAD_RESOLUTION, info); |     return ioctl(fd, FB_IOCTL_SET_HEAD_RESOLUTION, info); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ALWAYS_INLINE int fb_get_head_edid(int fd, FBHeadEDID* info) | ||||||
|  | { | ||||||
|  |     return ioctl(fd, FB_IOCTL_GET_HEAD_EDID, info); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| ALWAYS_INLINE int fb_get_head_vertical_offset_buffer(int fd, FBHeadVerticalOffset* vertical_offset) | ALWAYS_INLINE int fb_get_head_vertical_offset_buffer(int fd, FBHeadVerticalOffset* vertical_offset) | ||||||
| { | { | ||||||
|     return ioctl(fd, FB_IOCTL_GET_HEAD_VERTICAL_OFFSET_BUFFER, vertical_offset); |     return ioctl(fd, FB_IOCTL_GET_HEAD_VERTICAL_OFFSET_BUFFER, vertical_offset); | ||||||
|  |  | ||||||
|  | @ -343,6 +343,12 @@ set(AK_SOURCES | ||||||
|     ../AK/Utf16View.cpp |     ../AK/Utf16View.cpp | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | set(EDID_SOURCES | ||||||
|  |     ../Userland/Libraries/LibEDID/DMT.cpp | ||||||
|  |     ../Userland/Libraries/LibEDID/EDID.cpp | ||||||
|  |     ../Userland/Libraries/LibEDID/VIC.cpp | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| set(ELF_SOURCES | set(ELF_SOURCES | ||||||
|     ../Userland/Libraries/LibELF/Image.cpp |     ../Userland/Libraries/LibELF/Image.cpp | ||||||
|     ../Userland/Libraries/LibELF/Validation.cpp |     ../Userland/Libraries/LibELF/Validation.cpp | ||||||
|  | @ -368,6 +374,7 @@ if (NOT "${SERENITY_ARCH}" STREQUAL "aarch64") | ||||||
|     set(SOURCES |     set(SOURCES | ||||||
|         ${KERNEL_SOURCES} |         ${KERNEL_SOURCES} | ||||||
|         ${SOURCES} |         ${SOURCES} | ||||||
|  |         ${EDID_SOURCES} | ||||||
|         ${ELF_SOURCES} |         ${ELF_SOURCES} | ||||||
|         ${VT_SOURCES} |         ${VT_SOURCES} | ||||||
|         ${CRYPTO_SOURCES} |         ${CRYPTO_SOURCES} | ||||||
|  |  | ||||||
|  | @ -276,4 +276,15 @@ void BochsGraphicsAdapter::disable_consoles() | ||||||
|     m_framebuffer_device->activate_writes(); |     m_framebuffer_device->activate_writes(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ErrorOr<ByteBuffer> BochsGraphicsAdapter::get_edid(size_t output_port_index) const | ||||||
|  | { | ||||||
|  |     if (output_port_index != 0) | ||||||
|  |         return Error::from_errno(ENODEV); | ||||||
|  | 
 | ||||||
|  |     auto bytes = ByteBuffer::copy(const_cast<u8 const*>(m_registers->edid_data), sizeof(m_registers->edid_data)); | ||||||
|  |     if (!bytes.has_value()) | ||||||
|  |         return Error::from_errno(ENOMEM); | ||||||
|  |     return bytes.release_value(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -36,6 +36,8 @@ public: | ||||||
| 
 | 
 | ||||||
|     virtual bool vga_compatible() const override; |     virtual bool vga_compatible() const override; | ||||||
| 
 | 
 | ||||||
|  |     ErrorOr<ByteBuffer> get_edid(size_t output_port_index) const override; | ||||||
|  | 
 | ||||||
| private: | private: | ||||||
|     // ^GenericGraphicsAdapter
 |     // ^GenericGraphicsAdapter
 | ||||||
|     virtual bool try_to_set_resolution(size_t output_port_index, size_t width, size_t height) override; |     virtual bool try_to_set_resolution(size_t output_port_index, size_t width, size_t height) override; | ||||||
|  |  | ||||||
|  | @ -32,54 +32,4 @@ struct Modesetting { | ||||||
|     Timings vertical; |     Timings vertical; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| struct [[gnu::packed]] StandardTimings { |  | ||||||
|     u8 resolution; |  | ||||||
|     u8 frequency; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| struct [[gnu::packed]] DetailTimings { |  | ||||||
|     u16 pixel_clock; |  | ||||||
|     u8 horizontal_active; |  | ||||||
|     u8 horizontal_blank; |  | ||||||
|     u8 horizontal_active_blank_msb; |  | ||||||
|     u8 vertical_active; |  | ||||||
|     u8 vertical_blank; |  | ||||||
|     u8 vertical_active_blank_msb; |  | ||||||
|     u8 horizontal_sync_offset; |  | ||||||
|     u8 horizontal_sync_pulse; |  | ||||||
|     u8 vertical_sync; |  | ||||||
|     u8 sync_msb; |  | ||||||
|     u8 dimension_width; |  | ||||||
|     u8 dimension_height; |  | ||||||
|     u8 dimension_msb; |  | ||||||
|     u8 horizontal_border; |  | ||||||
|     u8 vertical_border; |  | ||||||
|     u8 features; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| struct [[gnu::packed]] VideoInfoBlock { |  | ||||||
|     u64 padding; |  | ||||||
|     u16 manufacture_id; |  | ||||||
|     u16 product_id; |  | ||||||
|     u32 serial_number; |  | ||||||
|     u8 manufacture_week; |  | ||||||
|     u8 manufacture_year; |  | ||||||
|     u8 edid_version; |  | ||||||
|     u8 edid_revision; |  | ||||||
|     u8 video_input_type; |  | ||||||
|     u8 max_horizontal_size; |  | ||||||
|     u8 max_vertical_size; |  | ||||||
|     u8 gama_factor; |  | ||||||
|     u8 dpms_flags; |  | ||||||
|     u8 chroma_info[10]; |  | ||||||
|     u8 established_timing[2]; |  | ||||||
|     u8 manufacture_reserved_timings; |  | ||||||
|     StandardTimings timings[8]; |  | ||||||
|     DetailTimings details[4]; |  | ||||||
|     u8 unused; |  | ||||||
|     u8 checksum; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| static_assert(AssertSize<VideoInfoBlock, 128>()); |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -243,4 +243,12 @@ ErrorOr<void> FramebufferDevice::flush_rectangle(size_t, FBRect const&) | ||||||
|     VERIFY_NOT_REACHED(); |     VERIFY_NOT_REACHED(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ErrorOr<ByteBuffer> FramebufferDevice::get_edid(size_t head) const | ||||||
|  | { | ||||||
|  |     auto adapter = m_graphics_adapter.strong_ref(); | ||||||
|  |     if (!adapter) | ||||||
|  |         return Error::from_errno(EIO); | ||||||
|  |     return adapter->get_edid(head); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -40,6 +40,8 @@ public: | ||||||
|     virtual ErrorOr<size_t> vertical_offset(size_t head) const override; |     virtual ErrorOr<size_t> vertical_offset(size_t head) const override; | ||||||
|     virtual ErrorOr<bool> vertical_offsetted(size_t head) const override; |     virtual ErrorOr<bool> vertical_offsetted(size_t head) const override; | ||||||
| 
 | 
 | ||||||
|  |     virtual ErrorOr<ByteBuffer> get_edid(size_t head) const override; | ||||||
|  | 
 | ||||||
| private: | private: | ||||||
|     virtual ErrorOr<void> set_head_resolution(size_t head, size_t width, size_t height, size_t pitch) override; |     virtual ErrorOr<void> set_head_resolution(size_t head, size_t width, size_t height, size_t pitch) override; | ||||||
|     virtual ErrorOr<void> set_head_buffer(size_t head, bool second_buffer) override; |     virtual ErrorOr<void> set_head_buffer(size_t head, bool second_buffer) override; | ||||||
|  |  | ||||||
|  | @ -33,7 +33,11 @@ ErrorOr<void> GenericFramebufferDevice::verify_head_index(int head_index) const | ||||||
| 
 | 
 | ||||||
| ErrorOr<void> GenericFramebufferDevice::ioctl(OpenFileDescription&, unsigned request, Userspace<void*> arg) | ErrorOr<void> GenericFramebufferDevice::ioctl(OpenFileDescription&, unsigned request, Userspace<void*> arg) | ||||||
| { | { | ||||||
|  |     if (request != FB_IOCTL_GET_HEAD_EDID) { | ||||||
|  |         // Allow anyone to query the EDID. Eventually we'll publish the current EDID on /sys
 | ||||||
|  |         // so it doesn't really make sense to require the video pledge to query it.
 | ||||||
|         TRY(Process::current().require_promise(Pledge::video)); |         TRY(Process::current().require_promise(Pledge::video)); | ||||||
|  |     } | ||||||
|     switch (request) { |     switch (request) { | ||||||
|     case FB_IOCTL_GET_PROPERTIES: { |     case FB_IOCTL_GET_PROPERTIES: { | ||||||
|         auto user_properties = static_ptr_cast<FBProperties*>(arg); |         auto user_properties = static_ptr_cast<FBProperties*>(arg); | ||||||
|  | @ -60,6 +64,26 @@ ErrorOr<void> GenericFramebufferDevice::ioctl(OpenFileDescription&, unsigned req | ||||||
|         head_properties.offset = TRY(vertical_offset(head_properties.head_index)); |         head_properties.offset = TRY(vertical_offset(head_properties.head_index)); | ||||||
|         return copy_to_user(user_head_properties, &head_properties); |         return copy_to_user(user_head_properties, &head_properties); | ||||||
|     } |     } | ||||||
|  |     case FB_IOCTL_GET_HEAD_EDID: { | ||||||
|  |         auto user_head_edid = static_ptr_cast<FBHeadEDID*>(arg); | ||||||
|  |         FBHeadEDID head_edid {}; | ||||||
|  |         TRY(copy_from_user(&head_edid, user_head_edid)); | ||||||
|  |         TRY(verify_head_index(head_edid.head_index)); | ||||||
|  | 
 | ||||||
|  |         auto edid_bytes = TRY(get_edid(head_edid.head_index)); | ||||||
|  |         if (head_edid.bytes != nullptr) { | ||||||
|  |             // Only return the EDID if a buffer was provided. Either way,
 | ||||||
|  |             // we'll write back the bytes_size with the actual size
 | ||||||
|  |             if (head_edid.bytes_size < edid_bytes.size()) { | ||||||
|  |                 head_edid.bytes_size = edid_bytes.size(); | ||||||
|  |                 TRY(copy_to_user(user_head_edid, &head_edid)); | ||||||
|  |                 return Error::from_errno(EOVERFLOW); | ||||||
|  |             } | ||||||
|  |             TRY(copy_to_user(head_edid.bytes, (void const*)edid_bytes.data(), edid_bytes.size())); | ||||||
|  |         } | ||||||
|  |         head_edid.bytes_size = edid_bytes.size(); | ||||||
|  |         return copy_to_user(user_head_edid, &head_edid); | ||||||
|  |     } | ||||||
|     case FB_IOCTL_SET_HEAD_RESOLUTION: { |     case FB_IOCTL_SET_HEAD_RESOLUTION: { | ||||||
|         auto user_head_resolution = static_ptr_cast<FBHeadResolution const*>(arg); |         auto user_head_resolution = static_ptr_cast<FBHeadResolution const*>(arg); | ||||||
|         auto head_resolution = TRY(copy_typed_from_user(user_head_resolution)); |         auto head_resolution = TRY(copy_typed_from_user(user_head_resolution)); | ||||||
|  |  | ||||||
|  | @ -57,6 +57,8 @@ protected: | ||||||
|     // FIXME: This method is too much specific to the VirtIO implementation (especially the buffer_index parameter)
 |     // FIXME: This method is too much specific to the VirtIO implementation (especially the buffer_index parameter)
 | ||||||
|     virtual ErrorOr<void> flush_rectangle(size_t buffer_index, FBRect const&) = 0; |     virtual ErrorOr<void> flush_rectangle(size_t buffer_index, FBRect const&) = 0; | ||||||
| 
 | 
 | ||||||
|  |     virtual ErrorOr<ByteBuffer> get_edid(size_t head) const = 0; | ||||||
|  | 
 | ||||||
|     ErrorOr<void> verify_head_index(int head_index) const; |     ErrorOr<void> verify_head_index(int head_index) const; | ||||||
| 
 | 
 | ||||||
|     GenericFramebufferDevice(const GenericGraphicsAdapter&); |     GenericFramebufferDevice(const GenericGraphicsAdapter&); | ||||||
|  |  | ||||||
|  | @ -29,6 +29,8 @@ public: | ||||||
| 
 | 
 | ||||||
|     virtual bool vga_compatible() const = 0; |     virtual bool vga_compatible() const = 0; | ||||||
| 
 | 
 | ||||||
|  |     virtual ErrorOr<ByteBuffer> get_edid(size_t output_port_index) const = 0; | ||||||
|  | 
 | ||||||
|     virtual bool try_to_set_resolution(size_t output_port_index, size_t width, size_t height) = 0; |     virtual bool try_to_set_resolution(size_t output_port_index, size_t width, size_t height) = 0; | ||||||
|     virtual bool set_y_offset(size_t output_port_index, size_t y) = 0; |     virtual bool set_y_offset(size_t output_port_index, size_t y) = 0; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -62,31 +62,29 @@ static size_t compute_dac_multiplier(size_t pixel_clock_in_khz) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static Graphics::Modesetting calculate_modesetting_from_edid(const Graphics::VideoInfoBlock& edid, size_t index) | static Graphics::Modesetting calculate_modesetting_from_edid(EDID::Parser& edid, size_t index) | ||||||
| { | { | ||||||
|     Graphics::Modesetting mode; |     auto details = edid.detailed_timing(index).release_value(); | ||||||
|     VERIFY(edid.details[0].pixel_clock); |  | ||||||
|     mode.pixel_clock_in_khz = edid.details[0].pixel_clock * 10; |  | ||||||
| 
 | 
 | ||||||
|     size_t horizontal_active = edid.details[index].horizontal_active | ((edid.details[index].horizontal_active_blank_msb >> 4) << 8); |     Graphics::Modesetting mode; | ||||||
|     size_t horizontal_blank = edid.details[index].horizontal_blank | ((edid.details[index].horizontal_active_blank_msb & 0xF) << 8); |     VERIFY(details.pixel_clock_khz()); | ||||||
|     size_t horizontal_sync_offset = edid.details[index].horizontal_sync_offset | ((edid.details[index].sync_msb >> 6) << 8); |     mode.pixel_clock_in_khz = details.pixel_clock_khz(); | ||||||
|     size_t horizontal_sync_pulse = edid.details[index].horizontal_sync_pulse | (((edid.details[index].sync_msb >> 4) & 0x3) << 8); | 
 | ||||||
|  |     size_t horizontal_active = details.horizontal_addressable_pixels(); | ||||||
|  |     size_t horizontal_sync_offset = details.horizontal_front_porch_pixels(); | ||||||
| 
 | 
 | ||||||
|     mode.horizontal.active = horizontal_active; |     mode.horizontal.active = horizontal_active; | ||||||
|     mode.horizontal.sync_start = horizontal_active + horizontal_sync_offset; |     mode.horizontal.sync_start = horizontal_active + horizontal_sync_offset; | ||||||
|     mode.horizontal.sync_end = horizontal_active + horizontal_sync_offset + horizontal_sync_pulse; |     mode.horizontal.sync_end = horizontal_active + horizontal_sync_offset + details.horizontal_sync_pulse_width_pixels(); | ||||||
|     mode.horizontal.total = horizontal_active + horizontal_blank; |     mode.horizontal.total = horizontal_active + details.horizontal_blanking_pixels(); | ||||||
| 
 | 
 | ||||||
|     size_t vertical_active = edid.details[index].vertical_active | ((edid.details[index].vertical_active_blank_msb >> 4) << 8); |     size_t vertical_active = details.vertical_addressable_lines(); | ||||||
|     size_t vertical_blank = edid.details[index].vertical_blank | ((edid.details[index].vertical_active_blank_msb & 0xF) << 8); |     size_t vertical_sync_offset = details.vertical_front_porch_lines(); | ||||||
|     size_t vertical_sync_offset = (edid.details[index].vertical_sync >> 4) | (((edid.details[index].sync_msb >> 2) & 0x3) << 4); |  | ||||||
|     size_t vertical_sync_pulse = (edid.details[index].vertical_sync & 0xF) | ((edid.details[index].sync_msb & 0x3) << 4); |  | ||||||
| 
 | 
 | ||||||
|     mode.vertical.active = vertical_active; |     mode.vertical.active = vertical_active; | ||||||
|     mode.vertical.sync_start = vertical_active + vertical_sync_offset; |     mode.vertical.sync_start = vertical_active + vertical_sync_offset; | ||||||
|     mode.vertical.sync_end = vertical_active + vertical_sync_offset + vertical_sync_pulse; |     mode.vertical.sync_end = vertical_active + vertical_sync_offset + details.vertical_sync_pulse_width_lines(); | ||||||
|     mode.vertical.total = vertical_active + vertical_blank; |     mode.vertical.total = vertical_active + details.vertical_blanking_lines(); | ||||||
|     return mode; |     return mode; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -202,7 +200,7 @@ IntelNativeGraphicsAdapter::IntelNativeGraphicsAdapter(PCI::Address address) | ||||||
|     } |     } | ||||||
|     gmbus_read_edid(); |     gmbus_read_edid(); | ||||||
| 
 | 
 | ||||||
|     auto modesetting = calculate_modesetting_from_edid(m_crt_edid, 0); |     auto modesetting = calculate_modesetting_from_edid(m_crt_edid.value(), 0); | ||||||
|     dmesgln("Intel Native Graphics Adapter @ {}, preferred resolution is {:d}x{:d}", pci_address(), modesetting.horizontal.active, modesetting.vertical.active); |     dmesgln("Intel Native Graphics Adapter @ {}, preferred resolution is {:d}x{:d}", pci_address(), modesetting.horizontal.active, modesetting.vertical.active); | ||||||
|     set_crt_resolution(modesetting.horizontal.active, modesetting.vertical.active); |     set_crt_resolution(modesetting.horizontal.active, modesetting.vertical.active); | ||||||
|     auto framebuffer_address = PhysicalAddress(PCI::get_BAR2(pci_address()) & 0xfffffff0); |     auto framebuffer_address = PhysicalAddress(PCI::get_BAR2(pci_address()) & 0xfffffff0); | ||||||
|  | @ -377,9 +375,17 @@ void IntelNativeGraphicsAdapter::gmbus_read(unsigned address, u8* buf, size_t le | ||||||
| 
 | 
 | ||||||
| void IntelNativeGraphicsAdapter::gmbus_read_edid() | void IntelNativeGraphicsAdapter::gmbus_read_edid() | ||||||
| { | { | ||||||
|  |     { | ||||||
|         SpinlockLocker control_lock(m_control_lock); |         SpinlockLocker control_lock(m_control_lock); | ||||||
|         gmbus_write(DDC2_I2C_ADDRESS, 0); |         gmbus_write(DDC2_I2C_ADDRESS, 0); | ||||||
|     gmbus_read(DDC2_I2C_ADDRESS, (u8*)&m_crt_edid, sizeof(Graphics::VideoInfoBlock)); |         gmbus_read(DDC2_I2C_ADDRESS, (u8*)&m_crt_edid_bytes, sizeof(m_crt_edid_bytes)); | ||||||
|  |     } | ||||||
|  |     if (auto parsed_edid = EDID::Parser::from_bytes({ m_crt_edid_bytes, sizeof(m_crt_edid_bytes) }); !parsed_edid.is_error()) { | ||||||
|  |         m_crt_edid = parsed_edid.release_value(); | ||||||
|  |     } else { | ||||||
|  |         dbgln("IntelNativeGraphicsAdapter: Parsing EDID failed: {}", parsed_edid.error()); | ||||||
|  |         m_crt_edid = {}; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool IntelNativeGraphicsAdapter::is_resolution_valid(size_t, size_t) | bool IntelNativeGraphicsAdapter::is_resolution_valid(size_t, size_t) | ||||||
|  | @ -420,7 +426,7 @@ bool IntelNativeGraphicsAdapter::set_crt_resolution(size_t width, size_t height) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // FIXME: Get the requested resolution from the EDID!!
 |     // FIXME: Get the requested resolution from the EDID!!
 | ||||||
|     auto modesetting = calculate_modesetting_from_edid(m_crt_edid, 0); |     auto modesetting = calculate_modesetting_from_edid(m_crt_edid.value(), 0); | ||||||
| 
 | 
 | ||||||
|     disable_output(); |     disable_output(); | ||||||
| 
 | 
 | ||||||
|  | @ -647,4 +653,22 @@ void IntelNativeGraphicsAdapter::initialize_framebuffer_devices() | ||||||
|     auto framebuffer_result = m_framebuffer_device->try_to_initialize(); |     auto framebuffer_result = m_framebuffer_device->try_to_initialize(); | ||||||
|     VERIFY(!framebuffer_result.is_error()); |     VERIFY(!framebuffer_result.is_error()); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | ErrorOr<ByteBuffer> IntelNativeGraphicsAdapter::get_edid(size_t output_port_index) const | ||||||
|  | { | ||||||
|  |     if (output_port_index != 0) { | ||||||
|  |         dbgln("IntelNativeGraphicsAdapter: get_edid: Only one output supported"); | ||||||
|  |         return Error::from_errno(ENODEV); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (m_crt_edid.has_value()) { | ||||||
|  |         auto bytes = ByteBuffer::copy(m_crt_edid_bytes, sizeof(m_crt_edid_bytes)); | ||||||
|  |         if (!bytes.has_value()) | ||||||
|  |             return Error::from_errno(ENOMEM); | ||||||
|  |         return bytes.release_value(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return ByteBuffer {}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ | ||||||
| #include <Kernel/Graphics/FramebufferDevice.h> | #include <Kernel/Graphics/FramebufferDevice.h> | ||||||
| #include <Kernel/Graphics/VGACompatibleAdapter.h> | #include <Kernel/Graphics/VGACompatibleAdapter.h> | ||||||
| #include <Kernel/PhysicalAddress.h> | #include <Kernel/PhysicalAddress.h> | ||||||
|  | #include <LibEDID/EDID.h> | ||||||
| 
 | 
 | ||||||
| namespace Kernel { | namespace Kernel { | ||||||
| 
 | 
 | ||||||
|  | @ -113,6 +114,7 @@ private: | ||||||
| 
 | 
 | ||||||
|     // ^GenericGraphicsAdapter
 |     // ^GenericGraphicsAdapter
 | ||||||
|     virtual void initialize_framebuffer_devices() override; |     virtual void initialize_framebuffer_devices() override; | ||||||
|  |     virtual ErrorOr<ByteBuffer> get_edid(size_t output_port_index) const override; | ||||||
| 
 | 
 | ||||||
|     bool pipe_a_enabled() const; |     bool pipe_a_enabled() const; | ||||||
|     bool pipe_b_enabled() const; |     bool pipe_b_enabled() const; | ||||||
|  | @ -162,7 +164,8 @@ private: | ||||||
|     Spinlock m_modeset_lock; |     Spinlock m_modeset_lock; | ||||||
|     mutable Spinlock m_registers_lock; |     mutable Spinlock m_registers_lock; | ||||||
| 
 | 
 | ||||||
|     Graphics::VideoInfoBlock m_crt_edid; |     EDID::Parser::RawBytes m_crt_edid_bytes {}; | ||||||
|  |     Optional<EDID::Parser> m_crt_edid; | ||||||
|     const PhysicalAddress m_registers; |     const PhysicalAddress m_registers; | ||||||
|     const PhysicalAddress m_framebuffer_addr; |     const PhysicalAddress m_framebuffer_addr; | ||||||
|     OwnPtr<Memory::Region> m_registers_region; |     OwnPtr<Memory::Region> m_registers_region; | ||||||
|  |  | ||||||
|  | @ -80,4 +80,9 @@ bool VGACompatibleAdapter::set_y_offset(size_t, size_t) | ||||||
|     return false; |     return false; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ErrorOr<ByteBuffer> VGACompatibleAdapter::get_edid(size_t) const | ||||||
|  | { | ||||||
|  |     return Error::from_errno(ENOTSUP); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -31,6 +31,8 @@ public: | ||||||
|     virtual bool try_to_set_resolution(size_t output_port_index, size_t width, size_t height) override; |     virtual bool try_to_set_resolution(size_t output_port_index, size_t width, size_t height) override; | ||||||
|     virtual bool set_y_offset(size_t output_port_index, size_t y) override; |     virtual bool set_y_offset(size_t output_port_index, size_t y) override; | ||||||
| 
 | 
 | ||||||
|  |     ErrorOr<ByteBuffer> get_edid(size_t output_port_index) const override; | ||||||
|  | 
 | ||||||
| protected: | protected: | ||||||
|     explicit VGACompatibleAdapter(PCI::Address); |     explicit VGACompatibleAdapter(PCI::Address); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -140,6 +140,15 @@ ErrorOr<void> FramebufferDevice::flush_rectangle(size_t buffer_index, FBRect con | ||||||
|     return {}; |     return {}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ErrorOr<ByteBuffer> FramebufferDevice::get_edid(size_t head) const | ||||||
|  | { | ||||||
|  |     // Note: This FramebufferDevice class doesn't support multihead setup.
 | ||||||
|  |     // We take care to verify this at the GenericFramebufferDevice::ioctl method
 | ||||||
|  |     // so if we happen to accidentally have a value different than 0, assert.
 | ||||||
|  |     VERIFY(head == 0); | ||||||
|  |     return adapter()->get_edid(m_scanout.value()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| FramebufferDevice::FramebufferDevice(GraphicsAdapter const& adapter, ScanoutID scanout) | FramebufferDevice::FramebufferDevice(GraphicsAdapter const& adapter, ScanoutID scanout) | ||||||
|     : GenericFramebufferDevice(adapter) |     : GenericFramebufferDevice(adapter) | ||||||
|     , m_scanout(scanout) |     , m_scanout(scanout) | ||||||
|  |  | ||||||
|  | @ -62,6 +62,8 @@ private: | ||||||
|     virtual ErrorOr<void> flush_head_buffer(size_t head) override; |     virtual ErrorOr<void> flush_head_buffer(size_t head) override; | ||||||
|     virtual ErrorOr<void> flush_rectangle(size_t head, FBRect const&) override; |     virtual ErrorOr<void> flush_rectangle(size_t head, FBRect const&) override; | ||||||
| 
 | 
 | ||||||
|  |     virtual ErrorOr<ByteBuffer> get_edid(size_t head) const override; | ||||||
|  | 
 | ||||||
|     void flush_dirty_window(Protocol::Rect const&, Buffer&); |     void flush_dirty_window(Protocol::Rect const&, Buffer&); | ||||||
|     void transfer_framebuffer_data_to_host(Protocol::Rect const&, Buffer&); |     void transfer_framebuffer_data_to_host(Protocol::Rect const&, Buffer&); | ||||||
|     void flush_displayed_image(Protocol::Rect const&, Buffer&); |     void flush_displayed_image(Protocol::Rect const&, Buffer&); | ||||||
|  |  | ||||||
|  | @ -78,7 +78,7 @@ void GraphicsAdapter::initialize() | ||||||
|             if (is_feature_set(supported_features, VIRTIO_GPU_F_VIRGL)) |             if (is_feature_set(supported_features, VIRTIO_GPU_F_VIRGL)) | ||||||
|                 dbgln_if(VIRTIO_DEBUG, "VirtIO::GraphicsAdapter: VIRGL is not yet supported!"); |                 dbgln_if(VIRTIO_DEBUG, "VirtIO::GraphicsAdapter: VIRGL is not yet supported!"); | ||||||
|             if (is_feature_set(supported_features, VIRTIO_GPU_F_EDID)) |             if (is_feature_set(supported_features, VIRTIO_GPU_F_EDID)) | ||||||
|                 dbgln_if(VIRTIO_DEBUG, "VirtIO::GraphicsAdapter: EDID is not yet supported!"); |                 negotiated |= VIRTIO_GPU_F_EDID; | ||||||
|             return negotiated; |             return negotiated; | ||||||
|         }); |         }); | ||||||
|         if (success) { |         if (success) { | ||||||
|  | @ -93,6 +93,7 @@ void GraphicsAdapter::initialize() | ||||||
|         MutexLocker locker(m_operation_lock); |         MutexLocker locker(m_operation_lock); | ||||||
|         // Get display information using VIRTIO_GPU_CMD_GET_DISPLAY_INFO
 |         // Get display information using VIRTIO_GPU_CMD_GET_DISPLAY_INFO
 | ||||||
|         query_display_information(); |         query_display_information(); | ||||||
|  |         query_display_edid({}); | ||||||
|     } else { |     } else { | ||||||
|         VERIFY_NOT_REACHED(); |         VERIFY_NOT_REACHED(); | ||||||
|     } |     } | ||||||
|  | @ -160,10 +161,96 @@ void GraphicsAdapter::query_display_information() | ||||||
|         dbgln_if(VIRTIO_DEBUG, "VirtIO::GraphicsAdapter: Scanout {}: enabled: {} x: {}, y: {}, width: {}, height: {}", i, !!scanout.enabled, scanout.rect.x, scanout.rect.y, scanout.rect.width, scanout.rect.height); |         dbgln_if(VIRTIO_DEBUG, "VirtIO::GraphicsAdapter: Scanout {}: enabled: {} x: {}, y: {}, width: {}, height: {}", i, !!scanout.enabled, scanout.rect.x, scanout.rect.y, scanout.rect.width, scanout.rect.height); | ||||||
|         if (scanout.enabled && !m_default_scanout.has_value()) |         if (scanout.enabled && !m_default_scanout.has_value()) | ||||||
|             m_default_scanout = i; |             m_default_scanout = i; | ||||||
|  | 
 | ||||||
|  |         m_scanouts[i].edid = {}; | ||||||
|     } |     } | ||||||
|     VERIFY(m_default_scanout.has_value()); |     VERIFY(m_default_scanout.has_value()); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void GraphicsAdapter::query_display_edid(Optional<ScanoutID> scanout_id) | ||||||
|  | { | ||||||
|  |     VERIFY(m_operation_lock.is_locked()); | ||||||
|  | 
 | ||||||
|  |     if (!is_feature_accepted(VIRTIO_GPU_F_EDID)) | ||||||
|  |         return; | ||||||
|  | 
 | ||||||
|  |     for (size_t i = 0; i < VIRTIO_GPU_MAX_SCANOUTS; ++i) { | ||||||
|  |         if (scanout_id.has_value() && scanout_id.value() != i) | ||||||
|  |             continue; | ||||||
|  | 
 | ||||||
|  |         // scanout.display_info.enabled doesn't seem to reflect the actual state,
 | ||||||
|  |         // even if we were to call query_display_information prior to calling
 | ||||||
|  |         // this function. So, just ignore, we seem to get EDID information regardless.
 | ||||||
|  | 
 | ||||||
|  |         auto query_edid_result = query_edid(i); | ||||||
|  |         if (query_edid_result.is_error()) { | ||||||
|  |             dbgln("VirtIO::GraphicsAdapater: Scanout {}: Failed to parse EDID: {}", i, query_edid_result.error()); | ||||||
|  |             m_scanouts[i].edid = {}; | ||||||
|  |         } else { | ||||||
|  |             m_scanouts[i].edid = query_edid_result.release_value(); | ||||||
|  |             if (m_scanouts[i].edid.has_value()) { | ||||||
|  |                 auto& parsed_edid = m_scanouts[i].edid.value(); | ||||||
|  |                 dbgln("VirtIO::GraphicsAdapater: Scanout {}: EDID {}: Manufacturer: {} Product: {} Serial #{}", i, | ||||||
|  |                     parsed_edid.version(), parsed_edid.legacy_manufacturer_id(), parsed_edid.product_code(), parsed_edid.serial_number()); | ||||||
|  |                 if (auto screen_size = parsed_edid.screen_size(); screen_size.has_value()) { | ||||||
|  |                     auto& size = screen_size.value(); | ||||||
|  |                     dbgln("VirtIO::GraphicsAdapater: Scanout {}:           Screen size: {}cm x {}cm", i, | ||||||
|  |                         size.horizontal_cm(), size.vertical_cm()); | ||||||
|  |                 } else if (auto aspect_ratio = parsed_edid.aspect_ratio(); aspect_ratio.has_value()) { | ||||||
|  |                     auto& ratio = aspect_ratio.value(); | ||||||
|  |                     dbgln("VirtIO::GraphicsAdapater: Scanout {}:           Aspect ratio: {} : 1", i, ratio.ratio()); | ||||||
|  |                 } else { | ||||||
|  |                     dbgln("VirtIO::GraphicsAdapater: Scanout {}:           Unknown screen size or aspect ratio", i); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 dbgln("VirtIO::GraphicsAdapater: Scanout {}: No EDID", i); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ErrorOr<ByteBuffer> GraphicsAdapter::get_edid(size_t output_port_index) const | ||||||
|  | { | ||||||
|  |     if (output_port_index >= VIRTIO_GPU_MAX_SCANOUTS) | ||||||
|  |         return Error::from_errno(ENODEV); | ||||||
|  |     auto& edid = m_scanouts[output_port_index].edid; | ||||||
|  |     if (edid.has_value()) { | ||||||
|  |         auto bytes = ByteBuffer::copy(edid.value().bytes()); | ||||||
|  |         if (!bytes.has_value()) | ||||||
|  |             return Error::from_errno(ENOMEM); | ||||||
|  |         return bytes.release_value(); | ||||||
|  |     } | ||||||
|  |     return ByteBuffer {}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | auto GraphicsAdapter::query_edid(u32 scanout_id) -> ErrorOr<Optional<EDID::Parser>> | ||||||
|  | { | ||||||
|  |     VERIFY(m_operation_lock.is_locked()); | ||||||
|  |     auto writer = create_scratchspace_writer(); | ||||||
|  |     auto& request = writer.append_structure<Protocol::GetEDID>(); | ||||||
|  |     auto& response = writer.append_structure<Protocol::GetEDIDResponse>(); | ||||||
|  | 
 | ||||||
|  |     populate_virtio_gpu_request_header(request.header, Protocol::CommandType::VIRTIO_GPU_CMD_GET_EDID, VIRTIO_GPU_FLAG_FENCE); | ||||||
|  | 
 | ||||||
|  |     request.scanout_id = scanout_id; | ||||||
|  |     request.padding = 0; | ||||||
|  | 
 | ||||||
|  |     synchronous_virtio_gpu_command(start_of_scratch_space(), sizeof(request), sizeof(response)); | ||||||
|  | 
 | ||||||
|  |     if (response.header.type != static_cast<u32>(Protocol::CommandType::VIRTIO_GPU_RESP_OK_EDID)) | ||||||
|  |         return Error::from_string_literal("VirtIO::GraphicsAdapter: Failed to get EDID"); | ||||||
|  | 
 | ||||||
|  |     if (response.size == 0) | ||||||
|  |         return Error::from_string_literal("VirtIO::GraphicsAdapter: Failed to get EDID, empty buffer"); | ||||||
|  | 
 | ||||||
|  |     auto edid_buffer = ByteBuffer::copy(response.edid, response.size); | ||||||
|  |     if (!edid_buffer.has_value()) | ||||||
|  |         return Error::from_errno(ENOMEM); | ||||||
|  | 
 | ||||||
|  |     auto edid = TRY(EDID::Parser::from_bytes(edid_buffer.release_value())); | ||||||
|  |     return edid; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| ResourceID GraphicsAdapter::create_2d_resource(Protocol::Rect rect) | ResourceID GraphicsAdapter::create_2d_resource(Protocol::Rect rect) | ||||||
| { | { | ||||||
|     VERIFY(m_operation_lock.is_locked()); |     VERIFY(m_operation_lock.is_locked()); | ||||||
|  | @ -235,6 +322,8 @@ void GraphicsAdapter::detach_backing_storage(ResourceID resource_id) | ||||||
| void GraphicsAdapter::set_scanout_resource(ScanoutID scanout, ResourceID resource_id, Protocol::Rect rect) | void GraphicsAdapter::set_scanout_resource(ScanoutID scanout, ResourceID resource_id, Protocol::Rect rect) | ||||||
| { | { | ||||||
|     VERIFY(m_operation_lock.is_locked()); |     VERIFY(m_operation_lock.is_locked()); | ||||||
|  |     { | ||||||
|  |         // We need to scope the request/response here so that we can query display information later on
 | ||||||
|         auto writer = create_scratchspace_writer(); |         auto writer = create_scratchspace_writer(); | ||||||
|         auto& request = writer.append_structure<Protocol::SetScanOut>(); |         auto& request = writer.append_structure<Protocol::SetScanOut>(); | ||||||
|         auto& response = writer.append_structure<Protocol::ControlHeader>(); |         auto& response = writer.append_structure<Protocol::ControlHeader>(); | ||||||
|  | @ -248,6 +337,10 @@ void GraphicsAdapter::set_scanout_resource(ScanoutID scanout, ResourceID resourc | ||||||
| 
 | 
 | ||||||
|         VERIFY(response.type == static_cast<u32>(Protocol::CommandType::VIRTIO_GPU_RESP_OK_NODATA)); |         VERIFY(response.type == static_cast<u32>(Protocol::CommandType::VIRTIO_GPU_RESP_OK_NODATA)); | ||||||
|         dbgln_if(VIRTIO_DEBUG, "VirtIO::GraphicsAdapter: Set backing scanout"); |         dbgln_if(VIRTIO_DEBUG, "VirtIO::GraphicsAdapter: Set backing scanout"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Now that the Scanout should be enabled, update the EDID
 | ||||||
|  |     query_display_edid(scanout); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GraphicsAdapter::transfer_framebuffer_data_to_host(ScanoutID scanout, ResourceID resource_id, Protocol::Rect const& dirty_rect) | void GraphicsAdapter::transfer_framebuffer_data_to_host(ScanoutID scanout, ResourceID resource_id, Protocol::Rect const& dirty_rect) | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ | ||||||
| #include <Kernel/Graphics/VirtIOGPU/Console.h> | #include <Kernel/Graphics/VirtIOGPU/Console.h> | ||||||
| #include <Kernel/Graphics/VirtIOGPU/FramebufferDevice.h> | #include <Kernel/Graphics/VirtIOGPU/FramebufferDevice.h> | ||||||
| #include <Kernel/Graphics/VirtIOGPU/Protocol.h> | #include <Kernel/Graphics/VirtIOGPU/Protocol.h> | ||||||
|  | #include <LibEDID/EDID.h> | ||||||
| 
 | 
 | ||||||
| namespace Kernel::Graphics::VirtIOGPU { | namespace Kernel::Graphics::VirtIOGPU { | ||||||
| 
 | 
 | ||||||
|  | @ -47,6 +48,8 @@ public: | ||||||
| 
 | 
 | ||||||
|     virtual void initialize() override; |     virtual void initialize() override; | ||||||
| 
 | 
 | ||||||
|  |     ErrorOr<ByteBuffer> get_edid(size_t output_port_index) const override; | ||||||
|  | 
 | ||||||
| private: | private: | ||||||
|     void flush_dirty_rectangle(ScanoutID, ResourceID, Protocol::Rect const& dirty_rect); |     void flush_dirty_rectangle(ScanoutID, ResourceID, Protocol::Rect const& dirty_rect); | ||||||
| 
 | 
 | ||||||
|  | @ -98,6 +101,7 @@ private: | ||||||
|         RefPtr<Graphics::VirtIOGPU::FramebufferDevice> framebuffer; |         RefPtr<Graphics::VirtIOGPU::FramebufferDevice> framebuffer; | ||||||
|         RefPtr<Console> console; |         RefPtr<Console> console; | ||||||
|         Protocol::DisplayInfoResponse::Display display_info {}; |         Protocol::DisplayInfoResponse::Display display_info {}; | ||||||
|  |         Optional<EDID::Parser> edid; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     virtual bool handle_device_config_change() override; |     virtual bool handle_device_config_change() override; | ||||||
|  | @ -117,6 +121,7 @@ private: | ||||||
|     void populate_virtio_gpu_request_header(Protocol::ControlHeader& header, Protocol::CommandType ctrl_type, u32 flags = 0); |     void populate_virtio_gpu_request_header(Protocol::ControlHeader& header, Protocol::CommandType ctrl_type, u32 flags = 0); | ||||||
| 
 | 
 | ||||||
|     void query_display_information(); |     void query_display_information(); | ||||||
|  |     void query_display_edid(Optional<ScanoutID>); | ||||||
|     ResourceID create_2d_resource(Protocol::Rect rect); |     ResourceID create_2d_resource(Protocol::Rect rect); | ||||||
|     void delete_resource(ResourceID resource_id); |     void delete_resource(ResourceID resource_id); | ||||||
|     void ensure_backing_storage(ResourceID resource_id, Memory::Region const& region, size_t buffer_offset, size_t buffer_length); |     void ensure_backing_storage(ResourceID resource_id, Memory::Region const& region, size_t buffer_offset, size_t buffer_length); | ||||||
|  | @ -124,6 +129,7 @@ private: | ||||||
|     void set_scanout_resource(ScanoutID scanout, ResourceID resource_id, Protocol::Rect rect); |     void set_scanout_resource(ScanoutID scanout, ResourceID resource_id, Protocol::Rect rect); | ||||||
|     void transfer_framebuffer_data_to_host(ScanoutID scanout, ResourceID resource_id, Protocol::Rect const& rect); |     void transfer_framebuffer_data_to_host(ScanoutID scanout, ResourceID resource_id, Protocol::Rect const& rect); | ||||||
|     void flush_displayed_image(ResourceID resource_id, Protocol::Rect const& dirty_rect); |     void flush_displayed_image(ResourceID resource_id, Protocol::Rect const& dirty_rect); | ||||||
|  |     ErrorOr<Optional<EDID::Parser>> query_edid(u32 scanout_id); | ||||||
| 
 | 
 | ||||||
|     bool m_created_framebuffer_devices { false }; |     bool m_created_framebuffer_devices { false }; | ||||||
|     Optional<ScanoutID> m_default_scanout; |     Optional<ScanoutID> m_default_scanout; | ||||||
|  |  | ||||||
|  | @ -156,4 +156,19 @@ struct ResourceFlush { | ||||||
|     u32 padding; |     u32 padding; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | // Specification equivalent: struct virtio_gpu_get_edid
 | ||||||
|  | struct GetEDID { | ||||||
|  |     ControlHeader header; | ||||||
|  |     u32 scanout_id; | ||||||
|  |     u32 padding; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // Specification equivalent: struct virtio_gpu_resp_edid
 | ||||||
|  | struct GetEDIDResponse { | ||||||
|  |     ControlHeader header; | ||||||
|  |     u32 size; | ||||||
|  |     u32 padding; | ||||||
|  |     u8 edid[1024]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -43,6 +43,13 @@ struct FBHeadResolution { | ||||||
|     int height; |     int height; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | struct FBHeadEDID { | ||||||
|  |     int head_index; | ||||||
|  | 
 | ||||||
|  |     unsigned char* bytes; | ||||||
|  |     unsigned bytes_size; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| struct FBHeadVerticalOffset { | struct FBHeadVerticalOffset { | ||||||
|     int head_index; |     int head_index; | ||||||
|     int offsetted; |     int offsetted; | ||||||
|  | @ -85,6 +92,7 @@ enum IOCtlNumber { | ||||||
|     FB_IOCTL_GET_PROPERTIES, |     FB_IOCTL_GET_PROPERTIES, | ||||||
|     FB_IOCTL_GET_HEAD_PROPERTIES, |     FB_IOCTL_GET_HEAD_PROPERTIES, | ||||||
|     FB_IOCTL_SET_HEAD_RESOLUTION, |     FB_IOCTL_SET_HEAD_RESOLUTION, | ||||||
|  |     FB_IOCTL_GET_HEAD_EDID, | ||||||
|     FB_IOCTL_SET_HEAD_VERTICAL_OFFSET_BUFFER, |     FB_IOCTL_SET_HEAD_VERTICAL_OFFSET_BUFFER, | ||||||
|     FB_IOCTL_GET_HEAD_VERTICAL_OFFSET_BUFFER, |     FB_IOCTL_GET_HEAD_VERTICAL_OFFSET_BUFFER, | ||||||
|     FB_IOCTL_FLUSH_HEAD_BUFFERS, |     FB_IOCTL_FLUSH_HEAD_BUFFERS, | ||||||
|  | @ -133,6 +141,7 @@ enum IOCtlNumber { | ||||||
| #define TIOCSWINSZ TIOCSWINSZ | #define TIOCSWINSZ TIOCSWINSZ | ||||||
| #define FB_IOCTL_GET_PROPERTIES FB_IOCTL_GET_PROPERTIES | #define FB_IOCTL_GET_PROPERTIES FB_IOCTL_GET_PROPERTIES | ||||||
| #define FB_IOCTL_GET_HEAD_PROPERTIES FB_IOCTL_GET_HEAD_PROPERTIES | #define FB_IOCTL_GET_HEAD_PROPERTIES FB_IOCTL_GET_HEAD_PROPERTIES | ||||||
|  | #define FB_IOCTL_GET_HEAD_EDID FB_IOCTL_GET_HEAD_EDID | ||||||
| #define FB_IOCTL_SET_HEAD_RESOLUTION FB_IOCTL_SET_HEAD_RESOLUTION | #define FB_IOCTL_SET_HEAD_RESOLUTION FB_IOCTL_SET_HEAD_RESOLUTION | ||||||
| #define FB_IOCTL_SET_HEAD_VERTICAL_OFFSET_BUFFER FB_IOCTL_SET_HEAD_VERTICAL_OFFSET_BUFFER | #define FB_IOCTL_SET_HEAD_VERTICAL_OFFSET_BUFFER FB_IOCTL_SET_HEAD_VERTICAL_OFFSET_BUFFER | ||||||
| #define FB_IOCTL_GET_HEAD_VERTICAL_OFFSET_BUFFER FB_IOCTL_GET_HEAD_VERTICAL_OFFSET_BUFFER | #define FB_IOCTL_GET_HEAD_VERTICAL_OFFSET_BUFFER FB_IOCTL_GET_HEAD_VERTICAL_OFFSET_BUFFER | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Tom
						Tom