llvm-native-core 0.1.5

LLVM-native core semantic engine — IR, CodeGen, X86 MC, Clang frontend pipeline
//! AMDGPU Kernel ABI Lowering — lowers kernel function entry points
//! to AMDGPU-specific calling conventions and implicit argument handling.
//!
//! Implements:
//! - Kernel argument ABI (implicit args in SGPRs)
//! - Work-item ID computation (local_id, global_id, group_id)
//! - Work-group size detection
//! - Dispatch packet handling
//! - Hidden kernel arguments (hidden_hostcall, printf buffer, etc.)
//!
//! Clean-room reconstruction from AMD ROCm and GFX ISA documentation.
//! Zero LLVM source code consultation.

use super::amdgpu_register_info::*;
use super::amdgpu_target_machine::{AmdgpuIsaVersion, AmdgpuTargetMachine};

// ═══════════════════════════════════════════════════════════════════════════
// Kernel Argument ABI
// ═══════════════════════════════════════════════════════════════════════════

/// Implicit kernel arguments provided by the hardware/loader in SGPRs.
///
/// On entry to a kernel, the following are available in specific SGPRs
/// (actual register assignment depends on the code object version and ISA):
///
/// | SGPR  | Content                                |
/// |-------|----------------------------------------|
/// | s0-s3 | Work-group ID (3 u32 values x,y,z)    |
/// | s4-s5 | Work-group ID (z continuation)        |
/// | s6-s7 | Work-group size (dispatch dimensions) |
/// | s8    | Dispatch packet pointer (low)         |
/// | s9    | Dispatch packet pointer (high)        |
/// | s10   | Kernel argument segment pointer (low) |
/// | s11   | Kernel argument segment pointer (high) |
#[derive(Debug, Clone)]
pub struct AmdgpuKernelImplicitArgs {
    /// Work-group ID (x, y, z).
    pub workgroup_id: [u32; 3],
    /// Work-group size / dispatch dimensions.
    pub workgroup_size: u32,
    /// Local work-group size (from metadata).
    pub local_size: [u32; 3],
    /// Dispatch packet pointer.
    pub dispatch_ptr: u64,
    /// Kernel argument segment pointer.
    pub kernarg_segment_ptr: u64,
    /// Whether this is a valid kernel entry.
    pub is_valid: bool,
}

