//***************************************************************************
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements.  See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.  The
// ASF licenses this file to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance with the
// License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
// License for the specific language governing permissions and limitations
// under the License.
//
//***************************************************************************

//! PinePhone Display Engine Driver for Apache NuttX RTOS, based on NuttX Framebuffers:
//! https://github.com/apache/incubator-nuttx/blob/master/include/nuttx/video/fb.h
//! "DE Page ???" refers to Allwinner Display Engine 2.0 Specification: https://linux-sunxi.org/images/7/7b/Allwinner_DE2.0_Spec_V1.0.pdf
//! "A64 Page ???" refers to Allwinner A64 User Manual: https://github.com/lupyuen/pinephone-nuttx/releases/download/doc/Allwinner_A64_User_Manual_V1.1.pdf
//! "A31 Page ???" refers to Allwinner A31 User Manual: https://github.com/lupyuen/pinephone-nuttx/releases/download/doc/A31_User_Manual_v1.3_20150510.pdf

/// Import the Zig Standard Library
const std = @import("std");

/// Import the MIPI Display Serial Interface Module
const dsi = @import("./display.zig");

/// Import the MIPI Display Physical Layer Module
const dphy = @import("./dphy.zig");

/// Import the Timing Controller Module
const tcon = @import("./tcon.zig");

/// Import the Backlight Module
const backlight = @import("./backlight.zig");

/// Import the Power Management IC Module
const pmic = @import("./pmic.zig");

/// Import the LCD Panel Module
const panel = @import("./panel.zig");

/// Import NuttX Functions from C
const c = @cImport({
    // NuttX Defines
    @cDefine("__NuttX__",  "");
    @cDefine("NDEBUG",     "");
    @cDefine("FAR",        "");

    // NuttX Framebuffer Defines
    @cDefine("CONFIG_FB_OVERLAY", "");

    // NuttX Header Files
    @cInclude("arch/types.h");
    @cInclude("../../nuttx/include/limits.h");
    @cInclude("nuttx/config.h");
    @cInclude("sys/ioctl.h");
    @cInclude("inttypes.h");
    @cInclude("unistd.h");
    @cInclude("stdlib.h");
    @cInclude("stdio.h");
    @cInclude("fcntl.h");
    @cInclude("nuttx/leds/userled.h");

    // NuttX Framebuffer Header Files
    @cInclude("nuttx/video/fb.h");
});

/// Render a Test Pattern on PinePhone's Display.
/// Calls Allwinner A64 Display Engine, Timing Controller and MIPI Display Serial Interface.
/// See https://lupyuen.github.io/articles/de#appendix-programming-the-allwinner-a64-display-engine
fn renderGraphics(
    comptime channels: u8  // Number of UI Channels to render: 1 or 3
) void {
    debug("renderGraphics: start", .{});
    defer { debug("renderGraphics: end", .{}); }

    // Validate the Framebuffer Sizes at Compile Time
    comptime {
        assert(channels == 1 or channels == 3);
        assert(planeInfo.xres_virtual == videoInfo.xres);
        assert(planeInfo.yres_virtual == videoInfo.yres);
        assert(planeInfo.fblen  == planeInfo.xres_virtual * planeInfo.yres_virtual * 4);
        assert(planeInfo.stride == planeInfo.xres_virtual * 4);
        assert(overlayInfo[0].fblen  == @intCast(usize, overlayInfo[0].sarea.w) * overlayInfo[0].sarea.h * 4);
        assert(overlayInfo[0].stride == overlayInfo[0].sarea.w * 4);
        assert(overlayInfo[1].fblen  == @intCast(usize, overlayInfo[1].sarea.w) * overlayInfo[1].sarea.h * 4);
        assert(overlayInfo[1].stride == overlayInfo[1].sarea.w * 4);
    }

    // TODO: Handle DMB non-relaxed write
    // https://developer.arm.com/documentation/dui0489/c/arm-and-thumb-instructions/miscellaneous-instructions/dmb--dsb--and-isb

    // Init Framebuffer 0:
    // Fill with Blue, Green and Red
    var i: usize = 0;
    while (i < fb0.len) : (i += 1) {
        // Colours are in XRGB 8888 format
        if (i < fb0.len / 4) {
            // Blue for top quarter
            fb0[i] = 0x8000_0080;
        } else if (i < fb0.len / 2) {
            // Green for next quarter
            fb0[i] = 0x8000_8000;
        } else {
            // Red for lower half
            fb0[i] = 0x8080_0000;
        }
    }

    // Init Framebuffer 1:
    // Fill with Semi-Transparent Blue
    i = 0;
    while (i < fb1.len) : (i += 1) {
        // Colours are in ARGB 8888 format
        fb1[i] = 0x8000_0080;
    }

    // Init Framebuffer 2:
    // Fill with Semi-Transparent Green Circle
    var y: usize = 0;
    while (y < PANEL_HEIGHT) : (y += 1) {
        var x: usize = 0;
        while (x < PANEL_WIDTH) : (x += 1) {
            // Get pixel index
            const p = (y * PANEL_WIDTH) + x;
            assert(p < fb2.len);

            // Shift coordinates so that centre of screen is (0,0)
            const half_width  = PANEL_WIDTH  / 2;
            const half_height = PANEL_HEIGHT / 2;
            const x_shift = @intCast(isize, x) - half_width;
            const y_shift = @intCast(isize, y) - half_height;

            // If x^2 + y^2 < radius^2, set the pixel to Semi-Transparent Green
            if (x_shift*x_shift + y_shift*y_shift < half_width*half_width) {
                fb2[p] = 0x8000_8000;  // Semi-Transparent Green in ARGB 8888 Format
            } else {  // Otherwise set to Transparent Black
                fb2[p] = 0x0000_0000;  // Transparent Black in ARGB 8888 Format
            }
        }
    }

    // Init the UI Blender for PinePhone's A64 Display Engine
    initUiBlender();

    // Init the Base UI Channel
    initUiChannel(
        1,  // UI Channel Number (1 for Base UI Channel)
        planeInfo.fbmem,    // Start of frame buffer memory
        planeInfo.fblen,    // Length of frame buffer memory in bytes
        planeInfo.stride,   // Length of a line in bytes (4 bytes per pixel)
        planeInfo.xres_virtual,  // Horizontal resolution in pixel columns
        planeInfo.yres_virtual,  // Vertical resolution in pixel rows
        planeInfo.xoffset,  // Horizontal offset in pixel columns
        planeInfo.yoffset,  // Vertical offset in pixel rows
    );

    // Init the 2 Overlay UI Channels
    inline for (overlayInfo) | ov, ov_index | {
        initUiChannel(
            @intCast(u8, ov_index + 2),  // UI Channel Number (2 and 3 for Overlay UI Channels)
            if (channels == 3) ov.fbmem else null,  // Start of frame buffer memory
            ov.fblen,    // Length of frame buffer memory in bytes
            ov.stride,   // Length of a line in bytes (4 bytes per pixel)
            ov.sarea.w,  // Horizontal resolution in pixel columns
            ov.sarea.h,  // Vertical resolution in pixel rows
            ov.sarea.x,  // Horizontal offset in pixel columns
            ov.sarea.y,  // Vertical offset in pixel rows
        );
    }

    // Set UI Blender Route, enable Blender Pipes and apply the settings
    applySettings(channels);
}

/// Render a Test Pattern on PinePhone's Display.
/// Called by test_display() in https://github.com/lupyuen/incubator-nuttx-apps/blob/de3/examples/hello/test_display.c
pub export fn test_render(
    channels: c_int  // Number of UI Channels to render: 0, 1 or 3
) void {
    debug("test_render: start, channels={}", .{ channels });
    defer { debug("test_render: end", .{}); }

    // Turn on Display Backlight
    if (channels != 0) {
        backlight.backlight_enable(90);
    }

    // Init PMIC not needed. Maybe already done by U-Boot?
    // https://megous.com/git/p-boot/tree/src/pmic.c#n279

    // Init Timing Controller TCON0
    tcon.tcon0_init();

    // Init Power Mgmt IC
    pmic.display_board_init();

    // Enable MIPI DSI Block
    dsi.enable_dsi_block();

    // Enable MIPI Display Physical Layer
    dphy.dphy_enable();

    // Reset LCD Panel
    panel.panel_reset();

    // Init LCD Panel
    dsi.panel_init();

    // Start MIPI DSI HSC and HSD
    dsi.start_dsi();

    // Init Display Engine
    de2_init();

    // Wait a while
    _ = c.usleep(160000);

    // Render Graphics with Display Engine
    switch (channels) {
        0 => renderGraphics(3),  // Render 3 UI Channels
        1 => renderGraphics(1),  // Render 1 UI Channel
        3 => renderGraphics(3),  // Render 3 UI Channels
        else => debug("Argument must be 1 or 3", .{}),
    }
}

