use glow::{Context, HasContext};
use std::cmp::Ordering;
use std::convert::{TryFrom, TryInto};
use std::borrow::Cow;
use std::collections::HashSet;
use std::str::FromStr;
unsafe fn checked_get_parameter_i32(
gl: &Context,
parameter: u32) -> Option<i32> {
let val = gl.get_parameter_i32(parameter);
match gl.get_error() {
glow::NO_ERROR => Some(val),
glow::INVALID_ENUM => None,
what =>
panic!("unexpected glError() value after glGet(0x{:08x}): \
0x{:08x}",
parameter,
what)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Information {
pub version: Version,
pub capabilities: Capabilities,
pub limits: Limits,
pub features: Features
}
impl Information {
const MIN_CORE: Release = Release { major: 4, minor: 3 };
const MIN_ES: Release = Release { major: 3, minor: 0 };
const MIN_WEB: Release = Release { major: 2, minor: 0 };
pub fn collect(context: &Context) -> Result<Self, UnsupportedContext> {
let gl = context;
let (version, major, minor) = unsafe {(
gl.get_parameter_string(glow::VERSION),
checked_get_parameter_i32(gl, glow::MAJOR_VERSION),
checked_get_parameter_i32(gl, glow::MINOR_VERSION),
)};
debug!("Reported OpenGL Version String: {}", version);
debug!("Reported OpenGL Version: {:?}.{:?}", major, minor);
let version = Version::parse(&version)
.map_err(|_| UnsupportedContext::InvalidVersion(version.clone()))?;
let dedicated = (
major.map(|major| u32::try_from(major)),
minor.map(|minor| u32::try_from(minor)));
match dedicated {
(Some(Ok(major)), Some(Ok(minor))) => {
let release = Release { major, minor };
if release != version.release {
return Err(UnsupportedContext::MismatchedRelease {
string: (version.release.major, version.release.minor),
dedicated: (major, minor)
})
}
},
(None, None) => warn!("implementation does not support dedicated \
version query targets. we will rely solely on the version \
string, which may not be as accurate"),
_ => return Err(UnsupportedContext::InvalidRelease(major, minor))
}
match version.profile {
Profile::Core if version.release >= Self::MIN_CORE => {},
Profile::Es if version.release >= Self::MIN_ES => {},
Profile::Web if version.release >= Self::MIN_WEB => {},
_ => return Err(UnsupportedContext::UnsupportedRelease {
profile: version.profile,
release: (version.release.major, version.release.minor)
})
}
let mut extensions = HashSet::new();
let _ = unsafe { Extension::enumerate(gl, &mut extensions) }?;
debug!("Discovered {} extensions: ", extensions.len());
for extension in &extensions {
debug!(" - {}", extension)
}
let capabilities = Capabilities {
buffer_mapping: version.profile != Profile::Web,
};
let limits = Limits::collect(context)?;
let features = Features {
sampler_anisotropy:
extensions.contains(&Extension::EXT_TEXTURE_FILTER_ANISOTROPIC),
readonly_framebuffer_feedback:
version.profile == Profile::Core
&& version.release >= Release { major: 4, minor: 5 }
};
if features.sampler_anisotropy && limits.max_sampler_anisotropy.is_none() {
return Err(UnsupportedContext::MissingMaxSamplerAnisotropy)
}
Ok(Self {
version,
capabilities,
limits,
features
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Extension {
vendor: Cow<'static, str>,
extension: Cow<'static, str>,
}
impl Extension {
pub const EXT_TEXTURE_FILTER_ANISOTROPIC: Self =
Self {
vendor: Cow::Borrowed("EXT"),
extension: Cow::Borrowed("texture_filter_anisotropic")
};
pub const ARB_TEXTURE_BARRIER: Self =
Self {
vendor: Cow::Borrowed("ARB"),
extension: Cow::Borrowed("texture_barrier")
};
}
impl FromStr for Extension {
type Err = ExtensionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let valid = s.chars()
.find(|c| {
let valid =
c.is_ascii_alphanumeric()
|| *c == '_';
!valid
})
.is_none();
if !valid {
return Err(Self::Err::InvalidCharacters)
}
let s = s.strip_prefix("GL_").unwrap_or(s);
let (vendor, extension) = s.split_once("_")
.ok_or(Self::Err::NotEnoughFields)?;
if &vendor.to_ascii_uppercase() != vendor {
return Err(Self::Err::NonUppercaseVendor)
}
Ok(Self {
vendor: Cow::Owned(vendor.to_string()),
extension: Cow::Owned(extension.to_string())
})
}
}
impl Extension {
unsafe fn enumerate(
gl: &glow::Context,
target: &mut impl Extend<Extension>) -> Result<usize, UnsupportedContext> {
let extension_count = gl.get_parameter_i32(glow::NUM_EXTENSIONS);
let num_supported = match gl.get_error() {
glow::INVALID_ENUM => false,
glow::NO_ERROR => true,
glow::INVALID_VALUE =>
panic!("glGetv(0x{:08x}) is out of range",
glow::NUM_EXTENSIONS),
what =>
panic!("glGet(0x{:08x}) returned error code 0x{:08x}",
glow::NUM_EXTENSIONS,
what)
};
let mut count = 0usize;
if num_supported {
for index in 0..extension_count {
let extension = gl.get_parameter_indexed_string(
glow::EXTENSIONS,
index.try_into().unwrap());
match gl.get_error() {
glow::INVALID_ENUM =>
return Err(UnsupportedContext::ExtensionEnumerationFailed),
glow::NO_ERROR => {},
glow::INVALID_VALUE =>
panic!("glGetv(0x{:08x}, index: {}) is out of range",
glow::NUM_EXTENSIONS,
index),
what =>
panic!("glGet(0x{:08x}) returned error code 0x{:08x}",
glow::NUM_EXTENSIONS,
what)
}
let extension = extension.trim().parse()
.expect("Invalid extension name");
target.extend(std::iter::once(extension));
count += 1;
}
} else {
warn!("Probing the extension count with GL_NUM_EXTENSIONS is not \
supported. Falling back to pulling the combined extension \
string using glGetString(GL_EXTENSIONS)");
let combined = gl.get_parameter_string(glow::EXTENSIONS);
let direct_supported = match gl.get_error() {
glow::INVALID_ENUM => false,
glow::NO_ERROR => true,
glow::INVALID_VALUE =>
panic!("glGetv(0x{:08x}) is out of range",
glow::NUM_EXTENSIONS),
what =>
panic!("glGet(0x{:08x}) returned error code 0x{:08x}",
glow::NUM_EXTENSIONS,
what)
};
if direct_supported {
let iterator = combined.split_ascii_whitespace()
.map(|slice| slice.to_string())
.map(|string| string.parse().expect("Invalid extension name"))
.inspect(|_| count += 1);
target.extend(iterator);
} else {
warn!("Probing the extension count with GL_EXTENSIONS is not \
supported. We're probably running under WebGL, so, we're \
falling back to glow::HasContext::supported_extensions()");
let iterator = gl.supported_extensions()
.iter()
.map(|name| name.parse()
.expect("Invalid extension name"));
target.extend(iterator);
}
}
Ok(count)
}
}
impl std::fmt::Display for Extension {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "GL_{}_{}", &self.vendor, &self.extension)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ExtensionParseError {
#[error("Invalid characters in extension name")]
InvalidCharacters,
#[error("Not enough fields for a valid extension name")]
NotEnoughFields,
#[error("The vendor name is not in upper case letters")]
NonUppercaseVendor,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Capabilities {
pub buffer_mapping: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Features {
pub sampler_anisotropy: bool,
pub readonly_framebuffer_feedback: bool,
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Limits {
pub max_textures: u32,
pub max_texture_size: u32,
pub max_texture_size_3d: u32,
pub max_texture_layers: u32,
pub max_uniform_block_bindings: u32,
pub max_uniform_block_size: u32,
pub max_framebuffer_color_attachments: u32,
pub max_framebuffer_attachment_width: Option<u32>,
pub max_framebuffer_attachment_height: Option<u32>,
pub max_viewport_width: Option<u32>,
pub max_viewport_height: Option<u32>,
pub max_sampler_anisotropy: Option<f32>,
}
impl Limits {
fn collect(gl: &Context) -> Result<Self, UnsupportedContext> {
let try_ensure_u32_indexed = |param: u32, index: u32| {
let value = unsafe {
let val = gl.get_parameter_indexed_i32(param, index);
match gl.get_error() {
glow::INVALID_ENUM => return Ok(None),
glow::NO_ERROR => {},
glow::INVALID_VALUE =>
panic!("glGetv(0x{:08x}, index: {}) is out of range",
param,
index),
what =>
panic!("glGet(0x{:08x}) returned error code 0x{:08x}",
param,
what)
}
val
};
u32::try_from(value)
.map(|value| Some(value))
.map_err(|_| UnsupportedContext::InvalidParameter {
value,
parameter: param
})
};
let try_ensure_u32 = |param: u32| {
let value = unsafe {
let val = gl.get_parameter_i32(param);
match gl.get_error() {
glow::INVALID_ENUM => return Ok(None),
glow::NO_ERROR => {},
what =>
panic!("glGet(0x{:08x}) returned error code 0x{:08x}",
param,
what)
}
val
};
u32::try_from(value)
.map(|value| Some(value))
.map_err(|_| UnsupportedContext::InvalidParameter {
value,
parameter: param
})
};
let ensure_u32 = |param: u32| {
let value = unsafe {
let val = gl.get_parameter_i32(param);
match gl.get_error() {
glow::INVALID_ENUM => return Err(
UnsupportedContext::UnsupportedParameter {
parameter: param
}),
glow::NO_ERROR => {},
what =>
panic!("glGet(0x{:08x}) returned error code 0x{:08x}",
param,
what)
}
val
};
u32::try_from(value)
.map_err(|_| UnsupportedContext::InvalidParameter {
value,
parameter: param
})
};
let try_ensure_f32 = |param: u32| {
let value = unsafe {
let val = gl.get_parameter_f32(param);
match gl.get_error() {
glow::INVALID_ENUM => return Ok(None),
glow::NO_ERROR => {},
what =>
panic!("glGet(0x{:08x}) returned error code 0x{:08x}",
param,
what)
}
val
};
Ok(Some(value))
};
Ok(Self {
max_textures: ensure_u32(glow::MAX_COMBINED_TEXTURE_IMAGE_UNITS)?,
max_texture_size: ensure_u32(glow::MAX_TEXTURE_SIZE)?,
max_texture_size_3d: ensure_u32(glow::MAX_3D_TEXTURE_SIZE)?,
max_texture_layers: ensure_u32(glow::MAX_ARRAY_TEXTURE_LAYERS)?,
max_uniform_block_bindings: ensure_u32(glow::MAX_UNIFORM_BUFFER_BINDINGS)?,
max_uniform_block_size: ensure_u32(glow::MAX_UNIFORM_BLOCK_SIZE)?,
max_framebuffer_color_attachments: ensure_u32(glow::MAX_COLOR_ATTACHMENTS)?,
max_framebuffer_attachment_width: try_ensure_u32(glow::MAX_FRAMEBUFFER_WIDTH)?,
max_framebuffer_attachment_height: try_ensure_u32(glow::MAX_FRAMEBUFFER_HEIGHT)?,
max_viewport_width: try_ensure_u32_indexed(glow::MAX_VIEWPORT_DIMS, 0)?,
max_viewport_height: try_ensure_u32_indexed(glow::MAX_VIEWPORT_DIMS, 1)?,
max_sampler_anisotropy: try_ensure_f32(glow::MAX_TEXTURE_MAX_ANISOTROPY_EXT)?,
})
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Version {
pub profile: Profile,
pub release: Release,
pub vendor: String,
}
impl Version {
fn parse(string: &str) -> Result<Self, &str> {
let (profile, string) = Profile::parse(string)?;
let (release, string) = Release::parse(string)?;
let vendor = string.trim().to_string();
Ok(Self { profile, release, vendor })
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Profile {
Core,
Es,
Web
}
impl Profile {
fn parse(string: &str) -> Result<(Self, &str), &str> {
let string = string.trim_start();
const WEB_SIGNATURE: &'static str = "WebGL ";
const ES_SIGNATURE: &'static str = "OpenGL ES ";
if string.is_empty() {
Err(string)
} else if string.starts_with(WEB_SIGNATURE) {
Ok((
Self::Web,
string.split_at(WEB_SIGNATURE.len()).1
))
} else if string.starts_with(ES_SIGNATURE) {
Ok((
Self::Es,
string.split_at(ES_SIGNATURE.len()).1
))
} else if string.chars().next().unwrap().is_numeric() {
Ok((
Self::Core,
string
))
} else {
Err(string)
}
}
}
impl std::fmt::Display for Profile {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Profile::Core =>
write!(f, "OpenGL"),
Profile::Es =>
write!(f, "OpenGL ES"),
Profile::Web =>
write!(f, "WebGL")
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Release {
pub major: u32,
pub minor: u32,
}
impl Release {
fn parse(string: &str) -> Result<(Self, &str), &str> {
let (major, minor) = string.split_once(".")
.ok_or(string)?;
let split = minor
.chars()
.enumerate()
.find_map(|(index, char)| if char.is_ascii() && char.is_numeric() {
None
} else {
Some(index)
});
let (minor, next) = match split {
Some(split) => minor.split_at(split),
None => (minor, "")
};
let cutoff = next
.chars()
.enumerate()
.find_map(|(index, char)| {
if char.is_ascii() && (char.is_numeric() || char == '.') {
None
} else {
Some(index)
}
});
let next = match cutoff {
Some(cutoff) => next.split_at(cutoff).1,
None => ""
};
let result = Self {
major: u32::from_str_radix(major, 10).map_err(|_| string)?,
minor: u32::from_str_radix(minor, 10).map_err(|_| string)?,
};
Ok((result, next))
}
}
impl PartialOrd for Release {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.major.partial_cmp(&other.major) {
None | Some(Ordering::Equal) => {},
Some(ordering) => return Some(ordering),
}
self.minor.partial_cmp(&other.minor)
}
}
impl Ord for Release {
fn cmp(&self, other: &Self) -> Ordering {
match self.major.cmp(&other.major) {
Ordering::Equal => {},
ordering => return ordering,
}
self.minor.cmp(&other.minor)
}
}
#[derive(Debug, thiserror::Error)]
pub enum UnsupportedContext {
#[error("the version string \"{0}\" is invalid")]
InvalidVersion(String),
#[error("the release {0:?}.{1:?} is invalid")]
InvalidRelease(Option<i32>, Option<i32>),
#[error("the release given by the version string ({string:?}) \
differ from the one gathered with dedicated calls ({dedicated:?})")]
MismatchedRelease {
string: (u32, u32),
dedicated: (u32, u32)
},
#[error("the valued returned by glGet(0x{parameter:08x}) is invalid: \
{value}")]
InvalidParameter {
value: i32,
parameter: u32,
},
#[error("the required paremeter 0x{parameter:08x} is not supported")]
UnsupportedParameter {
parameter: u32,
},
#[error("{profile} {release:?} is not supported")]
UnsupportedRelease {
profile: Profile,
release: (u32, u32)
},
#[error("extension enumeration is not supported")]
ExtensionEnumerationFailed,
#[error("sampler anisotropy is available, however, the implementation does \
not provide us with a maximum sampler anisotropy")]
MissingMaxSamplerAnisotropy,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile() {
assert_eq!(
Profile::parse("4.6.0 NVIDIA 457.51"),
Ok((Profile::Core, "4.6.0 NVIDIA 457.51")));
assert_eq!(Profile::parse("OpenGL ES 3.0"), Ok((Profile::Es, "3.0")));
assert_eq!(Profile::parse("WebGL 2.0"), Ok((Profile::Web, "2.0")))
}
#[test]
fn release() {
assert_eq!(
Release::parse("4.6.0 NVIDIA 457.51"),
Ok((Release { major: 4, minor: 6 }, " NVIDIA 457.51")));
assert_eq!(Release::parse("3.0"), Ok((Release { major: 3, minor: 0 }, "")));
assert_eq!(Release::parse("2.0"), Ok((Release { major: 2, minor: 0 }, "")));
}
#[test]
fn version() {
assert_eq!(
Version::parse("4.6 NVIDIA 457.51"),
Ok(Version {
profile: Profile::Core,
release: Release { major: 4, minor: 6 },
vendor: "NVIDIA 457.51".to_string()
}));
assert_eq!(
Version::parse("OpenGL ES 3.0"),
Ok(Version {
profile: Profile::Es,
release: Release { major: 3, minor: 0 },
vendor: "".to_string()
}));
assert_eq!(
Version::parse("WebGL 2.0"),
Ok(Version {
profile: Profile::Web,
release: Release { major: 2, minor: 0 },
vendor: "".to_string()
}));
}
}