1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! Tools for managing the device pool either implicitly or explicitly
//!
//! If you plan on not really delving into the WebGPU internals of Emu and just want to work from the other side of the abstraction,
//! the only important thing here is [`assert_device_pool_initialized`](fn.assert_device_pool_initialized.html).

use derive_more::{From, Into};
use std::cell::RefCell;
use std::sync::Mutex;

use crate::device::*;
use crate::error::*;

/// Represents a member of the device pool
///
/// This holds both a mutex to a `Device` and information about the device. You must create instances of `DevicePoolMember` to construct your own custom device pool using
/// [`pool`](fn.pool.html).
#[derive(From, Into)]
pub struct DevicePoolMember {
    pub device: Mutex<Device>, // this is a Mutex because we want to be able to mutate this from different threads
    pub device_info: Option<DeviceInfo>, // we duplicate data here because we don't want to have to lock the Mutex just to see info
}

// global state
// used for device pool stuff
lazy_static! {
    static ref CUSTOM_DEVICE_POOL: Mutex<Option<Vec<DevicePoolMember>>> = Mutex::new(None);
    static ref DEVICE_POOL: Option<Vec<DevicePoolMember>> = {
        if CUSTOM_DEVICE_POOL.lock().unwrap().is_some() {
            Some(CUSTOM_DEVICE_POOL.lock().unwrap().take().unwrap()) // we can unwrap since we know it is Some
        } else {
            panic!("pool of devices has not been initialized with `assert_device_pool_initialized`")
        }
    };
}

// thread local state
// used for selecting device for each thread
thread_local! {
    // this is the index of the device being used by the current thread in the above device pool Vec
    // it defaults to None (and not 0 or anything else) because it isn't known if there even is an available device
    // it shouldn't be used until the device pool is initialized
    static DEVICE_IDX: RefCell<Option<usize>> = RefCell::new(None); // the Option here is None when it isn't initialized or DEVICE_POOL is empty
}

// this should be called every time before you want to use DEVICE_POOL
fn maybe_initialize_device_pool() {
    lazy_static::initialize(&DEVICE_POOL);
}

// this should be called every time before you want to use DEVICE_IDX
fn maybe_initialize_device_idx() {
    if DEVICE_POOL.is_some() && DEVICE_IDX.with(|idx| idx.borrow().is_none()) {
        if DEVICE_POOL.as_ref().unwrap().len() > 0 {
            // we can only set device index if pool is Some and has length
            DEVICE_IDX.with(|idx| *idx.borrow_mut() = Some(0));
        }
    }
}

/// Sets the device pool to the given `Vec` of devices
///
/// You can use `pool` to set up a custom pool of devices. It can only be successfully called just once. Calling `pool` multiple times will result in a panic at runtime.
/// ```
/// # use {emu_core::prelude::*, emu_glsl::*, zerocopy::*, std::sync::Mutex};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut device = futures::executor::block_on(Device::all()).remove(0);
/// pool(vec![DevicePoolMember {
///     device: Mutex::new(device),
///     device_info: None
/// }])?;
/// // technically, we don't need this assertion because we know the pool is initialized
/// futures::executor::block_on(assert_device_pool_initialized());
/// # futures::executor::block_on(assert_device_pool_initialized());
/// let pi: DeviceBox<f32> = DeviceBox::with_size(std::mem::size_of::<f32>())?;
/// # Ok(())
/// # }
/// ```
///
/// This function can be useful if you want to work with the WebGPU internals with [`take`](fn.take.html).
/// You can call `pool` at the start of your application to initialize all the devices you plan on using.
/// You can then do graphics stuff using `take` and all of [`wgpu-rs`](https://crates.io/crates/wgpu) and compute stuff with high-level `get`/`set`/`compile`/`spawn`.
pub fn pool(new_device_pool: Vec<DevicePoolMember>) -> Result<(), PoolAlreadyInitializedError> {
    if CUSTOM_DEVICE_POOL.lock().unwrap().is_some() {
        Err(PoolAlreadyInitializedError)
    } else {
        // we only initialize the custom device pool right now
        // the actual device pool will be initialized automatically when it is used
        *CUSTOM_DEVICE_POOL.lock().unwrap() = Some(new_device_pool);
        Ok(())
    }
}