impl AmdgpuKernelImplicitArgs {
    pub fn new() -> Self {
        Self {
            workgroup_id: [0; 3],
            workgroup_size: 0,
            local_size: [0; 3],
            dispatch_ptr: 0,
            kernarg_segment_ptr: 0,
            is_valid: true,
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// Work-Item ID Computation
// ═══════════════════════════════════════════════════════════════════════════

/// Computes work-item identifiers for AMDGPU kernel execution.
///
/// AMDGPU hardware provides:
/// - workgroup_id (x,y,z): which workgroup this wave belongs to
/// - local_id (x,y,z): which work-item within the workgroup
/// - workitem_id: flat local ID = x + y*size_x + z*size_x*size_y
///
/// The global ID is computed as:
///   global_id = workgroup_id * local_size + local_id
pub struct AmdgpuWorkItemId {
    pub local_id_x: u32,
    pub local_id_y: u32,
    pub local_id_z: u32,
    pub global_id_x: u32,
    pub global_id_y: u32,
    pub global_id_z: u32,
    pub group_id_x: u32,
    pub group_id_y: u32,
    pub group_id_z: u32,
    pub local_size_x: u32,
    pub local_size_y: u32,
    pub local_size_z: u32,
}

impl AmdgpuWorkItemId {
    pub fn new(local_id: [u32; 3], group_id: [u32; 3], local_size: [u32; 3]) -> Self {
        Self {
            local_id_x: local_id[0],
            local_id_y: local_id[1],
            local_id_z: local_id[2],
            global_id_x: group_id[0] * local_size[0] + local_id[0],
            global_id_y: group_id[1] * local_size[1] + local_id[1],
            global_id_z: group_id[2] * local_size[2] + local_id[2],
            group_id_x: group_id[0],
            group_id_y: group_id[1],
            group_id_z: group_id[2],
            local_size_x: local_size[0],
            local_size_y: local_size[1],
            local_size_z: local_size[2],
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// Kernel Lowering
// ═══════════════════════════════════════════════════════════════════════════

/// Lowers an IR function to an AMDGPU kernel entry point.
///
/// Generates the prologue that:
/// 1. Saves implicit arguments from SGPRs to memory/stack
/// 2. Computes work-item IDs
/// 3. Sets up the kernel argument pointer
/// 4. Initializes FLAT_SCRATCH if needed
/// 5. Jumps to the user function body
pub struct AmdgpuKernelLowering {
    /// Target machine.
    target: AmdgpuTargetMachine,
    /// Implicit arguments.
    implicit_args: AmdgpuKernelImplicitArgs,
    /// Work-item IDs.
    work_item_id: Option<AmdgpuWorkItemId>,
}

impl AmdgpuKernelLowering {
    pub fn new(target: AmdgpuTargetMachine) -> Self {
        Self {
            target,
            implicit_args: AmdgpuKernelImplicitArgs::new(),
            work_item_id: None,
        }
    }

    /// Generate the kernel prologue assembly.
    pub fn gen_prologue_asm(&self) -> String {
        let mut asm = String::new();
        asm.push_str("; AMDGPU Kernel Prologue\n");
        asm.push_str("; Save implicit kernel arguments\n");

        // Save workgroup IDs from SGPRs
        asm.push_str("  s_mov_b32 s0, s0    ; workgroup_id_x in s0\n");
        asm.push_str("  s_mov_b32 s1, s1    ; workgroup_id_y in s1\n");
        asm.push_str("  s_mov_b32 s2, s2    ; workgroup_id_z in s2\n");

        // Compute local IDs from hardware
        asm.push_str("  ; Compute work-item IDs\n");
        asm.push_str("  v_mov_b32 v0, 0      ; local_id_x placeholder\n");
        asm.push_str("  v_mov_b32 v1, 0      ; local_id_y placeholder\n");
        asm.push_str("  v_mov_b32 v2, 0      ; local_id_z placeholder\n");

        // Set up FLAT_SCRATCH for scratch memory
        if self.target.features.has("flat-scratch") {
            asm.push_str("  ; Initialize FLAT_SCRATCH\n");
            asm.push_str("  s_mov_b32 flat_scratch_lo, 0\n");
            asm.push_str("  s_mov_b32 flat_scratch_hi, 0\n");
        }

        // Jump to user code
        asm.push_str("  ; Jump to kernel body\n");
        asm
    }

    /// Generate the kernel epilogue assembly.
    pub fn gen_epilogue_asm(&self) -> String {
        let mut asm = String::new();
        asm.push_str("; AMDGPU Kernel Epilogue\n");
        asm.push_str("  s_endpgm\n");
        asm
    }

    /// Generate LLVM IR for computing work-item IDs.
    pub fn gen_work_item_id_ir(&self) -> String {
        r#"
; AMDGPU Work-Item ID computation
  %local_id_x = call i32 @llvm.amdgcn.workitem.id.x()
  %local_id_y = call i32 @llvm.amdgcn.workitem.id.y()
  %local_id_z = call i32 @llvm.amdgcn.workitem.id.z()
  %group_id_x = call i32 @llvm.amdgcn.workgroup.id.x()
  %group_id_y = call i32 @llvm.amdgcn.workgroup.id.y()
  %group_id_z = call i32 @llvm.amdgcn.workgroup.id.z()
  %local_size_x = call i32 @llvm.r600.read.local.size.x()
  %local_size_y = call i32 @llvm.r600.read.local.size.y()
  %local_size_z = call i32 @llvm.r600.read.local.size.z()
  %global_id_x = add i32 %local_id_x, %group_id_x
  %global_id_y = add i32 %local_id_y, %group_id_y
  %global_id_z = add i32 %local_id_z, %group_id_z
"#
        .to_string()
    }

    /// Generate intrinsic declarations for AMDGPU-specific intrinsics.
    pub fn gen_intrinsic_decls() -> &'static str {
        r#"
declare i32 @llvm.amdgcn.workitem.id.x()
declare i32 @llvm.amdgcn.workitem.id.y()
declare i32 @llvm.amdgcn.workitem.id.z()
declare i32 @llvm.amdgcn.workgroup.id.x()
declare i32 @llvm.amdgcn.workgroup.id.y()
declare i32 @llvm.amdgcn.workgroup.id.z()
declare i32 @llvm.r600.read.local.size.x()
declare i32 @llvm.r600.read.local.size.y()
declare i32 @llvm.r600.read.local.size.z()
declare i32 @llvm.amdgcn.dispatch.id()
declare i32 @llvm.amdgcn.dispatch.ptr()
"#
    }

    /// Set work-item IDs from external source.
    pub fn set_work_item_id(
        &mut self,
        local_id: [u32; 3],
        group_id: [u32; 3],
        local_size: [u32; 3],
    ) {
        self.work_item_id = Some(AmdgpuWorkItemId::new(local_id, group_id, local_size));
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// Kernel Descriptor
// ═══════════════════════════════════════════════════════════════════════════

/// AMDGPU kernel descriptor — metadata embedded in the code object
/// that describes a kernel's resource requirements.
#[derive(Debug, Clone)]
pub struct AmdgpuKernelDescriptor {
    /// Group segment (LDS) fixed size in bytes.
    pub group_segment_fixed_size: u32,
    /// Private segment (scratch) fixed size in bytes.
    pub private_segment_fixed_size: u32,
    /// Maximum SGPRs used by the kernel.
    pub sgpr_count: u32,
    /// Maximum VGPRs used by the kernel.
    pub vgpr_count: u32,
    /// Kernel code entry byte offset.
    pub kernel_code_entry_byte_offset: u64,
    /// Whether this kernel uses FLAT_SCRATCH.
    pub uses_flat_scratch: bool,
    /// Whether this kernel uses dynamic LDS.
    pub uses_dynamic_lds: bool,
    /// Workgroup group segment (LDS) size.
    pub workgroup_group_segment_byte_size: u32,
}

impl AmdgpuKernelDescriptor {
    pub fn new() -> Self {
        Self {
            group_segment_fixed_size: 0,
            private_segment_fixed_size: 0,
            sgpr_count: 0,
            vgpr_count: 0,
            kernel_code_entry_byte_offset: 256, // after ELF header
            uses_flat_scratch: false,
            uses_dynamic_lds: false,
            workgroup_group_segment_byte_size: 0,
        }
    }

    /// Encode the kernel descriptor to a byte array (as stored in .note section).
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = Vec::with_capacity(64);

        // Group segment fixed size (4 bytes)
        bytes.extend_from_slice(&self.group_segment_fixed_size.to_le_bytes());
        // Private segment fixed size (4 bytes)
        bytes.extend_from_slice(&self.private_segment_fixed_size.to_le_bytes());
        // Reserved (8 bytes)
        bytes.extend_from_slice(&[0u8; 8]);
        // Kernel code entry byte offset (8 bytes)
        bytes.extend_from_slice(&self.kernel_code_entry_byte_offset.to_le_bytes());
        // Reserved (12 bytes)
        bytes.extend_from_slice(&[0u8; 12]);
        // Compute PGM RSrc1 (4 bytes)
        let pgm_rsrc1 = self.encode_pgm_rsrc1();
        bytes.extend_from_slice(&pgm_rsrc1.to_le_bytes());
        // Compute PGM RSrc2 (4 bytes)
        let pgm_rsrc2 = self.encode_pgm_rsrc2();
        bytes.extend_from_slice(&pgm_rsrc2.to_le_bytes());
        // SGPR/VGPR spill counts, etc. (8 bytes)
        bytes.extend_from_slice(&[0u8; 8]);
        // Reserved (8 bytes)
        bytes.extend_from_slice(&[0u8; 8]);

        bytes
    }

    /// Encode the PGM_RSRC1 register value.
    fn encode_pgm_rsrc1(&self) -> u32 {
        let mut rsrc1: u32 = 0;
        // VGPRs: bits [5:0]
        rsrc1 |= (self.vgpr_count.min(256) & 0x3F);
        // SGPRs: bits [9:6]
        rsrc1 |= ((self.sgpr_count.min(104) / 8) & 0x0F) << 6;
        // Priority: bits [11:10]
        // Float mode: bits [24:12]
        // DX10 clamp: bit 25
        // Debug mode: bit 26
        // IEEE mode: bit 27
        rsrc1 |= 1 << 27; // IEEE mode by default (GCN)
        rsrc1
    }

    /// Encode the PGM_RSRC2 register value.
    fn encode_pgm_rsrc2(&self) -> u32 {
        let mut rsrc2: u32 = 0;
        // SCRATCH_EN: bit 0
        if self.uses_flat_scratch {
            rsrc2 |= 1;
        }
        // LDS size: bits [8:1]
        if self.uses_dynamic_lds {
            // LDS_SIZE = 1 (dynamic)
            rsrc2 |= 1 << 1;
        }
        rsrc2
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// Tests
// ═══════════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_implicit_args_default() {
        let args = AmdgpuKernelImplicitArgs::new();
        assert!(args.is_valid);
        assert_eq!(args.workgroup_id, [0, 0, 0]);
    }

    #[test]
    fn test_work_item_id_computation() {
        let wid = AmdgpuWorkItemId::new([3, 2, 1], [0, 1, 0], [64, 1, 1]);
        assert_eq!(wid.local_id_x, 3);
        assert_eq!(wid.local_id_y, 2);
        assert_eq!(wid.group_id_x, 0);
        assert_eq!(wid.group_id_y, 1);
        assert_eq!(wid.global_id_x, 3);
        assert_eq!(wid.global_id_y, 3); // 1 * 1 + 2 = 3
    }

    #[test]
    fn test_kernel_lowering_prologue() {
        let tm = AmdgpuTargetMachine::from_cpu("gfx900").unwrap();
        let lowering = AmdgpuKernelLowering::new(tm);
        let asm = lowering.gen_prologue_asm();
        assert!(asm.contains("s_mov_b32"));
        assert!(asm.contains("FLAT_SCRATCH"));
    }

    #[test]
    fn test_kernel_lowering_epilogue() {
        let tm = AmdgpuTargetMachine::from_cpu("gfx900").unwrap();
        let lowering = AmdgpuKernelLowering::new(tm);
        let asm = lowering.gen_epilogue_asm();
        assert!(asm.contains("s_endpgm"));
    }

    #[test]
    fn test_kernel_descriptor_basic() {
        let mut kd = AmdgpuKernelDescriptor::new();
        kd.sgpr_count = 16;
        kd.vgpr_count = 32;
        kd.uses_flat_scratch = true;
        let bytes = kd.to_bytes(); assert_eq!(bytes.len(), 60);
    }

    #[test]
    fn test_kernel_descriptor_pgm_rsrc1() {
        let mut kd = AmdgpuKernelDescriptor::new();
        kd.vgpr_count = 64;
        kd.sgpr_count = 32;
        let rsrc1 = kd.encode_pgm_rsrc1();
        assert_eq!(rsrc1 & 0x3F, 0); // VGPRs in bits 5:0
        assert_eq!((rsrc1 >> 6) & 0x0F, 4); // SGPR count / 8
    }
}