public inbox for devel@edk2.groups.io
 help / color / mirror / Atom feed
From: Gerd Hoffmann <kraxel@redhat.com>
To: edk2-devel@lists.01.org
Subject: [PATCH 2/4] OvmfPkg: add QemuRamfbDxe
Date: Fri,  8 Jun 2018 13:39:40 +0200	[thread overview]
Message-ID: <20180608113942.17009-3-kraxel@redhat.com> (raw)
In-Reply-To: <20180608113942.17009-1-kraxel@redhat.com>

Add a driver for the qemu ramfb display device.

Contributed-under: TianoCore Contribution Agreement 1.1
Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 OvmfPkg/QemuRamfbDxe/QemuRamfb.c      | 308 ++++++++++++++++++++++++++++++++++
 OvmfPkg/OvmfPkgIa32.dsc               |   1 +
 OvmfPkg/OvmfPkgIa32.fdf               |   1 +
 OvmfPkg/OvmfPkgIa32X64.dsc            |   1 +
 OvmfPkg/OvmfPkgIa32X64.fdf            |   1 +
 OvmfPkg/OvmfPkgX64.dsc                |   1 +
 OvmfPkg/OvmfPkgX64.fdf                |   1 +
 OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf |  34 ++++
 8 files changed, 348 insertions(+)
 create mode 100644 OvmfPkg/QemuRamfbDxe/QemuRamfb.c
 create mode 100644 OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf

