public inbox for qemu-arm@nongnu.org
 help / color / mirror / Atom feed
From: Yucai Liu <yangyanglan718@gmail.com>
To: pbonzini@redhat.com, peter.maydell@linaro.org
Cc: jcd@tribudubois.net, qemu-devel@nongnu.org, qemu-arm@nongnu.org
Subject: [PATCH v3 1/2] hw/display: Add i.MX6UL LCDIF device model
Date: Sun, 12 Apr 2026 19:02:39 +0800	[thread overview]
Message-ID: <20260412110240.93116-2-yangyanglan718@gmail.com> (raw)
In-Reply-To: <20260412110240.93116-1-yangyanglan718@gmail.com>

From: Yucai Liu <1486344514@qq.com>

Implement a basic i.MX6UL LCDIF controller model with MMIO registers,
frame-done interrupt behavior, and framebuffer-backed display updates
for RGB565 and XRGB8888 input formats.

Place the LCDIF device under hw/display and build it via a dedicated
CONFIG_IMX6UL_LCDIF symbol. Model register fields with
registerfields.h helpers and provide migration support via vmstate.

Signed-off-by: Yucai Liu <1486344514@qq.com>
---
 MAINTAINERS                       |   2 +
 hw/display/Kconfig                |   4 +
 hw/display/imx6ul_lcdif.c         | 453 ++++++++++++++++++++++++++++++
 hw/display/meson.build            |   1 +
 include/hw/display/imx6ul_lcdif.h |  37 +++
 5 files changed, 497 insertions(+)
 create mode 100644 hw/display/imx6ul_lcdif.c
 create mode 100644 include/hw/display/imx6ul_lcdif.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 4918f41ec4..b58022eb28 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -895,8 +895,10 @@ L: qemu-arm@nongnu.org
 S: Odd Fixes
 F: hw/arm/mcimx6ul-evk.c
 F: hw/arm/fsl-imx6ul.c
+F: hw/display/imx6ul_lcdif.c
 F: hw/misc/imx6ul_ccm.c
 F: include/hw/arm/fsl-imx6ul.h
+F: include/hw/display/imx6ul_lcdif.h
 F: include/hw/misc/imx6ul_ccm.h
 F: docs/system/arm/mcimx6ul-evk.rst
 
diff --git a/hw/display/Kconfig b/hw/display/Kconfig
index 1e95ab28ef..b3593fe981 100644
--- a/hw/display/Kconfig
+++ b/hw/display/Kconfig
@@ -25,6 +25,10 @@ config PL110
     bool
     select FRAMEBUFFER
 
+config IMX6UL_LCDIF
+    bool
+    select FRAMEBUFFER
+
 config SII9022
     bool
     depends on I2C
diff --git a/hw/display/imx6ul_lcdif.c b/hw/display/imx6ul_lcdif.c
new file mode 100644
index 0000000000..33cd00fbe1
--- /dev/null
+++ b/hw/display/imx6ul_lcdif.c
@@ -0,0 +1,453 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * i.MX6UL LCDIF controller
+ *
+ * Copyright (c) 2026 Yucai Liu <1486344514@qq.com>
+ */
+
+#include "qemu/osdep.h"
+#include "hw/display/imx6ul_lcdif.h"
+#include "hw/core/irq.h"
+#include "hw/core/registerfields.h"
+#include "hw/display/framebuffer.h"
+#include "migration/vmstate.h"
+#include "system/address-spaces.h"
+#include "qemu/module.h"
+#include "qemu/units.h"
+#include "ui/pixel_ops.h"
+
+#define LCDIF_MMIO_SIZE             (16 * KiB)
+#define LCDIF_RESET_CTRL1           0x000f0000
+
+REG32(CTRL, 0x00)
+    FIELD(CTRL, RUN, 0, 1)
+    FIELD(CTRL, WORD_LENGTH, 8, 2)
+REG32(CTRL1, 0x10)
+    FIELD(CTRL1, CUR_FRAME_DONE_IRQ, 9, 1)
+    FIELD(CTRL1, CUR_FRAME_DONE_IRQ_EN, 13, 1)
+    FIELD(CTRL1, BYTE_PACKING_FORMAT, 16, 4)
+REG32(V4_TRANSFER_COUNT, 0x30)
+    FIELD(V4_TRANSFER_COUNT, H_COUNT, 0, 16)
+    FIELD(V4_TRANSFER_COUNT, V_COUNT, 16, 16)
+REG32(V4_CUR_BUF, 0x40)
+REG32(V4_NEXT_BUF, 0x50)
+REG32(AS_NEXT_BUF, 0x230)
+
+#define REG_SET                     0x4
+#define REG_CLR                     0x8
+#define REG_TOG                     0xc
+
+#define CTRL_WORD_LENGTH_16         0
+#define CTRL_WORD_LENGTH_24         3
+
+#define FRAME_PERIOD_NS             (16 * 1000 * 1000ULL)
+
+enum IMX6ULLCDIFReg {
+    IMX6UL_LCDIF_REG_CTRL = A_CTRL >> 4,
+    IMX6UL_LCDIF_REG_CTRL1 = A_CTRL1 >> 4,
+    IMX6UL_LCDIF_REG_V4_TRANSFER_COUNT = A_V4_TRANSFER_COUNT >> 4,
+    IMX6UL_LCDIF_REG_V4_CUR_BUF = A_V4_CUR_BUF >> 4,
+    IMX6UL_LCDIF_REG_V4_NEXT_BUF = A_V4_NEXT_BUF >> 4,
+    IMX6UL_LCDIF_REG_AS_NEXT_BUF = A_AS_NEXT_BUF >> 4,
+};
+
+static inline bool imx6ul_lcdif_reg_exists(hwaddr reg)
+{
+    return (reg >> 4) < IMX6UL_LCDIF_REGS_NUM;
+}
+
+static inline bool imx6ul_lcdif_reg_has_setclr(hwaddr reg)
+{
+    switch (reg) {
+    case A_CTRL:
+    case A_CTRL1:
+        return true;
+    default:
+        return false;
+    }
+}
+
+static inline bool imx6ul_lcdif_is_running(IMX6ULLCDIFState *s)
+{
+    uint32_t ctrl = s->regs[IMX6UL_LCDIF_REG_CTRL];
+
+    return FIELD_EX32(ctrl, CTRL, RUN);
+}
+
+static inline bool imx6ul_lcdif_frame_done_pending(IMX6ULLCDIFState *s)
+{
+    uint32_t ctrl1 = s->regs[IMX6UL_LCDIF_REG_CTRL1];
+
+    return FIELD_EX32(ctrl1, CTRL1, CUR_FRAME_DONE_IRQ);
+}
+
+static void imx6ul_lcdif_schedule_frame(IMX6ULLCDIFState *s)
+{
+    int64_t now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
+
+    timer_mod(s->frame_timer, now + FRAME_PERIOD_NS);
+}
+
+static void imx6ul_lcdif_maybe_schedule_frame(IMX6ULLCDIFState *s)
+{
+    if (imx6ul_lcdif_is_running(s) && !imx6ul_lcdif_frame_done_pending(s)) {
+        imx6ul_lcdif_schedule_frame(s);
+    } else {
+        timer_del(s->frame_timer);
+    }
+}
+
+static void imx6ul_lcdif_update_irq(IMX6ULLCDIFState *s)
+{
+    uint32_t ctrl1 = s->regs[IMX6UL_LCDIF_REG_CTRL1];
+    bool level = FIELD_EX32(ctrl1, CTRL1, CUR_FRAME_DONE_IRQ_EN) &&
+                 FIELD_EX32(ctrl1, CTRL1, CUR_FRAME_DONE_IRQ);
+
+    qemu_set_irq(s->irq, level);
+}
+
+static void imx6ul_lcdif_frame_done(IMX6ULLCDIFState *s)
+{
+    uint32_t ctrl1 = s->regs[IMX6UL_LCDIF_REG_CTRL1];
+
+    ctrl1 = FIELD_DP32(ctrl1, CTRL1, CUR_FRAME_DONE_IRQ, 1);
+    s->regs[IMX6UL_LCDIF_REG_CTRL1] = ctrl1;
+    imx6ul_lcdif_update_irq(s);
+}
+
+static void imx6ul_lcdif_draw_line_rgb565(void *opaque, uint8_t *dst,
+                                          const uint8_t *src, int width,
+                                          int dststep)
+{
+    uint32_t *dst32 = (uint32_t *)dst;
+    int i;
+
+    for (i = 0; i < width; i++) {
+        uint16_t pixel = lduw_le_p(src);
+        uint8_t r = ((pixel >> 11) & 0x1f) << 3;
+        uint8_t g = ((pixel >> 5) & 0x3f) << 2;
+        uint8_t b = (pixel & 0x1f) << 3;
+
+        *dst32++ = rgb_to_pixel32(r, g, b);
+        src += 2;
+    }
+}
+
+static void imx6ul_lcdif_draw_line_xrgb8888(void *opaque, uint8_t *dst,
+                                            const uint8_t *src, int width,
+                                            int dststep)
+{
+    uint32_t *dst32 = (uint32_t *)dst;
+    int i;
+
+    for (i = 0; i < width; i++) {
+        uint32_t pixel = ldl_le_p(src);
+        uint8_t r = (pixel >> 16) & 0xff;
+        uint8_t g = (pixel >> 8) & 0xff;
+        uint8_t b = pixel & 0xff;
+
+        *dst32++ = rgb_to_pixel32(r, g, b);
+        src += 4;
+    }
+}
+
+static void imx6ul_lcdif_update_display(void *opaque)
+{
+    IMX6ULLCDIFState *s = opaque;
+    DisplaySurface *surface = qemu_console_surface(s->con);
+    uint32_t transfer_count = s->regs[IMX6UL_LCDIF_REG_V4_TRANSFER_COUNT];
+    uint32_t width = FIELD_EX32(transfer_count, V4_TRANSFER_COUNT, H_COUNT);
+    uint32_t height = FIELD_EX32(transfer_count, V4_TRANSFER_COUNT, V_COUNT);
+    uint32_t ctrl = s->regs[IMX6UL_LCDIF_REG_CTRL];
+    uint32_t frame_base = s->regs[IMX6UL_LCDIF_REG_V4_CUR_BUF];
+    drawfn fn;
+    int first = 0;
+    int last = 0;
+    int src_width;
+
+    if (!imx6ul_lcdif_is_running(s) || width == 0 || height == 0) {
+        return;
+    }
+
+    switch (FIELD_EX32(ctrl, CTRL, WORD_LENGTH)) {
+    case CTRL_WORD_LENGTH_16:
+        s->src_bpp = 2;
+        fn = imx6ul_lcdif_draw_line_rgb565;
+        break;
+    case CTRL_WORD_LENGTH_24:
+        s->src_bpp = 4;
+        fn = imx6ul_lcdif_draw_line_xrgb8888;
+        break;
+    default:
+        return;
+    }
+
+    if (surface_width(surface) != width || surface_height(surface) != height) {
+        qemu_console_resize(s->con, width, height);
+        surface = qemu_console_surface(s->con);
+        s->invalidate = true;
+    }
+
+    src_width = width * s->src_bpp;
+    if (s->invalidate || s->fb_base != frame_base ||
+        s->src_width != src_width || s->rows != height) {
+        framebuffer_update_memory_section(&s->fbsection, get_system_memory(),
+                                          frame_base, height, src_width);
+        s->fb_base = frame_base;
+        s->src_width = src_width;
+        s->rows = height;
+    }
+
+    framebuffer_update_display(surface, &s->fbsection, width, height,
+                               src_width, surface_stride(surface), 0,
+                               s->invalidate, fn, s, &first, &last);
+    if (first >= 0) {
+        dpy_gfx_update(s->con, 0, first, width, last - first + 1);
+    }
+
+    s->invalidate = false;
+}
+
+static void imx6ul_lcdif_invalidate_display(void *opaque)
+{
+    IMX6ULLCDIFState *s = opaque;
+
+    s->invalidate = true;
+}
+
+static const GraphicHwOps imx6ul_lcdif_graphic_ops = {
+    .invalidate = imx6ul_lcdif_invalidate_display,
+    .gfx_update = imx6ul_lcdif_update_display,
+};
+
+static void imx6ul_lcdif_frame_timer_cb(void *opaque)
+{
+    IMX6ULLCDIFState *s = opaque;
+
+    if (!imx6ul_lcdif_is_running(s) || imx6ul_lcdif_frame_done_pending(s)) {
+        return;
+    }
+
+    imx6ul_lcdif_frame_done(s);
+}
+
+static uint64_t imx6ul_lcdif_read(void *opaque, hwaddr offset, unsigned size)
+{
+    IMX6ULLCDIFState *s = opaque;
+    hwaddr reg = offset & ~0xf;
+    uint32_t idx;
+
+    assert(size == 4);
+    assert(!(offset & 0x3));
+    assert(offset < LCDIF_MMIO_SIZE);
+
+    idx = reg >> 4;
+    if (idx >= ARRAY_SIZE(s->regs)) {
+        return 0;
+    }
+
+    return s->regs[idx];
+}
+
+static void imx6ul_lcdif_write(void *opaque, hwaddr offset,
+                               uint64_t value, unsigned size)
+{
+    IMX6ULLCDIFState *s = opaque;
+    hwaddr reg = offset & ~0xf;
+    uint32_t idx;
+    uint32_t oldv;
+
+    assert(size == 4);
+    assert(!(offset & 0x3));
+    assert(offset < LCDIF_MMIO_SIZE);
+
+    if (!imx6ul_lcdif_reg_exists(reg)) {
+        return;
+    }
+
+    idx = reg >> 4;
+    oldv = s->regs[idx];
+
+    switch (offset & 0xf) {
+    case 0:
+        s->regs[idx] = (uint32_t)value;
+        break;
+    case REG_SET:
+        if (!imx6ul_lcdif_reg_has_setclr(reg)) {
+            return;
+        }
+        s->regs[idx] = oldv | (uint32_t)value;
+        break;
+    case REG_CLR:
+        if (!imx6ul_lcdif_reg_has_setclr(reg)) {
+            return;
+        }
+        s->regs[idx] = oldv & ~(uint32_t)value;
+        break;
+    case REG_TOG:
+        if (!imx6ul_lcdif_reg_has_setclr(reg)) {
+            return;
+        }
+        s->regs[idx] = oldv ^ (uint32_t)value;
+        break;
+    default:
+        g_assert_not_reached();
+    }
+
+    switch (reg) {
+    case A_CTRL:
+        if (!FIELD_EX32(oldv, CTRL, RUN) &&
+            FIELD_EX32(s->regs[idx], CTRL, RUN)) {
+            s->invalidate = true;
+            graphic_hw_invalidate(s->con);
+            imx6ul_lcdif_maybe_schedule_frame(s);
+            break;
+        }
+        if (FIELD_EX32(oldv, CTRL, RUN) &&
+            !FIELD_EX32(s->regs[idx], CTRL, RUN)) {
+            timer_del(s->frame_timer);
+        }
+        break;
+    case A_CTRL1:
+        if (FIELD_EX32(oldv, CTRL1, CUR_FRAME_DONE_IRQ) &&
+            !FIELD_EX32(s->regs[idx], CTRL1, CUR_FRAME_DONE_IRQ)) {
+            imx6ul_lcdif_maybe_schedule_frame(s);
+        }
+        break;
+    case A_V4_TRANSFER_COUNT:
+        s->invalidate = true;
+        graphic_hw_invalidate(s->con);
+        break;
+    case A_V4_CUR_BUF:
+        s->invalidate = true;
+        graphic_hw_invalidate(s->con);
+        break;
+    case A_V4_NEXT_BUF:
+        s->regs[IMX6UL_LCDIF_REG_V4_CUR_BUF] = s->regs[idx];
+        imx6ul_lcdif_frame_done(s);
+        s->invalidate = true;
+        graphic_hw_invalidate(s->con);
+        imx6ul_lcdif_maybe_schedule_frame(s);
+        return;
+    case A_AS_NEXT_BUF:
+        imx6ul_lcdif_frame_done(s);
+        imx6ul_lcdif_maybe_schedule_frame(s);
+        return;
+    default:
+        break;
+    }
+
+    imx6ul_lcdif_update_irq(s);
+}
+
+static const MemoryRegionOps imx6ul_lcdif_ops = {
+    .read = imx6ul_lcdif_read,
+    .write = imx6ul_lcdif_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 4,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+static void imx6ul_lcdif_reset(DeviceState *dev)
+{
+    IMX6ULLCDIFState *s = IMX6UL_LCDIF(dev);
+
+    memset(s->regs, 0, sizeof(s->regs));
+    s->regs[IMX6UL_LCDIF_REG_CTRL1] = LCDIF_RESET_CTRL1;
+    s->fb_base = 0;
+    s->src_width = 0;
+    s->rows = 0;
+    s->src_bpp = 0;
+    s->invalidate = true;
+    timer_del(s->frame_timer);
+    imx6ul_lcdif_update_irq(s);
+}
+
+static int imx6ul_lcdif_post_load(void *opaque, int version_id)
+{
+    IMX6ULLCDIFState *s = opaque;
+
+    s->fb_base = 0;
+    s->src_width = 0;
+    s->rows = 0;
+    s->src_bpp = 0;
+    s->invalidate = true;
+
+    imx6ul_lcdif_update_irq(s);
+    if (imx6ul_lcdif_is_running(s) &&
+        !imx6ul_lcdif_frame_done_pending(s) &&
+        !timer_pending(s->frame_timer)) {
+        imx6ul_lcdif_schedule_frame(s);
+    }
+
+    return 0;
+}
+
+static const VMStateDescription vmstate_imx6ul_lcdif = {
+    .name = TYPE_IMX6UL_LCDIF,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .post_load = imx6ul_lcdif_post_load,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT32_ARRAY(regs, IMX6ULLCDIFState, IMX6UL_LCDIF_REGS_NUM),
+        VMSTATE_TIMER_PTR(frame_timer, IMX6ULLCDIFState),
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void imx6ul_lcdif_realize(DeviceState *dev, Error **errp)
+{
+    IMX6ULLCDIFState *s = IMX6UL_LCDIF(dev);
+
+    s->frame_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
+                                  imx6ul_lcdif_frame_timer_cb, s);
+    s->invalidate = true;
+    memory_region_init_io(&s->iomem, OBJECT(dev), &imx6ul_lcdif_ops, s,
+                          TYPE_IMX6UL_LCDIF, LCDIF_MMIO_SIZE);
+    sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->iomem);
+    sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);
+    s->con = graphic_console_init(dev, 0, &imx6ul_lcdif_graphic_ops, s);
+}
+
+static void imx6ul_lcdif_unrealize(DeviceState *dev)
+{
+    IMX6ULLCDIFState *s = IMX6UL_LCDIF(dev);
+
+    timer_del(s->frame_timer);
+    timer_free(s->frame_timer);
+    s->frame_timer = NULL;
+
+    if (s->con) {
+        graphic_console_close(s->con);
+        s->con = NULL;
+    }
+}
+
+static void imx6ul_lcdif_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = imx6ul_lcdif_realize;
+    dc->unrealize = imx6ul_lcdif_unrealize;
+    dc->vmsd = &vmstate_imx6ul_lcdif;
+    device_class_set_legacy_reset(dc, imx6ul_lcdif_reset);
+    dc->desc = "i.MX6UL LCDIF";
+}
+
+static const TypeInfo imx6ul_lcdif_info = {
+    .name = TYPE_IMX6UL_LCDIF,
+    .parent = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(IMX6ULLCDIFState),
+    .class_init = imx6ul_lcdif_class_init,
+};
+
+static void imx6ul_lcdif_register_types(void)
+{
+    type_register_static(&imx6ul_lcdif_info);
+}
+
+type_init(imx6ul_lcdif_register_types)
diff --git a/hw/display/meson.build b/hw/display/meson.build
index 90e6c041bd..9b0b1ddf63 100644
--- a/hw/display/meson.build
+++ b/hw/display/meson.build
@@ -11,6 +11,7 @@ system_ss.add(when: ['CONFIG_VGA_CIRRUS', 'CONFIG_VGA_ISA'], if_true: files('cir
 system_ss.add(when: 'CONFIG_G364FB', if_true: files('g364fb.c'))
 system_ss.add(when: 'CONFIG_JAZZ_LED', if_true: files('jazz_led.c'))
 system_ss.add(when: 'CONFIG_PL110', if_true: files('pl110.c'))
+system_ss.add(when: 'CONFIG_IMX6UL_LCDIF', if_true: files('imx6ul_lcdif.c'))
 system_ss.add(when: 'CONFIG_SII9022', if_true: files('sii9022.c'))
 system_ss.add(when: 'CONFIG_SSD0303', if_true: files('ssd0303.c'))
 system_ss.add(when: 'CONFIG_SSD0323', if_true: files('ssd0323.c'))
diff --git a/include/hw/display/imx6ul_lcdif.h b/include/hw/display/imx6ul_lcdif.h
new file mode 100644
index 0000000000..42fee2fd1d
--- /dev/null
+++ b/include/hw/display/imx6ul_lcdif.h
@@ -0,0 +1,37 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * i.MX6UL LCDIF controller
+ *
+ * Copyright (c) 2026 Yucai Liu <1486344514@qq.com>
+ */
+
+#ifndef IMX6UL_LCDIF_H
+#define IMX6UL_LCDIF_H
+
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+#include "qemu/timer.h"
+#include "ui/console.h"
+
+#define TYPE_IMX6UL_LCDIF "imx6ul-lcdif"
+#define IMX6UL_LCDIF_REGS_NUM ((0x230 >> 4) + 1)
+OBJECT_DECLARE_SIMPLE_TYPE(IMX6ULLCDIFState, IMX6UL_LCDIF)
+
+struct IMX6ULLCDIFState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion iomem;
+    MemoryRegionSection fbsection;
+    qemu_irq irq;
+    QemuConsole *con;
+    QEMUTimer *frame_timer;
+    uint32_t fb_base;
+    uint32_t src_width;
+    uint32_t rows;
+    uint8_t src_bpp;
+    bool invalidate;
+    uint32_t regs[IMX6UL_LCDIF_REGS_NUM];
+};
+
+#endif /* IMX6UL_LCDIF_H */
-- 
2.53.0



  reply	other threads:[~2026-04-12 11:03 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-12 11:02 [PATCH v3 0/2] i.MX6UL LCDIF device model and SoC wiring Yucai Liu
2026-04-12 11:02 ` Yucai Liu [this message]
2026-04-12 11:02 ` [PATCH v3 2/2] hw/arm/fsl-imx6ul: Wire in the LCDIF device model Yucai Liu

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-all 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=20260412110240.93116-2-yangyanglan718@gmail.com \
    --to=yangyanglan718@gmail.com \
    --cc=jcd@tribudubois.net \
    --cc=pbonzini@redhat.com \
    --cc=peter.maydell@linaro.org \
    --cc=qemu-arm@nongnu.org \
    --cc=qemu-devel@nongnu.org \
    /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