Files
miner/crates/engine-gpu/src/lib.rs
illuzen a47db3d1a2 Fix windows again (#72)
* handle lost device correctly

* Update lib.rs

* allow-integrated flag otherwise exclude

* DeviceLost is a status

* Integrated GPU skipped when discrete init fails

* nits
2026-06-30 16:30:11 +08:00

1141 lines
40 KiB
Rust

#![deny(rust_2018_idioms)]
#![forbid(unsafe_code)]
mod gpu_tiers;
pub mod end_to_end_tests;
pub mod tests;
use engine_cpu::{CancelCheck, Candidate, EngineStatus, FoundOrigin, MinerEngine, Range};
use pow_core::{format_hashrate, format_u512, JobContext};
use primitive_types::U512;
use std::cell::RefCell;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
/// Represents a single GPU device context.
struct GpuContext {
device: wgpu::Device,
queue: wgpu::Queue,
pipeline: wgpu::ComputePipeline,
// Cached vendor configuration
optimal_workgroups: u32,
}
#[derive(Clone)]
struct GpuResources {
header_buffer: wgpu::Buffer,
target_buffer: wgpu::Buffer,
start_nonce_buffer: wgpu::Buffer,
results_buffer: wgpu::Buffer,
dispatch_config_buffer: wgpu::Buffer,
staging_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
pub struct GpuEngine {
contexts: Vec<Arc<GpuContext>>,
device_counter: AtomicUsize,
batch_size: u32,
throttle_ms: u64,
}
// Thread-local storage for consistent GPU device assignment per worker thread
thread_local! {
static ASSIGNED_GPU_DEVICE: RefCell<Option<usize>> = const { RefCell::new(None) };
static WORKER_RESOURCES: RefCell<Option<GpuResources>> = const { RefCell::new(None) };
/// Set to true when this worker's GPU device is lost/unresponsive.
/// Once set, the worker will immediately return Cancelled on any search attempt.
static DEVICE_LOST: RefCell<bool> = const { RefCell::new(false) };
}
impl GpuContext {
fn create_resources(&self) -> GpuResources {
let bind_group_layout = self.pipeline.get_bind_group_layout(0);
// Header: 8 u32s
let header_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Header Buffer"),
size: 32,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Target: 16 u32s
let target_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Target Buffer"),
size: 64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Start Nonce: 16 u32s
let start_nonce_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Start Nonce Buffer"),
size: 64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Results: [flag (1), nonce (16), hash (16)] = 33 u32s
let results_size = (1 + 16 + 16) * 4;
let results_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Results Buffer"),
size: results_size,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Dispatch config: [total_threads, nonces_per_thread, total_nonces] = 3 u32s
let dispatch_config_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Dispatch Config Buffer"),
size: 12,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Staging Buffer"),
size: results_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Mining Bind Group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: results_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: header_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: start_nonce_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: target_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: dispatch_config_buffer.as_entire_binding(),
},
],
});
GpuResources {
header_buffer,
target_buffer,
start_nonce_buffer,
results_buffer,
dispatch_config_buffer,
staging_buffer,
bind_group,
}
}
}
/// Rank backends for mining: native compute APIs first.
fn backend_rank(backend: wgpu::Backend) -> u8 {
match backend {
wgpu::Backend::Vulkan | wgpu::Backend::Metal => 0,
wgpu::Backend::Dx12 => 1,
_ => 2,
}
}
/// Select which adapters to mine on, returning indices into `infos` ordered
/// discrete-first.
///
/// wgpu can enumerate the same physical GPU once per backend (Vulkan + DX12 on
/// Windows) plus CPU-emulated fallbacks such as "Microsoft Basic Render Driver".
/// Mining on every entry causes VRAM contention and OOM (issue #61), so CPU
/// adapters are dropped and only the best-ranked backend present is kept.
/// Within a single backend each physical GPU appears exactly once, so rigs with
/// multiple identical cards keep every card.
///
/// When discrete GPUs are present, integrated GPUs (APUs) are skipped by default
/// to avoid resource contention and driver instability from mining on both
/// simultaneously. Set `allow_integrated` to true to override this behavior.
///
/// Note: This function no longer filters integrated GPUs - that decision is made
/// after initialization, so we can fall back to integrated if discrete fails.
fn select_adapters(infos: &[wgpu::AdapterInfo]) -> Vec<usize> {
let usable: Vec<usize> = (0..infos.len())
.filter(|&i| {
if infos[i].device_type == wgpu::DeviceType::Cpu {
log::info!(
target: "gpu_engine",
"Skipping CPU-emulated adapter: {} ({:?})",
infos[i].name,
infos[i].backend
);
return false;
}
true
})
.collect();
let Some(best) = usable.iter().map(|&i| backend_rank(infos[i].backend)).min() else {
return Vec::new();
};
let mut selected: Vec<usize> = usable
.into_iter()
.filter(|&i| {
if backend_rank(infos[i].backend) != best {
log::info!(
target: "gpu_engine",
"Skipping adapter on lower-priority backend: {} ({:?})",
infos[i].name,
infos[i].backend
);
return false;
}
true
})
.collect();
// Sort discrete GPUs first, but keep all adapters for now
// (integrated filtering happens after init, based on what actually succeeded)
selected.sort_by_key(|&i| match infos[i].device_type {
wgpu::DeviceType::DiscreteGpu => 0,
wgpu::DeviceType::IntegratedGpu => 1,
_ => 2,
});
selected
}
/// Filter initialized GPUs based on device types.
/// Returns indices of GPUs to keep.
///
/// Rules:
/// - If any discrete GPU initialized successfully and `allow_integrated` is false,
/// drop all integrated GPUs
/// - Otherwise keep all GPUs
///
/// This is extracted as a pure function for testability.
fn filter_initialized_gpus(
device_types: &[wgpu::DeviceType],
allow_integrated: bool,
) -> Vec<usize> {
let has_discrete = device_types.contains(&wgpu::DeviceType::DiscreteGpu);
if has_discrete && !allow_integrated {
// Keep only discrete GPUs
device_types
.iter()
.enumerate()
.filter(|(_, &dt)| dt != wgpu::DeviceType::IntegratedGpu)
.map(|(i, _)| i)
.collect()
} else {
// Keep all
(0..device_types.len()).collect()
}
}
impl GpuEngine {
/// Try to initialize the GPU engine with the given batch size and throttle (ms between batches).
///
/// # Arguments
/// * `batch_size` - Number of nonces per batch
/// * `throttle_ms` - Delay between batches in milliseconds (0 = no throttle)
/// * `allow_integrated` - If true, use integrated GPUs even when discrete GPUs are available
///
/// # Errors
///
/// Returns an error if:
/// - `batch_size` is zero (no work would be performed)
/// - No usable GPU adapters are found
pub fn try_new(
batch_size: u32,
throttle_ms: u64,
allow_integrated: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
if batch_size == 0 {
return Err("batch_size must be non-zero".into());
}
// Handle both cases: called from within a tokio runtime or from outside
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
// We're inside a tokio runtime - use block_in_place to allow blocking
tokio::task::block_in_place(|| {
handle.block_on(Self::init(batch_size, throttle_ms, allow_integrated))
})
}
Err(_) => {
// No runtime exists - create a temporary one
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(Self::init(batch_size, throttle_ms, allow_integrated))
}
}
}
async fn init(
batch_size: u32,
throttle_ms: u64,
allow_integrated: bool,
) -> Result<Self, Box<dyn std::error::Error>> {
log::info!(target: "gpu_engine", "Initializing WGPU...");
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let adapters = instance.enumerate_adapters(wgpu::Backends::PRIMARY);
let infos: Vec<wgpu::AdapterInfo> = adapters.iter().map(|a| a.get_info()).collect();
let selected = select_adapters(&infos);
if selected.is_empty() {
log::error!(
target: "gpu_engine",
"No usable GPU adapters found ({} enumerated). Use --gpu-devices 0 to disable GPU mining.",
infos.len()
);
return Err("No usable GPU adapters found".into());
}
let mut adapters: Vec<Option<wgpu::Adapter>> = adapters.into_iter().map(Some).collect();
// Track successfully initialized contexts with their device type
struct InitializedGpu {
context: Arc<GpuContext>,
device_type: wgpu::DeviceType,
name: String,
}
let mut initialized: Vec<InitializedGpu> = Vec::new();
// Timeout for initializing each adapter (30 seconds should be plenty)
let init_timeout = std::time::Duration::from_secs(30);
for (i, idx) in selected.into_iter().enumerate() {
let adapter = adapters[idx].take().expect("adapter selected exactly once");
let info = &infos[idx];
log::info!(
target: "gpu_engine",
"Initializing GPU device {i}: {} ({:?}, {:?})",
info.name,
info.device_type,
info.backend
);
log::debug!(target: "gpu_engine", "Adapter {i} raw info: {info:?}");
// Try to initialize this adapter with a proper timeout.
// If the driver hangs, we'll skip this adapter after the timeout.
let device_future = adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("Mining Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: Default::default(),
..Default::default()
});
let device_result = match tokio::time::timeout(init_timeout, device_future).await {
Ok(result) => result,
Err(_) => {
log::warn!(
target: "gpu_engine",
"GPU device {i} ({}) timed out after {}s during initialization, skipping",
info.name, init_timeout.as_secs()
);
continue;
}
};
let (device, queue) = match device_result {
Ok(dq) => dq,
Err(e) => {
log::warn!(
target: "gpu_engine",
"Failed to initialize GPU device {i} ({}): {}. Skipping.",
info.name, e
);
continue;
}
};
// Log device limits at debug level
let limits = device.limits();
log::debug!(target: "gpu_engine", "GPU device {i} limits: max_workgroups={}, max_workgroup_size={}x{}x{}, max_buffer={}",
limits.max_compute_workgroups_per_dimension,
limits.max_compute_workgroup_size_x,
limits.max_compute_workgroup_size_y,
limits.max_compute_workgroup_size_z,
limits.max_buffer_size
);
// Shader and pipeline creation are synchronous - can't timeout, but usually fast
let pipeline_start = std::time::Instant::now();
let shader_source = include_str!("mining.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Mining Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Mining Pipeline"),
layout: None,
module: &shader,
entry_point: Some("mining_main"),
compilation_options: Default::default(),
cache: None,
});
let pipeline_elapsed = pipeline_start.elapsed();
log::info!(
target: "gpu_engine",
"GPU device {i} ({}) initialized successfully (pipeline compiled in {:.1}s)",
info.name, pipeline_elapsed.as_secs_f64()
);
// Calculate vendor-specific configuration once during initialization
let optimal_workgroups = get_vendor_specific_dispatch(info, &device);
initialized.push(InitializedGpu {
context: Arc::new(GpuContext {
device,
queue,
pipeline,
optimal_workgroups,
}),
device_type: info.device_type,
name: info.name.clone(),
});
}
if initialized.is_empty() {
log::error!(target: "gpu_engine", "No GPU adapters could be initialized successfully.");
return Err("No GPU adapters could be initialized".into());
}
// Filter integrated GPUs if discrete GPUs successfully initialized
let device_types: Vec<_> = initialized.iter().map(|g| g.device_type).collect();
let keep_indices = filter_initialized_gpus(&device_types, allow_integrated);
let contexts: Vec<Arc<GpuContext>> = initialized
.into_iter()
.enumerate()
.filter_map(|(i, g)| {
if keep_indices.contains(&i) {
Some(g.context)
} else {
log::info!(
target: "gpu_engine",
"Dropping integrated GPU (discrete GPU initialized successfully, use --allow-integrated to override): {}",
g.name
);
None
}
})
.collect();
log::info!(
target: "gpu_engine",
"GPU engine initialized with {} devices (batch size: {} nonces, throttle: {}ms)",
contexts.len(),
batch_size,
throttle_ms
);
Ok(Self {
contexts,
device_counter: AtomicUsize::new(0),
batch_size,
throttle_ms,
})
}
/// Returns the number of GPU devices available
pub fn device_count(&self) -> usize {
self.contexts.len()
}
/// Explicitly clear thread-local GPU resources.
/// Call this before thread exit to avoid TLS destruction order issues with wgpu.
pub fn clear_worker_resources() {
WORKER_RESOURCES.with(|resources| {
*resources.borrow_mut() = None;
});
}
}
impl MinerEngine for GpuEngine {
fn name(&self) -> &'static str {
"gpu-wgpu"
}
fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext {
JobContext::new(header_hash, difficulty)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn search_range(
&self,
ctx: &JobContext,
range: Range,
cancel: &dyn CancelCheck,
) -> EngineStatus {
if self.contexts.is_empty() {
log::warn!(target: "gpu_engine", "No GPUs available for search.");
return EngineStatus::Exhausted { hash_count: 0 };
}
// Check if this worker's GPU device was previously lost
let device_is_lost = DEVICE_LOST.with(|lost| *lost.borrow());
if device_is_lost {
// Device was lost in a previous call - signal worker should exit
return EngineStatus::DeviceLost { hash_count: 0 };
}
// Empty or inverted range: nothing to do.
if range.start > range.end {
return EngineStatus::Exhausted { hash_count: 0 };
}
// Check for pre-cancellation
if cancel.is_cancelled() {
return EngineStatus::Cancelled { hash_count: 0 };
}
// Use thread-local assignment for consistent worker-to-GPU mapping
let device_index = ASSIGNED_GPU_DEVICE.with(|assigned| {
let mut assigned_ref = assigned.borrow_mut();
if let Some(index) = *assigned_ref {
index
} else {
let index = if self.contexts.len() == 1 {
0
} else {
self.device_counter.fetch_add(1, Ordering::SeqCst) % self.contexts.len()
};
*assigned_ref = Some(index);
log::info!(
target: "gpu_engine",
"Worker thread assigned to GPU device {} (of {} total devices)",
index,
self.contexts.len()
);
index
}
});
let gpu_ctx = &self.contexts[device_index];
// Ensure resources are initialized for this thread
WORKER_RESOURCES.with(|resources_cell| {
let mut resources = resources_cell.borrow_mut();
if resources.is_none() {
*resources = Some(gpu_ctx.create_resources());
}
});
let resources = WORKER_RESOURCES
.with(|resources_cell| resources_cell.borrow().as_ref().unwrap().clone());
// Pre-convert header and target (only needs to be done once per job)
let mut header_u32s = [0u32; 8];
for (i, item) in header_u32s.iter_mut().enumerate() {
let chunk = &ctx.header[i * 4..(i + 1) * 4];
*item = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
gpu_ctx.queue.write_buffer(
&resources.header_buffer,
0,
bytemuck::cast_slice(&header_u32s),
);
let target_bytes = ctx.target.to_little_endian();
let mut target_u32s = [0u32; 16];
for i in 0..16 {
let chunk = &target_bytes[i * 4..(i + 1) * 4];
target_u32s[i] = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
gpu_ctx.queue.write_buffer(
&resources.target_buffer,
0,
bytemuck::cast_slice(&target_u32s),
);
let search_start = std::time::Instant::now();
let mut total_hashes: u64 = 0;
let mut current_start = range.start;
let mut batch_num = 0u64;
log::info!(
target: "gpu_engine",
"GPU {} search started: range {}..{}, batch size: {} nonces",
device_index,
format_u512(range.start),
format_u512(range.end),
self.batch_size
);
// Process in batches, checking for cancellation between each batch
while current_start <= range.end {
// Check for cancellation at host level BEFORE starting each batch
if cancel.is_cancelled() {
let elapsed = search_start.elapsed();
let hash_rate = total_hashes as f64 / elapsed.as_secs_f64();
log::info!(
target: "gpu_engine",
"GPU {} cancelled before batch {} ({} total hashes in {:.2}s, {})",
device_index,
batch_num,
total_hashes,
elapsed.as_secs_f64(),
format_hashrate(hash_rate)
);
return EngineStatus::Cancelled {
hash_count: total_hashes,
};
}
// Calculate batch range
let remaining = range
.end
.saturating_sub(current_start)
.saturating_add(U512::one());
let batch_size_u512 = U512::from(self.batch_size);
let this_batch_size: u32 = if remaining > batch_size_u512 {
self.batch_size
} else {
// remaining fits in u32 since it's <= batch_size which is u32
remaining.low_u32()
};
// Run single batch
let batch_result =
run_single_batch(gpu_ctx, &resources, current_start, this_batch_size);
match batch_result {
BatchResult::Found {
candidate,
hash_count,
} => {
total_hashes += hash_count;
let elapsed = search_start.elapsed();
let hash_rate = total_hashes as f64 / elapsed.as_secs_f64();
log::debug!(
target: "gpu_engine",
"GPU {} found solution in batch {}! Nonce: {}, Hash: {} ({} total hashes in {:.2}s, {})",
device_index,
batch_num,
format_u512(candidate.nonce),
format_u512(candidate.hash),
total_hashes,
elapsed.as_secs_f64(),
format_hashrate(hash_rate)
);
return EngineStatus::Found {
candidate,
hash_count: total_hashes,
origin: FoundOrigin::GpuG1,
};
}
BatchResult::NotFound { hash_count } => {
total_hashes += hash_count;
}
BatchResult::DeviceLost => {
// GPU device is lost/unresponsive - mark as permanently dead
// and clear resources to prevent "buffer already mapped" panics
DEVICE_LOST.with(|lost| *lost.borrow_mut() = true);
WORKER_RESOURCES.with(|res| *res.borrow_mut() = None);
log::error!(
target: "gpu_engine",
"GPU {} device lost or unresponsive - stopping worker. \
This GPU will not process further batches.",
device_index
);
return EngineStatus::DeviceLost {
hash_count: total_hashes,
};
}
}
// Move to next batch
current_start = current_start.saturating_add(U512::from(this_batch_size));
batch_num += 1;
// Apply throttle delay between batches (if configured and more batches remain)
// Sleep in small increments to remain responsive to cancellation
if self.throttle_ms > 0 && current_start <= range.end {
let sleep_interval =
std::time::Duration::from_millis((self.throttle_ms / 10).max(1));
let mut remaining = std::time::Duration::from_millis(self.throttle_ms);
while remaining > std::time::Duration::ZERO {
if cancel.is_cancelled() {
return EngineStatus::Cancelled {
hash_count: total_hashes,
};
}
let sleep_time = remaining.min(sleep_interval);
std::thread::sleep(sleep_time);
remaining = remaining.saturating_sub(sleep_time);
}
}
// Log progress periodically (every 10 batches)
if batch_num.is_multiple_of(10) {
let elapsed = search_start.elapsed();
let hash_rate = total_hashes as f64 / elapsed.as_secs_f64();
log::debug!(
target: "gpu_engine",
"GPU {} batch {} complete: {} hashes so far ({:.2}s, {})",
device_index,
batch_num,
total_hashes,
elapsed.as_secs_f64(),
format_hashrate(hash_rate)
);
}
}
// Range exhausted without finding solution
let elapsed = search_start.elapsed();
let hash_rate = total_hashes as f64 / elapsed.as_secs_f64();
log::info!(
target: "gpu_engine",
"GPU {} search exhausted: {} hashes in {} batches ({:.2}s, {})",
device_index,
total_hashes,
batch_num,
elapsed.as_secs_f64(),
format_hashrate(hash_rate)
);
EngineStatus::Exhausted {
hash_count: total_hashes,
}
}
}
/// Result from a single GPU batch
enum BatchResult {
Found {
candidate: Candidate,
hash_count: u64,
},
NotFound {
hash_count: u64,
},
/// GPU device is lost or unresponsive - caller should stop using this device
DeviceLost,
}
/// Run a single batch of GPU computation
fn run_single_batch(
gpu_ctx: &GpuContext,
resources: &GpuResources,
batch_start: U512,
batch_size: u32,
) -> BatchResult {
// Calculate dispatch configuration for this batch
let threads_per_workgroup = 256u32;
let limits = gpu_ctx.device.limits();
let max_workgroups = limits.max_compute_workgroups_per_dimension;
let hinted_workgroups = gpu_ctx.optimal_workgroups.max(1).min(max_workgroups);
let hinted_threads = hinted_workgroups as u64 * threads_per_workgroup as u64;
let logical_threads = (batch_size as u64).min(hinted_threads).max(1);
let num_workgroups = ((logical_threads as u32).div_ceil(threads_per_workgroup)).max(1);
let total_threads = (num_workgroups * threads_per_workgroup) as u64;
let nonces_per_thread = ((batch_size as u64).div_ceil(total_threads)).max(1) as u32;
// Dispatch config: [total_threads, nonces_per_thread, total_nonces]
let dispatch_config = [total_threads as u32, nonces_per_thread, batch_size];
// Write dispatch config
gpu_ctx.queue.write_buffer(
&resources.dispatch_config_buffer,
0,
bytemuck::cast_slice(&dispatch_config),
);
// Write start nonce for this batch
let start_nonce_bytes = batch_start.to_little_endian();
gpu_ctx
.queue
.write_buffer(&resources.start_nonce_buffer, 0, &start_nonce_bytes);
// Reset results buffer
const RESULTS_SIZE: usize = (1 + 16 + 16) * 4;
const ZEROS: [u8; RESULTS_SIZE] = [0; RESULTS_SIZE];
gpu_ctx
.queue
.write_buffer(&resources.results_buffer, 0, &ZEROS);
// Create and submit command buffer
let mut encoder = gpu_ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
{
let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None,
});
cpass.set_pipeline(&gpu_ctx.pipeline);
cpass.set_bind_group(0, &resources.bind_group, &[]);
cpass.dispatch_workgroups(num_workgroups, 1, 1);
}
encoder.copy_buffer_to_buffer(
&resources.results_buffer,
0,
&resources.staging_buffer,
0,
RESULTS_SIZE as u64,
);
gpu_ctx.queue.submit(Some(encoder.finish()));
// Wait for GPU to complete (blocking)
let buffer_slice = resources.staging_buffer.slice(..);
// Use atomic to track completion: 0 = pending, 1 = success, 2 = error
let map_status = std::sync::Arc::new(std::sync::atomic::AtomicU8::new(0));
let map_status_clone = map_status.clone();
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
map_status_clone.store(if result.is_ok() { 1 } else { 2 }, Ordering::Release);
});
// Poll until complete, error, or timeout (30 seconds max to prevent infinite hang)
let poll_start = std::time::Instant::now();
let max_poll_duration = std::time::Duration::from_secs(30);
let final_status = loop {
let _ = gpu_ctx.device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: Some(std::time::Duration::from_millis(10)),
});
match map_status.load(Ordering::Acquire) {
1 => break 1, // Success - buffer is mapped
2 => {
// Mapping failed - buffer was never successfully mapped, don't unmap
log::error!(
target: "gpu_engine",
"GPU buffer mapping failed - possible device lost or resource error"
);
return BatchResult::DeviceLost;
}
_ => {
// Still pending - check timeout
if poll_start.elapsed() > max_poll_duration {
log::error!(
target: "gpu_engine",
"GPU buffer mapping timed out after {}s - GPU may be unresponsive",
max_poll_duration.as_secs()
);
// Timeout: map_async callback never fired, buffer not mapped, don't unmap
return BatchResult::DeviceLost;
}
}
}
};
// Only reach here if final_status == 1 (success), buffer is mapped
debug_assert_eq!(final_status, 1);
// Read results
let data = buffer_slice.get_mapped_range();
let result_u32s: &[u32] = bytemuck::cast_slice(&data);
// Calculate the actual number of nonces dispatched
let dispatched_nonces = (total_threads * nonces_per_thread as u64).min(batch_size as u64);
if result_u32s[0] != 0 {
// Solution found!
let nonce_u32s = &result_u32s[1..17];
let hash_u32s = &result_u32s[17..33];
let nonce = U512::from_little_endian(bytemuck::cast_slice(nonce_u32s));
let hash = U512::from_little_endian(bytemuck::cast_slice(hash_u32s));
let work = nonce.to_big_endian();
// Calculate hashes computed based on GPU parallel execution model
let hashes_computed = if nonce >= batch_start {
let logical_index = (nonce - batch_start).as_u64();
let winning_iteration = logical_index % (nonces_per_thread as u64);
(total_threads * (winning_iteration + 1)).min(dispatched_nonces)
} else {
dispatched_nonces
};
drop(data);
resources.staging_buffer.unmap();
return BatchResult::Found {
candidate: Candidate { nonce, work, hash },
hash_count: hashes_computed,
};
}
drop(data);
resources.staging_buffer.unmap();
BatchResult::NotFound {
hash_count: dispatched_nonces,
}
}
/// Get vendor-specific optimal dispatch configuration
fn get_vendor_specific_dispatch(adapter_info: &wgpu::AdapterInfo, device: &wgpu::Device) -> u32 {
let limits = device.limits();
let max_workgroups = limits.max_compute_workgroups_per_dimension.min(65535);
let is_metal = adapter_info.backend == wgpu::Backend::Metal;
let tier = gpu_tiers::detect_gpu_tier(&adapter_info.name, adapter_info.vendor, is_metal);
let optimal_workgroups = (max_workgroups / tier.workgroup_divisor).max(tier.min_workgroups);
// Log GPU detection result
log::info!(
target: "gpu_engine",
"GPU detected: {} | tier: {} | workgroups: {} (max: {})",
adapter_info.name,
tier.name,
optimal_workgroups,
max_workgroups
);
if tier.is_fallback {
log::warn!(
target: "gpu_engine",
"GPU not recognized, using fallback config. Please report: name='{}', vendor=0x{:04X}, device={}",
adapter_info.name,
adapter_info.vendor,
adapter_info.device
);
log::warn!(target: "gpu_engine", "Report at: https://github.com/Quantus-Network/quantus-miner/issues");
}
optimal_workgroups
}
#[cfg(test)]
mod adapter_selection_tests {
use super::*;
fn info(
name: &str,
device_type: wgpu::DeviceType,
backend: wgpu::Backend,
) -> wgpu::AdapterInfo {
wgpu::AdapterInfo {
name: name.into(),
vendor: 0,
device: 0,
device_type,
driver: String::new(),
driver_info: String::new(),
backend,
}
}
#[test]
fn windows_multi_backend_keeps_one_context_per_physical_gpu() {
// The exact enumeration from issue #61
let infos = [
info(
"AMD Radeon(TM) Graphics",
wgpu::DeviceType::IntegratedGpu,
wgpu::Backend::Vulkan,
),
info(
"NVIDIA GeForce RTX 3070",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
info(
"AMD Radeon(TM) Graphics",
wgpu::DeviceType::IntegratedGpu,
wgpu::Backend::Dx12,
),
info(
"NVIDIA GeForce RTX 3070",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Dx12,
),
info(
"Microsoft Basic Render Driver",
wgpu::DeviceType::Cpu,
wgpu::Backend::Dx12,
),
];
// select_adapters returns all non-CPU adapters on best backend, discrete first
// Integrated filtering now happens after init in the init() function
assert_eq!(select_adapters(&infos), vec![1, 0]);
}
#[test]
fn identical_multi_gpu_rig_keeps_every_card() {
let infos = [
info(
"RTX 3090",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
info(
"RTX 3090",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
info(
"RTX 3090",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
];
assert_eq!(select_adapters(&infos), vec![0, 1, 2]);
}
#[test]
fn dx12_only_machine_returns_all_adapters_sorted() {
let infos = [
info("iGPU", wgpu::DeviceType::IntegratedGpu, wgpu::Backend::Dx12),
info("dGPU", wgpu::DeviceType::DiscreteGpu, wgpu::Backend::Dx12),
];
// select_adapters returns both, discrete first (integrated filtering is post-init)
assert_eq!(select_adapters(&infos), vec![1, 0]);
}
#[test]
fn integrated_only_machine_keeps_integrated() {
// When no discrete GPU exists, integrated GPUs should be used
let infos = [
info(
"Intel UHD",
wgpu::DeviceType::IntegratedGpu,
wgpu::Backend::Vulkan,
),
info(
"AMD Vega 8",
wgpu::DeviceType::IntegratedGpu,
wgpu::Backend::Vulkan,
),
];
assert_eq!(select_adapters(&infos), vec![0, 1]);
}
#[test]
fn software_only_environment_selects_nothing() {
let infos = [info(
"llvmpipe",
wgpu::DeviceType::Cpu,
wgpu::Backend::Vulkan,
)];
assert!(select_adapters(&infos).is_empty());
}
#[test]
fn empty_enumeration_selects_nothing() {
assert!(select_adapters(&[]).is_empty());
}
/// Exact scenario from Windows ASUS laptop with RX 560X + Vega 8 APU.
/// Both GPUs appear on both Vulkan and Dx12 backends.
/// select_adapters returns both Vulkan adapters (discrete first).
/// The init() function will later drop the integrated one if discrete succeeds.
#[test]
fn windows_amd_discrete_plus_apu_selects_both_on_best_backend() {
let infos = [
info(
"Microsoft Basic Render Driver",
wgpu::DeviceType::Cpu,
wgpu::Backend::Dx12,
),
info(
"AMD Radeon(TM) Vega 8 Graphics",
wgpu::DeviceType::IntegratedGpu,
wgpu::Backend::Dx12,
),
info(
"Radeon RX 560X",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Dx12,
),
info(
"AMD Radeon(TM) Vega 8 Graphics",
wgpu::DeviceType::IntegratedGpu,
wgpu::Backend::Vulkan,
),
info(
"Radeon RX 560X",
wgpu::DeviceType::DiscreteGpu,
wgpu::Backend::Vulkan,
),
];
// select_adapters returns both Vulkan adapters, discrete first
// - Index 0: Skipped (CPU emulated)
// - Index 1, 2: Skipped (Dx12 lower priority than Vulkan)
// - Index 4: Selected first (discrete, Vulkan)
// - Index 3: Selected second (integrated, Vulkan)
assert_eq!(select_adapters(&infos), vec![4, 3]);
}
// Tests for filter_initialized_gpus (post-init filtering)
#[test]
fn filter_discrete_present_drops_integrated() {
use wgpu::DeviceType::*;
// Discrete at index 0, integrated at index 1
let types = vec![DiscreteGpu, IntegratedGpu];
assert_eq!(filter_initialized_gpus(&types, false), vec![0]);
}
#[test]
fn filter_discrete_failed_keeps_integrated() {
use wgpu::DeviceType::*;
// Only integrated initialized (discrete failed/timed out)
let types = vec![IntegratedGpu];
assert_eq!(filter_initialized_gpus(&types, false), vec![0]);
}
#[test]
fn filter_allow_integrated_keeps_both() {
use wgpu::DeviceType::*;
let types = vec![DiscreteGpu, IntegratedGpu];
// With allow_integrated=true, keep both
assert_eq!(filter_initialized_gpus(&types, true), vec![0, 1]);
}
#[test]
fn filter_multiple_discrete_keeps_all_discrete() {
use wgpu::DeviceType::*;
let types = vec![DiscreteGpu, DiscreteGpu, IntegratedGpu];
// Drops integrated, keeps both discrete
assert_eq!(filter_initialized_gpus(&types, false), vec![0, 1]);
}
#[test]
fn filter_only_discrete_keeps_all() {
use wgpu::DeviceType::*;
let types = vec![DiscreteGpu, DiscreteGpu];
assert_eq!(filter_initialized_gpus(&types, false), vec![0, 1]);
}
#[test]
fn filter_multiple_integrated_no_discrete_keeps_all() {
use wgpu::DeviceType::*;
let types = vec![IntegratedGpu, IntegratedGpu];
// No discrete, so keep all integrated
assert_eq!(filter_initialized_gpus(&types, false), vec![0, 1]);
}
}