diff --git a/OvmfPkg/QemuRamfbDxe/QemuRamfb.c b/OvmfPkg/QemuRamfbDxe/QemuRamfb.c
new file mode 100644
index 0000000000..f04a314c24
--- /dev/null
+++ b/OvmfPkg/QemuRamfbDxe/QemuRamfb.c
@@ -0,0 +1,308 @@
+#include <Uefi.h>
+#include <Protocol/GraphicsOutput.h>
+
+#include <Library/BaseMemoryLib.h>
+#include <Library/DebugLib.h>
+#include <Library/DevicePathLib.h>
+#include <Library/FrameBufferBltLib.h>
+#include <Library/MemoryAllocationLib.h>
+#include <Library/UefiBootManagerLib.h>
+#include <Library/UefiBootServicesTableLib.h>
+#include <Library/UefiDriverEntryPoint.h>
+#include <Library/UefiLib.h>
+#include <Library/QemuFwCfgLib.h>
+
+#include <Guid/QemuRamfb.h>
+
+#define RAMFB_FORMAT  0x34325258 /* DRM_FORMAT_XRGB8888 */
+#define RAMFB_BPP     4
+
+EFI_GUID gQemuRamfbGuid = QEMU_RAMFB_GUID;
+
+typedef struct RAMFB_CONFIG {
+  UINT64 Address;
+  UINT32 FourCC;
+  UINT32 Flags;
+  UINT32 Width;
+  UINT32 Height;
+  UINT32 Stride;
+} RAMFB_CONFIG;
+
+EFI_HANDLE                    RamfbHandle;
+EFI_HANDLE                    GopHandle;
+FRAME_BUFFER_CONFIGURE        *QemuRamfbFrameBufferBltConfigure;
+UINTN                         QemuRamfbFrameBufferBltConfigureSize;
+
+EFI_GRAPHICS_OUTPUT_MODE_INFORMATION QemuRamfbModeInfo[] = {
+  {
+    .HorizontalResolution  = 640,
+    .VerticalResolution    = 480,
+  },{
+    .HorizontalResolution  = 800,
+    .VerticalResolution    = 600,
+  },{
+    .HorizontalResolution  = 1024,
+    .VerticalResolution    = 768,
+  }
+};
+#define QemuRamfbModeCount (sizeof(QemuRamfbModeInfo)/sizeof(QemuRamfbModeInfo[0]))
+
+EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE QemuRamfbMode = {
+  .MaxMode               = QemuRamfbModeCount,
+  .Mode                  = 0,
+  .Info                  = QemuRamfbModeInfo,
+  .SizeOfInfo            = sizeof(EFI_GRAPHICS_OUTPUT_MODE_INFORMATION),
+};
+
+EFI_STATUS
+EFIAPI
+QemuRamfbGraphicsOutputQueryMode (
+  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL          *This,
+  IN  UINT32                                ModeNumber,
+  OUT UINTN                                 *SizeOfInfo,
+  OUT EFI_GRAPHICS_OUTPUT_MODE_INFORMATION  **Info
+  )
+{
+  EFI_GRAPHICS_OUTPUT_MODE_INFORMATION  *ModeInfo;
+
+  if (Info == NULL || SizeOfInfo == NULL || ModeNumber > QemuRamfbMode.MaxMode) {
+    return EFI_INVALID_PARAMETER;
+  }
+  ModeInfo = &QemuRamfbModeInfo[ModeNumber];
+
+  *Info = AllocateCopyPool (sizeof (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION),
+                            ModeInfo);
+  if (*Info == NULL) {
+    return EFI_OUT_OF_RESOURCES;
+  }
+  *SizeOfInfo = sizeof (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION);
+
+  return EFI_SUCCESS;
+}
+
+EFI_STATUS
+EFIAPI
+QemuRamfbGraphicsOutputSetMode (
+  IN  EFI_GRAPHICS_OUTPUT_PROTOCOL *This,
+  IN  UINT32                       ModeNumber
+  )
+{
+  EFI_GRAPHICS_OUTPUT_MODE_INFORMATION  *ModeInfo;
+  RAMFB_CONFIG                          Config;
+  EFI_GRAPHICS_OUTPUT_BLT_PIXEL         Black;
+  RETURN_STATUS                         Ret;
+  FIRMWARE_CONFIG_ITEM                  Item;
+  UINTN                                 Size;
+
+  if (ModeNumber > QemuRamfbMode.MaxMode) {
+    return EFI_UNSUPPORTED;
+  }
+  ModeInfo = &QemuRamfbModeInfo[ModeNumber];
+
+  DEBUG ((EFI_D_INFO, "Ramfb: SetMode %d (%dx%d)\n", ModeNumber,
+          ModeInfo->HorizontalResolution,
+          ModeInfo->VerticalResolution));
+
+  QemuRamfbMode.Mode = ModeNumber;
+  QemuRamfbMode.Info = ModeInfo;
+
+  Config.Address = SwapBytes64( QemuRamfbMode.FrameBufferBase );
+  Config.FourCC  = SwapBytes32( RAMFB_FORMAT );
+  Config.Flags   = SwapBytes32( 0 );
+  Config.Width   = SwapBytes32( ModeInfo->HorizontalResolution );
+  Config.Height  = SwapBytes32( ModeInfo->VerticalResolution );
+  Config.Stride  = SwapBytes32( ModeInfo->HorizontalResolution * RAMFB_BPP );
+
+  QemuFwCfgFindFile("etc/ramfb", &Item, &Size);
+  QemuFwCfgSelectItem(Item);
+  QemuFwCfgWriteBytes(sizeof(Config), &Config);
+
+  Ret = FrameBufferBltConfigure (
+    (VOID*)(UINTN)QemuRamfbMode.FrameBufferBase,
+    ModeInfo,
+    QemuRamfbFrameBufferBltConfigure,
+    &QemuRamfbFrameBufferBltConfigureSize);
+
+  if (Ret == RETURN_BUFFER_TOO_SMALL) {
+    if (QemuRamfbFrameBufferBltConfigure != NULL) {
+      FreePool(QemuRamfbFrameBufferBltConfigure);
+    }
+    QemuRamfbFrameBufferBltConfigure =
+      AllocatePool(QemuRamfbFrameBufferBltConfigureSize);
+
+    Ret = FrameBufferBltConfigure (
+      (VOID*)(UINTN)QemuRamfbMode.FrameBufferBase,
+      ModeInfo,
+      QemuRamfbFrameBufferBltConfigure,
+      &QemuRamfbFrameBufferBltConfigureSize);
+  }
+
+  /* clear screen */
+  ZeroMem (&Black, sizeof (Black));
+  Ret = FrameBufferBlt (
+    QemuRamfbFrameBufferBltConfigure,
+    &Black,
+    EfiBltVideoFill,
+    0, 0,
+    0, 0,
+    ModeInfo->HorizontalResolution,
+    ModeInfo->VerticalResolution,
+    0
+    );
+
+  return EFI_SUCCESS;
+}
+
+EFI_STATUS
+EFIAPI
+QemuRamfbGraphicsOutputBlt (
+  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
+  )
+{
+  return FrameBufferBlt (
+    QemuRamfbFrameBufferBltConfigure,
+    BltBuffer,
+    BltOperation,
+    SourceX,
+    SourceY,
+    DestinationX,
+    DestinationY,
+    Width,
+    Height,
+    Delta);
+}
+
+EFI_GRAPHICS_OUTPUT_PROTOCOL QemuRamfbGraphicsOutput = {
+  .QueryMode        = QemuRamfbGraphicsOutputQueryMode,
+  .SetMode          = QemuRamfbGraphicsOutputSetMode,
+  .Blt              = QemuRamfbGraphicsOutputBlt,
+  .Mode             = &QemuRamfbMode,
+};
+
+EFI_STATUS
+EFIAPI
+InitializeQemuRamfb (
+  IN EFI_HANDLE           ImageHandle,
+  IN EFI_SYSTEM_TABLE     *SystemTable
+  )
+{
+  EFI_DEVICE_PATH_PROTOCOL  *RamfbDevicePath;
+  EFI_DEVICE_PATH_PROTOCOL  *GopDevicePath;
+  VOID                      *DevicePath;
+  VENDOR_DEVICE_PATH        VendorDeviceNode;
+  ACPI_ADR_DEVICE_PATH      AcpiDeviceNode;
+  EFI_STATUS                Status;
+  RETURN_STATUS             Ret;
+  FIRMWARE_CONFIG_ITEM      Item;
+  EFI_PHYSICAL_ADDRESS      FbBase;
+  UINTN                     Size, FbSize, MaxFbSize, Pages, Index;
+
+  DEBUG ((EFI_D_INFO, "InitializeQemuRamfb\n"));
+
+  if (!QemuFwCfgIsAvailable()) {
+    DEBUG ((EFI_D_INFO, "Ramfb: no FwCfg\n"));
+    return EFI_NOT_FOUND;
+  }
+
+  Ret = QemuFwCfgFindFile("etc/ramfb", &Item, &Size);
+  if (Ret != RETURN_SUCCESS) {
+    DEBUG ((EFI_D_INFO, "Ramfb: no etc/ramfb in FwCfg\n"));
+    return EFI_NOT_FOUND;
+  }
+
+  MaxFbSize = 0;
+  for (Index = 0; Index < QemuRamfbModeCount; Index++) {
+    QemuRamfbModeInfo[Index].PixelsPerScanLine =
+      QemuRamfbModeInfo[Index].HorizontalResolution;
+    QemuRamfbModeInfo[Index].PixelFormat =
+      PixelBlueGreenRedReserved8BitPerColor,
+    FbSize = RAMFB_BPP *
+      QemuRamfbModeInfo[Index].HorizontalResolution *
+      QemuRamfbModeInfo[Index].VerticalResolution;
+    if (MaxFbSize < FbSize)
+      MaxFbSize = FbSize;
+    DEBUG ((EFI_D_INFO, "Ramfb: Mode %d: %dx%d, %d kB\n", Index,
+            QemuRamfbModeInfo[Index].HorizontalResolution,
+            QemuRamfbModeInfo[Index].VerticalResolution,
+            FbSize / 1024));
+  }
+
+  Pages = EFI_SIZE_TO_PAGES(MaxFbSize);
+  MaxFbSize = EFI_PAGES_TO_SIZE(Pages);
+  FbBase = (EFI_PHYSICAL_ADDRESS)(UINTN)AllocateRuntimePages(Pages);
+  if (!FbBase) {
+    DEBUG ((EFI_D_INFO, "Ramfb: memory allocation failed\n"));
+    return EFI_OUT_OF_RESOURCES;
+  }
+  DEBUG ((EFI_D_INFO, "Ramfb: Framebuffer at 0x%lx, %d kB, %d pages\n",
+          FbBase, MaxFbSize / 1024, Pages));
+  QemuRamfbMode.FrameBufferSize = MaxFbSize;
+  QemuRamfbMode.FrameBufferBase = FbBase;
+
+  /* 800 x 600 */
+  QemuRamfbGraphicsOutputSetMode (&QemuRamfbGraphicsOutput, 1);
+
+  /* ramfb vendor devpath */
+  ZeroMem (&VendorDeviceNode, sizeof (VENDOR_DEVICE_PATH));
+  VendorDeviceNode.Header.Type = HARDWARE_DEVICE_PATH;
+  VendorDeviceNode.Header.SubType = HW_VENDOR_DP;
+  VendorDeviceNode.Guid = gQemuRamfbGuid;
+  SetDevicePathNodeLength (&VendorDeviceNode.Header, sizeof (VENDOR_DEVICE_PATH));
+
+  RamfbDevicePath = AppendDevicePathNode (
+    NULL,
+    (EFI_DEVICE_PATH_PROTOCOL *) &VendorDeviceNode);
+
+  Status = gBS->InstallMultipleProtocolInterfaces (
+    &RamfbHandle,
+    &gEfiDevicePathProtocolGuid, RamfbDevicePath,
+    NULL);
+  if (EFI_ERROR (Status)) {
+    DEBUG ((EFI_D_INFO, "Ramfb: install Ramfb Vendor DevicePath failed\n"));
+    FreePool((VOID*)(UINTN)QemuRamfbMode.FrameBufferBase);
+    return Status;
+  }
+
+  /* gop devpath + protocol */
+  ZeroMem (&AcpiDeviceNode, sizeof (ACPI_ADR_DEVICE_PATH));
+  AcpiDeviceNode.Header.Type = ACPI_DEVICE_PATH;
+  AcpiDeviceNode.Header.SubType = ACPI_ADR_DP;
+  AcpiDeviceNode.ADR = ACPI_DISPLAY_ADR (1, 0, 0, 1, 0,
+                                         ACPI_ADR_DISPLAY_TYPE_EXTERNAL_DIGITAL,
+                                         0, 0);
+  SetDevicePathNodeLength (&AcpiDeviceNode.Header, sizeof (ACPI_ADR_DEVICE_PATH));
+
+  GopDevicePath = AppendDevicePathNode (
+    RamfbDevicePath,
+    (EFI_DEVICE_PATH_PROTOCOL *) &AcpiDeviceNode);
+
+  Status = gBS->InstallMultipleProtocolInterfaces (
+    &GopHandle,
+    &gEfiDevicePathProtocolGuid, GopDevicePath,
+    &gEfiGraphicsOutputProtocolGuid, &QemuRamfbGraphicsOutput,
+    NULL);
+  if (EFI_ERROR (Status)) {
+    DEBUG ((EFI_D_INFO, "Ramfb: install GOP DevicePath failed\n"));
+    FreePool((VOID*)(UINTN)QemuRamfbMode.FrameBufferBase);
+    return Status;
+  }
+
+  gBS->OpenProtocol (
+    RamfbHandle,
+    &gEfiDevicePathProtocolGuid,
+    &DevicePath,
+    gImageHandle,
+    GopHandle,
+    EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER);
+
+  return EFI_SUCCESS;
+}
diff --git a/OvmfPkg/OvmfPkgIa32.dsc b/OvmfPkg/OvmfPkgIa32.dsc
index a2c995b910..7ddda89999 100644
--- a/OvmfPkg/OvmfPkgIa32.dsc
+++ b/OvmfPkg/OvmfPkgIa32.dsc
@@ -745,6 +745,7 @@
   MdeModulePkg/Universal/MemoryTest/NullMemoryTestDxe/NullMemoryTestDxe.inf
 
   OvmfPkg/QemuVideoDxe/QemuVideoDxe.inf
