audk/OvmfPkg/VirtioGpuDxe/Gop.c

775 lines
24 KiB
C

/** @file
EFI_GRAPHICS_OUTPUT_PROTOCOL member functions for the VirtIo GPU driver.
Copyright (C) 2016, Red Hat, Inc.
SPDX-License-Identifier: BSD-2-Clause-Patent
**/
#include <Library/MemoryAllocationLib.h>
#include <Library/PcdLib.h>
#include "VirtioGpu.h"
/**
Release guest-side and host-side resources that are related to an initialized
VGPU_GOP.Gop.
param[in,out] VgpuGop The VGPU_GOP object to release resources for.
On input, the caller is responsible for having called
VgpuGop->Gop.SetMode() at least once successfully.
(This is equivalent to the requirement that
VgpuGop->BackingStore be non-NULL. It is also
equivalent to the requirement that VgpuGop->ResourceId
be nonzero.)
On output, resources will be released, and
VgpuGop->BackingStore and VgpuGop->ResourceId will be
nulled.
param[in] DisableHead Whether this head (scanout) currently references the
resource identified by VgpuGop->ResourceId. Only pass
FALSE when VgpuGop->Gop.SetMode() calls this function
while switching between modes, and set it to TRUE
every other time.
**/
VOID
ReleaseGopResources (
IN OUT VGPU_GOP *VgpuGop,
IN BOOLEAN DisableHead
)
{
EFI_STATUS Status;
ASSERT (VgpuGop->ResourceId != 0);
ASSERT (VgpuGop->BackingStore != NULL);
//
// If any of the following host-side destruction steps fail, we can't get out
// of an inconsistent state, so we'll hang. In general errors in object
// destruction can hardly be recovered from.
//
if (DisableHead) {
//
// Dissociate head (scanout) #0 from the currently used 2D host resource,
// by setting ResourceId=0 for it.
//
Status = VirtioGpuSetScanout (
VgpuGop->ParentBus, // VgpuDev
0,
0,
0,
0, // X, Y, Width, Height
0, // ScanoutId
0 // ResourceId
);
//
// HACK BEGINS HERE
//
// According to the GPU Device section of the VirtIo specification, the
// above operation is valid:
//
// "The driver can use resource_id = 0 to disable a scanout."
//
// However, in practice QEMU does not allow us to disable head (scanout) #0
// -- it rejects the command with response code 0x1202
// (VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID). Looking at the QEMU source
// code, function virtio_gpu_set_scanout() in "hw/display/virtio-gpu.c",
// this appears fully intentional, despite not being documented in the
// spec.
//
// Surprisingly, ignoring the error here, and proceeding to release
// host-side resources that presumably underlie head (scanout) #0, work
// without any problems -- the driver survives repeated "disconnect" /
// "connect -r" commands in the UEFI shell.
//
// So, for now, let's just suppress the error.
//
Status = EFI_SUCCESS;
//
// HACK ENDS HERE
//
ASSERT_EFI_ERROR (Status);
if (EFI_ERROR (Status)) {
CpuDeadLoop ();
}
}
//
// Detach backing pages from the currently used 2D host resource.
//
Status = VirtioGpuResourceDetachBacking (
VgpuGop->ParentBus, // VgpuDev
VgpuGop->ResourceId // ResourceId
);
ASSERT_EFI_ERROR (Status);
if (EFI_ERROR (Status)) {
CpuDeadLoop ();
}
//
// Unmap and release backing pages.
//
VirtioGpuUnmapAndFreeBackingStore (
VgpuGop->ParentBus, // VgpuDev
VgpuGop->NumberOfPages, // NumberOfPages
VgpuGop->BackingStore, // HostAddress
VgpuGop->BackingStoreMap // Mapping
);
VgpuGop->BackingStore = NULL;
VgpuGop->NumberOfPages = 0;
VgpuGop->BackingStoreMap = NULL;
//
// Destroy the currently used 2D host resource.
//
Status = VirtioGpuResourceUnref (
VgpuGop->ParentBus, // VgpuDev
VgpuGop->ResourceId // ResourceId
);
ASSERT_EFI_ERROR (Status);
if (EFI_ERROR (Status)) {
CpuDeadLoop ();
}
VgpuGop->ResourceId = 0;
}
//
// The resolutions supported by this driver.
//
typedef struct {
UINT32 Width;
UINT32 Height;
} GOP_RESOLUTION;
STATIC CONST GOP_RESOLUTION mGopResolutions[] = {
{ 640, 480 },
{ 800, 480 },
{ 800, 600 },
{ 832, 624 },
{ 960, 640 },
{ 1024, 600 },
{ 1024, 768 },
{ 1152, 864 },
{ 1152, 870 },
{ 1280, 720 },
{ 1280, 760 },
{ 1280, 768 },
{ 1280, 800 },
{ 1280, 960 },
{ 1280, 1024 },
{ 1360, 768 },
{ 1366, 768 },
{ 1400, 1050 },
{ 1440, 900 },
{ 1600, 900 },
{ 1600, 1200 },
{ 1680, 1050 },
{ 1920, 1080 },
{ 1920, 1200 },
{ 1920, 1440 },
{ 2000, 2000 },
{ 2048, 1536 },
{ 2048, 2048 },
{ 2560, 1440 },
{ 2560, 1600 },
{ 2560, 2048 },
{ 2800, 2100 },
{ 3200, 2400 },
{ 3840, 2160 },
{ 4096, 2160 },
{ 7680, 4320 },
{ 8192, 4320 },
};
//
// Macro for casting VGPU_GOP.Gop to VGPU_GOP.
//
#define VGPU_GOP_FROM_GOP(GopPointer) \
CR (GopPointer, VGPU_GOP, Gop, VGPU_GOP_SIG)
STATIC
VOID
EFIAPI
GopNativeResolution (
IN VGPU_GOP *VgpuGop,
OUT UINT32 *XRes,
OUT UINT32 *YRes
)
{
volatile VIRTIO_GPU_RESP_DISPLAY_INFO DisplayInfo;
EFI_STATUS Status;
UINTN Index;
Status = VirtioGpuGetDisplayInfo (VgpuGop->ParentBus, &DisplayInfo);
if (Status != EFI_SUCCESS) {
return;
}
for (Index = 0; Index < VIRTIO_GPU_MAX_SCANOUTS; Index++) {
if (!DisplayInfo.Pmodes[Index].Enabled ||
!DisplayInfo.Pmodes[Index].Rectangle.Width ||
!DisplayInfo.Pmodes[Index].Rectangle.Height)
{
continue;
}
DEBUG ((
DEBUG_INFO,
"%a: #%d: %dx%d\n",
__func__,
Index,
DisplayInfo.Pmodes[Index].Rectangle.Width,
DisplayInfo.Pmodes[Index].Rectangle.Height
));
if ((*XRes == 0) || (*YRes == 0)) {
*XRes = DisplayInfo.Pmodes[Index].Rectangle.Width;
*YRes = DisplayInfo.Pmodes[Index].Rectangle.Height;
}
}
}
STATIC
VOID
EFIAPI
GopInitialize (
IN EFI_GRAPHICS_OUTPUT_PROTOCOL *This
)
{
VGPU_GOP *VgpuGop;
EFI_STATUS Status;
UINT32 XRes = 0, YRes = 0, Index;
VgpuGop = VGPU_GOP_FROM_GOP (This);
//
// Set up the Gop -> GopMode -> GopModeInfo pointer chain, and the other
// (nonzero) constant fields.
//
// No direct framebuffer access is supported, only Blt() is.
//
VgpuGop->Gop.Mode = &VgpuGop->GopMode;
VgpuGop->GopMode.MaxMode = (UINT32)(ARRAY_SIZE (mGopResolutions));
VgpuGop->GopMode.Info = &VgpuGop->GopModeInfo;
VgpuGop->GopMode.SizeOfInfo = sizeof VgpuGop->GopModeInfo;
VgpuGop->GopModeInfo.PixelFormat = PixelBltOnly;
//
// query host for display resolution
//
GopNativeResolution (VgpuGop, &XRes, &YRes);
if ((XRes == 0) || (YRes == 0)) {
return;
}
if (PcdGet8 (PcdVideoResolutionSource) == 0) {
Status = PcdSet32S (PcdVideoHorizontalResolution, XRes);
ASSERT_RETURN_ERROR (Status);
Status = PcdSet32S (PcdVideoVerticalResolution, YRes);
ASSERT_RETURN_ERROR (Status);
Status = PcdSet8S (PcdVideoResolutionSource, 2);
ASSERT_RETURN_ERROR (Status);
}
VgpuGop->NativeXRes = XRes;
VgpuGop->NativeYRes = YRes;
for (Index = 0; Index < ARRAY_SIZE (mGopResolutions); Index++) {
if ((mGopResolutions[Index].Width == XRes) &&
(mGopResolutions[Index].Height == YRes))
{
// native resolution already is in mode list
return;
}
}
// add to mode list
VgpuGop->GopMode.MaxMode++;
}
//
// EFI_GRAPHICS_OUTPUT_PROTOCOL member functions.
//
STATIC
EFI_STATUS
EFIAPI
GopQueryMode (
IN EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
IN UINT32 ModeNumber,
OUT UINTN *SizeOfInfo,
OUT EFI_GRAPHICS_OUTPUT_MODE_INFORMATION **Info
)
{
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *GopModeInfo;
if ((Info == NULL) ||
(SizeOfInfo == NULL) ||
(ModeNumber >= This->Mode->MaxMode))
{
return EFI_INVALID_PARAMETER;
}
GopModeInfo = AllocateZeroPool (sizeof *GopModeInfo);
if (GopModeInfo == NULL) {
return EFI_OUT_OF_RESOURCES;
}
if (ModeNumber < ARRAY_SIZE (mGopResolutions)) {
GopModeInfo->HorizontalResolution = mGopResolutions[ModeNumber].Width;
GopModeInfo->VerticalResolution = mGopResolutions[ModeNumber].Height;
} else {
VGPU_GOP *VgpuGop = VGPU_GOP_FROM_GOP (This);
GopModeInfo->HorizontalResolution = VgpuGop->NativeXRes;
GopModeInfo->VerticalResolution = VgpuGop->NativeYRes;
}
GopModeInfo->PixelFormat = PixelBltOnly;
GopModeInfo->PixelsPerScanLine = GopModeInfo->HorizontalResolution;
*SizeOfInfo = sizeof *GopModeInfo;
*Info = GopModeInfo;
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
EFIAPI
GopSetMode (
IN EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
IN UINT32 ModeNumber
)
{
VGPU_GOP *VgpuGop;
UINT32 NewResourceId;
UINTN NewNumberOfBytes;
UINTN NewNumberOfPages;
VOID *NewBackingStore;
EFI_PHYSICAL_ADDRESS NewBackingStoreDeviceAddress;
VOID *NewBackingStoreMap;
UINTN SizeOfInfo;
EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *GopModeInfo;
EFI_STATUS Status;
EFI_STATUS Status2;
if (!This->Mode) {
// SetMode() call in InitVgpuGop() triggers this.
GopInitialize (This);
}
Status = GopQueryMode (This, ModeNumber, &SizeOfInfo, &GopModeInfo);
if (Status != EFI_SUCCESS) {
return Status;
}
VgpuGop = VGPU_GOP_FROM_GOP (This);
//
// Distinguish the first (internal) call from the other (protocol consumer)
// calls.
//
if (VgpuGop->ResourceId == 0) {
//
// This is the first time we create a host side resource.
//
NewResourceId = 1;
} else {
//
// We already have an active host side resource. Create the new one without
// interfering with the current one, so that we can cleanly bail out on
// error, without disturbing the current graphics mode.
//
// The formula below will alternate between IDs 1 and 2.
//
NewResourceId = 3 - VgpuGop->ResourceId;
}
//
// Create the 2D host resource.
//
Status = VirtioGpuResourceCreate2d (
VgpuGop->ParentBus, // VgpuDev
NewResourceId, // ResourceId
VirtioGpuFormatB8G8R8X8Unorm, // Format
GopModeInfo->HorizontalResolution, // Width
GopModeInfo->VerticalResolution // Height
);
if (EFI_ERROR (Status)) {
return Status;
}
//
// Allocate, zero and map guest backing store, for bus master common buffer
// operation.
//
NewNumberOfBytes = GopModeInfo->HorizontalResolution *
GopModeInfo->VerticalResolution * sizeof (UINT32);
NewNumberOfPages = EFI_SIZE_TO_PAGES (NewNumberOfBytes);
Status = VirtioGpuAllocateZeroAndMapBackingStore (
VgpuGop->ParentBus, // VgpuDev
NewNumberOfPages, // NumberOfPages
&NewBackingStore, // HostAddress
&NewBackingStoreDeviceAddress, // DeviceAddress
&NewBackingStoreMap // Mapping
);
if (EFI_ERROR (Status)) {
goto DestroyHostResource;
}
//
// Attach backing store to the host resource.
//
Status = VirtioGpuResourceAttachBacking (
VgpuGop->ParentBus, // VgpuDev
NewResourceId, // ResourceId
NewBackingStoreDeviceAddress, // BackingStoreDeviceAddress
NewNumberOfPages // NumberOfPages
);
if (EFI_ERROR (Status)) {
goto UnmapAndFreeBackingStore;
}
//
// Point head (scanout) #0 to the host resource.
//
Status = VirtioGpuSetScanout (
VgpuGop->ParentBus, // VgpuDev
0, // X
0, // Y
GopModeInfo->HorizontalResolution, // Width
GopModeInfo->VerticalResolution, // Height
0, // ScanoutId
NewResourceId // ResourceId
);
if (EFI_ERROR (Status)) {
goto DetachBackingStore;
}
//
// If this is not the first (i.e., internal) call, then we have to (a) flush
// the new resource to head (scanout) #0, after having flipped the latter to
// the former above, plus (b) release the old resources.
//
if (VgpuGop->ResourceId != 0) {
Status = VirtioGpuResourceFlush (
VgpuGop->ParentBus, // VgpuDev
0, // X
0, // Y
GopModeInfo->HorizontalResolution, // Width
GopModeInfo->VerticalResolution, // Height
NewResourceId // ResourceId
);
if (EFI_ERROR (Status)) {
//
// Flip head (scanout) #0 back to the current resource. If this fails, we
// cannot continue, as this error occurs on the error path and is
// therefore non-recoverable.
//
Status2 = VirtioGpuSetScanout (
VgpuGop->ParentBus, // VgpuDev
0, // X
0, // Y
VgpuGop->GopModeInfo.HorizontalResolution, // Width
VgpuGop->GopModeInfo.VerticalResolution, // Height
0, // ScanoutId
VgpuGop->ResourceId // ResourceId
);
ASSERT_EFI_ERROR (Status2);
if (EFI_ERROR (Status2)) {
CpuDeadLoop ();
}
goto DetachBackingStore;
}
//
// Flush successful; release the old resources (without disabling head
// (scanout) #0).
//
ReleaseGopResources (VgpuGop, FALSE /* DisableHead */);
}
//
// This is either the first (internal) call when we have no old resources
// yet, or we've changed the mode successfully and released the old
// resources.
//
ASSERT (VgpuGop->ResourceId == 0);
ASSERT (VgpuGop->BackingStore == NULL);
VgpuGop->ResourceId = NewResourceId;
VgpuGop->BackingStore = NewBackingStore;
VgpuGop->NumberOfPages = NewNumberOfPages;
VgpuGop->BackingStoreMap = NewBackingStoreMap;
//
// Populate Mode and ModeInfo (mutable fields only).
//
VgpuGop->GopMode.Mode = ModeNumber;
CopyMem (&VgpuGop->GopModeInfo, GopModeInfo, sizeof VgpuGop->GopModeInfo);
FreePool (GopModeInfo);
return EFI_SUCCESS;
DetachBackingStore:
Status2 = VirtioGpuResourceDetachBacking (VgpuGop->ParentBus, NewResourceId);
ASSERT_EFI_ERROR (Status2);
if (EFI_ERROR (Status2)) {
CpuDeadLoop ();
}
UnmapAndFreeBackingStore:
VirtioGpuUnmapAndFreeBackingStore (
VgpuGop->ParentBus, // VgpuDev
NewNumberOfPages, // NumberOfPages
NewBackingStore, // HostAddress
NewBackingStoreMap // Mapping
);
DestroyHostResource:
Status2 = VirtioGpuResourceUnref (VgpuGop->ParentBus, NewResourceId);
ASSERT_EFI_ERROR (Status2);
if (EFI_ERROR (Status2)) {
CpuDeadLoop ();
}
FreePool (GopModeInfo);
return Status;
}
STATIC
EFI_STATUS
EFIAPI
GopBlt (
IN EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
IN EFI_GRAPHICS_OUTPUT_BLT_PIXEL *BltBuffer OPTIONAL,
IN EFI_GRAPHICS_OUTPUT_BLT_OPERATION BltOperation,
IN UINTN SourceX,
IN UINTN SourceY,
IN UINTN DestinationX,
IN UINTN DestinationY,
IN UINTN Width,
IN UINTN Height,
IN UINTN Delta OPTIONAL
)
{
VGPU_GOP *VgpuGop;
UINT32 CurrentHorizontal;
UINT32 CurrentVertical;
UINTN SegmentSize;
UINTN Y;
UINTN ResourceOffset;
EFI_STATUS Status;
VgpuGop = VGPU_GOP_FROM_GOP (This);
CurrentHorizontal = VgpuGop->GopModeInfo.HorizontalResolution;
CurrentVertical = VgpuGop->GopModeInfo.VerticalResolution;
//
// We can avoid pixel format conversion in the guest because the internal
// representation of EFI_GRAPHICS_OUTPUT_BLT_PIXEL and that of
// VirtioGpuFormatB8G8R8X8Unorm are identical.
//
SegmentSize = Width * sizeof (UINT32);
//
// Delta is relevant for operations that read a rectangle from, or write a
// rectangle to, BltBuffer.
//
// In these cases, Delta is the stride of BltBuffer, in bytes. If Delta is
// zero, then Width is the entire width of BltBuffer, and the stride is
// supposed to be calculated from Width.
//
if ((BltOperation == EfiBltVideoToBltBuffer) ||
(BltOperation == EfiBltBufferToVideo))
{
if (Delta == 0) {
Delta = SegmentSize;
}
}
//
// For operations that write to the display, check if the destination fits
// onto the display.
//
if ((BltOperation == EfiBltVideoFill) ||
(BltOperation == EfiBltBufferToVideo) ||
(BltOperation == EfiBltVideoToVideo))
{
if ((DestinationX > CurrentHorizontal) ||
(Width > CurrentHorizontal - DestinationX) ||
(DestinationY > CurrentVertical) ||
(Height > CurrentVertical - DestinationY))
{
return EFI_INVALID_PARAMETER;
}
}
//
// For operations that read from the display, check if the source fits onto
// the display.
//
if ((BltOperation == EfiBltVideoToBltBuffer) ||
(BltOperation == EfiBltVideoToVideo))
{
if ((SourceX > CurrentHorizontal) ||
(Width > CurrentHorizontal - SourceX) ||
(SourceY > CurrentVertical) ||
(Height > CurrentVertical - SourceY))
{
return EFI_INVALID_PARAMETER;
}
}
//
// Render the request. For requests that do not modify the display, there
// won't be further steps.
//
switch (BltOperation) {
case EfiBltVideoFill:
//
// Write data from the BltBuffer pixel (0, 0) directly to every pixel of
// the video display rectangle (DestinationX, DestinationY) (DestinationX +
// Width, DestinationY + Height). Only one pixel will be used from the
// BltBuffer. Delta is NOT used.
//
for (Y = 0; Y < Height; ++Y) {
SetMem32 (
VgpuGop->BackingStore +
(DestinationY + Y) * CurrentHorizontal + DestinationX,
SegmentSize,
*(UINT32 *)BltBuffer
);
}
break;
case EfiBltVideoToBltBuffer:
//
// Read data from the video display rectangle (SourceX, SourceY) (SourceX +
// Width, SourceY + Height) and place it in the BltBuffer rectangle
// (DestinationX, DestinationY ) (DestinationX + Width, DestinationY +
// Height). If DestinationX or DestinationY is not zero then Delta must be
// set to the length in bytes of a row in the BltBuffer.
//
for (Y = 0; Y < Height; ++Y) {
CopyMem (
(UINT8 *)BltBuffer +
(DestinationY + Y) * Delta + DestinationX * sizeof *BltBuffer,
VgpuGop->BackingStore +
(SourceY + Y) * CurrentHorizontal + SourceX,
SegmentSize
);
}
return EFI_SUCCESS;
case EfiBltBufferToVideo:
//
// Write data from the BltBuffer rectangle (SourceX, SourceY) (SourceX +
// Width, SourceY + Height) directly to the video display rectangle
// (DestinationX, DestinationY) (DestinationX + Width, DestinationY +
// Height). If SourceX or SourceY is not zero then Delta must be set to the
// length in bytes of a row in the BltBuffer.
//
for (Y = 0; Y < Height; ++Y) {
CopyMem (
VgpuGop->BackingStore +
(DestinationY + Y) * CurrentHorizontal + DestinationX,
(UINT8 *)BltBuffer +
(SourceY + Y) * Delta + SourceX * sizeof *BltBuffer,
SegmentSize
);
}
break;
case EfiBltVideoToVideo:
//
// Copy from the video display rectangle (SourceX, SourceY) (SourceX +
// Width, SourceY + Height) to the video display rectangle (DestinationX,
// DestinationY) (DestinationX + Width, DestinationY + Height). The
// BltBuffer and Delta are not used in this mode.
//
// A single invocation of CopyMem() handles overlap between source and
// destination (that is, within a single line), but for multiple
// invocations, we must handle overlaps.
//
if (SourceY < DestinationY) {
Y = Height;
while (Y > 0) {
--Y;
CopyMem (
VgpuGop->BackingStore +
(DestinationY + Y) * CurrentHorizontal + DestinationX,
VgpuGop->BackingStore +
(SourceY + Y) * CurrentHorizontal + SourceX,
SegmentSize
);
}
} else {
for (Y = 0; Y < Height; ++Y) {
CopyMem (
VgpuGop->BackingStore +
(DestinationY + Y) * CurrentHorizontal + DestinationX,
VgpuGop->BackingStore +
(SourceY + Y) * CurrentHorizontal + SourceX,
SegmentSize
);
}
}
break;
default:
return EFI_INVALID_PARAMETER;
}
//
// For operations that wrote to the display, submit the updated area to the
// host -- update the host resource from guest memory.
//
ResourceOffset = sizeof (UINT32) * (DestinationY * CurrentHorizontal +
DestinationX);
Status = VirtioGpuTransferToHost2d (
VgpuGop->ParentBus, // VgpuDev
(UINT32)DestinationX, // X
(UINT32)DestinationY, // Y
(UINT32)Width, // Width
(UINT32)Height, // Height
ResourceOffset, // Offset
VgpuGop->ResourceId // ResourceId
);
if (EFI_ERROR (Status)) {
return Status;
}
//
// Flush the updated resource to the display.
//
Status = VirtioGpuResourceFlush (
VgpuGop->ParentBus, // VgpuDev
(UINT32)DestinationX, // X
(UINT32)DestinationY, // Y
(UINT32)Width, // Width
(UINT32)Height, // Height
VgpuGop->ResourceId // ResourceId
);
return Status;
}
//
// Template for initializing VGPU_GOP.Gop.
//
CONST EFI_GRAPHICS_OUTPUT_PROTOCOL mGopTemplate = {
GopQueryMode,
GopSetMode,
GopBlt,
NULL // Mode, to be overwritten in the actual protocol instance
};