/// Asserts that the device pool has been initialized
///
/// This must be the first thing you call before using Emu for anything. The only thing you might call before this is [`pool`](fn.pool.html) if you are manually setting the pool of devices.
/// You can call this as many times as you like. If no custom pool has be set with `pool`, this will go ahead and initialize all detected devices and add them to the pool.
///
/// This function is asynchronous so you must pass the future it returns to an executor like so.
/// ```
/// # use {emu_core::prelude::*, emu_glsl::*, zerocopy::*, std::sync::Mutex};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// futures::executor::block_on(assert_device_pool_initialized());
/// # futures::executor::block_on(assert_device_pool_initialized());
/// # futures::executor::block_on(assert_device_pool_initialized());
/// # Ok(())
/// # }
/// ```
///
/// So if you are an application, definitely call this before you use Emu do anything on a GPU device.
/// If you are a library, definitely make sure that you call this before every possible first time that you use Emu.
/// You don't have to call it before _every_ API call of course - just before every time when it's possible that this is the first time you are using Emu.
pub async fn assert_device_pool_initialized() {
    if CUSTOM_DEVICE_POOL.lock().unwrap().is_none() {
        let devices = Device::all().await;
        *CUSTOM_DEVICE_POOL.lock().unwrap() = Some(
            devices
                .into_iter()
                .map(|device| {
                    let info = device.info.clone();
                    DevicePoolMember {
                        device: Mutex::new(device),
                        device_info: info,
                    }
                })
                .collect::<Vec<DevicePoolMember>>(),
        );
    }
}

/// Takes the device currently selected out of the device pool and hands you a mutex for mutating the device's sate
///
/// This function is the link between the high-level pool-based interface and the low-level WebGPU internals.
/// With `take`, you can mutate the WebGPU internals "hidden" behind the device pool.
/// Consequently, you can have full control over each device in the pool if you want or use high-level `get`/`set`/`compile`/`spawn`.
/// ```
/// # use {emu_core::prelude::*, emu_glsl::*, zerocopy::*, std::sync::Mutex};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// futures::executor::block_on(assert_device_pool_initialized());
/// # futures::executor::block_on(assert_device_pool_initialized());
/// let mut d = take()?.lock()?;
/// let pi: DeviceBox<f32> = d.create_with_size(std::mem::size_of::<f32>());
/// # Ok(())
/// # }
/// ```
pub fn take<'a>() -> Result<&'a Mutex<Device>, NoDeviceError> {
    maybe_initialize_device_pool();
    maybe_initialize_device_idx();

    DEVICE_IDX.with(|idx| {
        if idx.borrow().is_none() {
            // inv: there are no devices in the device pool, since idx could not be initialized to Some
            Err(NoDeviceError)
        } else {
            Ok(&(DEVICE_POOL
                .as_ref()
                .unwrap()
                .get(idx.borrow().unwrap())
                .unwrap()
                .device))
        }
    })
}

/// Holds information about a member of the device pool
#[derive(Clone, Debug, PartialEq)]
pub struct DevicePoolMemberInfo {
    /// The index of the device in the pool
    pub index: usize,
    /// The actual information wrapped by this structure
    pub info: Option<DeviceInfo>,
}

/// Returns information about all devices in the pool
pub fn info_all() -> Vec<DevicePoolMemberInfo> {
    maybe_initialize_device_pool();
    maybe_initialize_device_idx();

    DEVICE_POOL
        .as_ref()
        .unwrap()
        .iter()
        .enumerate()
        .map(|(i, device)| DevicePoolMemberInfo {
            index: i,
            info: device.device_info.clone(),
        })
        .collect()
}

/// Returns information about the currently selected device
pub fn info() -> Result<DevicePoolMemberInfo, NoDeviceError> {
    maybe_initialize_device_pool();
    maybe_initialize_device_idx();

    DEVICE_IDX.with(|idx| {
        if idx.borrow().is_none() {
            // inv: there are no devices in the device pool, since idx could not be initialized to Some
            Err(NoDeviceError)
        } else {
            Ok(DevicePoolMemberInfo {
                index: idx.borrow().unwrap(),
                info: DEVICE_POOL
                    .as_ref()
                    .unwrap()
                    .get(idx.borrow().unwrap())
                    .unwrap()
                    .device_info
                    .clone(),
            })
        }
    })
}

/// Selects a device from the pool using the given selector function
///
/// Emu uses thread-local storage to keep track of the selected device for each thread.
/// `select` lets you select a device for the thread it is called from.
/// ```
/// # use {emu_core::prelude::*, emu_glsl::*, zerocopy::*, std::sync::Mutex};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// futures::executor::block_on(assert_device_pool_initialized());
/// # futures::executor::block_on(assert_device_pool_initialized());
/// select(|idx, info| if let Some(info) = info {
///     info.name().to_ascii_lowercase().contains("intel")
/// } else {
///     false
/// })?;
/// let mut d = take()?.lock()?;
/// let pi: DeviceBox<f32> = d.create_with_size(std::mem::size_of::<f32>());
/// # Ok(())
/// # }
/// ```
pub fn select<F: FnMut(usize, Option<DeviceInfo>) -> bool>(
    mut selector: F,
) -> Result<(), NoDeviceError> {
    maybe_initialize_device_pool();
    maybe_initialize_device_idx();

    DEVICE_IDX.with(|idx| {
        if idx.borrow().is_none() {
            // inv: there are no devices in the device pool, since idx could not be initialized to Some
            Err(NoDeviceError)
        } else {
            *idx.borrow_mut() = Some(
                info_all()
                    .iter()
                    .position(|member_info| selector(member_info.index, member_info.info.clone()))
                    .ok_or(NoDeviceError)?,
            );

            Ok(())
        }
    })
}