+  OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf
   OvmfPkg/VirtioGpuDxe/VirtioGpu.inf
 
   #
diff --git a/OvmfPkg/OvmfPkgIa32.fdf b/OvmfPkg/OvmfPkgIa32.fdf
index b199713925..52b8b1fea1 100644
--- a/OvmfPkg/OvmfPkgIa32.fdf
+++ b/OvmfPkg/OvmfPkgIa32.fdf
@@ -351,6 +351,7 @@ INF  RuleOverride=CSM OvmfPkg/Csm/Csm16/Csm16.inf
 !endif
 
 INF  OvmfPkg/QemuVideoDxe/QemuVideoDxe.inf
+INF  OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf
 INF  OvmfPkg/VirtioGpuDxe/VirtioGpu.inf
 INF  OvmfPkg/PlatformDxe/Platform.inf
 INF  OvmfPkg/IoMmuDxe/IoMmuDxe.inf
diff --git a/OvmfPkg/OvmfPkgIa32X64.dsc b/OvmfPkg/OvmfPkgIa32X64.dsc
index bc7db229d2..3481cdc36b 100644
--- a/OvmfPkg/OvmfPkgIa32X64.dsc
+++ b/OvmfPkg/OvmfPkgIa32X64.dsc
@@ -754,6 +754,7 @@
   MdeModulePkg/Universal/MemoryTest/NullMemoryTestDxe/NullMemoryTestDxe.inf
 
   OvmfPkg/QemuVideoDxe/QemuVideoDxe.inf