/// Hardware Registers for PinePhone's A64 Display Engine.
/// See https://lupyuen.github.io/articles/de#appendix-overview-of-allwinner-a64-display-engine
/// Display Engine Base Address is 0x0100 0000 (DE Page 24)
const DISPLAY_ENGINE_BASE_ADDRESS = 0x0100_0000;

/// MIXER0 is at DE Offset 0x10 0000 (DE Page 24, 0x110 0000)
const MIXER0_BASE_ADDRESS = DISPLAY_ENGINE_BASE_ADDRESS + 0x10_0000;

/// GLB (Global Registers) is at MIXER0 Offset 0x0000 (DE Page 90, 0x110 0000)
const GLB_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x0000;

/// BLD (Blender) is at MIXER0 Offset 0x1000 (DE Page 90, 0x110 1000)
const BLD_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x1000;

/// OVL_UI(CH1) (UI Overlay 1) is at MIXER0 Offset 0x3000 (DE Page 102, 0x110 3000)
const OVL_UI_CH1_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x3000;

/// UI_SCALER1(CH1) (UI Scaler 1) is at MIXER0 Offset 0x04 0000 (DE Page 90, 0x114 0000)
const UI_SCALER1_CH1_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x04_0000;

/// Initialise the UI Blender for PinePhone's A64 Display Engine.
/// See https://lupyuen.github.io/articles/de#appendix-programming-the-allwinner-a64-display-engine
fn initUiBlender() void {
    debug("initUiBlender: start", .{});
    defer { debug("initUiBlender: end", .{}); }

    // Set Blender Background
    // BLD_BK_COLOR (Blender Background Color) at BLD Offset 0x88
    // Set to 0xFF00 0000 (Black Background Color)
    // RESERVED (Bits 24 to 31) = 0xFF (Undocumented)
    // RED   (Bits 16 to 23) = 0
    // GREEN (Bits 8  to 15) = 0
    // BLUE  (Bits 0  to 7)  = 0
    // (DE Page 109, 0x110 1088)
    debug("Set Blender Background", .{});
    const RESERVED: u32 = 0xFF << 24;
    const RED:      u24 = 0    << 16;
    const GREEN:    u16 = 0    << 8;
    const BLUE:     u8  = 0    << 0;
    const color = RESERVED
        | RED
        | GREEN
        | BLUE;
    comptime{ assert(color == 0xFF00_0000); }

    const BLD_BK_COLOR = BLD_BASE_ADDRESS + 0x88;
    comptime{ assert(BLD_BK_COLOR == 0x110_1088); }
    putreg32(color, BLD_BK_COLOR);

    // Set Blender Pre-Multiply
    // BLD_PREMUL_CTL (Blender Pre-Multiply Control) at BLD Offset 0x84
    // Set to 0 (No Pre-Multiply for Alpha, Pipes 0 to 3)
    // P3_ALPHA_MODE (Bit 3) = 0 (Pipe 3: No Pre-Multiply)
    // P2_ALPHA_MODE (Bit 2) = 0 (Pipe 2: No Pre-Multiply)
    // P1_ALPHA_MODE (Bit 1) = 0 (Pipe 1: No Pre-Multiply)
    // P0_ALPHA_MODE (Bit 0) = 0 (Pipe 0: No Pre-Multiply)
    // (DE Page 109, 0x110 1084)
    debug("Set Blender Pre-Multiply", .{});
    const P3_ALPHA_MODE: u4 = 0 << 3;  // Pipe 3: No Pre-Multiply
    const P2_ALPHA_MODE: u3 = 0 << 2;  // Pipe 2: No Pre-Multiply
    const P1_ALPHA_MODE: u2 = 0 << 1;  // Pipe 1: No Pre-Multiply
    const P0_ALPHA_MODE: u1 = 0 << 0;  // Pipe 0: No Pre-Multiply
    const premultiply = P3_ALPHA_MODE
        | P2_ALPHA_MODE
        | P1_ALPHA_MODE
        | P0_ALPHA_MODE;
    comptime{ assert(premultiply == 0); }

    const BLD_PREMUL_CTL = BLD_BASE_ADDRESS + 0x84;
    comptime{ assert(BLD_PREMUL_CTL == 0x110_1084); }
    putreg32(premultiply, BLD_PREMUL_CTL);
}

/// Set UI Blender Route, enable Blender Pipes and apply the settings for PinePhone's A64 Display Engine.
/// See https://lupyuen.github.io/articles/de#appendix-programming-the-allwinner-a64-display-engine
fn applySettings(
    comptime channels: u8  // Number of enabled UI Channels
) void {
    debug("applySettings: start", .{});
    defer { debug("applySettings: end", .{}); }
    comptime { assert(channels == 1 or channels == 3); }

    // Set Blender Route
    // BLD_CH_RTCTL (Blender Routing Control) at BLD Offset 0x080
    // If Rendering 3 UI Channels: Set to 0x321 (DMB)
    //   P2_RTCTL (Bits 8 to 11) = 3 (Pipe 2 from Channel 3)
    //   P1_RTCTL (Bits 4 to 7)  = 2 (Pipe 1 from Channel 2)
    //   P0_RTCTL (Bits 0 to 3)  = 1 (Pipe 0 from Channel 1)
    // If Rendering 1 UI Channel: Set to 1 (DMB)
    //   P0_RTCTL (Bits 0 to 3) = 1 (Pipe 0 from Channel 1)
    // (DE Page 108, 0x110 1080)
    debug("Set Blender Route", .{});
    const P2_RTCTL: u12 = switch (channels) {  // For Pipe 2...
        3 => 3,  // 3 UI Channels: Select Pipe 2 from UI Channel 3
        1 => 0,  // 1 UI Channel:  Unused Pipe 2
        else => unreachable,
    } << 8;  // Bits 8 to 11

    const P1_RTCTL: u8 = switch (channels) {  // For Pipe 1...
        3 => 2,  // 3 UI Channels: Select Pipe 1 from UI Channel 2
        1 => 0,  // 1 UI Channel:  Unused Pipe 1
        else => unreachable,
    } << 4;  // Bits 4 to 7

    const P0_RTCTL: u4 = 1 << 0;  // Select Pipe 0 from UI Channel 1
    const route = P2_RTCTL
        | P1_RTCTL
        | P0_RTCTL;
    comptime{ assert(route == 0x321 or route == 1); }

    const BLD_CH_RTCTL = BLD_BASE_ADDRESS + 0x080;
    comptime{ assert(BLD_CH_RTCTL == 0x110_1080); }
    putreg32(route, BLD_CH_RTCTL);  // TODO: DMB

    // Enable Blender Pipes
    // BLD_FILL_COLOR_CTL (Blender Fill Color Control) at BLD Offset 0x000
    // If Rendering 3 UI Channels: Set to 0x701 (DMB)
    //   P2_EN   (Bit 10) = 1 (Enable Pipe 2)
    //   P1_EN   (Bit 9)  = 1 (Enable Pipe 1)
    //   P0_EN   (Bit 8)  = 1 (Enable Pipe 0)
    //   P0_FCEN (Bit 0)  = 1 (Enable Pipe 0 Fill Color)
    // If Rendering 1 UI Channel: Set to 0x101 (DMB)
    //   P0_EN   (Bit 8)  = 1 (Enable Pipe 0)
    //   P0_FCEN (Bit 0)  = 1 (Enable Pipe 0 Fill Color)
    // (DE Page 106, 0x110 1000)
    debug("Enable Blender Pipes", .{});
    const P2_EN: u11 = switch (channels) {  // For Pipe 2...
        3 => 1,  // 3 UI Channels: Enable Pipe 2
        1 => 0,  // 1 UI Channel:  Disable Pipe 2
        else => unreachable,
    } << 10;  // Bit 10

    const P1_EN: u10 = switch (channels) {  // For Pipe 1...
        3 => 1,  // 3 UI Channels: Enable Pipe 1
        1 => 0,  // 1 UI Channel:  Disable Pipe 1
        else => unreachable,
    } << 9;  // Bit 9

    const P0_EN:   u9 = 1 << 8;  // Enable Pipe 0
    const P0_FCEN: u1 = 1 << 0;  // Enable Pipe 0 Fill Color
    const fill = P2_EN
        | P1_EN
        | P0_EN
        | P0_FCEN;
    comptime{ assert(fill == 0x701 or fill == 0x101); }

    const BLD_FILL_COLOR_CTL = BLD_BASE_ADDRESS + 0x000;
    comptime{ assert(BLD_FILL_COLOR_CTL == 0x110_1000); }
    putreg32(fill, BLD_FILL_COLOR_CTL);  // TODO: DMB

    // Apply Settings
    // GLB_DBUFFER (Global Double Buffer Control) at GLB Offset 0x008
    // Set to 1 (DMB)
    // DOUBLE_BUFFER_RDY (Bit 0) = 1
    // (Register Value is ready for update)
    // (DE Page 93, 0x110 0008)
    debug("Apply Settings", .{});
    const DOUBLE_BUFFER_RDY: u1 = 1 << 0;  // Register Value is ready for update
    comptime{ assert(DOUBLE_BUFFER_RDY == 1); }

    const GLB_DBUFFER = GLB_BASE_ADDRESS + 0x008;
    comptime{ assert(GLB_DBUFFER == 0x110_0008); }
    putreg32(DOUBLE_BUFFER_RDY, GLB_DBUFFER);  // TODO: DMB
}

