Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pomme-client/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ fn main() {
shaderc::ShaderKind::Fragment,
),
("post_blit.frag", shaderc::ShaderKind::Fragment),
("lightmap.frag", shaderc::ShaderKind::Fragment),
("post_gamma.frag", shaderc::ShaderKind::Fragment),
("post_invert.frag", shaderc::ShaderKind::Fragment),
("post_color_convolve.frag", shaderc::ShaderKind::Fragment),
Expand Down
2 changes: 2 additions & 0 deletions pomme-client/src/app/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ impl GameState {
ambient_light,
overworld_day,
default_clock,
light_environment,
} = dim;
tracing::info!(
"Dimension: height={height}, min_y={min_y}, skylight={has_skylight}, cardinal_light={cardinal_light:?}"
Expand All @@ -400,6 +401,7 @@ impl GameState {
self.ambient_light = ambient_light;
self.overworld_day = overworld_day;
self.default_clock = default_clock;
self.light_environment = light_environment;
self.chunk_store = ChunkStore::new_with_dimension(render_distance, height, min_y);
self.light_engine = crate::world::light::LevelLightEngine::new(height, min_y, has_skylight);
// The old level's block entities, moving blocks among them.
Expand Down
54 changes: 52 additions & 2 deletions pomme-client/src/app/phases/in_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use crate::renderer::pipelines::entity_renderer::{
standing_eye_height, wool_color_tint,
};
use crate::renderer::pipelines::entity_shadow::{ShadowCaster, ShadowScene};
use crate::renderer::pipelines::lightmap::LightmapInfo;
use crate::renderer::pipelines::menu_overlay::MenuElement;
use crate::renderer::pipelines::post_effect::PostEffect;
use crate::renderer::{Renderer, SkyState};
Expand Down Expand Up @@ -271,6 +272,10 @@ pub struct GameState {
pub overworld_day: bool,
/// The current dimension's `default_clock`, whose ticks are its day time.
pub default_clock: Option<String>,
/// The current dimension's lightmap attribute layers.
pub light_environment: crate::world::environment::LightEnvironment,
/// Vanilla `LightmapRenderStateExtractor.blockLightFlicker`.
pub block_light_flicker: f32,
/// F3+F4 game-mode switcher overlay, while open.
pub game_mode_switcher: Option<crate::ui::game_mode_switcher::GameModeSwitcherState>,
/// Spectator hotbar menu (vanilla `SpectatorGui`). Not a GUI screen: the
Expand Down Expand Up @@ -536,6 +541,8 @@ impl GameState {
ambient_light: 0.0,
overworld_day: true,
default_clock: None,
light_environment: Default::default(),
block_light_flicker: 0.0,
game_mode_switcher: None,
spectator: Default::default(),
switcher_was_open: false,
Expand Down Expand Up @@ -2315,11 +2322,13 @@ pub fn update_game(
// ticks, before them.
// TODO: vanilla also skips this while singleplayer is paused and under
// `/tick freeze` (`isLevelRunningNormally`); pomme tracks neither yet.
if core.tick_accumulator >= TICK_RATE {
let ticked = core.tick_accumulator >= TICK_RATE;
if ticked {
gfx.renderer.tick_texture_animations();
}
while core.tick_accumulator >= TICK_RATE {
game.tick_count = game.tick_count.wrapping_add(1);
game.block_light_flicker = tick_block_light_flicker(game.block_light_flicker);
// Vanilla `Minecraft.tick` order: `gameMode.tick` drives the connection
// tick (and so the level load tracker) before the level's entities,
// i.e. before the local player moves or sends anything.
Expand Down Expand Up @@ -2404,6 +2413,10 @@ pub fn update_game(
AppCore::send_client_tick_end(connection);
core.tick_accumulator -= TICK_RATE;
}
if ticked {
gfx.renderer
.tick_lightmap(lightmap_info(game, core.menu.brightness));
}

// Once per frame after the frame's ticks, where vanilla `Minecraft.runTick`
// calls `level.update()`.
Expand Down Expand Up @@ -5270,9 +5283,46 @@ fn sheep_eat_scales(eat_tick: u8, prev_eat_tick: u8, alpha: f32) -> (f32, f32) {
(pos_scale, angle_scale)
}

/// Vanilla `LightmapRenderStateExtractor.tick`: the block light's random
/// walk.
fn tick_block_light_flicker(flicker: f32) -> f32 {
let r = fastrand::f32;
(flicker + (r() - r()) * r() * r() * 0.1) * 0.9
}

/// Vanilla `LightmapRenderStateExtractor.extract`.
/// TODO: night vision, darkness, boss fog and the lightning and end flashes.
fn lightmap_info(game: &GameState, brightness: f32) -> LightmapInfo {
let sky = &game.sky_state;
let attributes = game
.light_environment
.resolve(sky.day_time as i64, sky.rain(), sky.thunder());
let with = |rgb: glam::Vec3, w: f32| rgb.extend(w).to_array();
LightmapInfo {
block_light_tint: with(attributes.block_light_tint, game.block_light_flicker + 1.4),
sky_light_color: with(attributes.sky_light_color, attributes.sky_light_factor),
ambient_color: with(attributes.ambient_light_color, brightness),
night_vision_color: with(attributes.night_vision_color, 0.0),
..bytemuck::Zeroable::zeroed()
}
}

#[cfg(test)]
mod tests {
use super::{detached_distance, has_red_overlay, scroll_wheel, section_bit, section_bits};
use super::{
detached_distance, has_red_overlay, scroll_wheel, section_bit, section_bits,
tick_block_light_flicker,
};

#[test]
fn block_light_flicker_stays_within_its_steady_bound() {
// Each step adds under 0.1 before the 0.9 decay, so it never passes 0.9.
let mut flicker = 0.0;
for _ in 0..100_000 {
flicker = tick_block_light_flicker(flicker);
assert!(flicker.abs() < 0.9, "{flicker}");
}
}

#[test]
fn third_person_distance_takes_the_further_of_player_and_mount() {
Expand Down
68 changes: 46 additions & 22 deletions pomme-client/src/cpu_bench/mesh_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,81 @@
//! hashed as bytes, and sprites are named rather than numbered, since atlas
//! sprite ids and vertex and triangle order vary between runs.

use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};

use super::fixtures::{Assets, Scene, build_world};
use crate::position::ChunkPos;
use crate::renderer::chunk::mesher::PackedVertex;

/// Hashes the centre column of `scene`, meshed like `mesh::bench` does.
pub fn scene_hash(assets: &Assets, scene: Scene) -> u64 {
/// Hashes the centre column of `scene`, meshed like `mesh::bench` does: its
/// geometry alone (each quad's positions, UVs and sprites, however it splits
/// into triangles), then every triangle with every vertex byte.
pub fn scene_hashes(assets: &Assets, scene: Scene) -> [u64; 2] {
let dispatcher = assets.mesh_dispatcher();
let store = build_world(scene, assets.block_state_table);
let data =
dispatcher.mesh_sections_now(&store, ChunkPos::new(0, 0), 0..store.section_count(), 0);
let names = assets.mesh.uv_map.sprite_names();

let mut hasher = DefaultHasher::new();
let geometry = |v: &PackedVertex| (v.pos, v.sprite_uv_fixed(), names.get(&v.sprite).copied());
let mut hashers = [DefaultHasher::new(), DefaultHasher::new()];
for section in &data.sections {
let (solid, cutout) = section.indices.split_at(section.solid_index_count as usize);
section.section_index.hash(&mut hasher);
for hasher in &mut hashers {
section.section_index.hash(hasher);
}
for indices in [solid, cutout, &section.water_indices[..]] {
triangles_hash(&section.vertices, indices, &names).hash(&mut hasher);
let vertex = |i: u32| &section.vertices[i as usize];
quads_hash(indices, |i| geometry(vertex(i))).hash(&mut hashers[0]);
triangles_hash(indices, |i| {
let v = vertex(i);
(geometry(v), v.uv >> 24, v.light_tint)
})
.hash(&mut hashers[1]);
}
}
hasher.finish()
hashers.map(|hasher| hasher.finish())
}

/// The pass's quads (every six indices) as a sorted set of their corners.
fn quads_hash<K: Ord + Hash>(indices: &[u32], decode: impl Fn(u32) -> K) -> u64 {
sorted_hash(
indices
.as_chunks::<6>()
.0
.iter()
.map(|quad| {
let mut corners: Vec<_> = quad.iter().map(|&i| decode(i)).collect();
corners.sort_unstable();
corners.dedup();
corners
})
.collect(),
)
}

/// The pass's triangles as a sorted set, each starting from its smallest
/// vertex so winding is kept but the starting corner isn't.
fn triangles_hash(vertices: &[PackedVertex], indices: &[u32], names: &HashMap<u16, &str>) -> u64 {
let decode = |i: u32| {
let v = &vertices[i as usize];
(
v.pos,
v.sprite_uv_fixed(),
names.get(&v.sprite).copied(),
v.light_tint,
)
};
let mut triangles: Vec<_> = indices
fn triangles_hash<K: Ord + Hash>(indices: &[u32], decode: impl Fn(u32) -> K) -> u64 {
let triangles = indices
.as_chunks::<3>()
.0
.iter()
.map(|tri| {
let mut corners = tri.map(decode);
let first = (0..3).min_by_key(|&i| corners[i]).unwrap_or(0);
let mut corners = tri.map(&decode);
let first = (0..3)
.min_by(|&a, &b| corners[a].cmp(&corners[b]))
.unwrap_or(0);
corners.rotate_left(first);
corners
})
.collect();
triangles.sort_unstable();
sorted_hash(triangles)
}

fn sorted_hash<T: Ord + Hash>(mut items: Vec<T>) -> u64 {
items.sort_unstable();
let mut hasher = DefaultHasher::new();
triangles.hash(&mut hasher);
items.hash(&mut hasher);
hasher.finish()
}
10 changes: 4 additions & 6 deletions pomme-client/src/cpu_bench/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,16 @@ pub fn run(version: &str, dirs: &DataDirs, filter: Option<&str>) -> i32 {
0
}

/// Prints [`mesh_hash::scene_hash`] for every bench scene; returns the
/// Prints [`mesh_hash::scene_hashes`] for every bench scene; returns the
/// process exit code.
pub fn mesh_hash(version: &str, dirs: &DataDirs) -> i32 {
let Some(assets) = load_assets(version, dirs, block_state_table(version)) else {
return 1;
};
println!("scene geometry all bytes");
for scene in fixtures::Scene::ALL {
println!(
"{:<8} {:016x}",
scene.name(),
mesh_hash::scene_hash(&assets, scene)
);
let [geometry, all] = mesh_hash::scene_hashes(&assets, scene);
println!("{:<8} {geometry:016x} {all:016x}", scene.name());
}
0
}
Expand Down
3 changes: 3 additions & 0 deletions pomme-client/src/harness/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ fn set_options(core: &mut AppCore, options: &protocol::OptionSet) -> Result<Valu
if let Some(radius) = options.menu_background_blur {
core.menu.set_menu_background_blurriness(radius);
}
if let Some(brightness) = options.brightness {
core.menu.brightness = brightness.clamp(0.0, 1.0);
}
core.menu.save_options(&core.input.key_bindings);
Ok(Value::Null)
}
Expand Down
2 changes: 2 additions & 0 deletions pomme-client/src/harness/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ pub struct OptionSet {
pub main_hand: Option<Hand>,
/// Menu Background Blur, 0 (off) to 10.
pub menu_background_blur: Option<u8>,
/// Brightness, 0 (Moody) to 1 (Bright).
pub brightness: Option<f32>,
}

#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
Expand Down
1 change: 1 addition & 0 deletions pomme-client/src/harness/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ mod tests {
Step::Run(Command::Option(OptionSet {
main_hand: Some(Hand::Left),
menu_background_blur: None,
brightness: None,
}))
);
assert_eq!(
Expand Down
39 changes: 27 additions & 12 deletions pomme-client/src/net/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ async fn config_sequence(
let mut received_registry_data = false;
let mut selected_known_packs = false;
let mut received_dialog_tags = None;
let mut received_timeline_tags = None;

// Vanilla sends brand and client information once, from the login
// listener; a reconfiguration sends neither.
Expand Down Expand Up @@ -611,9 +612,14 @@ async fn config_sequence(
}
ClientboundConfigPacket::UpdateTags(p) => {
// A later packet replaces an earlier one's tags per registry.
if let Some(tags) = dialog_tags(&p.tags) {
if let Some(tags) = registry_tags(&p.tags, &dialog_registry_key()) {
received_dialog_tags = Some(tags);
}
if let Some(tags) =
registry_tags(&p.tags, &super::environment_wire::TIMELINE_REGISTRY.into())
{
received_timeline_tags = Some(tags);
}
forward_tags(event_tx, &p.tags);
}
ClientboundConfigPacket::SelectKnownPacks(p) => {
Expand Down Expand Up @@ -664,13 +670,21 @@ async fn config_sequence(
None => previous.dialogs.clone(),
},
},
_ => Configured {
dialogs: std::sync::Arc::new(dialog_registry(
&registry_holder,
received_dialog_tags.unwrap_or_default(),
)),
registries: std::sync::Arc::new(registry_holder),
},
// TODO: timeline tags resent without registry data
// keep the previous resolution.
_ => {
super::environment_wire::resolve_timeline_tags(
&mut registry_holder,
&received_timeline_tags.unwrap_or_default(),
);
Configured {
dialogs: std::sync::Arc::new(dialog_registry(
&registry_holder,
received_dialog_tags.unwrap_or_default(),
)),
registries: std::sync::Arc::new(registry_holder),
}
}
});
}
ClientboundConfigPacket::Disconnect(p) => {
Expand Down Expand Up @@ -857,11 +871,12 @@ fn dialog_registry(
DialogRegistry::new(entries, tags)
}

/// The `minecraft:dialog` tags of an `update_tags` packet, if it has any.
fn dialog_tags(
/// One registry's tags from an `update_tags` packet, if it has any.
fn registry_tags(
tags: &azalea_protocol::common::tags::TagMap,
registry: &azalea_registry::identifier::Identifier,
) -> Option<std::collections::HashMap<String, Vec<usize>>> {
let tags = tags.0.get(&dialog_registry_key())?;
let tags = tags.0.get(registry)?;
Some(
tags.iter()
.map(|tag| {
Expand Down Expand Up @@ -1115,7 +1130,7 @@ async fn game_loop(
continue;
}
if let ClientboundGamePacket::UpdateTags(p) = &packet {
if let Some(tags) = dialog_tags(&p.tags) {
if let Some(tags) = registry_tags(&p.tags, &dialog_registry_key()) {
configured.dialogs =
std::sync::Arc::new(configured.dialogs.with_tags(tags));
send_ordered(
Expand Down
Loading
Loading