+  OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf
   OvmfPkg/VirtioGpuDxe/VirtioGpu.inf
 
   #
diff --git a/OvmfPkg/OvmfPkgIa32X64.fdf b/OvmfPkg/OvmfPkgIa32X64.fdf
index 4ebf64b2b9..70845d6972 100644
--- a/OvmfPkg/OvmfPkgIa32X64.fdf
+++ b/OvmfPkg/OvmfPkgIa32X64.fdf
@@ -357,6 +357,7 @@ INF  RuleOverride=CSM OvmfPkg/Csm/Csm16/Csm16.inf
 !endif
 
 INF  OvmfPkg/QemuVideoDxe/QemuVideoDxe.inf
+INF  OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf
 INF  OvmfPkg/VirtioGpuDxe/VirtioGpu.inf
 INF  OvmfPkg/PlatformDxe/Platform.inf
 INF  OvmfPkg/AmdSevDxe/AmdSevDxe.inf
diff --git a/OvmfPkg/OvmfPkgX64.dsc b/OvmfPkg/OvmfPkgX64.dsc
index 0767b34d18..8b0895b0ff 100644
--- a/OvmfPkg/OvmfPkgX64.dsc
+++ b/OvmfPkg/OvmfPkgX64.dsc
@@ -752,6 +752,7 @@
   MdeModulePkg/Universal/MemoryTest/NullMemoryTestDxe/NullMemoryTestDxe.inf
 
   OvmfPkg/QemuVideoDxe/QemuVideoDxe.inf
