pub mod cloud;
pub mod composite;
pub mod cpu;
#[cfg(feature = "cuda")]
pub mod cuda;
#[cfg(feature = "cuda")]
pub mod cuda_detect;
pub mod daemon;
#[cfg(feature = "gpu")]
pub mod gpu;
#[cfg(feature = "cuda")]
pub mod multi_cuda;
#[cfg(feature = "gpu")]
pub mod multi_gpu;
#[cfg(feature = "cuda")]
pub mod persistent_cuda;
pub mod protocol;
pub mod sha256;
pub mod simd_cpu;
pub mod stats;
pub mod work_unit;
use async_trait::async_trait;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::time::Duration;
use sha256::Sha256Midstate;
use work_unit::NonceTable;
pub const NONCE_SPACE_SIZE: u32 = 1_000_000;
pub type CancelFlag = Arc<AtomicBool>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendChoice {
Auto,
Gpu,
Cpu,
}
impl BackendChoice {
pub fn as_cli_str(self) -> &'static str {
match self {
BackendChoice::Auto => "auto",
BackendChoice::Gpu => "gpu",
BackendChoice::Cpu => "cpu",
}
}
}
#[derive(Debug, Clone)]
pub struct MinerConfig {
pub server_url: String,
pub wallet_path: std::path::PathBuf,
pub webcash_wallet_path: std::path::PathBuf,
pub max_difficulty: u32,
pub backend: BackendChoice,
pub cpu_threads: Option<usize>,
pub accept_terms: bool,
pub devices: Option<Vec<usize>>,
}
#[derive(Debug, Clone)]
pub struct MiningResult {
pub nonce1_idx: u16,
pub nonce2_idx: u16,
pub hash: [u8; 32],
pub difficulty_achieved: u32,
}
#[derive(Debug, Clone)]
pub struct MiningChunkResult {
pub result: Option<MiningResult>,
pub attempted: u64,
pub elapsed: Duration,
}
impl MiningChunkResult {
pub fn empty() -> Self {
MiningChunkResult {
result: None,
attempted: 0,
elapsed: Duration::from_secs(0),
}
}
}
#[async_trait]
pub trait MinerBackend: Send + Sync {
fn name(&self) -> &str;
fn startup_summary(&self) -> Vec<String> {
Vec::new()
}
async fn benchmark(&self) -> anyhow::Result<f64>;
fn max_batch_hint(&self) -> u32 {
NONCE_SPACE_SIZE
}
fn recommended_pipeline_depth(&self) -> usize {
1
}
async fn mine_range(
&self,
midstate: &Sha256Midstate,
nonce_table: &NonceTable,
difficulty: u32,
start_nonce: u32,
nonce_count: u32,
cancel: Option<CancelFlag>,
) -> anyhow::Result<MiningChunkResult>;
async fn mine_work_units(
&self,
midstates: &[Sha256Midstate],
nonce_table: &NonceTable,
difficulty: u32,
cancel: Option<CancelFlag>,
) -> anyhow::Result<Vec<MiningChunkResult>> {
let mut out = Vec::with_capacity(midstates.len());
for midstate in midstates {
out.push(
self.mine_range(
midstate,
nonce_table,
difficulty,
0,
NONCE_SPACE_SIZE,
cancel.clone(),
)
.await?,
);
}
Ok(out)
}
async fn mine_work_unit(
&self,
midstate: &Sha256Midstate,
nonce_table: &NonceTable,
difficulty: u32,
) -> anyhow::Result<MiningChunkResult> {
self.mine_range(midstate, nonce_table, difficulty, 0, NONCE_SPACE_SIZE, None)
.await
}
}
pub fn choose_best_result(
a: Option<MiningResult>,
b: Option<MiningResult>,
) -> Option<MiningResult> {
match (a, b) {
(None, None) => None,
(Some(x), None) => Some(x),
(None, Some(y)) => Some(y),
(Some(x), Some(y)) => {
if y.difficulty_achieved > x.difficulty_achieved {
Some(y)
} else {
Some(x)
}
}
}
}
pub(crate) fn split_assignments_for_weights(
weights: &[f64],
start_nonce: u32,
nonce_count: u32,
) -> Vec<(usize, u32, u32)> {
if weights.is_empty() {
return Vec::new();
}
let start = start_nonce.min(NONCE_SPACE_SIZE);
let end = start.saturating_add(nonce_count).min(NONCE_SPACE_SIZE);
if start >= end {
return Vec::new();
}
let total = end - start;
let weight_sum = weights.iter().sum::<f64>().max(1.0);
let mut assignments = Vec::with_capacity(weights.len());
let mut assigned = 0u32;
for idx in 0..weights.len() {
let remaining = total.saturating_sub(assigned);
if remaining == 0 {
break;
}
let chunk = if idx == weights.len() - 1 {
remaining
} else {
let ideal = ((total as f64) * (weights[idx] / weight_sum)).round() as u32;
ideal.clamp(1, remaining)
};
assignments.push((idx, start + assigned, chunk));
assigned = assigned.saturating_add(chunk);
}
assignments
}
#[cfg(feature = "cuda")]
fn cuda_device_count() -> usize {
cuda_detect::ensure_cuda_libraries();
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let n = std::panic::catch_unwind(|| cudarc::driver::CudaContext::device_count())
.ok()
.and_then(|r| r.ok())
.unwrap_or(0) as usize;
std::panic::set_hook(prev);
n
}
#[derive(Debug, Clone)]
pub enum DeviceKind {
#[cfg(feature = "cuda")]
Cuda { ordinal: usize },
#[cfg(feature = "gpu")]
Wgpu { adapters: Vec<gpu::AdapterIdentity> },
}
#[derive(Debug, Clone)]
pub struct DeviceInfo {
pub id: usize,
pub label: String,
pub kind: DeviceKind,
}
pub async fn enumerate_all_devices() -> Vec<DeviceInfo> {
let mut devices = Vec::new();
#[allow(unused_mut, unused_variables)]
let mut next_id = 0usize;
#[cfg(feature = "cuda")]
{
let cuda_count = cuda_device_count();
for ordinal in 0..cuda_count {
let name = cudarc::driver::CudaContext::new(ordinal)
.ok()
.and_then(|ctx| ctx.name().ok())
.unwrap_or_else(|| format!("CUDA device {ordinal}"));
devices.push(DeviceInfo {
id: next_id,
label: format!("{name} (CUDA)"),
kind: DeviceKind::Cuda { ordinal },
});
next_id += 1;
}
}
#[cfg(feature = "gpu")]
{
use std::collections::BTreeMap;
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: gpu::COMPUTE_BACKENDS,
..Default::default()
});
let adapters = instance.enumerate_adapters(gpu::COMPUTE_BACKENDS).await;
struct AdapterEntry {
name: String,
vendor: u32,
device: u32,
pci_bus: String,
identity: gpu::AdapterIdentity,
}
let mut entries = Vec::new();
for adapter in adapters {
let info = adapter.get_info();
if info.device_type == wgpu::DeviceType::Cpu {
continue;
}
entries.push(AdapterEntry {
name: info.name.clone(),
vendor: info.vendor,
device: info.device,
pci_bus: info.device_pci_bus_id.trim().to_string(),
identity: gpu::AdapterIdentity::from_info(&info),
});
}
let mut known_bus_ids: std::collections::HashMap<(u32, u32, String), String> =
std::collections::HashMap::new();
for e in &entries {
if !e.pci_bus.is_empty() {
known_bus_ids
.entry((e.vendor, e.device, e.name.clone()))
.or_insert_with(|| e.pci_bus.clone());
}
}
let mut device_groups: BTreeMap<String, (String, Vec<gpu::AdapterIdentity>)> =
BTreeMap::new();
for e in entries {
let bus = if !e.pci_bus.is_empty() {
e.pci_bus.clone()
} else {
known_bus_ids
.get(&(e.vendor, e.device, e.name.clone()))
.cloned()
.unwrap_or_default()
};
let phys_key = if !bus.is_empty() {
format!("pci:{bus}")
} else {
format!("dev:{}:{}:{}", e.vendor, e.device, e.name)
};
device_groups
.entry(phys_key)
.or_insert_with(|| (e.name.clone(), Vec::new()))
.1
.push(e.identity);
}
for (_phys_key, (name, adapters_for_device)) in device_groups {
if adapters_for_device.is_empty() {
continue;
}
devices.push(DeviceInfo {
id: next_id,
label: name,
kind: DeviceKind::Wgpu {
adapters: adapters_for_device,
},
});
next_id += 1;
}
}
devices
}
pub async fn select_backend_for_devices(
device_ids: &[usize],
) -> anyhow::Result<Box<dyn MinerBackend>> {
let all = enumerate_all_devices().await;
let mut backends: Vec<Arc<dyn MinerBackend>> = Vec::new();
for &id in device_ids {
let dev = all.iter().find(|d| d.id == id).ok_or_else(|| {
anyhow::anyhow!("device {id} not found (run `webminer list-devices`)")
})?;
#[allow(unreachable_code, unused_variables)]
let backend: Option<Arc<dyn MinerBackend>> = match &dev.kind {
#[cfg(feature = "cuda")]
DeviceKind::Cuda { ordinal } => {
let m = cuda::CudaMiner::try_new(*ordinal).await;
m.map(|m| Arc::new(m) as Arc<dyn MinerBackend>)
}
#[cfg(feature = "gpu")]
DeviceKind::Wgpu { adapters } => {
let mut result: Option<(Arc<dyn MinerBackend>, String)> = None;
for identity in adapters {
if !gpu::subprocess_probe(identity) {
continue;
}
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: gpu::COMPUTE_BACKENDS,
..Default::default()
});
let found = instance.enumerate_adapters(gpu::COMPUTE_BACKENDS).await;
if let Some(adapter) = identity.find_matching(found) {
if let Some(m) = gpu::GpuMiner::try_from_adapter(adapter).await {
result = Some((Arc::new(m) as _, identity.backend.clone()));
break;
}
}
}
if let Some((m, adapter_backend)) = result {
println!("Device {id}: {} ({adapter_backend})", dev.label);
backends.push(m);
} else {
eprintln!("Device {id}: {} — no working adapter found", dev.label);
}
continue;
}
#[allow(unreachable_patterns)]
_ => None,
};
if let Some(b) = backend {
println!("Device {id}: {}", dev.label);
backends.push(b);
} else {
eprintln!("Device {id}: {} — failed to initialize", dev.label);
}
}
if backends.is_empty() {
anyhow::bail!("no devices could be initialized from --device selection");
}
Ok(Box::new(composite::CompositeBackend::new(backends).await))
}
#[cfg(feature = "gpu")]
pub async fn init_wgpu_miners_from_devices() -> Vec<gpu::GpuMiner> {
let devices = enumerate_all_devices().await;
let wgpu_count = devices
.iter()
.filter(|d| matches!(&d.kind, DeviceKind::Wgpu { .. }))
.count();
if wgpu_count > 0 {
eprintln!("GPU: {wgpu_count} physical device(s) found, probing...");
}
let mut miners = Vec::new();
for dev in &devices {
#[allow(irrefutable_let_patterns)]
if let DeviceKind::Wgpu { adapters } = &dev.kind {
eprintln!(
"GPU[{}]: {} ({} adapter backend(s))",
dev.id,
dev.label,
adapters.len(),
);
let mut initialized = false;
for identity in adapters {
if !gpu::subprocess_probe(identity) {
continue;
}
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: gpu::COMPUTE_BACKENDS,
..Default::default()
});
let found = instance.enumerate_adapters(gpu::COMPUTE_BACKENDS).await;
if let Some(adapter) = identity.find_matching(found) {
if let Some(miner) = gpu::GpuMiner::try_from_adapter(adapter).await {
eprintln!(
"GPU[{}]: {} ready ({})",
dev.id, dev.label, identity.backend,
);
miners.push(miner);
initialized = true;
break;
}
}
}
if !initialized {
eprintln!("GPU[{}]: {} — no working adapter found", dev.id, dev.label);
}
}
}
if wgpu_count > 0 {
eprintln!("GPU: {}/{} device(s) initialized", miners.len(), wgpu_count);
}
miners
}
pub async fn select_backend(
choice: BackendChoice,
cpu_threads: Option<usize>,
) -> anyhow::Result<Box<dyn MinerBackend>> {
match choice {
BackendChoice::Cpu => {
let miner = simd_cpu::SimdCpuMiner::from_option(cpu_threads);
println!(
"Mining backend: {} ({} threads)",
miner.name(),
miner.thread_count()
);
Ok(Box::new(miner))
}
BackendChoice::Gpu => {
#[cfg(feature = "gpu")]
{
let gpu_miners = init_wgpu_miners_from_devices().await;
if let Some(miner) = multi_gpu::MultiGpuMiner::from_miners(gpu_miners).await {
println!("Mining backend: {} (Vulkan/wgpu)", miner.name());
return Ok(Box::new(miner));
}
}
#[cfg(not(feature = "gpu"))]
{
anyhow::bail!("wgpu/Vulkan GPU support not compiled (enable 'gpu' feature)")
}
#[cfg(feature = "gpu")]
{
anyhow::bail!("No compatible Vulkan/wgpu GPU found. Try --backend auto for CUDA.")
}
}
BackendChoice::Auto => {
#[cfg(feature = "cuda")]
{
let cuda_ok = cuda_device_count();
if cuda_ok > 0 {
if let Some(multi_cuda) = multi_cuda::MultiCudaMiner::try_new().await {
println!("Selected: {} (auto prefers CUDA)", multi_cuda.name());
return Ok(Box::new(multi_cuda));
}
}
}
#[cfg(feature = "gpu")]
{
let gpu_miners = init_wgpu_miners_from_devices().await;
if let Some(multi_gpu) = multi_gpu::MultiGpuMiner::from_miners(gpu_miners).await {
println!(
"Selected: {} (auto fallback: Vulkan/wgpu)",
multi_gpu.name()
);
return Ok(Box::new(multi_gpu));
}
}
let miner = simd_cpu::SimdCpuMiner::from_option(cpu_threads);
println!("Mining backend: {} (no GPU available)", miner.name());
Ok(Box::new(miner))
}
}
}