/// Initialise a UI Channel for PinePhone's A64 Display Engine.
/// We use 3 UI Channels: Base UI Channel (#1) plus 2 Overlay UI Channels (#2, #3).
/// See https://lupyuen.github.io/articles/de#appendix-programming-the-allwinner-a64-display-engine
fn initUiChannel(
    comptime channel: u8,   // UI Channel Number: 1, 2 or 3
    fbmem: ?*anyopaque,     // Start of frame buffer memory, or null if this channel should be disabled
    comptime fblen: usize,           // Length of frame buffer memory in bytes
    comptime stride:  c.fb_coord_t,  // Length of a line in bytes (4 bytes per pixel)
    comptime xres:    c.fb_coord_t,  // Horizontal resolution in pixel columns
    comptime yres:    c.fb_coord_t,  // Vertical resolution in pixel rows
    comptime xoffset: c.fb_coord_t,  // Horizontal offset in pixel columns
    comptime yoffset: c.fb_coord_t,  // Vertical offset in pixel rows
) void {
    debug("initUiChannel: start", .{});
    defer { debug("initUiChannel: end", .{}); }

    // Validate Framebuffer Size and Stride at Compile Time
    comptime {
        assert(channel >= 1 and channel <= 3);
        assert(fblen == @intCast(usize, xres) * yres * 4);
        assert(stride == @intCast(usize, xres) * 4);
    }

    // OVL_UI(CH1) (UI Overlay 1) is at MIXER0 Offset 0x3000
    // OVL_UI(CH2) (UI Overlay 2) is at MIXER0 Offset 0x4000
    // OVL_UI(CH3) (UI Overlay 3) is at MIXER0 Offset 0x5000
    // (DE Page 102, 0x110 3000 / 0x110 4000 / 0x110 5000)
    const OVL_UI_BASE_ADDRESS = OVL_UI_CH1_BASE_ADDRESS
        + @intCast(u64, channel - 1) * 0x1000;
    comptime{ assert(OVL_UI_BASE_ADDRESS == 0x110_3000 or OVL_UI_BASE_ADDRESS == 0x110_4000 or OVL_UI_BASE_ADDRESS == 0x110_5000); }

    // UI_SCALER1(CH1) is at MIXER0 Offset 0x04 0000
    // UI_SCALER2(CH2) is at MIXER0 Offset 0x05 0000
    // UI_SCALER3(CH3) is at MIXER0 Offset 0x06 0000
    // (DE Page 90, 0x114 0000 / 0x115 0000 / 0x116 0000)
    const UI_SCALER_BASE_ADDRESS = UI_SCALER1_CH1_BASE_ADDRESS
        + @intCast(u64, channel - 1) * 0x10000;

    // If UI Channel should be disabled...
    if (fbmem == null) {
        // Disable Overlay and Pipe:
        // OVL_UI_ATTR_CTL (UI Overlay Attribute Control) at OVL_UI Offset 0x00
        // Set to 0 (Disable UI Overlay Channel)
        // LAY_EN (Bit 0) = 0 (Disable Layer)
        // (DE Page 102)
        debug("Channel {}: Disable Overlay and Pipe", .{ channel });
        const OVL_UI_ATTR_CTL = OVL_UI_BASE_ADDRESS + 0x00;
        comptime{ assert(OVL_UI_ATTR_CTL == 0x110_3000 or OVL_UI_ATTR_CTL == 0x110_4000 or OVL_UI_ATTR_CTL == 0x110_5000); }
        putreg32(0, OVL_UI_ATTR_CTL);

        // Disable Scaler:
        // UIS_CTRL_REG at Offset 0 of UI_SCALER1(CH1) or UI_SCALER2(CH2) or UI_SCALER3(CH3)
        // Set to 0 (Disable UI Scaler)
        // EN (Bit 0) = 0 (Disable UI Scaler)
        // (DE Page 66)
        debug("Channel {}: Disable Scaler", .{ channel });
        const UIS_CTRL_REG = UI_SCALER_BASE_ADDRESS + 0;
        comptime{ assert(UIS_CTRL_REG == 0x114_0000 or UIS_CTRL_REG == 0x115_0000 or UIS_CTRL_REG == 0x116_0000); }
        putreg32(0, UIS_CTRL_REG);
        
        // Skip to next UI Channel
        return;
    }

    // Set Overlay (Assume Layer = 0)
    // OVL_UI_ATTR_CTL (UI Overlay Attribute Control) at OVL_UI Offset 0x00
    // For Channel 1: Set to 0xFF00 0405
    // For Channel 2: Set to 0xFF00 0005
    // For Channel 3: Set to 0x7F00 0005
    // LAY_GLBALPHA (Bits 24 to 31) = 0xFF or 0x7F
    //   (Global Alpha Value is Opaque or Semi-Transparent)
    // LAY_FBFMT (Bits 8 to 12) = 4 or 0
    //   (Input Data Format is XRGB 8888 or ARGB 8888)
    // LAY_ALPHA_MODE (Bits 1 to 2) = 2
    //   (Global Alpha is mixed with Pixel Alpha)
    //   (Input Alpha Value = Global Alpha Value * Pixel’s Alpha Value)
    // LAY_EN (Bit 0) = 1 (Enable Layer)
    // (DE Page 102, 0x110 3000 / 0x110 4000 / 0x110 5000)
    debug("Channel {}: Set Overlay ({} x {})", .{ channel, xres, yres });
    const LAY_GLBALPHA: u32 = switch (channel) {  // For Global Alpha Value...
        1 => 0xFF,  // Channel 1: Opaque
        2 => 0xFF,  // Channel 2: Opaque
        3 => 0x7F,  // Channel 3: Semi-Transparent
        else => unreachable,
    } << 24;  // Bits 24 to 31

    const LAY_FBFMT: u13 = switch (channel) {  // For Input Data Format...
        1 => 4,  // Channel 1: XRGB 8888
        2 => 0,  // Channel 2: ARGB 8888
        3 => 0,  // Channel 3: ARGB 8888
        else => unreachable,
    } << 8;  // Bits 8 to 12

    const LAY_ALPHA_MODE: u3 = 2 << 1;  // Global Alpha is mixed with Pixel Alpha
    const LAY_EN:         u1 = 1 << 0;  // Enable Layer
    const attr = LAY_GLBALPHA
        | LAY_FBFMT
        | LAY_ALPHA_MODE
        | LAY_EN;
    comptime{ assert(attr == 0xFF00_0405 or attr == 0xFF00_0005 or attr == 0x7F00_0005); }

    const OVL_UI_ATTR_CTL = OVL_UI_BASE_ADDRESS + 0x00;
    comptime{ assert(OVL_UI_ATTR_CTL == 0x110_3000 or OVL_UI_ATTR_CTL == 0x110_4000 or OVL_UI_ATTR_CTL == 0x110_5000); }
    putreg32(attr, OVL_UI_ATTR_CTL);

    // OVL_UI_TOP_LADD (UI Overlay Top Field Memory Block Low Address) at OVL_UI Offset 0x10
    // Set to Framebuffer Address: fb0, fb1 or fb2
    // (DE Page 104, 0x110 3010 / 0x110 4010 / 0x110 5010)
    const ptr = @ptrToInt(fbmem.?);
    const OVL_UI_TOP_LADD = OVL_UI_BASE_ADDRESS + 0x10;
    comptime{ assert(OVL_UI_TOP_LADD == 0x110_3010 or OVL_UI_TOP_LADD == 0x110_4010 or OVL_UI_TOP_LADD == 0x110_5010); }
    putreg32(@intCast(u32, ptr), OVL_UI_TOP_LADD);

    // OVL_UI_PITCH (UI Overlay Memory Pitch) at OVL_UI Offset 0x0C
    // Set to (width * 4), number of bytes per row
    // (DE Page 104, 0x110 300C / 0x110 400C / 0x110 500C)
    const OVL_UI_PITCH = OVL_UI_BASE_ADDRESS + 0x0C;
    comptime{ assert(OVL_UI_PITCH == 0x110_300C or OVL_UI_PITCH == 0x110_400C or OVL_UI_PITCH == 0x110_500C); }
    putreg32(xres * 4, OVL_UI_PITCH);

    // OVL_UI_MBSIZE (UI Overlay Memory Block Size) at OVL_UI Offset 0x04
    // Set to (height-1) << 16 + (width-1)
    // (DE Page 104, 0x110 3004 / 0x110 4004 / 0x110 5004)
    const height_width: u32 = @intCast(u32, yres - 1) << 16
        | (xres - 1);
    const OVL_UI_MBSIZE = OVL_UI_BASE_ADDRESS + 0x04;
    comptime{ assert(OVL_UI_MBSIZE == 0x110_3004 or OVL_UI_MBSIZE == 0x110_4004 or OVL_UI_MBSIZE == 0x110_5004); }
    putreg32(height_width, OVL_UI_MBSIZE);

    // OVL_UI_SIZE (UI Overlay Overlay Window Size) at OVL_UI Offset 0x88
    // Set to (height-1) << 16 + (width-1)
    // (DE Page 106, 0x110 3088 / 0x110 4088 / 0x110 5088)
    const OVL_UI_SIZE = OVL_UI_BASE_ADDRESS + 0x88;
    comptime{ assert(OVL_UI_SIZE == 0x110_3088 or OVL_UI_SIZE == 0x110_4088 or OVL_UI_SIZE == 0x110_5088); }
    putreg32(height_width, OVL_UI_SIZE);

    // OVL_UI_COOR (UI Overlay Memory Block Coordinate) at OVL_UI Offset 0x08
    // Set to 0 (Overlay at X=0, Y=0)
    // (DE Page 104, 0x110 3008 / 0x110 4008 / 0x110 5008)
    const OVL_UI_COOR = OVL_UI_BASE_ADDRESS + 0x08;
    comptime{ assert(OVL_UI_COOR == 0x110_3008 or OVL_UI_COOR == 0x110_4008 or OVL_UI_COOR == 0x110_5008); }
    putreg32(0, OVL_UI_COOR);

    // For Channel 1: Set Blender Output
    if (channel == 1) {
        // BLD_SIZE (Blender Output Size Setting) at BLD Offset 0x08C
        // Set to (height-1) << 16 + (width-1)
        // (DE Page 110, 0x110 108C)
        debug("Channel {}: Set Blender Output", .{ channel });
        const BLD_SIZE = BLD_BASE_ADDRESS + 0x08C;
        comptime{ assert(BLD_SIZE == 0x110_108C); }
        putreg32(height_width, BLD_SIZE);
                
        // GLB_SIZE (Global Size) at GLB Offset 0x00C
        // Set to (height-1) << 16 + (width-1)
        // (DE Page 93, 0x110 000C)
        const GLB_SIZE = GLB_BASE_ADDRESS + 0x00C;
        comptime{ assert(GLB_SIZE == 0x110_000C); }
        putreg32(height_width, GLB_SIZE);
    }

    // Set Blender Input Pipe (N = Pipe Number, from 0 to 2 for Channels 1 to 3)
    const pipe: u64 = channel - 1;
    debug("Channel {}: Set Blender Input Pipe {} ({} x {})", .{ channel, pipe, xres, yres });

    // Note: DE Page 91 shows incorrect offset N*0x14 for 
    // BLD_CH_ISIZE, BLD_FILL_COLOR and BLD_CH_OFFSET. 
    // Correct offset is N*0x10, see DE Page 108

    // BLD_CH_ISIZE (Blender Input Memory Size) at BLD Offset 0x008 + N*0x10 (N=0,1,2,3,4)
    // Set to (height-1) << 16 + (width-1)
    // (DE Page 108, 0x110 1008 / 0x110 1018 / 0x110 1028)
    const BLD_CH_ISIZE = BLD_BASE_ADDRESS + 0x008 + pipe * 0x10;
    comptime{ assert(BLD_CH_ISIZE == 0x110_1008 or BLD_CH_ISIZE == 0x110_1018 or BLD_CH_ISIZE == 0x110_1028); }
    putreg32(height_width, BLD_CH_ISIZE);

    // BLD_FILL_COLOR (Blender Fill Color) at BLD Offset 0x004 + N*0x10 (N=0,1,2,3,4)
    // Set to 0xFF00 0000 (Opaque Black)
    // ALPHA (Bits 24 to 31) = 0xFF
    // RED   (Bits 16 to 23) = 0
    // GREEN (Bits 8  to 15) = 0
    // BLUE  (Bits 0  to 7)  = 0
    // (DE Page 107, 0x110 1004 / 0x110 1014 / 0x110 1024)
    const ALPHA: u32 = 0xFF << 24;  // Opaque
    const RED:   u24 = 0    << 16;  // Black
    const GREEN: u18 = 0    << 8;
    const BLUE:  u8  = 0    << 0;
    const color = ALPHA
        | RED
        | GREEN
        | BLUE;
    comptime{ assert(color == 0xFF00_0000); }

    const BLD_FILL_COLOR = BLD_BASE_ADDRESS + 0x004 + pipe * 0x10;
    comptime{ assert(BLD_FILL_COLOR == 0x110_1004 or BLD_FILL_COLOR == 0x110_1014 or BLD_FILL_COLOR == 0x110_1024); }
    putreg32(color, BLD_FILL_COLOR);

    // BLD_CH_OFFSET (Blender Input Memory Offset) at BLD Offset 0x00C + N*0x10 (N=0,1,2,3,4)
    // Set to y_offset << 16 + x_offset
    // For Channel 1: Set to 0
    // For Channel 2: Set to 0x34 0034
    // For Channel 3: Set to 0
    // (DE Page 108, 0x110 100C / 0x110 101C / 0x110 102C)
    const offset = @intCast(u32, yoffset) << 16
        | xoffset;
    comptime{ assert(offset == 0 or offset == 0x34_0034); }

    const BLD_CH_OFFSET = BLD_BASE_ADDRESS + 0x00C + pipe * 0x10;
    comptime{ assert(BLD_CH_OFFSET == 0x110_100C or BLD_CH_OFFSET == 0x110_101C or BLD_CH_OFFSET == 0x110_102C); }
    putreg32(offset, BLD_CH_OFFSET);

    // BLD_CTL (Blender Control) at BLD Offset 0x090 + N*4
    // Set to 0x301 0301
    // BLEND_AFD (Bits 24 to 27) = 3
    //   (Coefficient for destination alpha data Q[d] is 1-A[s])
    // BLEND_AFS (Bits 16 to 19) = 1
    //   (Coefficient for source alpha data Q[s] is 1)
    // BLEND_PFD (Bits 8 to 11) = 3
    //   (Coefficient for destination pixel data F[d] is 1-A[s])
    // BLEND_PFS (Bits 0 to 3) = 1
    //   (Coefficient for source pixel data F[s] is 1)
    // (DE Page 110, 0x110 1090 / 0x110 1094 / 0x110 1098)
    const BLEND_AFD: u28 = 3 << 24;  // Coefficient for destination alpha data Q[d] is 1-A[s]
    const BLEND_AFS: u20 = 1 << 16;  // Coefficient for source alpha data Q[s] is 1
    const BLEND_PFD: u12 = 3 << 8;   // Coefficient for destination pixel data F[d] is 1-A[s]
    const BLEND_PFS: u4  = 1 << 0;   // Coefficient for source pixel data F[s] is 1
    const blend = BLEND_AFD
        | BLEND_AFS
        | BLEND_PFD
        | BLEND_PFS;

    const BLD_CTL = BLD_BASE_ADDRESS + 0x090 + pipe * 4;
    comptime{ assert(BLD_CTL == 0x110_1090 or BLD_CTL == 0x110_1094 or BLD_CTL == 0x110_1098); }
    putreg32(blend, BLD_CTL);

    // Disable Scaler (Assume we’re not scaling)
    // UIS_CTRL_REG at Offset 0 of UI_SCALER1(CH1) or UI_SCALER2(CH2) or UI_SCALER3(CH3)
    // Set to 0 (Disable UI Scaler)
    // EN (Bit 0) = 0 (Disable UI Scaler)
    // (DE Page 66, 0x114 0000 / 0x115 0000 / 0x116 0000)
    debug("Channel {}: Disable Scaler", .{ channel });
    const UIS_CTRL_REG = UI_SCALER_BASE_ADDRESS + 0;
    comptime{ assert(UIS_CTRL_REG == 0x114_0000 or UIS_CTRL_REG == 0x115_0000 or UIS_CTRL_REG == 0x116_0000); }
    putreg32(0, UIS_CTRL_REG);
}