+  OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf
   OvmfPkg/VirtioGpuDxe/VirtioGpu.inf
 
   #
diff --git a/OvmfPkg/OvmfPkgX64.fdf b/OvmfPkg/OvmfPkgX64.fdf
index 9ca96f9282..1eb46ac9a2 100644
--- a/OvmfPkg/OvmfPkgX64.fdf
+++ b/OvmfPkg/OvmfPkgX64.fdf
@@ -357,6 +357,7 @@ INF  RuleOverride=CSM OvmfPkg/Csm/Csm16/Csm16.inf
 !endif
 
 INF  OvmfPkg/QemuVideoDxe/QemuVideoDxe.inf
+INF  OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf
 INF  OvmfPkg/VirtioGpuDxe/VirtioGpu.inf
 INF  OvmfPkg/PlatformDxe/Platform.inf
 INF  OvmfPkg/AmdSevDxe/AmdSevDxe.inf
diff --git a/OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf b/OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf
new file mode 100644
index 0000000000..75a7d86832
--- /dev/null
+++ b/OvmfPkg/QemuRamfbDxe/QemuRamfbDxe.inf
@@ -0,0 +1,34 @@
+[Defines]
+  INF_VERSION                    = 0x00010005
+  BASE_NAME                      = QemuRamfbDxe
+  FILE_GUID                      = dce1b094-7dc6-45d0-9fdd-d7fc3cc3e4ef
+  MODULE_TYPE                    = DXE_DRIVER
+  VERSION_STRING                 = 1.0
+
+  ENTRY_POINT                    = InitializeQemuRamfb
+
+[Sources.common]
+  QemuRamfb.c
+
+[Packages]
+  MdePkg/MdePkg.dec
+  MdeModulePkg/MdeModulePkg.dec
+  OvmfPkg/OvmfPkg.dec
+
+[LibraryClasses]
+  BaseMemoryLib
+  DebugLib
+  DevicePathLib
+  FrameBufferBltLib
+  MemoryAllocationLib
+  UefiBootManagerLib
+  UefiBootServicesTableLib
+  UefiDriverEntryPoint
+  UefiLib
+  QemuFwCfgLib
+
+[Protocols]
+  gEfiGraphicsOutputProtocolGuid                # PROTOCOL BY_START
+
+[Depex]
+  TRUE
-- 
2.9.3



  parent reply	other threads:[~2018-06-08 11:39 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-06-08 11:39 [PATCH 0/4] Add QemuRamfbDxe driver Gerd Hoffmann
2018-06-08 11:39 ` [PATCH 1/4] OvmfPkg: add QEMU_RAMFB_GUID Gerd Hoffmann
2018-06-11 13:06   ` Laszlo Ersek
2018-06-08 11:39 ` Gerd Hoffmann [this message]
2018-06-10  5:57   ` [PATCH 2/4] OvmfPkg: add QemuRamfbDxe Ard Biesheuvel
2018-06-11 15:56   ` Laszlo Ersek
2018-06-12  9:15     ` Gerd Hoffmann
2018-06-12 13:01       ` Laszlo Ersek
2018-06-08 11:39 ` [PATCH 3/4] OvmfPkg: add QemuRamfb to platform console Gerd Hoffmann
2018-06-11 16:24   ` Laszlo Ersek
2018-06-11 16:58     ` Laszlo Ersek
2018-06-08 11:39 ` [PATCH 4/4] ArmVirtPkg: add QemuRamfbDxe Gerd Hoffmann
2018-06-11 16:29   ` Laszlo Ersek
2018-06-11 16:35 ` [PATCH 0/4] Add QemuRamfbDxe driver Laszlo Ersek
2018-06-12  9:21   ` Gerd Hoffmann
2018-06-12 12:53     ` Laszlo Ersek
2018-06-13  7:40       ` Gerd Hoffmann
2018-06-13 16:16         ` Laszlo Ersek

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-list from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20180608113942.17009-3-kraxel@redhat.com \
    --to=devel@edk2.groups.io \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox