Fix extended-region scene rendering and delivery
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
All code and API mappings must be cross-platform; do not use macOS-only APIs unless equivalent Linux and Windows implementations are provided.
|
All code and API mappings must be cross-platform; do not use macOS-only APIs unless equivalent Linux and Windows implementations are provided.
|
||||||
Gitea Actions workflows must target only `ubuntu-latest`; do not add macOS or Windows Gitea jobs.
|
Gitea Actions workflows must target only `ubuntu-latest`; do not add macOS or Windows Gitea jobs.
|
||||||
|
Crates named `libremetaverse-*` are restricted to compatibility mappings of the C# LibreMetaverse libraries. New MetaCrate-native functionality must use a `metacrate-*` crate name; the reusable wgpu renderer is `metacrate-rendering-wgpu`.
|
||||||
|
|||||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -2535,6 +2535,7 @@ dependencies = [
|
|||||||
"libremetaverse-types",
|
"libremetaverse-types",
|
||||||
"mentra",
|
"mentra",
|
||||||
"metacrate-lsl-tools",
|
"metacrate-lsl-tools",
|
||||||
|
"metacrate-rendering-wgpu",
|
||||||
"rustls",
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -2544,7 +2545,6 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"unicode-width 0.2.2",
|
"unicode-width 0.2.2",
|
||||||
"url",
|
"url",
|
||||||
"wgpu",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2572,6 +2572,13 @@ dependencies = [
|
|||||||
"stats_alloc",
|
"stats_alloc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metacrate-rendering-wgpu"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"wgpu",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ members = [
|
|||||||
"crates/libremetaverse-rendering-simple",
|
"crates/libremetaverse-rendering-simple",
|
||||||
"crates/libremetaverse-rendering-mesh-foundry",
|
"crates/libremetaverse-rendering-mesh-foundry",
|
||||||
"crates/metacrate-lsl-tools",
|
"crates/metacrate-lsl-tools",
|
||||||
|
"crates/metacrate-rendering-wgpu",
|
||||||
"crates/libremetaverse-rlv",
|
"crates/libremetaverse-rlv",
|
||||||
"crates/libremetaverse-utilities",
|
"crates/libremetaverse-utilities",
|
||||||
"crates/libremetaverse-voice-vivox",
|
"crates/libremetaverse-voice-vivox",
|
||||||
@@ -36,6 +37,7 @@ default-members = [
|
|||||||
"crates/libremetaverse-rendering-simple",
|
"crates/libremetaverse-rendering-simple",
|
||||||
"crates/libremetaverse-rendering-mesh-foundry",
|
"crates/libremetaverse-rendering-mesh-foundry",
|
||||||
"crates/metacrate-lsl-tools",
|
"crates/metacrate-lsl-tools",
|
||||||
|
"crates/metacrate-rendering-wgpu",
|
||||||
"crates/libremetaverse-rlv",
|
"crates/libremetaverse-rlv",
|
||||||
"crates/libremetaverse-utilities",
|
"crates/libremetaverse-utilities",
|
||||||
"crates/libremetaverse-voice-vivox",
|
"crates/libremetaverse-voice-vivox",
|
||||||
|
|||||||
@@ -14,12 +14,15 @@
|
|||||||
|
|
||||||
use crate::interfaces::IMessage;
|
use crate::interfaces::IMessage;
|
||||||
use crate::packets::{
|
use crate::packets::{
|
||||||
AgentPausePacket, AgentResumePacket, CloseCircuitPacket, CompleteAgentMovementPacket,
|
AgentPausePacket, AgentResumePacket, AgentThrottlePacket, CloseCircuitPacket,
|
||||||
CompletePingCheckPacket, GenericStreamingMessagePacket, KickUserPacket, LogoutReplyPacket,
|
CompleteAgentMovementPacket, CompletePingCheckPacket, GenericStreamingMessagePacket,
|
||||||
LogoutRequestPacket, Packet, PacketType, RegionHandshakePacket, RegionHandshakeReplyPacket,
|
KickUserPacket, LogoutReplyPacket, LogoutRequestPacket, Packet, PacketType,
|
||||||
SimStatsPacket, StartPingCheckPacket, UseCircuitCodePacket,
|
RegionHandshakePacket, RegionHandshakeReplyPacket, SimStatsPacket, StartPingCheckPacket,
|
||||||
|
UseCircuitCodePacket,
|
||||||
|
};
|
||||||
|
use crate::udp_transport::{
|
||||||
|
AgentThrottleSender, UDPBase, UDPPacketBuffer, UdpPacketHandler, UdpTransportConfig,
|
||||||
};
|
};
|
||||||
use crate::udp_transport::{UDPBase, UDPPacketBuffer, UdpPacketHandler, UdpTransportConfig};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AccountLevelBenefits, Avatar, Caps, Error, GenericStreamingMethod, GridClient, Helpers,
|
AccountLevelBenefits, Avatar, Caps, Error, GenericStreamingMethod, GridClient, Helpers,
|
||||||
LoginCredential, LoginParams, LoginProgressEventArgs, LoginResponseData, LoginStatus,
|
LoginCredential, LoginParams, LoginProgressEventArgs, LoginResponseData, LoginStatus,
|
||||||
@@ -1509,17 +1512,10 @@ impl Simulator {
|
|||||||
patch: TerrainPatch,
|
patch: TerrainPatch,
|
||||||
large_region: bool,
|
large_region: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let (width, height) = if large_region {
|
let (width, height) = if large_region { (256, 256) } else { (16, 16) };
|
||||||
(
|
|
||||||
usize::try_from(self.size_x.max(16) / 16).map_err(|_| Error::Argument)?,
|
|
||||||
usize::try_from(self.size_y.max(16) / 16).map_err(|_| Error::Argument)?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(16, 16)
|
|
||||||
};
|
|
||||||
let x = usize::try_from(patch.x).map_err(|_| Error::Argument)?;
|
let x = usize::try_from(patch.x).map_err(|_| Error::Argument)?;
|
||||||
let y = usize::try_from(patch.y).map_err(|_| Error::Argument)?;
|
let y = usize::try_from(patch.y).map_err(|_| Error::Argument)?;
|
||||||
if x >= width || y >= height || width > 256 || height > 256 {
|
if x >= width || y >= height {
|
||||||
return Err(Error::Argument);
|
return Err(Error::Argument);
|
||||||
}
|
}
|
||||||
let index = y
|
let index = y
|
||||||
@@ -1528,6 +1524,23 @@ impl Simulator {
|
|||||||
.ok_or(Error::Argument)?;
|
.ok_or(Error::Argument)?;
|
||||||
let patch_count = width.checked_mul(height).ok_or(Error::Argument)?;
|
let patch_count = width.checked_mul(height).ok_or(Error::Argument)?;
|
||||||
let mut terrain = write(&self.terrain);
|
let mut terrain = write(&self.terrain);
|
||||||
|
if !terrain.is_empty() && terrain.len() != patch_count {
|
||||||
|
let previous = std::mem::take(&mut *terrain);
|
||||||
|
terrain.resize_with(patch_count, || TerrainPatch {
|
||||||
|
data: Vec::new(),
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
for old in previous.into_iter().filter(|old| !old.data.is_empty()) {
|
||||||
|
let (Ok(old_x), Ok(old_y)) = (usize::try_from(old.x), usize::try_from(old.y))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if old_x < width && old_y < height {
|
||||||
|
terrain[old_y * width + old_x] = old;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
terrain.resize_with(patch_count, || TerrainPatch {
|
terrain.resize_with(patch_count, || TerrainPatch {
|
||||||
data: Vec::new(),
|
data: Vec::new(),
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -1622,14 +1635,21 @@ impl Simulator {
|
|||||||
let (Ok(x), Ok(y)) = (u32::try_from(x), u32::try_from(y)) else {
|
let (Ok(x), Ok(y)) = (u32::try_from(x), u32::try_from(y)) else {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
if x >= self.size_x || y >= self.size_y {
|
let terrain = read(&self.terrain);
|
||||||
|
let per_edge = if terrain.len() > 256 {
|
||||||
|
256
|
||||||
|
} else {
|
||||||
|
usize::try_from(self.size_x.max(16) / 16).map_err(|_| Error::Argument)?
|
||||||
|
};
|
||||||
|
if usize::try_from(x / 16).map_err(|_| Error::Argument)? >= per_edge
|
||||||
|
|| usize::try_from(y / 16).map_err(|_| Error::Argument)?
|
||||||
|
>= terrain.len().div_ceil(per_edge)
|
||||||
|
{
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
let per_edge = usize::try_from(self.size_x.max(16) / 16).map_err(|_| Error::Argument)?;
|
|
||||||
let patch_x = usize::try_from(x / 16).map_err(|_| Error::Argument)?;
|
let patch_x = usize::try_from(x / 16).map_err(|_| Error::Argument)?;
|
||||||
let patch_y = usize::try_from(y / 16).map_err(|_| Error::Argument)?;
|
let patch_y = usize::try_from(y / 16).map_err(|_| Error::Argument)?;
|
||||||
let point = usize::try_from((y % 16) * 16 + (x % 16)).map_err(|_| Error::Argument)?;
|
let point = usize::try_from((y % 16) * 16 + (x % 16)).map_err(|_| Error::Argument)?;
|
||||||
let terrain = read(&self.terrain);
|
|
||||||
let Some(patch) = terrain.get(patch_y * per_edge + patch_x) else {
|
let Some(patch) = terrain.get(patch_y * per_edge + patch_x) else {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
@@ -2927,6 +2947,36 @@ impl fmt::Debug for NetworkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NetworkAgentThrottleSender {
|
||||||
|
inner: Weak<NetworkManagerInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentThrottleSender for NetworkAgentThrottleSender {
|
||||||
|
fn send_throttle(
|
||||||
|
&self,
|
||||||
|
throttle_bytes: &[u8],
|
||||||
|
simulator: Option<&Simulator>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if throttle_bytes.len() != 28 {
|
||||||
|
return Err(Error::Argument);
|
||||||
|
}
|
||||||
|
let inner = self.inner.upgrade().ok_or(Error::InvalidOperation)?;
|
||||||
|
let manager = NetworkManager::native_from_inner(inner);
|
||||||
|
let simulator = simulator
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| manager.native_current_sim())
|
||||||
|
.ok_or(Error::InvalidOperation)?;
|
||||||
|
let mut packet = AgentThrottlePacket::new_with_constructor()?;
|
||||||
|
packet.agent_data.agent_id = manager.native_agent_id();
|
||||||
|
packet.agent_data.session_id = manager.native_session_id();
|
||||||
|
packet.agent_data.circuit_code = manager.native_circuit_code();
|
||||||
|
packet.throttle.throttles = throttle_bytes.to_vec();
|
||||||
|
let data = packet.to_bytes_with_method()?;
|
||||||
|
let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?;
|
||||||
|
simulator.native_send_packet_data(data, length, PacketType::AgentThrottle, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl NetworkManager {
|
impl NetworkManager {
|
||||||
pub(crate) fn native_from_inner(inner: Arc<NetworkManagerInner>) -> Self {
|
pub(crate) fn native_from_inner(inner: Arc<NetworkManagerInner>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -3019,11 +3069,18 @@ impl NetworkManager {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)?;
|
)?;
|
||||||
Ok(Self {
|
let manager = Self {
|
||||||
login_response_data: Some(login_response),
|
login_response_data: Some(login_response),
|
||||||
simulators,
|
simulators,
|
||||||
inner,
|
inner,
|
||||||
})
|
};
|
||||||
|
let mut client = manager.inner.client.clone();
|
||||||
|
client
|
||||||
|
.set_agent_throttle_sender(Arc::new(NetworkAgentThrottleSender {
|
||||||
|
inner: Arc::downgrade(&manager.inner),
|
||||||
|
}))
|
||||||
|
.map_err(|_| Error::InvalidOperation)?;
|
||||||
|
Ok(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn native_subscribe_login_progress(
|
pub fn native_subscribe_login_progress(
|
||||||
@@ -4324,4 +4381,28 @@ mod tests {
|
|||||||
("UpdateAgentLanguage".to_owned(), true, 11)
|
("UpdateAgentLanguage".to_owned(), true, 11)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extended_terrain_patch_is_stored_outside_the_legacy_region_extent() {
|
||||||
|
let client = GridClient::new().expect("client");
|
||||||
|
let simulator =
|
||||||
|
Simulator::new(client, "127.0.0.1:14002".parse().unwrap(), 12, None, None).unwrap();
|
||||||
|
simulator
|
||||||
|
.native_store_terrain_patch(
|
||||||
|
TerrainPatch {
|
||||||
|
data: vec![27.0; 256],
|
||||||
|
x: 42,
|
||||||
|
y: 33,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut height = 0.0;
|
||||||
|
assert!(
|
||||||
|
simulator
|
||||||
|
.native_terrain_height_at_point(675, 538, &mut height)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!((height - 27.0).abs() < f32::EPSILON);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rectangular_large_region_uses_independent_patch_dimensions() {
|
fn large_region_packets_can_extend_stale_declared_dimensions() {
|
||||||
let client = GridClient::new().unwrap();
|
let client = GridClient::new().unwrap();
|
||||||
let sim = Simulator::new(
|
let sim = Simulator::new(
|
||||||
client,
|
client,
|
||||||
@@ -450,16 +450,16 @@ mod tests {
|
|||||||
let mut height = 0.0;
|
let mut height = 0.0;
|
||||||
assert!(sim.terrain_height_at_point(511, 255, &mut height).unwrap());
|
assert!(sim.terrain_height_at_point(511, 255, &mut height).unwrap());
|
||||||
assert_eq!(height, 41.5);
|
assert_eq!(height, 41.5);
|
||||||
assert!(
|
|
||||||
sim.native_store_terrain_patch(
|
sim.native_store_terrain_patch(
|
||||||
TerrainPatch {
|
TerrainPatch {
|
||||||
data: vec![0.0; 256],
|
data: vec![42.5; 256],
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 16,
|
y: 16,
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.is_err()
|
.unwrap();
|
||||||
);
|
assert!(sim.terrain_height_at_point(0, 256, &mut height).unwrap());
|
||||||
|
assert_eq!(height, 42.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ use libremetaverse::messages::linden::{
|
|||||||
EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock,
|
EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock,
|
||||||
};
|
};
|
||||||
use libremetaverse::packets::{
|
use libremetaverse::packets::{
|
||||||
CompleteAgentMovementPacket, DisableSimulatorPacket, LogoutReplyPacket,
|
AgentThrottlePacket, CompleteAgentMovementPacket, DisableSimulatorPacket, LogoutReplyPacket,
|
||||||
LogoutReplyPacketInventoryDataBlock, LogoutRequestPacket, Packet, PacketAckPacket,
|
LogoutReplyPacketInventoryDataBlock, LogoutRequestPacket, Packet, PacketAckPacket,
|
||||||
PacketAckPacketPacketsBlock, PacketType, RegionHandshakePacket, RegionHandshakeReplyPacket,
|
PacketAckPacketPacketsBlock, PacketType, RegionHandshakePacket, RegionHandshakeReplyPacket,
|
||||||
SimStatsPacket, SimStatsPacketStatBlock, StartPingCheckPacket, UseCircuitCodePacket,
|
SimStatsPacket, SimStatsPacketStatBlock, StartPingCheckPacket, UseCircuitCodePacket,
|
||||||
};
|
};
|
||||||
use libremetaverse::{
|
use libremetaverse::{
|
||||||
CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, HttpCapsClient,
|
AgentThrottle, CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers,
|
||||||
InventoryItem, LoginState, LoginStatus, NetworkManager, NetworkManagerDisconnectType,
|
HttpCapsClient, InventoryItem, LoginState, LoginStatus, NetworkManager,
|
||||||
NetworkManagerLoginResponseCallback, PacketEventDictionary, Simulator,
|
NetworkManagerDisconnectType, NetworkManagerLoginResponseCallback, PacketEventDictionary,
|
||||||
|
Simulator,
|
||||||
};
|
};
|
||||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||||
use libremetaverse_types::compat::{
|
use libremetaverse_types::compat::{
|
||||||
@@ -417,6 +418,7 @@ enum ServerCommand {
|
|||||||
Disable,
|
Disable,
|
||||||
ExpectEconomy,
|
ExpectEconomy,
|
||||||
Handoff,
|
Handoff,
|
||||||
|
ExpectThrottle,
|
||||||
ExpectLogout,
|
ExpectLogout,
|
||||||
LogoutReply(Vec<UUID>),
|
LogoutReply(Vec<UUID>),
|
||||||
Stop,
|
Stop,
|
||||||
@@ -431,6 +433,7 @@ enum ServerReport {
|
|||||||
Logout(UUID, UUID),
|
Logout(UUID, UUID),
|
||||||
Movement(u32),
|
Movement(u32),
|
||||||
Ping(u8),
|
Ping(u8),
|
||||||
|
Throttle(u32, Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FakeServer {
|
struct FakeServer {
|
||||||
@@ -650,6 +653,35 @@ fn spawn_fake_server() -> FakeServer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ServerCommand::ExpectThrottle => loop {
|
||||||
|
let (length, _) = socket.recv_from(&mut buffer).expect("AgentThrottle");
|
||||||
|
if packet_type(&buffer[..length]) != Some(PacketType::AgentThrottle) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut bytes = buffer[..length].to_vec();
|
||||||
|
if bytes[0] & Helpers::MSG_ZEROCODED != 0 {
|
||||||
|
let mut decoded = vec![0; 8192];
|
||||||
|
let decoded_length = Helpers::zero_decode(
|
||||||
|
Some(&bytes),
|
||||||
|
i32::try_from(bytes.len()).unwrap(),
|
||||||
|
Some(&mut decoded),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
decoded.truncate(usize::try_from(decoded_length).unwrap());
|
||||||
|
decoded[0] &= !Helpers::MSG_ZEROCODED;
|
||||||
|
bytes = decoded;
|
||||||
|
}
|
||||||
|
let mut position = 0;
|
||||||
|
let packet =
|
||||||
|
AgentThrottlePacket::new_with_bytes_int32(bytes, &mut position).unwrap();
|
||||||
|
report_sender
|
||||||
|
.send(ServerReport::Throttle(
|
||||||
|
packet.agent_data.circuit_code,
|
||||||
|
packet.throttle.throttles,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
break;
|
||||||
|
},
|
||||||
ServerCommand::ExpectLogout => loop {
|
ServerCommand::ExpectLogout => loop {
|
||||||
let (length, _) = socket.recv_from(&mut buffer).expect("LogoutRequest");
|
let (length, _) = socket.recv_from(&mut buffer).expect("LogoutRequest");
|
||||||
if packet_type(&buffer[..length]) != Some(PacketType::LogoutRequest) {
|
if packet_type(&buffer[..length]) != Some(PacketType::LogoutRequest) {
|
||||||
@@ -772,6 +804,46 @@ fn assert_reuse_preserves_connected_override(
|
|||||||
manager.set_connected(true);
|
manager.set_connected(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_client_throttle_sender_writes_agent_throttle_to_the_simulator() {
|
||||||
|
let _network_guard = network_test_guard();
|
||||||
|
let server = spawn_fake_server();
|
||||||
|
let client = GridClient::new().expect("client");
|
||||||
|
let mut manager = NetworkManager::new(client.clone()).expect("manager");
|
||||||
|
manager.set_circuit_code(0x4455_6677);
|
||||||
|
let simulator = manager
|
||||||
|
.connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32(
|
||||||
|
server.endpoint,
|
||||||
|
0x1234,
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
256,
|
||||||
|
256,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.expect("simulator");
|
||||||
|
assert_eq!(
|
||||||
|
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||||
|
ServerReport::Circuit(0x4455_6677)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||||
|
ServerReport::HandshakeReply(0x1 | 0x2 | 0x4)
|
||||||
|
);
|
||||||
|
|
||||||
|
let throttle = AgentThrottle::new_with_grid_client(client).unwrap();
|
||||||
|
server.commands.send(ServerCommand::ExpectThrottle).unwrap();
|
||||||
|
throttle.set_with_simulator(Some(simulator)).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
server.reports.recv_timeout(Duration::from_secs(1)).unwrap(),
|
||||||
|
ServerReport::Throttle(0x4455_6677, throttle.to_bytes().unwrap())
|
||||||
|
);
|
||||||
|
|
||||||
|
manager
|
||||||
|
.shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fake_server_drives_circuit_handshake_ping_disable_and_disconnect_reasons() {
|
fn fake_server_drives_circuit_handshake_ping_disable_and_disconnect_reasons() {
|
||||||
let _network_guard = network_test_guard();
|
let _network_guard = network_test_guard();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ libremetaverse-imaging = { version = "0.0.1", path = "../libremetaverse-imaging"
|
|||||||
libremetaverse-rendering-simple = { version = "0.0.1", path = "../libremetaverse-rendering-simple", optional = true }
|
libremetaverse-rendering-simple = { version = "0.0.1", path = "../libremetaverse-rendering-simple", optional = true }
|
||||||
libremetaverse-types = { version = "0.0.1", path = "../libremetaverse-types" }
|
libremetaverse-types = { version = "0.0.1", path = "../libremetaverse-types" }
|
||||||
metacrate-lsl-tools = { version = "0.0.1", path = "../metacrate-lsl-tools" }
|
metacrate-lsl-tools = { version = "0.0.1", path = "../metacrate-lsl-tools" }
|
||||||
|
metacrate-rendering-wgpu = { version = "0.0.1", path = "../metacrate-rendering-wgpu" }
|
||||||
mentra = { version = "0.18.3", default-features = false }
|
mentra = { version = "0.18.3", default-features = false }
|
||||||
jpeg-encoder = "0.6.1"
|
jpeg-encoder = "0.6.1"
|
||||||
rustls = { version = "0.23.43", default-features = false, features = ["aws_lc_rs", "std", "tls12"] }
|
rustls = { version = "0.23.43", default-features = false, features = ["aws_lc_rs", "std", "tls12"] }
|
||||||
@@ -28,7 +29,6 @@ tokio = { version = "1.53.1", features = ["macros", "rt", "sync", "time"] }
|
|||||||
tokio-rustls = { version = "0.26.4", default-features = false, features = ["aws_lc_rs", "tls12"] }
|
tokio-rustls = { version = "0.26.4", default-features = false, features = ["aws_lc_rs", "tls12"] }
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
unicode-width = "0.2"
|
unicode-width = "0.2"
|
||||||
wgpu = { version = "30.0.1", default-features = false, features = ["std", "dx12", "metal", "gles", "vulkan", "wgsl"], optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
jpeg-decoder = { version = "0.3.2", default-features = false }
|
jpeg-decoder = { version = "0.3.2", default-features = false }
|
||||||
@@ -39,7 +39,7 @@ tokio = { version = "1.53.1", features = ["io-util", "net", "rt-multi-thread", "
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
live-grid = ["dep:libremetaverse", "dep:libremetaverse-imaging", "dep:libremetaverse-rendering-simple", "dep:wgpu"]
|
live-grid = ["dep:libremetaverse", "dep:libremetaverse-imaging", "dep:libremetaverse-rendering-simple", "metacrate-rendering-wgpu/wgpu"]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ impl OfflineGridBackend {
|
|||||||
#[cfg(feature = "live-grid")]
|
#[cfg(feature = "live-grid")]
|
||||||
pub struct LibremetaverseClientOwner {
|
pub struct LibremetaverseClientOwner {
|
||||||
client: libremetaverse::GridClient,
|
client: libremetaverse::GridClient,
|
||||||
|
network: libremetaverse::NetworkManager,
|
||||||
agent: Arc<libremetaverse::AgentManager>,
|
agent: Arc<libremetaverse::AgentManager>,
|
||||||
delivery_generation: Arc<std::sync::RwLock<Option<u64>>>,
|
delivery_generation: Arc<std::sync::RwLock<Option<u64>>>,
|
||||||
}
|
}
|
||||||
@@ -132,6 +133,7 @@ impl fmt::Debug for LibremetaverseClientOwner {
|
|||||||
pub struct LibremetaverseSessionBackend {
|
pub struct LibremetaverseSessionBackend {
|
||||||
network: libremetaverse::NetworkManager,
|
network: libremetaverse::NetworkManager,
|
||||||
agent: Arc<libremetaverse::AgentManager>,
|
agent: Arc<libremetaverse::AgentManager>,
|
||||||
|
throttle: libremetaverse::AgentThrottle,
|
||||||
login_url: String,
|
login_url: String,
|
||||||
first_name: String,
|
first_name: String,
|
||||||
last_name: String,
|
last_name: String,
|
||||||
@@ -168,7 +170,11 @@ impl LibremetaverseClientOwner {
|
|||||||
.map_err(|_| BackendError::Configuration {
|
.map_err(|_| BackendError::Configuration {
|
||||||
component: "libremetaverse client defaults",
|
component: "libremetaverse client defaults",
|
||||||
})?;
|
})?;
|
||||||
|
// The compatibility client caches its network manager weakly. Retain the
|
||||||
|
// composition root before managers register packet subscriptions on it.
|
||||||
|
let network = client.network();
|
||||||
let _grid = client.grid();
|
let _grid = client.grid();
|
||||||
|
let _terrain = client.terrain();
|
||||||
let agent = Arc::new(
|
let agent = Arc::new(
|
||||||
libremetaverse::AgentManager::new(Some(Arc::new(client.clone()))).map_err(|_| {
|
libremetaverse::AgentManager::new(Some(Arc::new(client.clone()))).map_err(|_| {
|
||||||
BackendError::Configuration {
|
BackendError::Configuration {
|
||||||
@@ -178,6 +184,7 @@ impl LibremetaverseClientOwner {
|
|||||||
);
|
);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client,
|
client,
|
||||||
|
network,
|
||||||
agent,
|
agent,
|
||||||
delivery_generation: Arc::new(std::sync::RwLock::new(None)),
|
delivery_generation: Arc::new(std::sync::RwLock::new(None)),
|
||||||
})
|
})
|
||||||
@@ -273,8 +280,12 @@ impl LibremetaverseClientOwner {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(LibremetaverseSessionBackend {
|
Ok(LibremetaverseSessionBackend {
|
||||||
network: self.client.network(),
|
network: self.network.clone(),
|
||||||
agent: Arc::clone(&self.agent),
|
agent: Arc::clone(&self.agent),
|
||||||
|
throttle: libremetaverse::AgentThrottle::new_with_grid_client(self.client.clone())
|
||||||
|
.map_err(|_| BackendError::Configuration {
|
||||||
|
component: "libremetaverse agent throttle",
|
||||||
|
})?,
|
||||||
login_url: connection.login_url.expose_url().to_owned(),
|
login_url: connection.login_url.expose_url().to_owned(),
|
||||||
first_name: first_name.to_owned(),
|
first_name: first_name.to_owned(),
|
||||||
last_name: last_name.to_owned(),
|
last_name: last_name.to_owned(),
|
||||||
@@ -794,30 +805,7 @@ impl crate::behavior::EmbodimentSink for LibremetaverseEmbodimentSink {
|
|||||||
return Err(crate::behavior::BehaviorError::Cancelled);
|
return Err(crate::behavior::BehaviorError::Cancelled);
|
||||||
}
|
}
|
||||||
self.ensure_generation(generation)?;
|
self.ensure_generation(generation)?;
|
||||||
let avatar = self.agent.sim_position();
|
native_reset_camera(&self.agent)
|
||||||
let forward = libremetaverse_types::Vector3::mul_with_vector3_quaternion(
|
|
||||||
libremetaverse_types::Vector3::unit_x(),
|
|
||||||
self.agent.sim_rotation(),
|
|
||||||
);
|
|
||||||
let position = libremetaverse_types::Vector3 {
|
|
||||||
x: avatar.x,
|
|
||||||
y: avatar.y,
|
|
||||||
z: avatar.z + 1.6,
|
|
||||||
};
|
|
||||||
let target = libremetaverse_types::Vector3 {
|
|
||||||
x: position.x + forward.x * 4.0,
|
|
||||||
y: position.y + forward.y * 4.0,
|
|
||||||
z: position.z + forward.z * 4.0,
|
|
||||||
};
|
|
||||||
self.agent
|
|
||||||
.movement
|
|
||||||
.camera
|
|
||||||
.look_at_with_vector3_vector3(position, target)
|
|
||||||
.and_then(|()| {
|
|
||||||
self.agent
|
|
||||||
.movement
|
|
||||||
.set_fov_vertical_angle(60.0_f32.to_radians())
|
|
||||||
})
|
|
||||||
.and_then(|()| self.agent.movement.send_update_with_boolean(Some(true)))
|
.and_then(|()| self.agent.movement.send_update_with_boolean(Some(true)))
|
||||||
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)
|
.map_err(|_| crate::behavior::BehaviorError::NativeOperation)
|
||||||
})
|
})
|
||||||
@@ -847,6 +835,30 @@ fn native_position(value: libremetaverse_types::Vector3) -> crate::perception::W
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "live-grid")]
|
||||||
|
fn native_reset_camera(agent: &libremetaverse::AgentManager) -> Result<(), libremetaverse::Error> {
|
||||||
|
let avatar = agent.sim_position();
|
||||||
|
let forward = libremetaverse_types::Vector3::mul_with_vector3_quaternion(
|
||||||
|
libremetaverse_types::Vector3::unit_x(),
|
||||||
|
agent.sim_rotation(),
|
||||||
|
);
|
||||||
|
let position = libremetaverse_types::Vector3 {
|
||||||
|
x: avatar.x,
|
||||||
|
y: avatar.y,
|
||||||
|
z: avatar.z + 1.6,
|
||||||
|
};
|
||||||
|
let target = libremetaverse_types::Vector3 {
|
||||||
|
x: position.x + forward.x * 4.0,
|
||||||
|
y: position.y + forward.y * 4.0,
|
||||||
|
z: position.z + forward.z * 4.0,
|
||||||
|
};
|
||||||
|
agent
|
||||||
|
.movement
|
||||||
|
.camera
|
||||||
|
.look_at_with_vector3_vector3(position, target)?;
|
||||||
|
agent.movement.set_fov_vertical_angle(60.0_f32.to_radians())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "live-grid")]
|
#[cfg(feature = "live-grid")]
|
||||||
fn native_inventory_metadata(
|
fn native_inventory_metadata(
|
||||||
client: &libremetaverse::GridClient,
|
client: &libremetaverse::GridClient,
|
||||||
@@ -1089,6 +1101,23 @@ impl crate::session::GridSessionBackend for LibremetaverseSessionBackend {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
if logged_in {
|
if logged_in {
|
||||||
|
let simulator = self.network.current_sim().ok_or_else(|| {
|
||||||
|
crate::session::SessionFailure::new(
|
||||||
|
crate::session::SessionFailureKind::TransientTransport,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
self.throttle
|
||||||
|
.set_with_simulator(Some(simulator))
|
||||||
|
.map_err(|_| {
|
||||||
|
crate::session::SessionFailure::new(
|
||||||
|
crate::session::SessionFailureKind::TransientTransport,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
native_reset_camera(&self.agent).map_err(|_| {
|
||||||
|
crate::session::SessionFailure::new(
|
||||||
|
crate::session::SessionFailureKind::TransientTransport,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let error_key = self.network.native_login_error_key();
|
let error_key = self.network.native_login_error_key();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -35,9 +35,76 @@ fn uuid(value: u32) -> UUID {
|
|||||||
fn triangle(z: f32, color: [u8; 4]) -> SceneTriangle {
|
fn triangle(z: f32, color: [u8; 4]) -> SceneTriangle {
|
||||||
SceneTriangle {
|
SceneTriangle {
|
||||||
vertices: [[-1.0, -1.0, z], [1.0, -1.0, z], [0.0, 1.0, z]],
|
vertices: [[-1.0, -1.0, z], [1.0, -1.0, z], [0.0, 1.0, z]],
|
||||||
color_srgb: color,
|
colors_srgb: [color; 3],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "live-grid")]
|
||||||
|
#[test]
|
||||||
|
fn extended_terrain_layout_uses_received_patch_coordinates() {
|
||||||
|
let mut terrain = (0..65_536)
|
||||||
|
.map(|_| libremetaverse::TerrainPatch {
|
||||||
|
data: Vec::new(),
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
terrain[33 * 256 + 42] = libremetaverse::TerrainPatch {
|
||||||
|
data: vec![20.0; 256],
|
||||||
|
x: 42,
|
||||||
|
y: 33,
|
||||||
|
};
|
||||||
|
assert_eq!(terrain_layout(&terrain, (256.0, 256.0)), (688, 544, 256));
|
||||||
|
assert_eq!(terrain_height(&terrain, 256, 675, 538), Some(20.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "live-grid")]
|
||||||
|
#[test]
|
||||||
|
fn renderer_uses_the_camera_axis_that_points_at_the_requested_target() {
|
||||||
|
let camera = libremetaverse::AgentManagerAgentMovementAgentCamera::new().unwrap();
|
||||||
|
camera
|
||||||
|
.look_at_with_vector3_vector3(
|
||||||
|
libremetaverse_types::Vector3::zero(),
|
||||||
|
libremetaverse_types::Vector3::unit_y(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
native_camera_forward(&camera),
|
||||||
|
libremetaverse_types::Vector3::unit_y()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "live-grid")]
|
||||||
|
#[test]
|
||||||
|
fn scene_faces_are_shaded_without_changing_alpha() {
|
||||||
|
let shade = triangle_shade([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
|
||||||
|
let color = shade_color([200, 160, 120, 77], shade);
|
||||||
|
assert!(color[0] < 200);
|
||||||
|
assert_eq!(color[3], 77);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "live-grid")]
|
||||||
|
#[test]
|
||||||
|
fn scene_broad_phase_rejects_object_centers_behind_the_camera() {
|
||||||
|
assert!(scene_center_in_front(
|
||||||
|
libremetaverse_types::Vector3 {
|
||||||
|
x: 4.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 0.0
|
||||||
|
},
|
||||||
|
[0.0; 3],
|
||||||
|
[1.0, 0.0, 0.0]
|
||||||
|
));
|
||||||
|
assert!(!scene_center_in_front(
|
||||||
|
libremetaverse_types::Vector3 {
|
||||||
|
x: -4.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 0.0
|
||||||
|
},
|
||||||
|
[0.0; 3],
|
||||||
|
[1.0, 0.0, 0.0]
|
||||||
|
));
|
||||||
|
}
|
||||||
fn scene() -> SceneSnapshot {
|
fn scene() -> SceneSnapshot {
|
||||||
SceneSnapshot {
|
SceneSnapshot {
|
||||||
generation: 7,
|
generation: 7,
|
||||||
@@ -123,7 +190,7 @@ async fn golden_scene_is_deterministic_depth_ordered_and_privacy_marked() {
|
|||||||
#[cfg(feature = "live-grid")]
|
#[cfg(feature = "live-grid")]
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn offscreen_wgpu_renders_depth_ordered_scene_when_an_adapter_is_available() {
|
async fn offscreen_wgpu_renders_depth_ordered_scene_when_an_adapter_is_available() {
|
||||||
let Ok(renderer) = WgpuRenderer::new().await else {
|
let Ok(renderer) = metacrate_rendering_wgpu::Renderer::new().await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let limits = VisionLimits {
|
let limits = VisionLimits {
|
||||||
@@ -132,7 +199,25 @@ async fn offscreen_wgpu_renders_depth_ordered_scene_when_an_adapter_is_available
|
|||||||
minimum_interval: std::time::Duration::ZERO,
|
minimum_interval: std::time::Duration::ZERO,
|
||||||
..VisionLimits::default()
|
..VisionLimits::default()
|
||||||
};
|
};
|
||||||
let rgba = tokio::task::spawn_blocking(move || renderer.render(&scene(), limits))
|
let scene = scene();
|
||||||
|
let triangles = scene
|
||||||
|
.entities
|
||||||
|
.iter()
|
||||||
|
.flat_map(|entity| entity.triangles.iter().cloned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let rgba = tokio::task::spawn_blocking(move || {
|
||||||
|
renderer.render(
|
||||||
|
scene.camera,
|
||||||
|
&triangles,
|
||||||
|
[72, 96, 120, 255],
|
||||||
|
metacrate_rendering_wgpu::RenderLimits {
|
||||||
|
width: limits.width,
|
||||||
|
height: limits.height,
|
||||||
|
max_triangles: limits.max_triangles,
|
||||||
|
far_distance: 512,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.expect("render worker")
|
.expect("render worker")
|
||||||
.expect("offscreen render");
|
.expect("offscreen render");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const ALLOWED_DEPENDENCIES: [&str; 20] = [
|
|||||||
"libremetaverse-imaging",
|
"libremetaverse-imaging",
|
||||||
"libremetaverse-rendering-simple",
|
"libremetaverse-rendering-simple",
|
||||||
"metacrate-lsl-tools",
|
"metacrate-lsl-tools",
|
||||||
|
"metacrate-rendering-wgpu",
|
||||||
"mentra",
|
"mentra",
|
||||||
"jpeg-encoder",
|
"jpeg-encoder",
|
||||||
"libremetaverse-types",
|
"libremetaverse-types",
|
||||||
@@ -22,7 +23,6 @@ const ALLOWED_DEPENDENCIES: [&str; 20] = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"url",
|
"url",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"wgpu",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -101,7 +101,10 @@ fn vision_path_selects_only_reviewed_rust_renderers_and_codec() {
|
|||||||
.expect("simple renderer manifest");
|
.expect("simple renderer manifest");
|
||||||
assert!(!renderer.contains("imaging-skia"));
|
assert!(!renderer.contains("imaging-skia"));
|
||||||
assert!(!renderer.contains("openjpeg"));
|
assert!(!renderer.contains("openjpeg"));
|
||||||
assert!(manifest.contains("wgpu = { version = \"30.0.1\""));
|
assert!(manifest.contains("metacrate-rendering-wgpu ="));
|
||||||
|
let renderer = fs::read_to_string(root.join("../metacrate-rendering-wgpu/Cargo.toml"))
|
||||||
|
.expect("wgpu renderer manifest");
|
||||||
|
assert!(renderer.contains("wgpu = { version = \"30.0.1\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
19
crates/metacrate-rendering-wgpu/Cargo.toml
Normal file
19
crates/metacrate-rendering-wgpu/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "metacrate-rendering-wgpu"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
description = "Reusable headless wgpu scene renderer and projection camera"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wgpu = { version = "30.0.1", default-features = false, features = ["std", "dx12", "metal", "gles", "vulkan", "wgsl"], optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
wgpu = ["dep:wgpu"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
426
crates/metacrate-rendering-wgpu/src/lib.rs
Normal file
426
crates/metacrate-rendering-wgpu/src/lib.rs
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
//! Cross-platform projection camera and headless `wgpu` triangle renderer.
|
||||||
|
|
||||||
|
#![allow(clippy::missing_errors_doc)]
|
||||||
|
|
||||||
|
type CameraBasis = ([f32; 3], [f32; 3], [f32; 3]);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Camera {
|
||||||
|
pub position: [f32; 3],
|
||||||
|
pub forward: [f32; 3],
|
||||||
|
pub up: [f32; 3],
|
||||||
|
pub vertical_fov_degrees: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Camera {
|
||||||
|
pub fn look_at(
|
||||||
|
position: [f32; 3],
|
||||||
|
target: [f32; 3],
|
||||||
|
up: [f32; 3],
|
||||||
|
vertical_fov_degrees: f32,
|
||||||
|
) -> Result<Self, RenderError> {
|
||||||
|
let forward = normalize([
|
||||||
|
target[0] - position[0],
|
||||||
|
target[1] - position[1],
|
||||||
|
target[2] - position[2],
|
||||||
|
])?;
|
||||||
|
let camera = Self {
|
||||||
|
position,
|
||||||
|
forward,
|
||||||
|
up: normalize(up)?,
|
||||||
|
vertical_fov_degrees,
|
||||||
|
};
|
||||||
|
camera.basis()?;
|
||||||
|
Ok(camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn basis(self) -> Result<CameraBasis, RenderError> {
|
||||||
|
if !(10.0..=140.0).contains(&self.vertical_fov_degrees)
|
||||||
|
|| self
|
||||||
|
.position
|
||||||
|
.iter()
|
||||||
|
.chain(self.forward.iter())
|
||||||
|
.chain(self.up.iter())
|
||||||
|
.any(|value| !value.is_finite())
|
||||||
|
{
|
||||||
|
return Err(RenderError::InvalidScene);
|
||||||
|
}
|
||||||
|
let forward = normalize(self.forward)?;
|
||||||
|
let right = normalize(cross(forward, self.up))?;
|
||||||
|
Ok((right, cross(right, forward), forward))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Triangle {
|
||||||
|
pub vertices: [[f32; 3]; 3],
|
||||||
|
pub colors_srgb: [[u8; 4]; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RenderLimits {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub max_triangles: usize,
|
||||||
|
pub far_distance: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RenderError {
|
||||||
|
AdapterUnavailable,
|
||||||
|
InvalidScene,
|
||||||
|
ResourceLimit,
|
||||||
|
TimedOut,
|
||||||
|
Readback,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RenderError {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
formatter.write_str(match self {
|
||||||
|
Self::AdapterUnavailable => "wgpu adapter is unavailable",
|
||||||
|
Self::InvalidScene => "render scene is invalid",
|
||||||
|
Self::ResourceLimit => "render resource limit exceeded",
|
||||||
|
Self::TimedOut => "wgpu render timed out",
|
||||||
|
Self::Readback => "wgpu readback failed",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for RenderError {}
|
||||||
|
|
||||||
|
fn normalize(value: [f32; 3]) -> Result<[f32; 3], RenderError> {
|
||||||
|
let length = dot(value, value).sqrt();
|
||||||
|
if length < 0.0001 {
|
||||||
|
return Err(RenderError::InvalidScene);
|
||||||
|
}
|
||||||
|
Ok([value[0] / length, value[1] / length, value[2] / length])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dot(left: [f32; 3], right: [f32; 3]) -> f32 {
|
||||||
|
left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cross(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
|
||||||
|
[
|
||||||
|
left[1] * right[2] - left[2] * right[1],
|
||||||
|
left[2] * right[0] - left[0] * right[2],
|
||||||
|
left[0] * right[1] - left[1] * right[0],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
pub struct Renderer {
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
pipeline: wgpu::RenderPipeline,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 2] =
|
||||||
|
wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x4];
|
||||||
|
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
impl Renderer {
|
||||||
|
pub async fn new() -> Result<Self, RenderError> {
|
||||||
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::LowPower,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| RenderError::AdapterUnavailable)?;
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu"),
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
required_limits: wgpu::Limits::downlevel_defaults(),
|
||||||
|
memory_hints: wgpu::MemoryHints::MemoryUsage,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| RenderError::AdapterUnavailable)?;
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(
|
||||||
|
r"
|
||||||
|
struct VertexInput { @location(0) position: vec3<f32>, @location(1) color: vec4<f32>, };
|
||||||
|
struct VertexOutput { @builtin(position) position: vec4<f32>, @location(0) color: vec4<f32>, };
|
||||||
|
@vertex fn vs_main(input: VertexInput) -> VertexOutput {
|
||||||
|
var output: VertexOutput;
|
||||||
|
output.position = vec4<f32>(input.position, 1.0);
|
||||||
|
output.color = input.color;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
@fragment fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { return input.color; }
|
||||||
|
"
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-layout"),
|
||||||
|
bind_group_layouts: &[],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-pipeline"),
|
||||||
|
layout: Some(&layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
buffers: &[Some(wgpu::VertexBufferLayout {
|
||||||
|
array_stride: 7 * std::mem::size_of::<f32>() as u64,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &VERTEX_ATTRIBUTES,
|
||||||
|
})],
|
||||||
|
},
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
cull_mode: None,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
depth_write_enabled: Some(true),
|
||||||
|
depth_compare: Some(wgpu::CompareFunction::Less),
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
bias: wgpu::DepthBiasState::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
blend: None,
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
Ok(Self {
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
pipeline,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub fn render(
|
||||||
|
&self,
|
||||||
|
camera: Camera,
|
||||||
|
triangles: &[Triangle],
|
||||||
|
background_srgb: [u8; 4],
|
||||||
|
limits: RenderLimits,
|
||||||
|
) -> Result<Vec<u8>, RenderError> {
|
||||||
|
let (vertices, vertex_count) = gpu_vertices(camera, triangles, limits)?;
|
||||||
|
let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-vertices"),
|
||||||
|
size: vertices.len().max(4) as u64,
|
||||||
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
if !vertices.is_empty() {
|
||||||
|
self.queue.write_buffer(&vertex_buffer, 0, &vertices);
|
||||||
|
}
|
||||||
|
let extent = wgpu::Extent3d {
|
||||||
|
width: limits.width,
|
||||||
|
height: limits.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
};
|
||||||
|
let color = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-color"),
|
||||||
|
size: extent,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let depth = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-depth"),
|
||||||
|
size: extent,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Depth32Float,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let row_bytes = limits
|
||||||
|
.width
|
||||||
|
.checked_mul(4)
|
||||||
|
.ok_or(RenderError::ResourceLimit)?;
|
||||||
|
let padded_row_bytes = row_bytes.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
|
||||||
|
* wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||||
|
let readback = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-readback"),
|
||||||
|
size: u64::from(padded_row_bytes)
|
||||||
|
.checked_mul(u64::from(limits.height))
|
||||||
|
.ok_or(RenderError::ResourceLimit)?,
|
||||||
|
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
let color_view = color.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let depth_view = depth.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-commands"),
|
||||||
|
});
|
||||||
|
{
|
||||||
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("metacrate-rendering-wgpu-pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &color_view,
|
||||||
|
depth_slice: None,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
|
r: f64::from(background_srgb[0]) / 255.0,
|
||||||
|
g: f64::from(background_srgb[1]) / 255.0,
|
||||||
|
b: f64::from(background_srgb[2]) / 255.0,
|
||||||
|
a: f64::from(background_srgb[3]) / 255.0,
|
||||||
|
}),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||||
|
view: &depth_view,
|
||||||
|
depth_ops: Some(wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(1.0),
|
||||||
|
store: wgpu::StoreOp::Discard,
|
||||||
|
}),
|
||||||
|
stencil_ops: None,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
if vertex_count != 0 {
|
||||||
|
pass.set_pipeline(&self.pipeline);
|
||||||
|
pass.set_vertex_buffer(0, vertex_buffer.slice(..));
|
||||||
|
pass.draw(0..vertex_count, 0..1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoder.copy_texture_to_buffer(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: &color,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
wgpu::TexelCopyBufferInfo {
|
||||||
|
buffer: &readback,
|
||||||
|
layout: wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(padded_row_bytes),
|
||||||
|
rows_per_image: Some(limits.height),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extent,
|
||||||
|
);
|
||||||
|
let submission = self.queue.submit([encoder.finish()]);
|
||||||
|
let slice = readback.slice(..);
|
||||||
|
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
|
slice.map_async(wgpu::MapMode::Read, move |result| {
|
||||||
|
let _ = sender.send(result);
|
||||||
|
});
|
||||||
|
self.device
|
||||||
|
.poll(wgpu::PollType::Wait {
|
||||||
|
submission_index: Some(submission),
|
||||||
|
timeout: Some(std::time::Duration::from_secs(5)),
|
||||||
|
})
|
||||||
|
.map_err(|_| RenderError::TimedOut)?;
|
||||||
|
receiver
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(1))
|
||||||
|
.map_err(|_| RenderError::TimedOut)?
|
||||||
|
.map_err(|_| RenderError::Readback)?;
|
||||||
|
let mapped = slice
|
||||||
|
.get_mapped_range()
|
||||||
|
.map_err(|_| RenderError::Readback)?;
|
||||||
|
let mut rgba = Vec::with_capacity(row_bytes as usize * limits.height as usize);
|
||||||
|
for row in mapped
|
||||||
|
.chunks_exact(padded_row_bytes as usize)
|
||||||
|
.take(limits.height as usize)
|
||||||
|
{
|
||||||
|
rgba.extend_from_slice(&row[..row_bytes as usize]);
|
||||||
|
}
|
||||||
|
drop(mapped);
|
||||||
|
readback.unmap();
|
||||||
|
Ok(rgba)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wgpu")]
|
||||||
|
#[allow(clippy::cast_precision_loss)] // Render dimensions and distance are bounded well below f32's exact integer range.
|
||||||
|
fn gpu_vertices(
|
||||||
|
camera: Camera,
|
||||||
|
triangles: &[Triangle],
|
||||||
|
limits: RenderLimits,
|
||||||
|
) -> Result<(Vec<u8>, u32), RenderError> {
|
||||||
|
if limits.width == 0
|
||||||
|
|| limits.height == 0
|
||||||
|
|| limits.far_distance == 0
|
||||||
|
|| triangles.len() > limits.max_triangles
|
||||||
|
|| triangles
|
||||||
|
.iter()
|
||||||
|
.flat_map(|triangle| triangle.vertices.iter().flatten())
|
||||||
|
.any(|value| !value.is_finite())
|
||||||
|
{
|
||||||
|
return Err(RenderError::InvalidScene);
|
||||||
|
}
|
||||||
|
let basis = camera.basis()?;
|
||||||
|
let aspect = limits.width as f32 / limits.height as f32;
|
||||||
|
let focal = 1.0 / (camera.vertical_fov_degrees.to_radians() / 2.0).tan();
|
||||||
|
let mut bytes = Vec::with_capacity(triangles.len() * 3 * 7 * std::mem::size_of::<f32>());
|
||||||
|
let mut count = 0u32;
|
||||||
|
for triangle in triangles {
|
||||||
|
let projected = triangle.vertices.map(|world| {
|
||||||
|
let delta = [
|
||||||
|
world[0] - camera.position[0],
|
||||||
|
world[1] - camera.position[1],
|
||||||
|
world[2] - camera.position[2],
|
||||||
|
];
|
||||||
|
let z = dot(delta, basis.2);
|
||||||
|
[
|
||||||
|
dot(delta, basis.0) * focal / aspect / z,
|
||||||
|
dot(delta, basis.1) * focal / z,
|
||||||
|
(z / limits.far_distance as f32).clamp(0.0, 1.0),
|
||||||
|
z,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
if projected.iter().any(|vertex| vertex[3] <= 0.05) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (vertex, color) in projected.into_iter().zip(triangle.colors_srgb) {
|
||||||
|
let color = color.map(|channel| f32::from(channel) / 255.0);
|
||||||
|
for value in vertex[..3].iter().chain(color.iter()) {
|
||||||
|
bytes.extend_from_slice(&value.to_ne_bytes());
|
||||||
|
}
|
||||||
|
count = count.checked_add(1).ok_or(RenderError::ResourceLimit)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((bytes, count))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn look_at_builds_a_valid_projection_camera() {
|
||||||
|
let camera =
|
||||||
|
Camera::look_at([1.0, 2.0, 3.0], [5.0, 2.0, 3.0], [0.0, 0.0, 1.0], 60.0).unwrap();
|
||||||
|
assert!(
|
||||||
|
camera
|
||||||
|
.forward
|
||||||
|
.into_iter()
|
||||||
|
.zip([1.0, 0.0, 0.0])
|
||||||
|
.all(|(actual, expected)| (actual - expected).abs() < f32::EPSILON)
|
||||||
|
);
|
||||||
|
assert!(Camera::look_at([0.0; 3], [0.0; 3], [0.0, 0.0, 1.0], 60.0).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ const MAX_INPUT_LINES: usize = 4096;
|
|||||||
const MAX_CLIENTS: usize = 64;
|
const MAX_CLIENTS: usize = 64;
|
||||||
const MAX_COMMAND_BYTES: usize = 4096;
|
const MAX_COMMAND_BYTES: usize = 4096;
|
||||||
const MAX_MESSAGE_BYTES: usize = 1023;
|
const MAX_MESSAGE_BYTES: usize = 1023;
|
||||||
const EVENT_QUEUE_CAPACITY: usize = 512;
|
const EVENT_QUEUE_CAPACITY: usize = 16_384;
|
||||||
const MAX_PACKET_LOG_COUNT: usize = 10_000;
|
const MAX_PACKET_LOG_COUNT: usize = 10_000;
|
||||||
const MAX_PACKET_LOG_BYTES: u64 = 16 * 1024 * 1024;
|
const MAX_PACKET_LOG_BYTES: u64 = 16 * 1024 * 1024;
|
||||||
const MAX_LOADED_COMMANDS: usize = 128;
|
const MAX_LOADED_COMMANDS: usize = 128;
|
||||||
|
|||||||
Reference in New Issue
Block a user