/// LCD Panel Width and Height (pixels)
const PANEL_WIDTH  = 720;
const PANEL_HEIGHT = 1440;

/// NuttX Video Controller for PinePhone (3 UI Channels)
const videoInfo = c.fb_videoinfo_s {
    .fmt       = c.FB_FMT_RGBA32,  // Pixel format (XRGB 8888)
    .xres      = PANEL_WIDTH,      // Horizontal resolution in pixel columns
    .yres      = PANEL_HEIGHT,     // Vertical resolution in pixel rows
    .nplanes   = 1,     // Number of color planes supported (Base UI Channel)
    .noverlays = 2,     // Number of overlays supported (2 Overlay UI Channels)
};

/// NuttX Color Plane for PinePhone (Base UI Channel):
/// Fullscreen 720 x 1440 (4 bytes per XRGB 8888 pixel)
const planeInfo = c.fb_planeinfo_s {
    .fbmem   = &fb0,     // Start of frame buffer memory
    .fblen   = @sizeOf( @TypeOf(fb0) ),  // Length of frame buffer memory in bytes
    .stride  = PANEL_WIDTH * 4,  // Length of a line in bytes (4 bytes per pixel)
    .display = 0,        // Display number (Unused)
    .bpp     = 32,       // Bits per pixel (XRGB 8888)
    .xres_virtual = PANEL_WIDTH,   // Virtual Horizontal resolution in pixel columns
    .yres_virtual = PANEL_HEIGHT,  // Virtual Vertical resolution in pixel rows
    .xoffset      = 0,     // Offset from virtual to visible resolution
    .yoffset      = 0,     // Offset from virtual to visible resolution
};

/// NuttX Overlays for PinePhone (2 Overlay UI Channels)
const overlayInfo = [2] c.fb_overlayinfo_s {
    // First Overlay UI Channel:
    // Square 600 x 600 (4 bytes per ARGB 8888 pixel)
    .{
        .fbmem     = &fb1,     // Start of frame buffer memory
        .fblen     = @sizeOf( @TypeOf(fb1) ),  // Length of frame buffer memory in bytes
        .stride    = 600 * 4,  // Length of a line in bytes
        .overlay   = 0,        // Overlay number (First Overlay)
        .bpp       = 32,       // Bits per pixel (ARGB 8888)
        .blank     = 0,        // TODO: Blank or unblank
        .chromakey = 0,        // TODO: Chroma key argb8888 formatted
        .color     = 0,        // TODO: Color argb8888 formatted
        .transp    = c.fb_transp_s { .transp = 0, .transp_mode = 0 },  // TODO: Transparency
        .sarea     = c.fb_area_s { .x = 52, .y = 52, .w = 600, .h = 600 },  // Selected area within the overlay
        .accl      = 0,        // TODO: Supported hardware acceleration
    },
    // Second Overlay UI Channel:
    // Fullscreen 720 x 1440 (4 bytes per ARGB 8888 pixel)
    .{
        .fbmem     = &fb2,     // Start of frame buffer memory
        .fblen     = @sizeOf( @TypeOf(fb2) ),  // Length of frame buffer memory in bytes
        .stride    = PANEL_WIDTH * 4,  // Length of a line in bytes
        .overlay   = 1,        // Overlay number (Second Overlay)
        .bpp       = 32,       // Bits per pixel (ARGB 8888)
        .blank     = 0,        // TODO: Blank or unblank
        .chromakey = 0,        // TODO: Chroma key argb8888 formatted
        .color     = 0,        // TODO: Color argb8888 formatted
        .transp    = c.fb_transp_s { .transp = 0, .transp_mode = 0 },  // TODO: Transparency
        .sarea     = c.fb_area_s { .x = 0, .y = 0, .w = PANEL_WIDTH, .h = PANEL_HEIGHT },  // Selected area within the overlay
        .accl      = 0,        // TODO: Supported hardware acceleration
    },
};

// Framebuffer 0: (Base UI Channel)
// Fullscreen 720 x 1440 (4 bytes per XRGB 8888 pixel)
// TODO: Does alignment prevent flickering?
var fb0 align(0x1000) = std.mem.zeroes([PANEL_WIDTH * PANEL_HEIGHT] u32);

// Framebuffer 1: (First Overlay UI Channel)
// Square 600 x 600 (4 bytes per ARGB 8888 pixel)
// TODO: Does alignment prevent flickering?
var fb1 align(0x1000) = std.mem.zeroes([600 * 600] u32);

// Framebuffer 2: (Second Overlay UI Channel)
// Fullscreen 720 x 1440 (4 bytes per ARGB 8888 pixel)
// TODO: Does alignment prevent flickering?
var fb2 align(0x1000) = std.mem.zeroes([PANEL_WIDTH * PANEL_HEIGHT] u32);

///////////////////////////////////////////////////////////////////////////////
//  Init Display Engine

// SRAM Registers Base Address is 0x01C0 0000 (A31 Page 191)
const SRAM_REGISTERS_BASE_ADDRESS = 0x01C0_0000;

// CCU (Clock Control Unit) Base Address is 0x01C2 0000 (A64 Page 81)
const CCU_BASE_ADDRESS = 0x01C2_0000;

// VIDEO_SCALER(CH0) is at MIXER0 Offset 0x02 0000 (DE Page 90, 0x112 0000)
const VIDEO_SCALER_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x02_0000;

// UI_SCALER1(CH1) is at MIXER0 Offset 0x04 0000 (DE Page 90, 0x114 0000)
const UI_SCALER1_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x04_0000;

// UI_SCALER2(CH2) is at MIXER0 Offset 0x05 0000 (DE Page 90, 0x115 0000)
const UI_SCALER2_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x05_0000;

// FCE (Fresh and Contrast Enhancement) is at MIXER0 Offset 0x0A 0000 (DE Page 61, 0x11A 0000)
const FCE_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x0A_0000;

// BWS (Black and White Stretch) is at MIXER0 Offset 0x0A 2000 (DE Page 42, 0x11A 2000)
const BWS_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x0A_2000;

// LTI (Luminance Transient Improvement) is at MIXER0 Offset 0x0A 4000 (DE Page 71, 0x11A 4000)
const LTI_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x0A_4000;

// PEAKING (Luma Peaking) is at MIXER0 Offset 0x0A 6000 (DE Page 80, 0x11A 6000)
const PEAKING_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x0A_6000;

// ASE (Adaptive Saturation Enhancement) is at MIXER0 Offset 0x0A 8000 (DE Page 40, 0x11A 8000)
const ASE_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x0A_8000;

// FCC (Fancy Color Curvature Change) is at MIXER0 Offset 0x0A A000 (DE Page 56, 0x11A A000)
const FCC_BASE_ADDRESS = MIXER0_BASE_ADDRESS + 0x0A_A000;

// DRC (Dynamic Range Controller) is at Address 0x011B 0000 (DE Page 48, 0x11B 0000)
const DRC_BASE_ADDRESS = 0x011B_0000;

// Init PinePhone's Allwinner A64 Display Engine.
// Called by display_init() in p-boot Display Code.
// See https://lupyuen.github.io/articles/de#appendix-initialising-the-allwinner-a64-display-engine
pub export fn de2_init() void {
    debug("de2_init: start", .{});
    defer { debug("de2_init: end", .{}); }

    // Set High Speed SRAM to DMA Mode
    // Set BIST_DMA_CTRL_SEL to 0 for DMA (DMB) (A31 Page 191, 0x1C0 0004)
    // BIST_DMA_CTRL_SEL (Bist and DMA Control Select) is Bit 31 of SRAM_CTRL_REG1
    // SRAM_CTRL_REG1 (SRAM Control Register 1) is at SRAM Registers Offset 0x4
    debug("Set High Speed SRAM to DMA Mode", .{});
    const SRAM_CTRL_REG1 = SRAM_REGISTERS_BASE_ADDRESS + 0x4;
    comptime{ assert(SRAM_CTRL_REG1 == 0x1C0_0004); }
    putreg32(0x0, SRAM_CTRL_REG1);  // TODO: DMB

    // Set Display Engine PLL to 297 MHz
    // Set PLL_DE_CTRL_REG to 0x8100 1701 (DMB)
    //   PLL_ENABLE    (Bit 31)       = 1  (Enable PLL)
    //   PLL_MODE_SEL  (Bit 24)       = 1  (Integer Mode)
    //   PLL_FACTOR_N  (Bits 8 to 14) = 23 (N = 24)
    //   PLL_PRE_DIV_M (Bits 0 to 3)  = 1  (M = 2)
    // Actual PLL Output = 24 MHz * N / M = 288 MHz
    // (Slighltly below 297 MHz due to truncation)
    // PLL_DE_CTRL_REG (PLL Display Engine Control Register) is at CCU Offset 0x0048
    // (A64 Page 96, 0x1C2 0048)
    debug("Set Display Engine PLL to 297 MHz", .{});
    const PLL_ENABLE:   u32 = 1  << 31;  // Enable PLL
    const PLL_MODE_SEL: u25 = 1  << 24;  // Integer Mode
    const PLL_FACTOR_N: u15 = 23 <<  8;  // N = 24
    const PLL_PRE_DIV_M: u4 = 1  <<  0;  // M = 2
    const pll = PLL_ENABLE
        | PLL_MODE_SEL
        | PLL_FACTOR_N
        | PLL_PRE_DIV_M;
    comptime{ assert(pll == 0x8100_1701); }

    const PLL_DE_CTRL_REG = CCU_BASE_ADDRESS + 0x0048;
    comptime{ assert(PLL_DE_CTRL_REG == 0x1C2_0048); }
    putreg32(pll, PLL_DE_CTRL_REG);  // TODO: DMB

    // Wait for Display Engine PLL to be stable
    // Poll PLL_DE_CTRL_REG (from above) until LOCK (Bit 28) is 1
    // (PLL is Locked and Stable)
    debug("Wait for Display Engine PLL to be stable", .{});
    while (getreg32(PLL_DE_CTRL_REG) & (1 << 28) == 0) {}

    // Set Special Clock to Display Engine PLL
    // Clear DE_CLK_REG bits 0x0300 0000
    // Set DE_CLK_REG bits 0x8100 0000
    // SCLK_GATING (Bit 31)        = 1 (Enable Special Clock)
    // CLK_SRC_SEL (Bits 24 to 26) = 1 (Clock Source is Display Engine PLL)
    // DE_CLK_REG (Display Engine Clock Register) is at CCU Offset 0x0104
    // (A64 Page 117, 0x1C2 0104)
    debug("Set Special Clock to Display Engine PLL", .{});
    const SCLK_GATING: u32 = 1 << 31;  // Enable Special Clock
    const CLK_SRC_SEL: u27 = 1 << 24;  // Clock Source is Display Engine PLL
    const clk = SCLK_GATING
        | CLK_SRC_SEL;
    comptime{ assert(clk == 0x8100_0000); }

    const SCLK_GATING_MASK: u32 = 0b1   << 31;
    const CLK_SRC_SEL_MASK: u27 = 0b111 << 24;
    const clk_mask = SCLK_GATING_MASK
        | CLK_SRC_SEL_MASK;

    const DE_CLK_REG = CCU_BASE_ADDRESS + 0x0104;
    comptime{ assert(DE_CLK_REG == 0x1C2_0104); }
    modreg32(clk, clk_mask, DE_CLK_REG);

    // Enable AHB (AMBA High-speed Bus) for Display Engine: De-Assert Display Engine
    // Set BUS_SOFT_RST_REG1 bits 0x1000
    // DE_RST (Bit 12) = 1 (De-Assert Display Engine)
    // BUS_SOFT_RST_REG1 (Bus Software Reset Register 1) is at CCU Offset 0x02C4
    // (A64 Page 140, 0x1C2 02C4)
    debug("Enable AHB for Display Engine: De-Assert Display Engine", .{});
    const DE_RST: u13 = 1 << 12;  // De-Assert Display Engine
    const BUS_SOFT_RST_REG1 = CCU_BASE_ADDRESS + 0x02C4;
    comptime{ assert(BUS_SOFT_RST_REG1 == 0x1C2_02C4); }
    modreg32(DE_RST, DE_RST, BUS_SOFT_RST_REG1);

    // Enable AHB (AMBA High-speed Bus) for Display Engine: Pass Display Engine
    // Set BUS_CLK_GATING_REG1 bits 0x1000
    // DE_GATING (Bit 12) = 1 (Pass Display Engine)
    // BUS_CLK_GATING_REG1 (Bus Clock Gating Register 1) is at CCU Offset 0x0064
    // (A64 Page 102, 0x1C2 0064)
    debug("Enable AHB for Display Engine: Pass Display Engine", .{});
    const DE_GATING: u13 = 1 << 12;  // Pass Display Engine
    const BUS_CLK_GATING_REG1 = CCU_BASE_ADDRESS + 0x0064;
    comptime{ assert(BUS_CLK_GATING_REG1 == 0x1C2_0064); }
    modreg32(DE_GATING, DE_GATING, BUS_CLK_GATING_REG1);

    // Enable Clock for MIXER0: SCLK Clock Pass
    // Set SCLK_GATE bits 0x1
    // CORE0_SCLK_GATE (Bit 0) = 1 (Clock Pass)
    // SCLK_GATE is at DE Offset 0x000
    // (DE Page 25, 0x100 0000)
    debug("Enable Clock for MIXER0: SCLK Clock Pass", .{});
    const CORE0_SCLK_GATE: u1 = 1 << 0;  // Clock Pass
    const SCLK_GATE = DISPLAY_ENGINE_BASE_ADDRESS + 0x000;
    comptime{ assert(SCLK_GATE == 0x100_0000); }
    modreg32(CORE0_SCLK_GATE, CORE0_SCLK_GATE, SCLK_GATE);

    // Enable Clock for MIXER0: HCLK Clock Reset Off
    // Set AHB_RESET bits 0x1
    // CORE0_HCLK_RESET (Bit 0) = 1 (Reset Off)
    // AHB_RESET is at DE Offset 0x008
    // (DE Page 25, 0x100 0008)
    debug("Enable Clock for MIXER0: HCLK Clock Reset Off", .{});
    const CORE0_HCLK_RESET: u1 = 1 << 0;  // Reset Off
    const AHB_RESET = DISPLAY_ENGINE_BASE_ADDRESS + 0x008;
    comptime{ assert(AHB_RESET == 0x100_0008); }
    modreg32(CORE0_HCLK_RESET, CORE0_HCLK_RESET, AHB_RESET);

    // Enable Clock for MIXER0: HCLK Clock Pass
    // Set HCLK_GATE bits 0x1
    // CORE0_HCLK_GATE (Bit 0) = 1 (Clock Pass)
    // HCLK_GATE is at DE Offset 0x004
    // (DE Page 25, 0x100 0004)
    debug("Enable Clock for MIXER0: HCLK Clock Pass", .{});
    const CORE0_HCLK_GATE: u1 = 1 << 0;  // Clock Pass
    const HCLK_GATE = DISPLAY_ENGINE_BASE_ADDRESS + 0x004;
    comptime{ assert(HCLK_GATE == 0x100_0004); }
    modreg32(CORE0_HCLK_GATE, CORE0_HCLK_GATE, HCLK_GATE);

    // Route MIXER0 to TCON0
    // Clear DE2TCON_MUX bits 0x1
    // DE2TCON_MUX (Bit 0) = 0
    // (Route MIXER0 to TCON0; Route MIXER1 to TCON1)
    // DE2TCON_MUX is at DE Offset 0x010
    // (DE Page 26, 0x100 0010)
    debug("Route MIXER0 to TCON0", .{});
    const DE2TCON_MUX_MASK: u1 = 1 << 0;  // Route MIXER0 to TCON0; Route MIXER1 to TCON1
    const DE2TCON_MUX = DISPLAY_ENGINE_BASE_ADDRESS + 0x010;
    comptime{ assert(DE2TCON_MUX == 0x100_0010); }
    modreg32(0, DE2TCON_MUX_MASK, DE2TCON_MUX);

    // Clear MIXER0 Registers: Global Registers (GLB), Blender (BLD), Video Overlay (OVL_V), UI Overlay (OVL_UI)
    // Set MIXER0 Offsets 0x0000 - 0x5FFF to 0
    // GLB (Global Regisers) at MIXER0 Offset 0x0000
    // BLD (Blender) at MIXER0 Offset 0x1000
    // OVL_V(CH0) (Video Overlay) at MIXER0 Offset 0x2000
    // OVL_UI(CH1) (UI Overlay 1) at MIXER0 Offset 0x3000
    // OVL_UI(CH2) (UI Overlay 2) at MIXER0 Offset 0x4000
    // OVL_UI(CH3) (UI Overlay 3) at MIXER0 Offset 0x5000
    // (DE Page 90, 0x110 0000 - 0x110 5FFF)
    debug("Clear MIXER0 Registers: GLB, BLD, OVL_V, OVL_UI", .{});
    var i: usize = 0;
    while (i < 0x6000) : (i += 4) {
        putreg32(0, MIXER0_BASE_ADDRESS + i);
        enableLog = false;
    }
    enableLog = true;
    debug("  to *0x{x} = 0x0", .{ MIXER0_BASE_ADDRESS + i - 1 });

    // Disable MIXER0 Video Scaler (VSU)
    // Set to 0: VS_CTRL_REG at VIDEO_SCALER(CH0) Offset 0
    // EN (Bit 0) = 0 (Disable Video Scaler)
    // (DE Page 130, 0x112 0000)
    debug("Disable MIXER0 VSU", .{});
    const VS_CTRL_REG = VIDEO_SCALER_BASE_ADDRESS + 0;
    comptime{ assert(VS_CTRL_REG == 0x112_0000); }
    putreg32(0, VS_CTRL_REG);

    // TODO: 0x113 0000 is undocumented
    // Is there a mixup with UI_SCALER3?
    debug("Disable MIXER0 Undocumented", .{});
    const _1130000 = 0x1130000;
    putreg32(0, _1130000);

    // Disable MIXER0 UI_SCALER1
    // Set to 0: UIS_CTRL_REG at UI_SCALER1(CH1) Offset 0
    // EN (Bit 0) = 0 (Disable UI Scaler)
    // (DE Page 66, 0x114 0000)
    debug("Disable MIXER0 UI_SCALER1", .{});
    const UIS_CTRL_REG1 = UI_SCALER1_BASE_ADDRESS + 0;
    comptime{ assert(UIS_CTRL_REG1 == 0x114_0000); }
    putreg32(0, UIS_CTRL_REG1);

    // Disable MIXER0 UI_SCALER2
    // Set to 0: UIS_CTRL_REG at UI_SCALER2(CH2) Offset 0
    // EN (Bit 0) = 0 (Disable UI Scaler)
    // (DE Page 66, 0x115 0000)
    debug("Disable MIXER0 UI_SCALER2", .{});
    const UIS_CTRL_REG2 = UI_SCALER2_BASE_ADDRESS + 0;
    comptime{ assert(UIS_CTRL_REG2 == 0x115_0000); }
    putreg32(0, UIS_CTRL_REG2);

    // TODO: Missing UI_SCALER3(CH3) at MIXER0 Offset 0x06 0000 (DE Page 90, 0x116 0000)
    // Is there a mixup with 0x113 0000 above?

    // Disable MIXER0 FCE
    // Set to 0: GCTRL_REG(FCE) at FCE Offset 0
    // EN (Bit 0) = 0 (Disable FCE)
    // (DE Page 62, 0x11A 0000)
    debug("Disable MIXER0 FCE", .{});
    const GCTRL_REG_FCE = FCE_BASE_ADDRESS + 0;
    comptime{ assert(GCTRL_REG_FCE == 0x11A_0000); }
    putreg32(0, GCTRL_REG_FCE);

    // Disable MIXER0 BWS
    // Set to 0: GCTRL_REG(BWS) at BWS Offset 0
    // EN (Bit 0) = 0 (Disable BWS)
    // (DE Page 42, 0x11A 2000)
    debug("Disable MIXER0 BWS", .{});
    const GCTRL_REG_BWS = BWS_BASE_ADDRESS + 0;
    comptime{ assert(GCTRL_REG_BWS == 0x11A_2000); }
    putreg32(0, GCTRL_REG_BWS);

    // Disable MIXER0 LTI
    // Set to 0: LTI_CTL at LTI Offset 0
    // LTI_EN (Bit 0) = 0 (Close LTI)
    // (DE Page 72, 0x11A 4000)
    debug("Disable MIXER0 LTI", .{});
    const LTI_CTL = LTI_BASE_ADDRESS + 0;
    comptime{ assert(LTI_CTL == 0x11A_4000); }
    putreg32(0, LTI_CTL);

    // Disable MIXER0 PEAKING
    // Set to 0: LP_CTRL_REG at PEAKING Offset 0
    // EN (Bit 0) = 0 (Disable PEAKING)
    // (DE Page 80, 0x11A 6000)
    debug("Disable MIXER0 PEAKING", .{});
    const LP_CTRL_REG = PEAKING_BASE_ADDRESS + 0;
    comptime{ assert(LP_CTRL_REG == 0x11A_6000); }
    putreg32(0, LP_CTRL_REG);

    // Disable MIXER0 ASE
    // Set to 0: ASE_CTL_REG at ASE Offset 0
    // ASE_EN (Bit 0) = 0 (Disable ASE)
    // (DE Page 40, 0x11A 8000)
    debug("Disable MIXER0 ASE", .{});
    const ASE_CTL_REG = ASE_BASE_ADDRESS + 0;
    comptime{ assert(ASE_CTL_REG == 0x11A_8000); }
    putreg32(0, ASE_CTL_REG);

    // Disable MIXER0 FCC
    // Set to 0: FCC_CTL_REG at FCC Offset 0
    // Enable (Bit 0) = 0 (Disable FCC)
    // (DE Page 56, 0x11A A000)
    debug("Disable MIXER0 FCC", .{});
    const FCC_CTL_REG = FCC_BASE_ADDRESS + 0;
    comptime{ assert(FCC_CTL_REG == 0x11A_A000); }
    putreg32(0, FCC_CTL_REG);

    // Disable MIXER0 DRC
    // Set to 0: GNECTL_REG at DRC Offset 0
    // BIST_EN (Bit 0) = 0 (Disable BIST)
    // (DE Page 49, 0x11B 0000)
    debug("Disable MIXER0 DRC", .{});
    const GNECTL_REG = DRC_BASE_ADDRESS + 0;
    comptime{ assert(GNECTL_REG == 0x11B_0000); }
    putreg32(0, GNECTL_REG);

    // Enable MIXER0
    // Set GLB_CTL to 1 (DMB)
    // EN (Bit 0) = 1 (Enable Mixer)
    // (DE Page 92)
    // GLB_CTL is at MIXER0 Offset 0
    // (DE Page 90, 0x110 0000)
    debug("Enable MIXER0", .{});
    const EN_MIXER: u1 = 1 << 0;  // Enable Mixer
    const GLB_CTL = MIXER0_BASE_ADDRESS + 0;
    comptime{ assert(GLB_CTL == 0x110_0000); }
    putreg32(EN_MIXER, GLB_CTL);  // TODO: DMB
}

/// Export Zig Functions to C. (Why is this needed?)
pub export fn export_dsi_functions() void {
    // Export DSI Functions
    dsi.panel_init();
    dsi.enable_dsi_block();
    dsi.start_dsi();

    // Export Backlight Functions
    backlight.backlight_enable(100);

    // Export PMIC Functions
    pmic.display_board_init();

    // Export TCON Functions
    tcon.tcon0_init();

    // Export DPHY Functions
    dphy.dphy_enable();

    // Export Panel Functions
    panel.panel_reset();
}

/// Modify the specified bits in a memory mapped register.
/// Based on https://github.com/apache/nuttx/blob/master/arch/arm64/src/common/arm64_arch.h#L473
fn modreg32(
    comptime val: u32,   // Bits to set, like (1 << bit)
    comptime mask: u32,  // Bits to clear, like (1 << bit)
    addr: u64  // Address to modify
) void {
    comptime{ assert(val & mask == val); }
    debug("  *0x{x}: clear 0x{x}, set 0x{x}", .{ addr, mask, val & mask });
    putreg32(
        (getreg32(addr) & ~(mask))
            | ((val) & (mask)),
        (addr)
    );
}

/// Get the 32-bit value at the address
fn getreg32(addr: u64) u32 {
    const ptr = @intToPtr(*const volatile u32, addr);
    return ptr.*;
}

/// Set the 32-bit value at the address
fn putreg32(val: u32, addr: u64) void {
    if (enableLog) { debug("  *0x{x} = 0x{x}", .{ addr, val }); }
    const ptr = @intToPtr(*volatile u32, addr);
    ptr.* = val;
}

/// Set to False to disable log 
var enableLog = true;

///////////////////////////////////////////////////////////////////////////////
//  Main Function

/// Main Function that will be called by NuttX when we run the `hello` app.
/// Comment out this function if NuttX Build fails due to duplicate `hello_main`.
pub export fn hello_main(
    argc: c_int, 
    argv: [*c]const [*c]u8
) c_int {
    _ = c.usleep(100);  // TODO: Fix garbled printf
    debug("pinephone-nuttx/render.zig: hello_main", .{});
    _ = c.usleep(100);  // TODO: Fix garbled printf

    // Quit if no args specified
    if (argc <= 1) { usage(); return -1; }

    // Run a command like "a" or "b"
    if (argc == 2) {
        const cmd = std.mem.span(argv[1]);
        if (std.mem.eql(u8, cmd, "a")) {
            // Turn on Display Backlight (in Zig)
            backlight.backlight_enable(90);

        } else if (std.mem.eql(u8, cmd, "b")) {
            // Init Timing Controller TCON0 (in Zig)
            tcon.tcon0_init();

        } else if (std.mem.eql(u8, cmd, "c")) {
            // Init PMIC (in C)
            _ = a64_tcon0_init(PANEL_WIDTH, PANEL_HEIGHT);

        } else if (std.mem.eql(u8, cmd, "d")) {
            // Enable MIPI DSI Block (in C)
            _ = a64_mipi_dsi_enable();

        } else if (std.mem.eql(u8, cmd, "e")) {
            // Enable MIPI Display Physical Layer (in C)
            _ = a64_mipi_dphy_enable();

        } else if (std.mem.eql(u8, cmd, "f")) {
            // Reset LCD Panel (in Zig)
            panel.panel_reset();

        } else if (std.mem.eql(u8, cmd, "g")) {
            // TODO: Init LCD Panel (in C)
            // _ = pinephone_panel_init();

            // Init LCD Panel (in Zig)
            dsi.panel_init();  // TODO: Remove this

        } else if (std.mem.eql(u8, cmd, "h")) {
            // Start MIPI DSI HSC and HSD (in C)
            _ = a64_mipi_dsi_start();

        } else if (std.mem.eql(u8, cmd, "i")) {
            // Init Display Engine (in Zig)
            de2_init();

            // Wait a while
            _ = c.usleep(160000);

            // Render Graphics with Display Engine (in Zig)
            renderGraphics(3);  // Render 3 UI Channels

        } else if (std.mem.eql(u8, cmd, "0")) {
            // Render 3 UI Channels in Zig and C

            // Turn on Display Backlight (in Zig)
            // https://github.com/lupyuen/pinephone-nuttx/blob/main/backlight.zig
            backlight.backlight_enable(90);
            // _ = c.sleep(1);  // TODO: Remove this when Backlight is converted to C

            // Init Timing Controller TCON0 (in C)
            // PANEL_WIDTH is 720, PANEL_HEIGHT is 1440
            // https://github.com/lupyuen2/wip-pinephone-nuttx/blob/tcon2/arch/arm64/src/a64/a64_tcon0.c#L180-L474
            _ = a64_tcon0_init(PANEL_WIDTH, PANEL_HEIGHT);

            // Init PMIC (in C)
            // https://github.com/lupyuen/pinephone-nuttx/blob/main/test/test_a64_rsb.c
            _ = pinephone_pmic_init();            

            // Wait 15 milliseconds for power supply and power-on init
            debug("Wait for power supply and power-on init", .{});
            up_mdelay(15);

            // Enable MIPI DSI Block (in C)
            // https://github.com/apache/nuttx/blob/master/arch/arm64/src/a64/a64_mipi_dsi.c#L526-L914
            _ = a64_mipi_dsi_enable();

            // Enable MIPI Display Physical Layer (in C)
            // https://github.com/apache/nuttx/blob/master/arch/arm64/src/a64/a64_mipi_dphy.c#L86-L162
            _ = a64_mipi_dphy_enable();

            // Reset LCD Panel (in Zig)
            // https://github.com/lupyuen/pinephone-nuttx/blob/main/panel.zig
            panel.panel_reset();
            // _ = c.sleep(1);  // TODO: Remove this when Panel is converted to C

            // Init LCD Panel (in C)
            // https://github.com/lupyuen/pinephone-nuttx/blob/main/test/test_a64_mipi_dsi.c
            _ = pinephone_lcd_panel_init();

            // Start MIPI DSI HSC and HSD (in C)
            // https://github.com/apache/nuttx/blob/master/arch/arm64/src/a64/a64_mipi_dsi.c#L914-L993
            _ = a64_mipi_dsi_start();

            // Init Display Engine (in C)
            // https://github.com/lupyuen2/wip-pinephone-nuttx/blob/tcon2/arch/arm64/src/a64/a64_de.c
            _ = a64_de_init();

            // Wait 160 milliseconds
            up_mdelay(160);

            // Render Graphics with Display Engine (in C)
            // https://github.com/lupyuen/pinephone-nuttx/blob/main/test/test_a64_de.c
            _ = pinephone_render_graphics();

        } else if (std.mem.eql(u8, cmd, "1")) {
            // Render 1 UI Channel in Zig
            test_render(1);

        } else if (std.mem.eql(u8, cmd, "3")) {
            // Render 3 UI Channels in Zig
            test_render(3);

        } else {
            usage(); return -1;
        }
    }
    return 0;
}

/// Print the Command-Line Options
fn usage() void {
    const err = std.log.err;
    err("hello 1", .{});
    err(" Render 1 UI Channel (in Zig)", .{});
    err("hello 3", .{});
    err(" Render 3 UI Channels (in Zig)", .{});
    err("hello 0", .{});
    err(" Render 3 UI Channels (in Zig and C)", .{});
    err("hello a", .{});
    err(" Turn on Display Backlight (in Zig)", .{});
    err("hello b", .{});
    err(" Init Timing Controller TCON0 (in Zig)", .{});
    err("hello c", .{});
    err(" Init PMIC (in Zig)", .{});
    err("hello d", .{});
    err(" Enable MIPI DSI Block (a64_mipi_dsi_enable)", .{});
    err("hello e", .{});
    err(" Enable MIPI Display Physical Layer (a64_mipi_dphy_enable)", .{});
    err("hello f", .{});
    err(" Reset LCD Panel (in Zig)", .{});
    err("hello g", .{});
    err(" Init LCD Panel (pinephone_panel_init)", .{});
    err("hello h", .{});
    err(" Start MIPI DSI HSC and HSD (a64_mipi_dsi_start)", .{});
    err("hello i", .{});
    err(" Render Graphics with Display Engine (in Zig)", .{});

    // Calibrate CONFIG_BOARD_LOOPSPERMSEC (default is 5000)
    debug("Calibrate CONFIG_BOARD_LOOPSPERMSEC", .{});
    debug("Start 10 seconds", .{});
    up_mdelay(10 * 1000);
    debug("End 10 seconds", .{});
}

///////////////////////////////////////////////////////////////////////////////
//  Panic Handler

/// Called by Zig when it hits a Panic. We print the Panic Message, Stack Trace and halt. See 
/// https://andrewkelley.me/post/zig-stack-traces-kernel-panic-bare-bones-os.html
/// https://github.com/ziglang/zig/blob/master/lib/std/builtin.zig#L763-L847
pub fn panic(
    message: []const u8, 
    _stack_trace: ?*std.builtin.StackTrace
) noreturn {
    // Print the Panic Message
    _ = _stack_trace;
    _ = puts("\n!ZIG PANIC!");
    _ = puts(@ptrCast([*c]const u8, message));

    // Print the Stack Trace
    _ = puts("Stack Trace:");
    var it = std.debug.StackIterator.init(@returnAddress(), null);
    while (it.next()) |return_address| {
        _ = printf("%p\n", return_address);
    }

    // Halt
    c.exit(1);
}

///////////////////////////////////////////////////////////////////////////////
//  Logging

/// Called by Zig for `std.log.debug`, `std.log.info`, `std.log.err`, ...
/// https://gist.github.com/leecannon/d6f5d7e5af5881c466161270347ce84d
pub fn log(
    comptime _message_level: std.log.Level,
    comptime _scope: @Type(.EnumLiteral),
    comptime format: []const u8,
    args: anytype,
) void {
    _ = _message_level;
    _ = _scope;

    // Format the message
    var buf: [100]u8 = undefined;  // Limit to 100 chars
    var slice = std.fmt.bufPrint(&buf, format, args)
        catch { _ = puts("*** log error: buf too small"); return; };
    
    // Terminate the formatted message with a null
    var buf2: [buf.len + 1 : 0]u8 = undefined;
    std.mem.copy(
        u8, 
        buf2[0..slice.len], 
        slice[0..slice.len]
    );
    buf2[slice.len] = 0;

    // Print the formatted message
    _ = puts(&buf2);
}

///////////////////////////////////////////////////////////////////////////////
//  Imported Functions and Variables

/// NuttX Drivers and Test Functions
extern fn a64_de_init() c_int;
extern fn a64_mipi_dphy_enable() c_int;
extern fn a64_mipi_dsi_enable() c_int;
extern fn a64_mipi_dsi_start() c_int;
extern fn a64_tcon0_init(width: u16, height: u16) c_int;
extern fn pinephone_lcd_panel_init() c_int;
extern fn pinephone_pmic_init() c_int;
extern fn pinephone_render_graphics() c_int;
extern fn up_mdelay(milliseconds: c_uint) void;

/// For safety, we import these functions ourselves to enforce Null-Terminated Strings.
/// We changed `[*c]const u8` to `[*:0]const u8`
extern fn printf(format: [*:0]const u8, ...) c_int;
extern fn puts(str: [*:0]const u8) c_int;

/// Aliases for Zig Standard Library
const assert = std.debug.assert;
const debug  = std.log.debug;
