├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── admiral-p4kbar ├── Cargo.lock ├── Cargo.toml └── src │ └── main.rs ├── basic.inc ├── common.inc ├── demo.asm ├── final ├── pbn-lgc-witchcraft.prg └── screenshot.png ├── gfxrig ├── Cargo.lock ├── Cargo.toml └── src │ └── main.rs ├── graphics ├── background.png ├── font.png ├── originals │ ├── StarSprites_60mspf.ase │ └── Withcraft14_cleanup.ase ├── stars1.png ├── stars2.png ├── stars3.png ├── stars4.png ├── stars5.png ├── stars6.png ├── stars7.png └── stars8.png ├── music.prg └── witchcraft.asm /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | target/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Jake "ferris" Taylor and Alexander 2 | "Flipside" Odden 3 | 4 | Permission is hereby granted, free of charge, to any 5 | person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the 7 | Software without restriction, including without 8 | limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software 11 | is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice 15 | shall be included in all copies or substantial portions 16 | of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 19 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 20 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 21 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 22 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 25 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_DIR=build 2 | 3 | PROJECT=witchcraft 4 | PROJECT_SRC=$(PROJECT).asm 5 | PROJECT_TARGET=$(BUILD_DIR)/$(PROJECT).prg 6 | 7 | PACKED_DEMO=$(BUILD_DIR)/packed-demo.bin 8 | PACK_REPORT=$(BUILD_DIR)/report.html 9 | 10 | DEMO=demo 11 | DEMO_SRC=$(DEMO).asm 12 | DEMO_TARGET=$(BUILD_DIR)/$(DEMO).bin 13 | 14 | MUSIC=music.prg 15 | 16 | GRAPHICS_DIR=graphics 17 | 18 | GFXRIG_DIR=gfxrig 19 | GFXRIG_SRC=$(wildcard $(GFXRIG_DIR)/**/*.rs) 20 | GFXRIG=$(GFXRIG_DIR)/target/release/gfxrig 21 | 22 | BACKGROUND=$(GRAPHICS_DIR)/background.png 23 | BACKGROUND_BITMAP=$(BUILD_DIR)/background_bitmap.bin 24 | 25 | FONT=$(GRAPHICS_DIR)/font.png 26 | FONT_CHARSET=$(BUILD_DIR)/font.bin 27 | 28 | SPRITES_PREFIX=$(GRAPHICS_DIR)/stars 29 | SPRITES=$(wildcard $(SPRITES_PREFIX)*) 30 | SPRITES_BLOB=$(BUILD_DIR)/sprites_blob.bin 31 | 32 | COMMON_INC=common.inc 33 | BASIC_INC=basic.inc 34 | 35 | ASM=kickass 36 | ASM_FLAGS=-showmem -odir $(BUILD_DIR) 37 | 38 | EMU=x64 39 | EMUSC=x64sc 40 | EMU_FLAGS= 41 | 42 | RM=rm 43 | RM_FLAGS=-rf 44 | 45 | PACKER_DIR=admiral-p4kbar 46 | PACKER_SRC=$(wildcard $(PACKER_DIR)/**/*.rs) 47 | PACKER=$(PACKER_DIR)/target/release/admiral-p4kbar 48 | PACKER_SPEED=instant 49 | 50 | .PHONY: all dirs packer pack test testsc clean 51 | 52 | all: dirs $(PROJECT_TARGET) 53 | 54 | dirs: $(BUILD_DIR) 55 | 56 | $(BUILD_DIR): 57 | mkdir -p $(BUILD_DIR) 58 | 59 | $(GFXRIG): $(GFXRIG_SRC) 60 | cd $(GFXRIG_DIR) && cargo build --release 61 | 62 | packer: dirs $(PACKER) 63 | 64 | $(PACKER): $(PACKER_SRC) 65 | cd $(PACKER_DIR) && cargo build --release 66 | 67 | $(BACKGROUND_BITMAP) $(FONT_CHARSET) $(SPRITES_BLOB): $(GFXRIG) $(BACKGROUND) $(FONT) $(SPRITES) 68 | $(GFXRIG) $(BACKGROUND) $(BACKGROUND_BITMAP) $(FONT) $(FONT_CHARSET) $(SPRITES_PREFIX) $(SPRITES_BLOB) 69 | 70 | $(DEMO_TARGET): $(DEMO_SRC) $(COMMON_INC) $(MUSIC) $(BACKGROUND_BITMAP) $(SPRITES_BLOB) 71 | $(ASM) $(ASM_FLAGS) -binfile $(DEMO_SRC) 72 | 73 | pack: dirs $(PACKED_DEMO) 74 | 75 | $(PACKED_DEMO) $(PACK_REPORT): $(PACKER) $(DEMO_TARGET) 76 | $(PACKER) $(DEMO_TARGET) $(PACKED_DEMO) $(PACK_REPORT) $(PACKER_SPEED) 77 | 78 | $(PROJECT_TARGET): $(PROJECT_SRC) $(COMMON_INC) $(BASIC_INC) $(PACKED_DEMO) 79 | $(ASM) $(ASM_FLAGS) $(PROJECT_SRC) 80 | 81 | test: dirs $(PROJECT_TARGET) 82 | $(EMU) $(EMU_FLAGS) $(PROJECT_TARGET) 83 | 84 | testsc: dirs $(PROJECT_TARGET) 85 | $(EMUSC) $(EMU_FLAGS) $(PROJECT_TARGET) 86 | 87 | clean: 88 | $(RM) $(RM_FLAGS) $(BUILD_DIR) 89 | cd $(GFXRIG_DIR) && cargo clean 90 | cd $(PACKER_DIR) && cargo clean 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # witchcraft 2 | 3 | Source code for Witchcraft by Pegboard Nerds and Logicoma, a 16 kilobyte demo for the Commodore 64 4 | 5 | ![[](final/screenshot.png)](final/screenshot.png) 6 | 7 | ## description 8 | 9 | This is the source code for our 16 kilobyte demo, Witchcraft, released at Datastorm Summer 2017 for the Commodore 64. It's a small demo built to showcase a cool SID cover of the song Witchcraft by Pendulum. This is the exact repo we worked in - none of it has been edited or modified, except for some additional source images added that were originally on dropbox. As such, there are some quirks to how things are built and some things are kindof messy/broken. I have no intention of fixing this; this is meant to be a snapshot of the exact data we worked with while making the intro, shared publicly because why not. 10 | 11 | Enjoy! 12 | 13 | ## license 14 | 15 | This code is licensed under the MIT license (see LICENSE). 16 | -------------------------------------------------------------------------------- /admiral-p4kbar/Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "admiral-p4kbar" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "typed-arena 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 7 | ] 8 | 9 | [[package]] 10 | name = "fnv" 11 | version = "1.0.5" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | 14 | [[package]] 15 | name = "typed-arena" 16 | version = "1.3.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | 19 | [metadata] 20 | "checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344" 21 | "checksum typed-arena 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5934776c3ac1bea4a9d56620d6bf2d483b20d394e49581db40f187e1118ff667" 22 | -------------------------------------------------------------------------------- /admiral-p4kbar/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "admiral-p4kbar" 3 | version = "0.1.0" 4 | authors = ["ferris "] 5 | 6 | [profile.release] 7 | debug = true 8 | 9 | [dependencies] 10 | typed-arena = "1.3.0" 11 | fnv = "1.0" 12 | -------------------------------------------------------------------------------- /admiral-p4kbar/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate typed_arena; 2 | 3 | extern crate fnv; 4 | 5 | use typed_arena::Arena; 6 | 7 | use fnv::FnvHashMap; 8 | 9 | use std::cmp::Ordering; 10 | use std::collections::{BTreeMap, HashMap, BTreeSet}; 11 | use std::env; 12 | use std::fmt; 13 | use std::fs::File; 14 | use std::io::{Read, Write, stdout}; 15 | use std::path::Path; 16 | use std::time::Instant; 17 | 18 | #[derive(Clone, Copy)] 19 | enum SpeedSetting { 20 | Optimal, 21 | Instant, 22 | } 23 | 24 | fn main() { 25 | // Garish banner in order to more easily see the packer output amongst all the crap KickAssembler spits out :) 26 | println!("//------------------------------------------------------"); 27 | println!("//------------------------------------------------------"); 28 | println!("// Admiral P4kbar v1.337 by Ferris / Wheel "); 29 | println!("//------------------------------------------------------"); 30 | println!("//------------------------------------------------------"); 31 | println!(); 32 | 33 | let input_file_name = env::args().skip(1).nth(0).expect("Input file argument missing"); 34 | let output_file_name = env::args().skip(1).nth(1).expect("Output file argument missing"); 35 | let report_file_name = env::args().skip(1).nth(2).expect("Report file argument missing"); 36 | let speed_setting_name = env::args().skip(1).nth(3).expect("Speed setting argument missing"); 37 | 38 | let speed_setting = match speed_setting_name.as_str() { 39 | "optimal" => SpeedSetting::Optimal, 40 | "instant" => SpeedSetting::Instant, 41 | _ => panic!("Unrecognized speed setting: {}", speed_setting_name) 42 | }; 43 | 44 | let total_start_time = Instant::now(); 45 | 46 | // Read input file 47 | let input = { 48 | let mut input = Vec::new(); 49 | File::open(input_file_name).expect("Couldn't open input file").read_to_end(&mut input).expect("Couldn't read input file"); 50 | input 51 | }; 52 | 53 | let uncompressed_size = input.len(); 54 | 55 | // Parse 56 | let parse_start_time = Instant::now(); 57 | 58 | let edge_lists = parse(input.clone(), speed_setting, |pos| { 59 | let percent_complete = pos * 100 / uncompressed_size; 60 | print!("\rParsing ... {}%", percent_complete); 61 | stdout().flush().expect("Couldn't flush stdout"); 62 | }); 63 | println!(); 64 | 65 | let parse_duration = parse_start_time.elapsed(); 66 | let parse_elapsed_time = (parse_duration.as_secs() as f64) + (parse_duration.subsec_nanos() as f64) / 1.0e9; 67 | let parse_data_rate_kb_s = (uncompressed_size as f64) / 1024.0 / (parse_elapsed_time as f64); 68 | 69 | let num_edges = edge_lists.iter().fold(0, |acc, edge_list| acc + edge_list.len()); 70 | 71 | println!(" {} edges built in {:.*}s (~{:.*}kb/s)", num_edges, 2, parse_elapsed_time, 2, parse_data_rate_kb_s); 72 | println!(); 73 | 74 | // Use gamma encoding as a heuristic to find the seed edge path 75 | let seed_encoding = TableEncoding::default(); 76 | let (seed_path, seed_path_size) = compress_and_print_stats(&edge_lists, &seed_encoding, uncompressed_size, "Finding seed edge path"); 77 | println!(); 78 | 79 | // Refine encoding and recompress until no more improvements are made 80 | let mut encoding = None; 81 | let mut path: Option<(Vec, u32)> = None; 82 | 83 | match speed_setting { 84 | SpeedSetting::Optimal => { 85 | for _ in 0..65536 { 86 | print!("Searching for optimal encoding ... "); 87 | stdout().flush().expect("Couldn't flush stdout"); 88 | 89 | let encoding_search_start_time = Instant::now(); 90 | 91 | let new_encoding = TableEncoding::new(path.as_ref().map(|path| &path.0).unwrap_or(&seed_path)); 92 | 93 | let encoding_search_duration = encoding_search_start_time.elapsed(); 94 | let encoding_search_elapsed_time = (encoding_search_duration.as_secs() as f64) + (encoding_search_duration.subsec_nanos() as f64) / 1.0e9; 95 | 96 | println!("found in {:.*}s", 2, encoding_search_elapsed_time); 97 | println!(" {}", new_encoding); 98 | 99 | let (new_path, new_path_size) = compress_and_print_stats(&edge_lists, &new_encoding, uncompressed_size, "Compressing"); 100 | if path.as_ref().map(|&(_, best_path_size)| new_path_size < best_path_size).unwrap_or(true) { 101 | encoding = Some(new_encoding); 102 | path = Some((new_path, new_path_size)); 103 | } else { 104 | break; 105 | } 106 | println!(); 107 | } 108 | } 109 | SpeedSetting::Instant => { 110 | println!("Speed setting is instant, skipping optimal encoding search"); 111 | 112 | encoding = Some(seed_encoding); 113 | path = Some((seed_path, seed_path_size)); 114 | } 115 | } 116 | println!(); 117 | 118 | let encoding = encoding.unwrap(); 119 | 120 | println!("Best encoding:"); 121 | println!(" {}", encoding); 122 | 123 | let path = path.unwrap().0; 124 | 125 | // Encode 126 | let output = encode(&input, &path, &encoding, |pos| { 127 | let percent_complete = pos * 100 / uncompressed_size; 128 | print!("\rEncoding ... {}%", percent_complete); 129 | stdout().flush().expect("Couldn't flush stdout"); 130 | }); 131 | println!(); 132 | println!(" Encoded size: {} bytes", output.len()); 133 | println!(); 134 | 135 | // Decompression check 136 | let decompressed = decompress(output.clone(), uncompressed_size, |pos| { 137 | let percent_complete = pos * 100 / uncompressed_size; 138 | print!("\rDecompression check ... {}%", percent_complete); 139 | stdout().flush().expect("Couldn't flush stdout"); 140 | }); 141 | 142 | if input != decompressed { 143 | println!(""); 144 | panic!("Input/decompressed don't match"); 145 | } 146 | println!("\rDecompression check OK! "); 147 | println!(); 148 | 149 | // Write output file 150 | File::create(&output_file_name).expect("Couldn't create output file").write_all(&output).expect("Couldn't write output file"); 151 | 152 | println!("Output written to {}", output_file_name); 153 | 154 | // Write report 155 | generate_report(&report_file_name, &input, &path, &encoding); 156 | 157 | println!("Report written to {}", report_file_name); 158 | println!(); 159 | 160 | // Final stats 161 | let total_duration = total_start_time.elapsed(); 162 | let total_elapsed_time = (total_duration.as_secs() as f64) + (total_duration.subsec_nanos() as f64) / 1.0e9; 163 | 164 | let total_compressed_size = output.len(); 165 | 166 | let total_ratio = (1.0 - (total_compressed_size as f64) / (uncompressed_size as f64)) * 100.0; 167 | 168 | println!("Final results:"); 169 | println!(" {} -> {} bytes ({:.*}%)", uncompressed_size, total_compressed_size, 2, total_ratio); 170 | println!(" completed in {:.*}s", 2, total_elapsed_time); 171 | 172 | println!(); 173 | } 174 | 175 | fn compress_and_print_stats(edge_lists: &Vec>, encoding: &Encoding, uncompressed_size: usize, action_name: &str) -> (Vec, u32) { 176 | let compress_start_time = Instant::now(); 177 | 178 | let edges = compress(&edge_lists, encoding, uncompressed_size, |pos| { 179 | let percent_complete = pos * 100 / uncompressed_size; 180 | print!("\r{} ... {}%", action_name, percent_complete); 181 | stdout().flush().expect("Couldn't flush stdout"); 182 | }); 183 | println!(); 184 | 185 | let compress_duration = compress_start_time.elapsed(); 186 | let compress_elapsed_time = (compress_duration.as_secs() as f64) + (compress_duration.subsec_nanos() as f64) / 1.0e9; 187 | let compress_data_rate_kb_s = (uncompressed_size as f64) / 1024.0 / (compress_elapsed_time as f64); 188 | 189 | let compressed_size_bits = edges.iter().fold(0, |acc, edge| acc + encoding.bit_length(edge)); 190 | let compressed_size_bytes = compressed_size_bits / 8; 191 | 192 | let ratio = (1.0 - (compressed_size_bytes as f64) / (uncompressed_size as f64)) * 100.0; 193 | 194 | println!(" {} -> ~{} bytes ({} bits, {:.*}%)", uncompressed_size, compressed_size_bytes, compressed_size_bits, 2, ratio); 195 | println!(" completed in {:.*}s (~{:.*}kb/s)", 2, compress_elapsed_time, 2, compress_data_rate_kb_s); 196 | 197 | (edges, compressed_size_bits) 198 | } 199 | 200 | #[derive(Clone, Copy)] 201 | enum Edge { 202 | Literal { byte: u8 }, 203 | Backreference { distance: u32, length: u32 }, 204 | } 205 | 206 | impl Edge { 207 | fn byte_length(&self) -> u32 { 208 | match self { 209 | &Edge::Literal { .. } => 1, 210 | &Edge::Backreference { length, .. } => length, 211 | } 212 | } 213 | } 214 | 215 | trait Encoding { 216 | fn bit_length(&self, edge: &Edge) -> u32; 217 | } 218 | 219 | #[derive(Debug, Clone)] 220 | struct TableEncodingEntry { 221 | min: u32, 222 | max: u32, 223 | num_additional_bits: u32, 224 | } 225 | 226 | struct TableEncoding { 227 | distance_table_1: Vec, 228 | distance_table_2: Vec, 229 | length_table: Vec, 230 | } 231 | 232 | struct TableEncodingSearchNode<'a> { 233 | entry: TableEncodingEntry, 234 | total_bits: u32, 235 | next: Option<&'a TableEncodingSearchNode<'a>>, 236 | } 237 | 238 | impl TableEncoding { 239 | fn new(path: &Vec) -> TableEncoding { 240 | // Get stats 241 | let mut distance_stats_1 = BTreeMap::new(); 242 | let mut distance_stats_2 = BTreeMap::new(); 243 | let mut length_stats = BTreeMap::new(); 244 | 245 | for edge in path.iter() { 246 | if let &Edge::Backreference { distance, length } = edge { 247 | if length == 1 { 248 | *distance_stats_1.entry(distance).or_insert(0) += 1; 249 | } else { 250 | *distance_stats_2.entry(distance).or_insert(0) += 1; 251 | } 252 | *length_stats.entry(length).or_insert(0) += 1; 253 | } 254 | } 255 | 256 | // Ensure max values are represented in stats 257 | // Technically this shouldn't be necessary to find an encoding for a given set of edges, 258 | // but since we'll use this encoding to compress the stream again and we may find a new 259 | // edge path that uses different lengths, and we want to be sure we can encode all 260 | // possible values. 261 | // TODO: Replace magic numbers with the same max constants the search uses 262 | distance_stats_1.entry(65535).or_insert(1); 263 | distance_stats_2.entry(65535).or_insert(1); 264 | length_stats.entry(65535).or_insert(1); 265 | 266 | // Turn stats into ordered lists for convenience while searching 267 | let distance_stats_1 = distance_stats_1.into_iter().collect::>(); 268 | let distance_stats_2 = distance_stats_2.into_iter().collect::>(); 269 | let length_stats = length_stats.into_iter().collect::>(); 270 | 271 | // Perform searches 272 | TableEncoding { 273 | distance_table_1: TableEncoding::find_best_table(&distance_stats_1), 274 | distance_table_2: TableEncoding::find_best_table(&distance_stats_2), 275 | length_table: TableEncoding::find_best_table(&length_stats), 276 | } 277 | } 278 | 279 | fn find_best_table(stats: &[(u32, u32)]) -> Vec { 280 | // Transform stats into a reverse running total over all possible values 281 | // This eliminates a lot of redundant calculations during the search 282 | let mut reverse_running_total = vec![0; 65537]; 283 | for &(value, count) in stats.iter() { 284 | reverse_running_total[value as usize] = count; 285 | } 286 | let mut acc = 0; 287 | let mut i = 65536; 288 | loop { 289 | acc += reverse_running_total[i]; 290 | reverse_running_total[i] = acc; 291 | 292 | if i == 0 { 293 | break; 294 | } 295 | i -= 1; 296 | } 297 | 298 | let arena = Arena::new(); 299 | // TODO: Replace magic numbers for min with the same min constants the search uses 300 | let mut head = TableEncoding::find_best_tail(0, 1, &reverse_running_total, &arena, &mut FnvHashMap::with_capacity_and_hasher(1024 * 512, Default::default())); 301 | let mut table = Vec::with_capacity(16); 302 | let mut min = 1; 303 | for _ in 0..16 { 304 | if let Some(node) = head { 305 | let entry = node.entry.clone(); 306 | min = entry.max; 307 | table.push(entry); 308 | head = node.next; 309 | } else { 310 | let max = min + 1; 311 | table.push(TableEncodingEntry { 312 | min: min, 313 | max: max, 314 | num_additional_bits: 0, 315 | }); 316 | min = max; 317 | } 318 | } 319 | table 320 | } 321 | 322 | fn find_best_tail<'a>( 323 | level: u32, 324 | min: u32, 325 | reverse_running_total: &[u32], 326 | arena: &'a Arena>, 327 | cache: &mut FnvHashMap<(u32, u32), Option<&'a TableEncodingSearchNode<'a>>>) -> Option<&'a TableEncodingSearchNode<'a>> { 328 | // Early-out if we've already calculated and cached the result for these param's 329 | let cache_key = (level, min); 330 | if let Some(tail) = cache.get(&cache_key) { 331 | return tail.clone(); 332 | } 333 | 334 | let mut best_tail: Option<&'a TableEncodingSearchNode> = None; 335 | 336 | let num_prefix_bits = unary_bit_length(level); 337 | let min_index = if min > 65536 { 65536 } else { min } as usize; 338 | 339 | // Test different bit lengths for current entry 340 | for num_additional_bits in 0..16 { 341 | // Find range of numbers we can cover for a particular number of bits 342 | let max = min + (1 << num_additional_bits); 343 | let max_index = if max > 65536 { 65536 } else { max } as usize; 344 | 345 | // If we're at the last level but can't encode the last value, we've reached the end of an invalid tail, 346 | // so we need to reject it and try adding more bits 347 | if level == 15 && reverse_running_total[max_index] != 0 { 348 | continue; 349 | } 350 | 351 | // Calculate cost to encode the range we're testing 352 | let num_values = reverse_running_total[min_index] - reverse_running_total[max_index]; 353 | let num_bits_per_value = num_prefix_bits + num_additional_bits; 354 | let mut total_bits = num_values * num_bits_per_value; 355 | 356 | // If this configuration is already not going to be cheaper than the current best, we can early-out 357 | // here before testing potential tails and additional bits, as those are only going to make it even 358 | // more expensive 359 | if best_tail.map(|best_tail| total_bits >= best_tail.total_bits).unwrap_or(false) { 360 | break; 361 | } 362 | 363 | // If we're not at the end of the table or the range of numbers, find the best available tail, and add 364 | // its cost to the tail we're testing 365 | let tail = if level < 15 && reverse_running_total[max_index] != 0 { 366 | let tail = TableEncoding::find_best_tail(level + 1, max, reverse_running_total, arena, cache); 367 | if let Some(tail_node) = tail { 368 | total_bits += tail_node.total_bits; 369 | tail 370 | } else { 371 | // If we couldn't find a suitable tail, then we're not going to be able to encode all possible 372 | // values, so we need to skip this iteration 373 | continue; 374 | } 375 | } else { 376 | None 377 | }; 378 | 379 | // Final test for this configuration 380 | if best_tail.map(|best_tail| total_bits < best_tail.total_bits).unwrap_or(true) { 381 | // We passed, make a new head for this tail 382 | let head = arena.alloc(TableEncodingSearchNode { 383 | entry: TableEncodingEntry { 384 | min: min, 385 | max: max, 386 | num_additional_bits: num_additional_bits, 387 | }, 388 | total_bits: total_bits, 389 | next: tail, 390 | }); 391 | best_tail = Some(head); 392 | } 393 | } 394 | 395 | // Insert best entry into cache 396 | cache.insert(cache_key, best_tail); 397 | 398 | best_tail 399 | } 400 | 401 | fn distance_entry_1(&self, value: u32) -> Option<(&TableEncodingEntry, usize)> { 402 | TableEncoding::search_table(&self.distance_table_1, value) 403 | } 404 | 405 | fn distance_entry_2(&self, value: u32) -> Option<(&TableEncodingEntry, usize)> { 406 | TableEncoding::search_table(&self.distance_table_2, value) 407 | } 408 | 409 | fn length_entry(&self, value: u32) -> Option<(&TableEncodingEntry, usize)> { 410 | TableEncoding::search_table(&self.length_table, value) 411 | } 412 | 413 | fn search_table(table: &Vec, value: u32) -> Option<(&TableEncodingEntry, usize)> { 414 | match table.binary_search_by(|entry| { 415 | if entry.max <= value { 416 | Ordering::Less 417 | } else if entry.min > value { 418 | Ordering::Greater 419 | } else { 420 | Ordering::Equal 421 | } 422 | }) { 423 | Ok(index) => Some((&table[index], index)), 424 | _ => None 425 | } 426 | } 427 | } 428 | 429 | impl Default for TableEncoding { 430 | // The default TableEncoding is simply one of the results from the optimal search at some 431 | // point. It's not guaranteed to be optimal for any source, but it's trivial to construct 432 | // and useful for having some encoding when a path isn't known, and for extremely fast 433 | // compression where optimal ratio isn't as important. 434 | fn default() -> TableEncoding { 435 | let distance_1_encoding = "200056789abcdef5"; 436 | let distance_2_encoding = "456858abc9839efd"; 437 | let length_encoding = "112335639c686efe"; 438 | 439 | TableEncoding { 440 | distance_table_1: decode_table(distance_1_encoding), 441 | distance_table_2: decode_table(distance_2_encoding), 442 | length_table: decode_table(length_encoding), 443 | } 444 | } 445 | } 446 | 447 | fn decode_table(encoding: &str) -> Vec { 448 | let mut ret = Vec::with_capacity(16); 449 | let mut min = 1; 450 | for i in 0..16 { 451 | let c = encoding.chars().nth(i).unwrap() as u32; 452 | let num_additional_bits = if c >= '0' as u32 && c <= '9' as u32 { 453 | c - ('0' as u32) 454 | } else { 455 | c - ('a' as u32) + 0x0a 456 | }; 457 | let max = min + (1 << num_additional_bits); 458 | ret.push(TableEncodingEntry { 459 | min: min, 460 | max: max, 461 | num_additional_bits: num_additional_bits, 462 | }); 463 | min = max; 464 | } 465 | ret 466 | } 467 | 468 | impl fmt::Display for TableEncoding { 469 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 470 | for i in 0..16 { 471 | write!(f, "{:x}", self.distance_table_1[i].num_additional_bits)?; 472 | } 473 | write!(f, ",")?; 474 | for i in 0..16 { 475 | write!(f, "{:x}", self.distance_table_2[i].num_additional_bits)?; 476 | } 477 | write!(f, ",")?; 478 | for i in 0..16 { 479 | write!(f, "{:x}", self.length_table[i].num_additional_bits)?; 480 | } 481 | 482 | Ok(()) 483 | } 484 | } 485 | 486 | impl Encoding for TableEncoding { 487 | fn bit_length(&self, edge: &Edge) -> u32 { 488 | match edge { 489 | &Edge::Literal { .. } => 1 + 8, 490 | &Edge::Backreference { distance, length } => { 491 | let (distance_entry, distance_entry_index) = if length == 1 { self.distance_entry_1(distance) } else { self.distance_entry_2(distance) }.unwrap(); 492 | let distance_bit_length = unary_bit_length(distance_entry_index as _) + distance_entry.num_additional_bits; 493 | let (length_entry, length_entry_index) = self.length_entry(length).unwrap(); 494 | let length_bit_length = unary_bit_length(length_entry_index as _) + length_entry.num_additional_bits; 495 | 1 + distance_bit_length + length_bit_length 496 | } 497 | } 498 | } 499 | } 500 | 501 | fn unary_bit_length(x: u32) -> u32 { 502 | x + 1 503 | } 504 | 505 | fn parse(input: Vec, speed_setting: SpeedSetting, f: F) -> Vec> where F: Fn(usize) { 506 | f(0); 507 | 508 | let input_len = input.len(); 509 | 510 | let mut edge_lists = vec![Vec::new(); input_len]; 511 | 512 | let mut match_dictionary = HashMap::new(); 513 | let mut byte_index = 0; 514 | while byte_index < input_len { 515 | let edge_list = &mut edge_lists[byte_index]; 516 | 517 | // Add literal edge 518 | edge_list.push(Edge::Literal { byte: input[byte_index] }); 519 | 520 | // Search for match edges 521 | let max_distance = 65535; 522 | let max_length = 65535; 523 | let min_length = 1; 524 | 525 | // If we find a match with a length above a certain threshold, it's highly likely that it's with a long string of the 526 | // same value over and over. Since this is our algorithm's degenerate case causing very large exponential execution 527 | // times, in that situation we're just going to greedily take the match, rather than searching properly through it. 528 | // This technically makes the parse potentially sub-optimal, but makes my test cases parse ~60x faster and produces 529 | // nearly identical (+/-8b) results. Should look into removing this eventually and writing a smarter parser/matcher, 530 | // but for now this should be a pretty good shortcut to enable faster iteration times while tweaking the compression/ 531 | // encoding. 532 | let min_greedy_len = match speed_setting { 533 | SpeedSetting::Optimal => 4096, 534 | SpeedSetting::Instant => 512, 535 | }; 536 | 537 | // Update match dictionary 538 | if byte_index > max_distance { 539 | let remove_index = byte_index - max_distance - 1; 540 | match_dictionary.entry(input[remove_index]).or_insert(BTreeSet::new()).remove(&remove_index); 541 | } 542 | 543 | if byte_index >= 1 { 544 | let add_index = byte_index - 1; 545 | match_dictionary.entry(input[add_index]).or_insert(BTreeSet::new()).insert(add_index); 546 | } 547 | 548 | let mut longest_match_len = None; 549 | 550 | if byte_index >= 1 { 551 | let matches = match_dictionary.entry(input[byte_index]).or_insert(BTreeSet::new()); 552 | for search_start_index in matches.iter().cloned() { 553 | let mut search_index = search_start_index + 1; 554 | let mut search_cmp_index = byte_index + 1; 555 | while search_index - search_start_index < max_length && search_cmp_index < input_len && input[search_index] == input[search_cmp_index] { 556 | search_index += 1; 557 | search_cmp_index += 1; 558 | } 559 | 560 | let length = search_index - search_start_index; 561 | if length >= min_length { 562 | // Add match edge to list 563 | edge_list.push(Edge::Backreference { 564 | distance: (byte_index - search_start_index) as _, 565 | length: (search_index - search_start_index) as _, 566 | }); 567 | 568 | if longest_match_len.map(|x| length > x).unwrap_or(true) { 569 | longest_match_len = Some(length); 570 | } 571 | } 572 | } 573 | } 574 | 575 | if (byte_index & 0x3f) == 0 { 576 | f(byte_index); 577 | } 578 | 579 | byte_index += longest_match_len.map(|x| if x >= min_greedy_len { x } else { 1 }).unwrap_or(1); 580 | } 581 | 582 | f(input_len); 583 | 584 | edge_lists 585 | } 586 | 587 | #[derive(Default, Clone)] 588 | struct Node { 589 | cost: Option, 590 | cost_src: Option<(usize, Edge)>, 591 | } 592 | 593 | fn compress(edge_lists: &Vec>, encoding: &Encoding, input_len: usize, f: F) -> Vec where F: Fn(usize) { 594 | f(0); 595 | 596 | // Allocate nodes 597 | let mut nodes = vec![Node::default(); input_len + 1]; 598 | 599 | // Ensure first node's cost is 0 600 | nodes[0].cost = Some(0); 601 | 602 | // For each node corresponding to a byte in the sequence, test edges from edge lists and uncompressed blocks, and relax node 603 | for byte_index in 0..input_len { 604 | let edge_list = &edge_lists[byte_index]; 605 | 606 | // Skip this byte if it has an empty edge list 607 | if edge_list.is_empty() { 608 | continue; 609 | } 610 | 611 | // Test edges from edge list 612 | for edge in edge_list.iter().cloned() { 613 | compress_test_edge(edge, encoding, &mut nodes, byte_index); 614 | } 615 | 616 | if (byte_index & 0x3f) == 0 { 617 | f(byte_index); 618 | } 619 | } 620 | 621 | f(input_len); 622 | 623 | // Trace shortest path 624 | let mut path = Vec::new(); 625 | let mut node_index = input_len; 626 | loop { 627 | let (next_node_index, edge) = nodes[node_index].cost_src.unwrap(); 628 | path.push(edge); 629 | node_index = next_node_index; 630 | 631 | if node_index == 0 { 632 | break; 633 | } 634 | } 635 | path.reverse(); 636 | 637 | path 638 | } 639 | 640 | fn compress_test_edge(edge: Edge, encoding: &Encoding, nodes: &mut Vec, src_node_index: usize) { 641 | let src_cost = nodes[src_node_index].cost.unwrap(); 642 | let path_cost = src_cost + encoding.bit_length(&edge); 643 | 644 | let dst_node_index = src_node_index + (edge.byte_length() as usize); 645 | let dst_node = &mut nodes[dst_node_index]; 646 | 647 | if dst_node.cost.map(|cost| path_cost < cost).unwrap_or(true) { 648 | dst_node.cost = Some(path_cost); 649 | dst_node.cost_src = Some((src_node_index, edge)); 650 | } 651 | } 652 | 653 | struct EncoderState { 654 | output: Vec, 655 | 656 | bit_buffer: u32, 657 | bit_index: u32, 658 | } 659 | 660 | fn encode(input: &[u8], path: &Vec, encoding: &TableEncoding, f: F) -> Vec where F: Fn(usize) { 661 | f(0); 662 | 663 | let input_len = input.len(); 664 | 665 | let mut state = EncoderState { 666 | output: Vec::new(), 667 | 668 | bit_buffer: 0, 669 | bit_index: 0, 670 | }; 671 | 672 | // Output encoding table entries 673 | for i in 0..16 { 674 | encode_write_n_bit_value(&mut state, 4, encoding.length_table[i].num_additional_bits); 675 | } 676 | for i in 0..16 { 677 | encode_write_n_bit_value(&mut state, 4, encoding.distance_table_2[i].num_additional_bits); 678 | } 679 | for i in 0..16 { 680 | encode_write_n_bit_value(&mut state, 4, encoding.distance_table_1[i].num_additional_bits); 681 | } 682 | 683 | let mut byte_index = 0; 684 | for edge in path.iter() { 685 | match edge { 686 | &Edge::Literal { byte } => { 687 | encode_write_bit(&mut state, 0); 688 | encode_write_byte(&mut state, byte as _); 689 | byte_index += 1; 690 | } 691 | &Edge::Backreference { distance, length } => { 692 | encode_write_bit(&mut state, 1); 693 | 694 | let (length_entry, length_entry_index) = encoding.length_entry(length).unwrap(); 695 | encode_write_unary(&mut state, length_entry_index as _); 696 | encode_write_n_bit_value(&mut state, length_entry.num_additional_bits, length - length_entry.min); 697 | 698 | let (distance_entry, distance_entry_index) = if length == 1 { encoding.distance_entry_1(distance) } else { encoding.distance_entry_2(distance) }.unwrap(); 699 | encode_write_unary(&mut state, distance_entry_index as _); 700 | encode_write_n_bit_value(&mut state, distance_entry.num_additional_bits, distance - distance_entry.min); 701 | 702 | byte_index += length as usize; 703 | } 704 | } 705 | 706 | if (byte_index & 0x3f) == 0 { 707 | f(byte_index); 708 | } 709 | } 710 | 711 | encode_flush(&mut state); 712 | 713 | f(input_len); 714 | 715 | state.output 716 | } 717 | 718 | fn encode_write_bit(state: &mut EncoderState, bit: u32) { 719 | encode_push_buffer_bit(state, bit); 720 | 721 | if state.bit_index == 8 { 722 | encode_flush(state); 723 | } 724 | } 725 | 726 | fn encode_write_byte(state: &mut EncoderState, byte: u32) { 727 | for i in 0..8 { 728 | // Write msb -> lsb 729 | encode_write_bit(state, (byte >> (7 - i)) & 1); 730 | } 731 | } 732 | 733 | fn encode_write_unary(state: &mut EncoderState, n: u32) { 734 | for _ in 0..n { 735 | encode_write_bit(state, 0); 736 | } 737 | encode_write_bit(state, 1); 738 | } 739 | 740 | fn encode_write_n_bit_value(state: &mut EncoderState, n: u32, x: u32) { 741 | for digit_index in 0..n { 742 | // Write msb -> lsb 743 | encode_write_bit(state, (x >> (n - 1 - digit_index)) & 1); 744 | } 745 | } 746 | 747 | fn encode_flush(state: &mut EncoderState) { 748 | if state.bit_index == 0 { 749 | return; 750 | } 751 | 752 | while state.bit_index < 8 { 753 | encode_push_buffer_bit(state, 0); 754 | } 755 | 756 | state.output.push(state.bit_buffer as _); 757 | 758 | state.bit_buffer = 0; 759 | state.bit_index = 0; 760 | } 761 | 762 | fn encode_push_buffer_bit(state: &mut EncoderState, bit: u32) { 763 | state.bit_buffer = (state.bit_buffer >> 1) | (bit << 7); 764 | state.bit_index += 1; 765 | } 766 | 767 | struct DecompressorState { 768 | input: Vec, 769 | output: Vec, 770 | 771 | input_index: usize, 772 | bit_buffer: u32, 773 | bit_index: u32, 774 | } 775 | 776 | fn decompress(input: Vec, uncompressed_size: usize, f: F) -> Vec where F: Fn(usize) { 777 | let mut state = DecompressorState { 778 | input: input, 779 | output: Vec::new(), 780 | 781 | input_index: 0, 782 | bit_buffer: 0, 783 | bit_index: 0, 784 | }; 785 | 786 | f(0); 787 | 788 | // Decode encoding table entries 789 | let mut table = Vec::with_capacity(48); 790 | let mut min = 0; 791 | for i in 0..48 { 792 | if (i & 0x0f) == 0 { 793 | min = 1; 794 | } 795 | let num_additional_bits = decompress_read_n_bit_value(&mut state, 4); 796 | table.push((min, num_additional_bits)); 797 | min += 1 << num_additional_bits; 798 | } 799 | 800 | while state.output.len() < uncompressed_size { 801 | match decompress_read_bit(&mut state) { 802 | 1 => { 803 | // Backreference 804 | let length_index = decompress_read_unary(&mut state); 805 | let ref length_entry = table[length_index as usize]; 806 | let length = length_entry.0 + decompress_read_n_bit_value(&mut state, length_entry.1); 807 | 808 | let mut distance_index = 16 + decompress_read_unary(&mut state); 809 | if length == 1 { 810 | distance_index += 16; 811 | } 812 | let ref distance_entry = table[distance_index as usize]; 813 | let distance = distance_entry.0 + decompress_read_n_bit_value(&mut state, distance_entry.1); 814 | 815 | let mut index = state.output.len() - (distance as usize); 816 | for _ in 0..length { 817 | let byte = state.output[index]; 818 | decompress_write_byte(&mut state, byte as _); 819 | index += 1; 820 | } 821 | } 822 | _ => { 823 | // Literal 824 | let byte = decompress_read_byte(&mut state); 825 | decompress_write_byte(&mut state, byte); 826 | } 827 | } 828 | 829 | if (state.output.len() & 0x3f) == 0 { 830 | f(state.output.len()); 831 | } 832 | } 833 | 834 | f(uncompressed_size); 835 | 836 | state.output 837 | } 838 | 839 | fn decompress_read_bit(state: &mut DecompressorState) -> u32 { 840 | if state.bit_index == 0 { 841 | state.bit_buffer = state.input[state.input_index] as _; 842 | state.input_index += 1; 843 | } 844 | 845 | let ret = state.bit_buffer & 1; 846 | state.bit_buffer >>= 1; 847 | 848 | state.bit_index = (state.bit_index + 1) & 7; 849 | 850 | ret 851 | } 852 | 853 | fn decompress_read_byte(state: &mut DecompressorState) -> u32 { 854 | let mut ret = 0; 855 | 856 | for _ in 0..8 { 857 | // Read msb -> lsb 858 | ret <<= 1; 859 | ret |= decompress_read_bit(state); 860 | } 861 | 862 | ret 863 | } 864 | 865 | fn decompress_read_unary(state: &mut DecompressorState) -> u32 { 866 | let mut n = 0; 867 | while decompress_read_bit(state) == 0 { 868 | n += 1; 869 | } 870 | 871 | n 872 | } 873 | 874 | fn decompress_read_n_bit_value(state: &mut DecompressorState, n: u32) -> u32 { 875 | let mut x = 0; 876 | for _ in 0..n { 877 | // Read msb -> lsb 878 | x <<= 1; 879 | x |= decompress_read_bit(state); 880 | } 881 | 882 | x 883 | } 884 | 885 | fn decompress_write_byte(state: &mut DecompressorState, byte: u32) { 886 | state.output.push(byte as _); 887 | } 888 | 889 | fn generate_report>(output_file_name: P, input: &[u8], edges: &[Edge], encoding: &Encoding) { 890 | let mut output = File::create(output_file_name).expect("Couldn't open output file"); 891 | write!(output, " 892 | 893 | admiral p4kbar report 894 | 983 | 984 | 985 |

admiral p4kbar report

986 |

please, don't make me say the tagline..

").unwrap(); 987 | 988 | let mut num_literal_edges = 0; 989 | let mut num_literal_bits = 0; 990 | let mut num_backreference_edges = 0; 991 | let mut num_backreference_bits = 0; 992 | 993 | let mut backreference_distance_counts = BTreeMap::new(); 994 | let mut backreference_length_counts = BTreeMap::new(); 995 | 996 | for edge in edges.iter() { 997 | let compressed_bits = encoding.bit_length(edge); 998 | 999 | match edge { 1000 | &Edge::Literal { .. } => { 1001 | num_literal_edges += 1; 1002 | num_literal_bits += compressed_bits; 1003 | } 1004 | &Edge::Backreference { distance, length } => { 1005 | num_backreference_edges += 1; 1006 | num_backreference_bits += compressed_bits; 1007 | 1008 | *backreference_distance_counts.entry(distance).or_insert(0) += 1; 1009 | *backreference_length_counts.entry(length).or_insert(0) += 1; 1010 | } 1011 | } 1012 | } 1013 | 1014 | write!(output, "
1015 |

overview

").unwrap(); 1016 | 1017 | write!(output, "

edge totals

1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 |
edge typecountcount%total comp. bitstotal comp. bytes
Literal{}{:.*}%{} bits~{} bytes
Backreference{}{:.*}%{} bits~{} bytes
", 1041 | num_literal_edges, 2, (num_literal_edges as f64) / (edges.len() as f64) * 100.0, num_literal_bits, num_literal_bits / 8, 1042 | num_backreference_edges, 2, (num_backreference_edges as f64) / (edges.len() as f64) * 100.0, num_backreference_bits, num_backreference_bits / 8).unwrap(); 1043 | 1044 | write!(output, "
").unwrap(); 1045 | 1046 | write!(output, "
1047 |

key

1048 |

").unwrap(); 1049 | for i in 0..10 { 1050 | if i < 8 { 1051 | write!(output, "00 {}-{} bits/byte
", i, i, i + 1).unwrap(); 1052 | } else { 1053 | write!(output, "00   {} bits/byte
", i, i).unwrap(); 1054 | } 1055 | } 1056 | 1057 | write!(output, "

1058 |
").unwrap(); 1059 | 1060 | write!(output, "
1061 |

heatmap

1062 |

").unwrap(); 1063 | 1064 | let mut byte_index = 0; 1065 | for edge in edges { 1066 | let num_bytes = edge.byte_length(); 1067 | let uncompressed_bits = num_bytes * 8; 1068 | let compressed_bits = encoding.bit_length(edge); 1069 | let byte_size = (8.0 * (compressed_bits as f64) / (uncompressed_bits as f64)).round() as u32; 1070 | 1071 | let title = match edge { 1072 | &Edge::Literal { byte } => format!("literal (byte: 0x{:02x})", byte), 1073 | &Edge::Backreference { distance, length } => format!("backreference (distance: {}, length: {})", distance, length), 1074 | }; 1075 | let start_span = format!("", title, encoding.bit_length(edge), byte_size); 1076 | write!(output, "{}", start_span).unwrap(); 1077 | 1078 | for _ in 0..num_bytes { 1079 | let row_len = 64; 1080 | let x = byte_index % row_len; 1081 | 1082 | let byte = input[byte_index]; 1083 | 1084 | write!(output, "{:02x}", byte).unwrap(); 1085 | 1086 | if x == row_len - 1 { 1087 | write!(output, "").unwrap(); 1088 | write!(output, " 
").unwrap(); 1089 | write!(output, "{}", start_span).unwrap(); 1090 | } 1091 | 1092 | byte_index += 1; 1093 | } 1094 | 1095 | write!(output, "").unwrap(); 1096 | } 1097 | 1098 | write!(output, " 1099 |

1100 |
").unwrap(); 1101 | 1102 | /*write!(output, "
").unwrap(); 1103 | write!(output, "

backreference distance distribution

1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 1112 | 1113 | ").unwrap(); 1114 | for (distance, count) in backreference_distance_counts.clone().into_iter() { 1115 | write!(output, " 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | ", distance, count, 2, (count as f64) / (num_backreference_edges as f64) * 100.0, gamma_bit_length(distance), gamma_bit_length(distance) * count, gamma_bit_length(distance) * count / 8).unwrap(); 1122 | write!(output, "").unwrap(); 1127 | write!(output, "").unwrap(); 1128 | } 1129 | write!(output, "
distancecountcount%coded bitstotal bitstotal byteshistogram ((count + 1) / 2)
{}{}{:.*}%{}{}~{}").unwrap(); 1123 | for _ in 0..(count + 1) / 2 { 1124 | write!(output, "*").unwrap(); 1125 | } 1126 | write!(output, "
").unwrap(); 1130 | write!(output, "
").unwrap(); 1131 | 1132 | write!(output, "
").unwrap(); 1133 | write!(output, "

backreference length distribution

1134 | 1135 | 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | ").unwrap(); 1144 | for (length, count) in backreference_length_counts.clone().into_iter() { 1145 | let coded_bits = encoding.bit_length() 1146 | write!(output, " 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | ", length, count, 2, (count as f64) / (num_backreference_edges as f64) * 100.0, gamma_bit_length(length), gamma_bit_length(length) * count, gamma_bit_length(length) * count / 8).unwrap(); 1153 | write!(output, "").unwrap(); 1158 | write!(output, "").unwrap(); 1159 | } 1160 | write!(output, "
lengthcountcount%coded bitstotal bitstotal byteshistogram ((count + 1) / 2)
{}{}{:.*}%{}{}~{}").unwrap(); 1154 | for _ in 0..(count + 1) / 2 { 1155 | write!(output, "*").unwrap(); 1156 | } 1157 | write!(output, "
").unwrap(); 1161 | write!(output, "
").unwrap();*/ 1162 | 1163 | write!(output, " 1164 | ").unwrap(); 1165 | } 1166 | -------------------------------------------------------------------------------- /basic.inc: -------------------------------------------------------------------------------- 1 | .const basic_token_for = $81 2 | .const basic_token_next = $82 3 | .const basic_token_poke = $97 4 | .const basic_token_print = $99 5 | .const basic_token_sys = $9e 6 | .const basic_token_to = $a4 7 | .const basic_token_step = $a9 8 | .const basic_token_addition = $aa 9 | .const basic_token_subtraction = $ab 10 | .const basic_token_multiplication = $ac 11 | .const basic_token_division = $ad 12 | .const basic_token_and = $af 13 | .const basic_token_equals = $b2 14 | .const basic_token_int = $b5 15 | .const basic_token_cos = $be 16 | .const basic_token_sin = $bf 17 | .const basic_token_peek = $c2 18 | .const basic_token_pi = $ff 19 | 20 | .macro StartBasicLineWithoutToken(next_addr, line) { 21 | .word next_addr 22 | .word line 23 | } 24 | 25 | .macro StartBasicLine(next_addr, line, token) { 26 | StartBasicLineWithoutToken(next_addr, line) 27 | .byte token 28 | } 29 | 30 | .macro EndBasicLine() { 31 | .byte $00 32 | } 33 | 34 | .macro EndBasicProgram() { 35 | .byte $00, $00 36 | } 37 | 38 | .macro EmptyRealVar(name) { 39 | .if(name.size() == 0) { 40 | .error "Variable name cannot be empty" 41 | } 42 | .if(name.size() > 2) { 43 | .error "Variable name must be 1 or 2 characters long: " + name 44 | } 45 | .text name 46 | .if(name.size() == 1) { 47 | .byte $00 48 | } 49 | .fill 5, $00 50 | } 51 | 52 | .macro EmptyIntegerVar(name) { 53 | .if(name.size() == 0) { 54 | .error "Variable name cannot be empty" 55 | } 56 | .if(name.size() > 2) { 57 | .error "Variable name must be 1 or 2 characters long: " + name 58 | } 59 | .for(var i = 0; i < 2; i++) { 60 | .var b = $80 61 | .if(i < name.size()) { 62 | .eval b += name.charAt(i) 63 | } 64 | .byte b 65 | } 66 | .fill 5, $00 67 | } 68 | -------------------------------------------------------------------------------- /common.inc: -------------------------------------------------------------------------------- 1 | .const start_addr = $801 2 | .const demo_entry = $80d -------------------------------------------------------------------------------- /demo.asm: -------------------------------------------------------------------------------- 1 | #import "common.inc" 2 | 3 | .pc = demo_entry "entry" 4 | entry: 5 | jsr mask_nmi 6 | 7 | // Turn off CIA interrupts 8 | lda #$7f 9 | sta $dc0d 10 | sta $dd0d 11 | 12 | // Enable raster interrupts 13 | lda #$01 14 | sta $d01a 15 | 16 | jsr init 17 | 18 | // Ack CIA interrupts 19 | lda $dc0d 20 | lda $dd0d 21 | 22 | // Ack VIC interrupts 23 | asl $d019 24 | 25 | cli 26 | 27 | jmp * 28 | 29 | .pc = * "mask nmi" 30 | mask_nmi: 31 | // Stop timer A 32 | lda #$00 33 | sta $dd0e 34 | 35 | // Set timer A to 0 after starting 36 | sta $dd04 37 | sta $dd05 38 | 39 | // Set timer A as NMI source 40 | lda #$81 41 | sta $dd0d 42 | 43 | // Set NMI vector 44 | lda #nmi 47 | sta $fffb 48 | 49 | // Start timer A (NMI triggers immediately) 50 | lda #$01 51 | sta $dd0e 52 | 53 | rts 54 | 55 | nmi: 56 | rti 57 | 58 | .const zp_base = $02 59 | 60 | .const frame_counter = zp_base 61 | .const frame_counter_low = frame_counter 62 | .const frame_counter_high = zp_base + 1 63 | 64 | .const sprite_frame_index = zp_base + 2 65 | .const sprite_frame_counter = zp_base + 3 66 | 67 | .const scroller_offset = zp_base + 4 68 | .const scroller_effect_index = zp_base + 5 69 | .const scroller_temp = zp_base + 6 70 | 71 | .const bg_fade_fade_index = zp_base + 7 72 | 73 | .const background_bitmap_pos = $4000 74 | .const background_screen_mem_pos = $6000 75 | 76 | .const scroller_stretcher_lines = 24 - 2 77 | .const scroller_font_pos = $6800 78 | .const scroller_text_pos = $8000 79 | .const scroller_color_table = $9000 80 | .const scroller_d018_table = scroller_color_table + scroller_stretcher_lines 81 | 82 | .const sprite_pos = $7000 83 | .const sprite_data_ptr_pos = background_screen_mem_pos + $3f8 84 | 85 | .pc = * "init" 86 | init: 87 | // Reset graphics mode/scroll 88 | lda #$1b 89 | sta $d011 90 | 91 | // Reset vars 92 | lda #$00 93 | sta frame_counter_low 94 | sta frame_counter_high 95 | sta sprite_frame_index 96 | sta sprite_frame_counter 97 | sta scroller_offset 98 | sta scroller_effect_index 99 | sta bg_fade_fade_index 100 | 101 | // Set background colors 102 | lda #$00 103 | sta $d020 104 | sta $d021 105 | 106 | // Set initial color+screen mem contents 107 | lda #$00 108 | tax 109 | !: sta $d800, x 110 | sta $d900, x 111 | sta $da00, x 112 | sta $db00, x 113 | sta background_screen_mem_pos, x 114 | sta background_screen_mem_pos + $100, x 115 | sta background_screen_mem_pos + $200, x 116 | sta background_screen_mem_pos + $300, x 117 | inx 118 | bne !- 119 | 120 | // Unpack scroller font 121 | // Bank out io regs 122 | lda #$34 123 | sta $01 124 | 125 | ldx #$00 126 | unpack_font_char_loop: 127 | txa 128 | pha 129 | 130 | ldx #$00 131 | unpack_font_line_loop: 132 | // Read char line byte 133 | unpack_font_read_instr: 134 | lda scroller_font_pos, x 135 | 136 | // Write char line byte 8x 137 | ldy #$00 138 | unpack_font_write_loop: 139 | unpack_font_write_instr: 140 | sta $c000, y 141 | iny 142 | cpy #$08 143 | bne unpack_font_write_loop 144 | 145 | // Move write ptr to next charset 146 | lda unpack_font_write_instr + 2 147 | clc 148 | adc #$08 149 | sta unpack_font_write_instr + 2 150 | inx 151 | cpx #$08 152 | bne unpack_font_line_loop 153 | 154 | // Increment read ptr for next char 155 | lda unpack_font_read_instr + 1 156 | clc 157 | adc #$08 158 | sta unpack_font_read_instr + 1 159 | bcc !+ 160 | inc unpack_font_read_instr + 2 161 | 162 | // Subtract charset offsets from write ptr 163 | !: lda unpack_font_write_instr + 2 164 | sec 165 | sbc #$40 166 | sta unpack_font_write_instr + 2 167 | 168 | // Increment write ptr for next char 169 | lda unpack_font_write_instr + 1 170 | clc 171 | adc #$08 172 | sta unpack_font_write_instr + 1 173 | bcc !+ 174 | inc unpack_font_write_instr + 2 175 | 176 | !: pla 177 | tax 178 | inx 179 | cpx #$80 180 | bne unpack_font_char_loop 181 | 182 | // Bank in io regs 183 | lda #$35 184 | sta $01 185 | 186 | // Clear out original font data 187 | // This way we get a blank charset, useful for hiding some buggy stuff while doing the scroll rastersplits 188 | ldx #$00 189 | clear_font_outer_loop: 190 | lda #$00 191 | tay 192 | clear_font_inner_loop: 193 | clear_font_write_instr: 194 | // Clear char line byte 195 | sta scroller_font_pos, y 196 | iny 197 | bne clear_font_inner_loop 198 | 199 | // Increment write ptr for next char 200 | inc clear_font_write_instr + 2 201 | inx 202 | cpx #$08 203 | bne clear_font_outer_loop 204 | 205 | // Set scroller color mem contents 206 | lda #$00 207 | ldx #$00 208 | !: sta $d800 + 20 * 40, x 209 | inx 210 | cpx #40 211 | bne !- 212 | 213 | // Clear scroller screen mem (set to spaces, $20) 214 | lda #$20 215 | ldx #$00 216 | !: sta background_screen_mem_pos + 20 * 40, x 217 | inx 218 | cpx #40 219 | bne !- 220 | 221 | // Set sprite positions 222 | // Note these initial positions were taken straight from the spec image, so they'll need some transformation for actual reg values 223 | .const sprite_positions_x = List().add( 1, 8, 11, 76, 135, 143, 133, 138).lock() 224 | .const sprite_positions_y = List().add( 63, 43, 101, 76, 20, 47, 71, 110).lock() 225 | .var sprite_pos_x_msbs = 0 226 | .for (var i = 0; i < 8; i++) { 227 | .var x = sprite_positions_x.get(i) * 2 + $18 228 | .var y = sprite_positions_y.get(i) + $32 229 | .eval sprite_pos_x_msbs = (sprite_pos_x_msbs >> 1) | ((x >> 1) & $80) 230 | lda #(x & $ff) 231 | sta $d000 + i * 2 232 | lda #y 233 | sta $d001 + i * 2 234 | } 235 | lda #sprite_pos_x_msbs 236 | sta $d010 237 | 238 | // Set initial sprite colors 239 | lda #$00 240 | sta $d025 241 | sta $d026 242 | .for (var i = 0; i < 8; i++) { 243 | sta $d027 + i 244 | } 245 | 246 | // Enable sprites 247 | lda #$ff 248 | sta $d015 249 | 250 | // Set sprite multicolor 251 | lda #$ff 252 | sta $d01c 253 | 254 | // Set up frame interrupt 255 | lda #frame 258 | sta $ffff 259 | lda #$ff 260 | sta $d012 261 | 262 | // Init music 263 | lda #$00 264 | tax 265 | tay 266 | jsr music 267 | 268 | rts 269 | 270 | .pc = * "frame" 271 | frame: 272 | pha 273 | txa 274 | pha 275 | tya 276 | pha 277 | 278 | //inc $d020 279 | 280 | // Set multicolor bitmap mode 281 | lda #$3b 282 | sta $d011 283 | lda #$18 284 | sta $d016 285 | 286 | // Set graphics/screen pointers 287 | lda #$80 288 | sta $d018 289 | 290 | // Set graphics bank 1 291 | lda #$c6 292 | sta $dd00 293 | 294 | // Increment frame counter 295 | inc frame_counter_low 296 | bne !+ 297 | inc frame_counter_high 298 | 299 | // Update sprite ptrs 300 | !: lda sprite_frame_index 301 | and #$07 302 | clc 303 | adc #$c0 304 | 305 | .for (var i = 0; i < 8; i++) { 306 | sta sprite_data_ptr_pos + i 307 | .if (i < 7) { 308 | clc 309 | adc #$08 310 | } 311 | } 312 | 313 | inc sprite_frame_counter 314 | lda sprite_frame_counter 315 | cmp #$03 316 | bne !+ 317 | inc sprite_frame_index 318 | 319 | lda #$00 320 | sta sprite_frame_counter 321 | 322 | // Update scroller 323 | !: jsr scroller_update 324 | 325 | // Update bg fade 326 | jsr bg_fade_update 327 | 328 | // Update music 329 | //inc $d020 330 | jsr music + 3 331 | //dec $d020 332 | 333 | // Set 2x interrupt 334 | lda #music2x 337 | sta $ffff 338 | lda #99 339 | sta $d012 340 | 341 | //dec $d020 342 | 343 | pla 344 | tay 345 | pla 346 | tax 347 | pla 348 | asl $d019 349 | rti 350 | 351 | .pc = * "music 2x" 352 | music2x: 353 | pha 354 | txa 355 | pha 356 | tya 357 | pha 358 | 359 | // Update music 2x 360 | //inc $d020 361 | jsr music + 6 362 | //dec $d020 363 | 364 | // Set scroller display interrupt 365 | lda #scroller_display 368 | sta $ffff 369 | lda #206 370 | sta $d012 371 | 372 | pla 373 | tay 374 | pla 375 | tax 376 | pla 377 | asl $d019 378 | rti 379 | 380 | .align $100 381 | .pc = * "scroller display" 382 | scroller_display: 383 | pha 384 | txa 385 | pha 386 | tya 387 | pha 388 | 389 | // Set up next interrupt stage 390 | lda #frame 517 | sta $ffff 518 | lda #$ff 519 | sta $d012 520 | 521 | //dec $d020 522 | 523 | pla 524 | tay 525 | pla 526 | tax 527 | pla 528 | asl $d019 529 | rti 530 | 531 | .pc = $1000 "music" 532 | music: 533 | .import c64 "music.prg" 534 | 535 | .pc = * "scroller effect jump table" 536 | scroller_effect_jump_table: 537 | .word static_y_scroller - 1 538 | .word dynamic_y_scroller - 1 539 | .word mirrored_scroller - 1 540 | .word layered_scrollers - 1 541 | .word squishy_scroller - 1 542 | .word repeating_scroller - 1 543 | 544 | .pc = * "scroller update" 545 | scroller_update: 546 | // Update effect index 547 | lda frame_counter_low 548 | bne scroller_effect_index_update_done 549 | lda frame_counter_high 550 | and #$01 551 | bne scroller_effect_index_update_done 552 | inc scroller_effect_index 553 | lda scroller_effect_index 554 | cmp #$06 555 | bne scroller_effect_index_update_done 556 | lda #$01 557 | sta scroller_effect_index 558 | scroller_effect_index_update_done: 559 | 560 | // Dispatch effect 561 | lda scroller_effect_index 562 | asl 563 | tax 564 | lda scroller_effect_jump_table + 1, x 565 | pha 566 | lda scroller_effect_jump_table, x 567 | pha 568 | rts 569 | 570 | // Static y scroller 571 | static_y_scroller: 572 | ldy #(scroller_stretcher_lines / 2 - 8 / 2) 573 | ldx #$00 574 | !: lda #$01 575 | sta scroller_color_table, y 576 | txa 577 | asl 578 | sta scroller_d018_table, y 579 | iny 580 | inx 581 | cpx #$08 582 | bne !- 583 | jmp scroller_effect_done 584 | 585 | // Dynamic y scroller 586 | dynamic_y_scroller: 587 | lda frame_counter_low 588 | asl 589 | asl 590 | clc 591 | adc frame_counter_low 592 | tax 593 | lda scroller_y_offset_tab, x 594 | pha 595 | lda frame_counter_low 596 | asl 597 | asl 598 | tax 599 | pla 600 | clc 601 | adc scroller_y_offset_tab, x 602 | lsr 603 | tay 604 | ldx #$00 605 | !: lda #$01 606 | sta scroller_color_table, y 607 | txa 608 | asl 609 | sta scroller_d018_table, y 610 | iny 611 | inx 612 | cpx #$08 613 | bne !- 614 | jmp scroller_effect_done 615 | 616 | // Mirrored scroller 617 | mirrored_scroller: 618 | // Top part 619 | lda #(scroller_stretcher_lines / 2 - 7) 620 | ldx frame_counter_low 621 | sec 622 | sbc mirrored_scroller_y_tab, x 623 | tay 624 | ldx #$00 625 | !: lda #$01 626 | sta scroller_color_table, y 627 | txa 628 | asl 629 | sta scroller_d018_table, y 630 | iny 631 | inx 632 | cpx #$07 633 | bne !- 634 | 635 | // Bottom part 636 | lda #(scroller_stretcher_lines / 2) 637 | ldx frame_counter_low 638 | clc 639 | adc mirrored_scroller_y_tab, x 640 | tay 641 | ldx #$00 642 | !: lda #$0b 643 | sta scroller_color_table, y 644 | txa 645 | eor #$ff 646 | clc 647 | adc #$07 648 | asl 649 | sta scroller_d018_table, y 650 | iny 651 | inx 652 | cpx #$07 653 | bne !- 654 | jmp scroller_effect_done 655 | 656 | // Layered scrollers 657 | layered_scrollers: 658 | lda frame_counter_low 659 | lsr 660 | and #$0f 661 | tax 662 | lda layered_scrollers_color_tab_1, x 663 | sta scroller_temp 664 | lda frame_counter_low 665 | asl 666 | asl 667 | clc 668 | adc frame_counter_low 669 | tax 670 | lda scroller_y_offset_tab, x 671 | pha 672 | lda frame_counter_low 673 | asl 674 | asl 675 | tax 676 | pla 677 | clc 678 | adc scroller_y_offset_tab, x 679 | lsr 680 | tay 681 | ldx #$00 682 | !: lda scroller_temp 683 | sta scroller_color_table, y 684 | txa 685 | asl 686 | sta scroller_d018_table, y 687 | iny 688 | inx 689 | cpx #$07 690 | bne !- 691 | 692 | lda frame_counter_low 693 | lsr 694 | clc 695 | adc #$04 696 | and #$0f 697 | tax 698 | lda layered_scrollers_color_tab_2, x 699 | sta scroller_temp 700 | lda frame_counter_low 701 | clc 702 | adc #$30 703 | asl 704 | clc 705 | adc frame_counter_low 706 | tax 707 | lda scroller_y_offset_tab, x 708 | pha 709 | lda frame_counter_low 710 | tax 711 | pla 712 | clc 713 | adc scroller_y_offset_tab, x 714 | lsr 715 | tay 716 | ldx #$00 717 | !: lda scroller_temp 718 | sta scroller_color_table, y 719 | txa 720 | eor #$ff 721 | clc 722 | adc #$07 723 | asl 724 | sta scroller_d018_table, y 725 | iny 726 | inx 727 | cpx #$07 728 | bne !- 729 | 730 | lda frame_counter_low 731 | lsr 732 | clc 733 | adc #$08 734 | and #$0f 735 | tax 736 | lda layered_scrollers_color_tab_3, x 737 | sta scroller_temp 738 | lda frame_counter_low 739 | clc 740 | adc #$67 741 | asl 742 | asl 743 | tax 744 | lda scroller_y_offset_tab, x 745 | tay 746 | ldx #$00 747 | !: lda scroller_temp 748 | sta scroller_color_table, y 749 | txa 750 | asl 751 | sta scroller_d018_table, y 752 | iny 753 | inx 754 | cpx #$07 755 | bne !- 756 | jmp scroller_effect_done 757 | 758 | layered_scrollers_color_tab_1: 759 | .byte $0b, $02, $04, $03, $01, $03, $04, $02, $02, $02, $02, $02, $02, $02, $02, $02 760 | 761 | layered_scrollers_color_tab_2: 762 | .byte $0b, $0c, $0f, $01, $01, $0f, $0c, $0b, $0b, $0b, $0b, $0b, $0b, $0b, $0b, $0b 763 | 764 | layered_scrollers_color_tab_3: 765 | .byte $0b, $06, $0e, $0d, $01, $0d, $0e, $06, $06, $06, $06, $06, $06, $06, $06, $06 766 | 767 | // Squishy scroller 768 | squishy_scroller: 769 | lda frame_counter_low 770 | asl 771 | asl 772 | asl 773 | asl 774 | sta scroller_temp 775 | ldx #$00 776 | squishy_scroller_loop: 777 | lda scroller_temp 778 | and #$80 779 | bne !+ 780 | lda scroller_temp 781 | lsr 782 | lsr 783 | lsr 784 | sta scroller_d018_table, x 785 | sec 786 | sbc frame_counter_low 787 | lsr 788 | and #$0f 789 | tay 790 | lda squishy_scroller_color_tab, y 791 | sta scroller_color_table, x 792 | !: ldy frame_counter_low 793 | txa 794 | eor #$ff 795 | asl 796 | clc 797 | adc scroller_y_offset_tab_2, y 798 | tay 799 | lda scroller_temp 800 | clc 801 | adc squishy_scroller_y_tab, y 802 | sta scroller_temp 803 | inx 804 | cpx #scroller_stretcher_lines 805 | bne squishy_scroller_loop 806 | jmp scroller_effect_done 807 | 808 | squishy_scroller_color_tab: 809 | .byte $0b, $0f, $0c, $0f, $01, $01, $01, $01 810 | .byte $01, $01, $01, $01, $0f, $0c, $0f, $0b 811 | 812 | // Repeating scroller 813 | repeating_scroller: 814 | lda frame_counter_low 815 | asl 816 | clc 817 | adc frame_counter_low 818 | tax 819 | lda scroller_y_offset_tab_2, x 820 | lsr 821 | pha 822 | lda frame_counter_low 823 | asl 824 | tax 825 | pla 826 | clc 827 | adc scroller_y_offset_tab_2, x 828 | tay 829 | ldx #$00 830 | !: tya 831 | pha 832 | lsr 833 | clc 834 | adc frame_counter_low 835 | and #$3f 836 | tay 837 | lda repeating_scroller_color_tab, y 838 | sta scroller_color_table, x 839 | pla 840 | tay 841 | sta scroller_d018_table, x 842 | iny 843 | iny 844 | inx 845 | cpx #scroller_stretcher_lines 846 | bne !- 847 | jmp scroller_effect_done 848 | 849 | repeating_scroller_color_tab: 850 | .byte $0b, $02, $0b, $0b, $0b, $0b, $04, $0b 851 | .byte $04, $0b, $04, $04, $04, $04, $03, $04 852 | .byte $03, $04, $03, $03, $03, $03, $0d, $03 853 | .byte $0d, $03, $0d, $0d, $0d, $0d, $01, $0d 854 | .byte $01, $0d, $01, $01, $01, $01, $07, $01 855 | .byte $07, $01, $07, $07, $07, $07, $0a, $07 856 | .byte $0a, $07, $0a, $0a, $0a, $0a, $02, $0a 857 | .byte $02, $0a, $02, $02, $02, $02, $04, $02 858 | 859 | // Scroller transition effect 860 | scroller_effect_done: 861 | lda frame_counter_low 862 | cmp #scroller_stretcher_lines 863 | bcs scroller_transition_out_test 864 | lda frame_counter_high 865 | and #$01 866 | bne scroller_transition_out_test 867 | // Transition in 868 | lda #scroller_stretcher_lines 869 | sec 870 | sbc frame_counter_low 871 | jmp scroller_transition 872 | 873 | scroller_transition_out_test: 874 | lda frame_counter_low 875 | cmp #(256 - scroller_stretcher_lines) 876 | bcc scroller_transition_done 877 | lda frame_counter_high 878 | and #$01 879 | beq scroller_transition_done 880 | // Transition out 881 | lda frame_counter_low 882 | sec 883 | sbc #(256 - scroller_stretcher_lines) 884 | 885 | scroller_transition: 886 | lsr 887 | pha 888 | 889 | // Top half 890 | tax 891 | inx 892 | lda #$00 893 | tay 894 | !: sta scroller_color_table, y 895 | iny 896 | dex 897 | bne !- 898 | 899 | // Bottom half 900 | pla 901 | 902 | tax 903 | inx 904 | lda #$00 905 | ldy #(scroller_stretcher_lines - 1) 906 | !: sta scroller_color_table, y 907 | dey 908 | dex 909 | bne !- 910 | 911 | scroller_transition_done: 912 | dec scroller_offset 913 | lda scroller_offset 914 | and #$07 915 | sta scroller_offset 916 | 917 | cmp #$07 918 | beq !+ 919 | jmp scroller_update_done 920 | // Shift screen mem 921 | !: .for (var i = 0; i < 39; i++) { 922 | lda background_screen_mem_pos + 20 * 40 + i + 1 923 | sta background_screen_mem_pos + 20 * 40 + i 924 | } 925 | 926 | // Load next char 927 | scroller_text_load_instr: 928 | lda scroller_text 929 | sta background_screen_mem_pos + 20 * 40 + 39 930 | 931 | // Update (and possibly reset) text pointer 932 | inc scroller_text_load_instr + 1 933 | bne !+ 934 | inc scroller_text_load_instr + 2 935 | !: lda scroller_text_load_instr + 1 936 | cmp #scroller_text_end 940 | bne scroller_update_done 941 | lda #scroller_text 944 | sta scroller_text_load_instr + 2 945 | 946 | scroller_update_done: 947 | rts 948 | 949 | .pc = * "bg fade update" 950 | bg_fade_update: 951 | //inc $d020 952 | 953 | lda frame_counter_low 954 | bne !+ 955 | lda frame_counter_high 956 | and #$03 957 | bne !+ 958 | inc bg_fade_fade_index 959 | lda bg_fade_fade_index 960 | cmp #$05 961 | bne !+ 962 | lda #$01 963 | sta bg_fade_fade_index 964 | 965 | !: lda frame_counter_high 966 | and #$03 967 | beq !+ 968 | jmp bg_fade_update_done 969 | 970 | // Fade sprites 971 | !: lda bg_fade_fade_index 972 | asl 973 | tax 974 | lda bg_fade_screen_mem_index_tab, x 975 | sta bg_fade_sprite_color_1_2_read_instr + 1 976 | lda bg_fade_screen_mem_index_tab + 1, x 977 | sta bg_fade_sprite_color_1_2_read_instr + 2 978 | lda bg_fade_color_mem_index_tab, x 979 | sta bg_fade_sprite_color_3_read_instr + 1 980 | lda bg_fade_color_mem_index_tab + 1, x 981 | sta bg_fade_sprite_color_3_read_instr + 2 982 | 983 | lda frame_counter_low 984 | lsr 985 | lsr 986 | sec 987 | sbc #$08 988 | cmp #$10 989 | bcs !+ 990 | // Invert table index 991 | eor #$ff 992 | clc 993 | adc #$10 994 | tax 995 | bg_fade_sprite_color_1_2_read_instr: 996 | lda bg_fade_screen_mem_tab_0, x 997 | pha 998 | lsr 999 | lsr 1000 | lsr 1001 | lsr 1002 | sta $d025 1003 | pla 1004 | and #$0f 1005 | .for (var i = 0; i < 8; i++) { 1006 | sta $d027 + i 1007 | } 1008 | bg_fade_sprite_color_3_read_instr: 1009 | lda bg_fade_color_mem_tab_0, x 1010 | sta $d026 1011 | 1012 | // Fade BG 1013 | !: lda bg_fade_fade_index 1014 | asl 1015 | tax 1016 | lda bg_fade_screen_mem_index_tab, x 1017 | sta bg_fade_loop_screen_mem_read_instr + 1 1018 | lda bg_fade_screen_mem_index_tab + 1, x 1019 | sta bg_fade_loop_screen_mem_read_instr + 2 1020 | lda bg_fade_color_mem_index_tab, x 1021 | sta bg_fade_loop_color_mem_read_instr + 1 1022 | lda bg_fade_color_mem_index_tab + 1, x 1023 | sta bg_fade_loop_color_mem_read_instr + 2 1024 | 1025 | lda frame_counter_low 1026 | lsr 1027 | sec 1028 | sbc #$10 1029 | tax 1030 | ldy #$00 1031 | bg_fade_loop: 1032 | cpx #40 1033 | bcc !+ 1034 | jmp bg_fade_loop_continue 1035 | bg_fade_loop_screen_mem_read_instr: 1036 | !: lda bg_fade_screen_mem_tab_0, y 1037 | .for (var y = 0; y < 25; y++) { 1038 | .if (y < 20 || y >= 23) { 1039 | sta background_screen_mem_pos + y * 40, x 1040 | } 1041 | } 1042 | bg_fade_loop_color_mem_read_instr: 1043 | lda bg_fade_color_mem_tab_0, y 1044 | .for (var y = 0; y < 25; y++) { 1045 | .if (y < 20 || y >= 23) { 1046 | sta $d800 + y * 40, x 1047 | } 1048 | } 1049 | bg_fade_loop_continue: 1050 | inx 1051 | iny 1052 | cpy #$10 1053 | beq bg_fade_update_done 1054 | jmp bg_fade_loop 1055 | 1056 | bg_fade_update_done: 1057 | //dec $d020 1058 | 1059 | rts 1060 | 1061 | bg_fade_screen_mem_index_tab: 1062 | .word bg_fade_screen_mem_tab_0 1063 | .word bg_fade_screen_mem_tab_1 1064 | .word bg_fade_screen_mem_tab_2 1065 | .word bg_fade_screen_mem_tab_3 1066 | .word bg_fade_screen_mem_tab_4 1067 | 1068 | bg_fade_color_mem_index_tab: 1069 | .word bg_fade_color_mem_tab_0 1070 | .word bg_fade_color_mem_tab_1 1071 | .word bg_fade_color_mem_tab_2 1072 | .word bg_fade_color_mem_tab_3 1073 | .word bg_fade_color_mem_tab_4 1074 | 1075 | bg_fade_screen_mem_tab_0: 1076 | .byte $6e, $6e, $6c, $04, $0b, $06, $00, $00, $00, $00, $00, $00, $00, $00, $00, $00 1077 | bg_fade_color_mem_tab_0: 1078 | .byte $01, $0d, $03, $0c, $04, $0b, $06, $00, $00, $00, $00, $00, $00, $00, $00, $00 1079 | 1080 | bg_fade_screen_mem_tab_1: 1081 | .byte $2a, $24, $92, $92, $99, $09, $00, $00, $00, $00, $06, $0b, $04, $6c, $6e, $6e 1082 | bg_fade_color_mem_tab_1: 1083 | .byte $07, $0f, $0a, $08, $02, $09, $00, $00, $00, $06, $0b, $04, $0c, $03, $0d, $01 1084 | 1085 | bg_fade_screen_mem_tab_2: 1086 | .byte $5d, $cf, $4c, $48, $b2, $69, $00, $00, $00, $00, $09, $99, $92, $92, $24, $2a 1087 | bg_fade_color_mem_tab_2: 1088 | .byte $01, $0d, $03, $0c, $04, $0b, $06, $00, $00, $00, $09, $02, $08, $0a, $0f, $07 1089 | 1090 | bg_fade_screen_mem_tab_3: 1091 | .byte $bc, $b4, $bb, $6b, $66, $60, $00, $00, $00, $00, $69, $b2, $48, $4c, $cf, $5d 1092 | bg_fade_color_mem_tab_3: 1093 | .byte $0f, $0c, $04, $04, $0b, $0b, $06, $00, $00, $06, $0b, $04, $0c, $03, $0d, $01 1094 | 1095 | bg_fade_screen_mem_tab_4: 1096 | .byte $6e, $6e, $6c, $04, $0b, $06, $00, $00, $00, $00, $60, $66, $6b, $bb, $b4, $bc 1097 | bg_fade_color_mem_tab_4: 1098 | .byte $01, $0d, $03, $0c, $04, $0b, $06, $00, $00, $06, $0b, $0b, $04, $04, $0c, $0f 1099 | 1100 | .align $100 1101 | .pc = * "scroller tables" 1102 | scroller_y_offset_tab: 1103 | .for (var i = 0; i < 256; i++) { 1104 | .byte round((sin(toRadians(i / 256 * 360)) * 0.5 + 0.5) * 15) 1105 | } 1106 | scroller_y_offset_tab_2: 1107 | .for (var i = 0; i < 256; i++) { 1108 | .byte round((sin(toRadians(i / 256 * 360)) * 0.5 + 0.5) * 128) 1109 | } 1110 | 1111 | mirrored_scroller_y_tab: 1112 | .for (var i = 0; i < 256; i++) { 1113 | .byte round(abs(sin(toRadians(i / 256 * 360 * 6))) * 4) 1114 | } 1115 | 1116 | squishy_scroller_y_tab: 1117 | .for (var i = 0; i < 256; i++) { 1118 | .byte round((sin(toRadians((i / 256 - 0.3) * 360 * 2)) * 0.5 + 0.5) * 26 - 12) 1119 | } 1120 | 1121 | .pc = background_bitmap_pos "background bitmap" 1122 | background_bitmap: 1123 | .import binary "build/background_bitmap.bin" 1124 | 1125 | .pc = scroller_font_pos "scroller font" 1126 | scroller_font: 1127 | .import binary "build/font.bin" 1128 | 1129 | .pc = sprite_pos "sprites" 1130 | sprites: 1131 | .import binary "build/sprites_blob.bin" 1132 | 1133 | .pc = scroller_text_pos "scroller text" 1134 | scroller_text: 1135 | // Delay scroll intro a bit by adding some spaces at the beginning 1136 | .text " " 1137 | .text "Hello Datastorm, WHAT IS UP?? " 1138 | .text "Ferris on the keys here for this small demo by Pegboard Nerds and Logicoma. " 1139 | .text "Credits: " 1140 | .text "- Music: Flipside (Witchcraft/Pendulum cover) " 1141 | .text "- Graphics: Flipside " 1142 | .text "- Code: Ferris " 1143 | .text "- " 1144 | .text "This thing started with Flipside doing a sick SID cover of this fab tune as well as a 'graphic cover' of the original album artwork! " 1145 | .text "He then approached me asking if we could make a 'cool oldschool thing with crazy color scroller' and seeing as I've been itching to do some more cool hw tricks (I'm still kinda " 1146 | .text "new to c64 after all) it was pretty easy to take the bait! " 1147 | .text "In quite short time we got the gfx converted and added some scroller/fade tech porn, and the timing just so happened to coincide nicely with Datastorm, so here we are! " 1148 | .text "This was pretty fun to put together, and I doubt this will be our last collab together.. :) " 1149 | .text " " 1150 | .text "What you're looking at is a beauty of a 4-color multicolor pic with a sprite layer for the stars, as well as a FPP scroller display. Perhaps nothing particularly innovative, " 1151 | .text "but a nice homage to the oldschool style and really fun to make :) . " 1152 | .text "The track was composed with SID-Wizard and runs in double speed for extra PUNCH! " 1153 | .text "All of this is packed with my own custom packer called admiral p4kbar. " 1154 | .text " " 1155 | .text "Well that's about it for this little prod. Thanks and greets to everyone at Datastorm for a lovely party; this won't be the last you hear from us! :) " 1156 | // 40 chars of spaces at the end to make sure the screen goes blank before looping 1157 | .text " " 1158 | scroller_text_end: 1159 | -------------------------------------------------------------------------------- /final/pbn-lgc-witchcraft.prg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/final/pbn-lgc-witchcraft.prg -------------------------------------------------------------------------------- /final/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/final/screenshot.png -------------------------------------------------------------------------------- /gfxrig/Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "gfxrig" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "image 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", 6 | ] 7 | 8 | [[package]] 9 | name = "adler32" 10 | version = "1.0.2" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [[package]] 14 | name = "bitflags" 15 | version = "0.7.0" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | 18 | [[package]] 19 | name = "bitflags" 20 | version = "0.9.1" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | 23 | [[package]] 24 | name = "byteorder" 25 | version = "1.1.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | 28 | [[package]] 29 | name = "coco" 30 | version = "0.1.1" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | dependencies = [ 33 | "either 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 34 | "scopeguard 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 35 | ] 36 | 37 | [[package]] 38 | name = "color_quant" 39 | version = "1.0.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | 42 | [[package]] 43 | name = "conv" 44 | version = "0.3.3" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | dependencies = [ 47 | "custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 48 | ] 49 | 50 | [[package]] 51 | name = "custom_derive" 52 | version = "0.1.7" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | 55 | [[package]] 56 | name = "deflate" 57 | version = "0.7.16" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | dependencies = [ 60 | "adler32 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 61 | "byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 62 | ] 63 | 64 | [[package]] 65 | name = "either" 66 | version = "1.1.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | 69 | [[package]] 70 | name = "enum_primitive" 71 | version = "0.1.1" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | dependencies = [ 74 | "num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 75 | ] 76 | 77 | [[package]] 78 | name = "futures" 79 | version = "0.1.15" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | 82 | [[package]] 83 | name = "gif" 84 | version = "0.9.2" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | dependencies = [ 87 | "color_quant 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", 89 | ] 90 | 91 | [[package]] 92 | name = "image" 93 | version = "0.15.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | dependencies = [ 96 | "byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 97 | "enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 98 | "gif 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 99 | "jpeg-decoder 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", 100 | "num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "num-rational 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", 102 | "num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 103 | "png 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", 104 | "scoped_threadpool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 105 | ] 106 | 107 | [[package]] 108 | name = "inflate" 109 | version = "0.2.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | 112 | [[package]] 113 | name = "jpeg-decoder" 114 | version = "0.1.13" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | dependencies = [ 117 | "byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 118 | "rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", 119 | ] 120 | 121 | [[package]] 122 | name = "lazy_static" 123 | version = "0.2.8" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | 126 | [[package]] 127 | name = "libc" 128 | version = "0.2.29" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | 131 | [[package]] 132 | name = "lzw" 133 | version = "0.10.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | 136 | [[package]] 137 | name = "magenta" 138 | version = "0.1.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | dependencies = [ 141 | "conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 142 | "magenta-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 143 | ] 144 | 145 | [[package]] 146 | name = "magenta-sys" 147 | version = "0.1.1" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | dependencies = [ 150 | "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 151 | ] 152 | 153 | [[package]] 154 | name = "num-integer" 155 | version = "0.1.35" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | dependencies = [ 158 | "num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 159 | ] 160 | 161 | [[package]] 162 | name = "num-iter" 163 | version = "0.1.34" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | dependencies = [ 166 | "num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 167 | "num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 168 | ] 169 | 170 | [[package]] 171 | name = "num-rational" 172 | version = "0.1.39" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | dependencies = [ 175 | "num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 176 | "num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 177 | ] 178 | 179 | [[package]] 180 | name = "num-traits" 181 | version = "0.1.40" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | 184 | [[package]] 185 | name = "num_cpus" 186 | version = "1.6.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | dependencies = [ 189 | "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", 190 | ] 191 | 192 | [[package]] 193 | name = "png" 194 | version = "0.9.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | dependencies = [ 197 | "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 198 | "deflate 0.7.16 (registry+https://github.com/rust-lang/crates.io-index)", 199 | "inflate 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 200 | "num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", 201 | ] 202 | 203 | [[package]] 204 | name = "rand" 205 | version = "0.3.16" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | dependencies = [ 208 | "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", 209 | "magenta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 210 | ] 211 | 212 | [[package]] 213 | name = "rayon" 214 | version = "0.8.2" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | dependencies = [ 217 | "rayon-core 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 218 | ] 219 | 220 | [[package]] 221 | name = "rayon-core" 222 | version = "1.2.1" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | dependencies = [ 225 | "coco 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 226 | "futures 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", 227 | "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 228 | "libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", 229 | "num_cpus 1.6.2 (registry+https://github.com/rust-lang/crates.io-index)", 230 | "rand 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", 231 | ] 232 | 233 | [[package]] 234 | name = "scoped_threadpool" 235 | version = "0.1.7" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | 238 | [[package]] 239 | name = "scopeguard" 240 | version = "0.3.2" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | 243 | [metadata] 244 | "checksum adler32 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6cbd0b9af8587c72beadc9f72d35b9fbb070982c9e6203e46e93f10df25f8f45" 245 | "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" 246 | "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" 247 | "checksum byteorder 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff81738b726f5d099632ceaffe7fb65b90212e8dce59d518729e7e8634032d3d" 248 | "checksum coco 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c06169f5beb7e31c7c67ebf5540b8b472d23e3eade3b2ec7d1f5b504a85f91bd" 249 | "checksum color_quant 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a475fc4af42d83d28adf72968d9bcfaf035a1a9381642d8e85d8a04957767b0d" 250 | "checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" 251 | "checksum custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" 252 | "checksum deflate 0.7.16 (registry+https://github.com/rust-lang/crates.io-index)" = "c4b2a7e3365fa1e8afd32147b543adaa3390f0115e8af5884abc2f854052792b" 253 | "checksum either 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18785c1ba806c258137c937e44ada9ee7e69a37e3c72077542cd2f069d78562a" 254 | "checksum enum_primitive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" 255 | "checksum futures 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a82bdc62350ca9d7974c760e9665102fc9d740992a528c2254aa930e53b783c4" 256 | "checksum gif 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e2e41945ba23db3bf51b24756d73d81acb4f28d85c3dccc32c6fae904438c25f" 257 | "checksum image 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "634700d4a51fa91ceaa798001d46bf862c7b712bd691085d7ba6afd5521e21f7" 258 | "checksum inflate 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d1238524675af3938a7c74980899535854b88ba07907bb1c944abe5b8fc437e5" 259 | "checksum jpeg-decoder 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2805ccb10ffe4d10e06ef68a158ff94c255211ecbae848fbde2146b098f93ce7" 260 | "checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf" 261 | "checksum libc 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)" = "8a014d9226c2cc402676fbe9ea2e15dd5222cd1dd57f576b5b283178c944a264" 262 | "checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" 263 | "checksum magenta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf0336886480e671965f794bc9b6fce88503563013d1bfb7a502c81fe3ac527" 264 | "checksum magenta-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40d014c7011ac470ae28e2f76a02bfea4a8480f73e701353b49ad7a8d75f4699" 265 | "checksum num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "d1452e8b06e448a07f0e6ebb0bb1d92b8890eea63288c0b627331d53514d0fba" 266 | "checksum num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)" = "7485fcc84f85b4ecd0ea527b14189281cf27d60e583ae65ebc9c088b13dffe01" 267 | "checksum num-rational 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "288629c76fac4b33556f4b7ab57ba21ae202da65ba8b77466e6d598e31990790" 268 | "checksum num-traits 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "99843c856d68d8b4313b03a17e33c4bb42ae8f6610ea81b28abe076ac721b9b0" 269 | "checksum num_cpus 1.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aec53c34f2d0247c5ca5d32cca1478762f301740468ee9ee6dcb7a0dd7a0c584" 270 | "checksum png 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f256476eee4447f55909d52d22a16cfa6e5e55e5cb77fa182c7fcc8c4456ee3c" 271 | "checksum rand 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "eb250fd207a4729c976794d03db689c9be1d634ab5a1c9da9492a13d8fecbcdf" 272 | "checksum rayon 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b614fe08b6665cb9a231d07ac1364b0ef3cb3698f1239ee0c4c3a88a524f54c8" 273 | "checksum rayon-core 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7febc28567082c345f10cddc3612c6ea020fc3297a1977d472cf9fdb73e6e493" 274 | "checksum scoped_threadpool 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3ef399c8893e8cb7aa9696e895427fab3a6bf265977bb96e126f24ddd2cda85a" 275 | "checksum scopeguard 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c79eb2c3ac4bc2507cda80e7f3ac5b88bd8eae4c0914d5663e6a8933994be918" 276 | -------------------------------------------------------------------------------- /gfxrig/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gfxrig" 3 | version = "0.1.0" 4 | authors = ["ferris "] 5 | 6 | [dependencies] 7 | image = "*" -------------------------------------------------------------------------------- /gfxrig/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate image; 2 | 3 | use image::{GenericImage, Pixel}; 4 | 5 | use std::env; 6 | use std::fs::File; 7 | use std::io::Write; 8 | 9 | fn main() { 10 | let background_input_file_name = env::args().skip(1).nth(0).unwrap(); 11 | let background_output_file_name = env::args().skip(1).nth(1).unwrap(); 12 | 13 | let font_input_file_name = env::args().skip(1).nth(2).unwrap(); 14 | let font_output_file_name = env::args().skip(1).nth(3).unwrap(); 15 | 16 | let sprites_input_file_name_prefix = env::args().skip(1).nth(4).unwrap(); 17 | let sprites_output_file_name = env::args().skip(1).nth(5).unwrap(); 18 | 19 | const WIDTH: usize = 160; 20 | const HEIGHT: usize = 200; 21 | 22 | const CHAR_WIDTH: usize = 4; // 4 due to multicolor 23 | const CHAR_HEIGHT: usize = 8; 24 | 25 | const WIDTH_CHARS: usize = WIDTH / CHAR_WIDTH; 26 | const HEIGHT_CHARS: usize = HEIGHT / CHAR_HEIGHT; 27 | 28 | const SPRITE_WIDTH: usize = 12; // 12 due to multicolor 29 | const SPRITE_HEIGHT: usize = 20; 30 | 31 | let palette = [ 32 | 0x000000, 33 | 0xffffff, 34 | 0x883932, 35 | 0x67b6bd, 36 | 0x8b3f96, 37 | 0x55a049, 38 | 0x40318d, 39 | 0xbfce72, 40 | 0x8b5429, 41 | 0x574200, 42 | 0xb86962, 43 | 0x505050, 44 | 0x787878, 45 | 0x94e089, 46 | 0x7869c4, 47 | 0x9f9f9f, 48 | ]; 49 | 50 | let input_color_indices = [ 51 | 0, 11, 15, 1 52 | ]; 53 | 54 | // Convert background image 55 | { 56 | let input = image::open(background_input_file_name).unwrap(); 57 | 58 | let mut output = Vec::new(); 59 | 60 | for char_y in 0..HEIGHT_CHARS { 61 | for char_x in 0..WIDTH_CHARS { 62 | for y in 0..CHAR_HEIGHT { 63 | let mut acc = 0; 64 | 65 | for x in 0..CHAR_WIDTH { 66 | let pixel_x = char_x * CHAR_WIDTH + x; 67 | let pixel_y = char_y * CHAR_HEIGHT + y; 68 | 69 | let pixel = input.get_pixel(pixel_x as _, pixel_y as _).to_rgb(); 70 | let rgb = ((pixel.data[0] as u32) << 16) | ((pixel.data[1] as u32) << 8) | (pixel.data[2] as u32); 71 | let palette_index = palette.iter().position(|x| *x == rgb).unwrap(); 72 | let color_index = input_color_indices.iter().position(|x| *x == palette_index).unwrap(); 73 | 74 | acc <<= 2; 75 | acc |= color_index as u8; 76 | } 77 | 78 | output.push(acc); 79 | } 80 | } 81 | } 82 | 83 | let mut file = File::create(background_output_file_name).unwrap(); 84 | file.write(&output).unwrap(); 85 | } 86 | 87 | // Convert font 88 | { 89 | let input = image::open(font_input_file_name).unwrap(); 90 | 91 | let mut output = Vec::new(); 92 | 93 | for char_y in 0..(input.height() as usize) / CHAR_HEIGHT { 94 | for char_x in 0..(input.width() as usize) / (CHAR_WIDTH * 2) { 95 | for y in 0..CHAR_HEIGHT { 96 | let mut acc = 0; 97 | 98 | for x in 0..(CHAR_WIDTH * 2) { 99 | let pixel_x = char_x * (CHAR_WIDTH * 2) + x; 100 | let pixel_y = char_y * CHAR_HEIGHT + y; 101 | 102 | let pixel = input.get_pixel(pixel_x as _, pixel_y as _).to_rgb(); 103 | 104 | acc <<= 1; 105 | acc |= pixel.data[0] & 0x01; 106 | } 107 | 108 | output.push(acc); 109 | } 110 | } 111 | } 112 | 113 | let mut file = File::create(font_output_file_name).unwrap(); 114 | file.write(&output).unwrap(); 115 | } 116 | 117 | // Convert sprite sheets 118 | { 119 | let mut output = Vec::new(); 120 | 121 | for sheet in 0..8 { 122 | let input_file_name = format!("{}{}.png", sprites_input_file_name_prefix, sheet + 1); 123 | 124 | let input = image::open(input_file_name).unwrap(); 125 | 126 | for frame in 0..8 { 127 | for sprite_y in 0..SPRITE_HEIGHT { 128 | let mut acc = 0; 129 | 130 | for sprite_x in 0..SPRITE_WIDTH { 131 | let pixel_x = frame * SPRITE_WIDTH + sprite_x; 132 | let pixel_y = sprite_y; 133 | 134 | let pixel = input.get_pixel(pixel_x as _, pixel_y as _).to_rgb(); 135 | let rgb = ((pixel.data[0] as u32) << 16) | ((pixel.data[1] as u32) << 8) | (pixel.data[2] as u32); 136 | let palette_index = palette.iter().position(|x| *x == rgb).unwrap(); 137 | let color_index = input_color_indices.iter().position(|x| *x == palette_index).unwrap(); 138 | 139 | acc <<= 2; 140 | acc |= color_index as u32; 141 | } 142 | 143 | output.push((acc >> 16) as u8); 144 | output.push((acc >> 8) as u8); 145 | output.push(acc as u8); 146 | } 147 | 148 | // Output last dummy row (sprites are actually 21 pixels high) 149 | output.push(0x00); 150 | output.push(0x00); 151 | output.push(0x00); 152 | 153 | // Final padding byte so each sprite is 64 bytes exactly 154 | output.push(0x00); 155 | } 156 | } 157 | 158 | let mut file = File::create(sprites_output_file_name).unwrap(); 159 | file.write(&output).unwrap(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /graphics/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/background.png -------------------------------------------------------------------------------- /graphics/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/font.png -------------------------------------------------------------------------------- /graphics/originals/StarSprites_60mspf.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/originals/StarSprites_60mspf.ase -------------------------------------------------------------------------------- /graphics/originals/Withcraft14_cleanup.ase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/originals/Withcraft14_cleanup.ase -------------------------------------------------------------------------------- /graphics/stars1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/stars1.png -------------------------------------------------------------------------------- /graphics/stars2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/stars2.png -------------------------------------------------------------------------------- /graphics/stars3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/stars3.png -------------------------------------------------------------------------------- /graphics/stars4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/stars4.png -------------------------------------------------------------------------------- /graphics/stars5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/stars5.png -------------------------------------------------------------------------------- /graphics/stars6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/stars6.png -------------------------------------------------------------------------------- /graphics/stars7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/stars7.png -------------------------------------------------------------------------------- /graphics/stars8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/graphics/stars8.png -------------------------------------------------------------------------------- /music.prg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicomacorp/witchcraft/0733bce631e844afcfcdc070e83d04984c6c2e2c/music.prg -------------------------------------------------------------------------------- /witchcraft.asm: -------------------------------------------------------------------------------- 1 | #import "common.inc" 2 | #import "basic.inc" 3 | 4 | .const uncompressed_start_addr = demo_entry 5 | 6 | .pc = start_addr "autostart" 7 | autostart: 8 | line_10: 9 | // Call into entry subroutine 10 | StartBasicLine(line_20, 10, basic_token_sys) 11 | .text "2061" 12 | EndBasicLine() 13 | line_20: 14 | EndBasicProgram() 15 | 16 | .const reloc_addr = $9000 17 | 18 | .pc = * "stub entry" 19 | entry: 20 | // Disable interrupts 21 | // Technically we should be enabling them again after the decompression routine, 22 | // but since we know we'll jump back into code that'll disable them anyways, it 23 | // should be fine. 24 | sei 25 | 26 | // Bank out BASIC and kernal ROMs 27 | lda #$35 28 | sta $01 29 | 30 | // Place @ character at end of screen (we'll use this as a progress indicator) 31 | lda #$00 32 | sta $0400 + 999 33 | 34 | // Copy 12kb block starting at the packed data to $9000-$bfff in 48 256-byte chunks 35 | ldy #$00 36 | ldx #$30 37 | reloc_block_loop: 38 | reloc_block_load_instr: 39 | !: lda decomp_start, y 40 | reloc_block_store_instr: 41 | sta reloc_addr, y 42 | iny 43 | bne !- 44 | 45 | inc reloc_block_load_instr + 2 46 | inc reloc_block_store_instr + 2 47 | dex 48 | bne reloc_block_loop 49 | 50 | jmp decomp_entry 51 | 52 | .var uncompressed_data = LoadBinary("build/demo.bin"); 53 | .var uncompressed_demo_end = uncompressed_start_addr + uncompressed_data.getSize() 54 | 55 | // Available zp addrs: 56 | // $02 (unused) 57 | // $92-97 (only used during either rs232 or datasette io) 58 | // $a3-$b1 (only used during either rs232 or datasette io) 59 | // $f7-$fa (only used during rs232 io) 60 | // $fb-$fe (unused) 61 | // $ff (only used during fp -> string conversion) 62 | 63 | .const table_base = $c000 64 | 65 | .const table_offset_low = table_base 66 | .const table_offset_high = table_base + $30 67 | .const table_additional_bits = table_base + $60 68 | 69 | .const length_table_offset_low = table_offset_low 70 | .const length_table_offset_high = table_offset_high 71 | .const length_table_additional_bits = table_additional_bits 72 | 73 | .const distance_table_offset_low = table_offset_low + $10 74 | .const distance_table_offset_high = table_offset_high + $10 75 | .const distance_table_additional_bits = table_additional_bits + $10 76 | 77 | .const table_offset_low_temp = $92 78 | .const table_offset_high_temp = $93 79 | .const table_offset_add_low_temp = $94 80 | .const table_offset_add_high_temp = $95 81 | 82 | .const bit_buffer = $fb 83 | .const bit_index = $fc 84 | 85 | .const backref_length_low = $92 86 | .const backref_length_high = $93 87 | 88 | // These addr's have to be consecutive 89 | .const backref_ptr_low = $fd 90 | .const backref_ptr_high = $fe 91 | 92 | .const read_bit_temp = $02 93 | 94 | .const read_n_bit_value_low = $a3 95 | .const read_n_bit_value_high = $a4 96 | 97 | .pc = * "decompressor and packed demo" 98 | decomp_start: 99 | .pseudopc reloc_addr { 100 | decomp_entry: 101 | // Expects y to be 0 on entry 102 | sty bit_buffer 103 | sty bit_index 104 | 105 | // Decode table entries 106 | decomp_decode_table: 107 | // Reset current offset add amount 108 | ldx #$00 109 | stx table_offset_add_high_temp 110 | inx 111 | stx table_offset_add_low_temp 112 | 113 | // if (i & 0x0f) == 0 then reset current offset to 1 114 | tya 115 | and #$0f 116 | bne !+ 117 | sta table_offset_high_temp 118 | txa 119 | sta table_offset_low_temp 120 | 121 | // Read additional bit count 122 | !: lda #$04 123 | jsr decomp_read_n_bit_value 124 | 125 | // Store additional bit count into table 126 | sta table_additional_bits, y 127 | 128 | // Shift current offset add amount (should be 1) by number of additional bits 129 | tax 130 | beq decomp_decode_table_store_and_add 131 | !: asl table_offset_add_low_temp 132 | rol table_offset_add_high_temp 133 | dex 134 | bne !- 135 | 136 | // Store current offset into tables, and add shifted amount 137 | decomp_decode_table_store_and_add: 138 | lda table_offset_low_temp 139 | sta table_offset_low, y 140 | clc 141 | adc table_offset_add_low_temp 142 | sta table_offset_low_temp 143 | lda table_offset_high_temp 144 | sta table_offset_high, y 145 | adc table_offset_add_high_temp 146 | sta table_offset_high_temp 147 | iny 148 | cpy #$30 149 | bne decomp_decode_table 150 | 151 | // Reset y, as the rest of the decompressor assumes its value is always 0 152 | ldy #$00 153 | 154 | decomp_packet: 155 | // Read packet bit 156 | jsr decomp_read_bit 157 | 158 | beq decomp_literal 159 | // Backreference 160 | 161 | // Read length 162 | // Read length index 163 | jsr decomp_read_unary 164 | // Read additional bits 165 | pha 166 | lda length_table_additional_bits, x 167 | jsr decomp_read_n_bit_value 168 | // Add offset to additional bits, and store in backref_length 169 | pla 170 | tax 171 | lda length_table_offset_low, x 172 | clc 173 | adc read_n_bit_value_low 174 | sta backref_length_low 175 | lda length_table_offset_high, x 176 | adc read_n_bit_value_high 177 | sta backref_length_high 178 | 179 | // Read distance 180 | // Read distance index 181 | jsr decomp_read_unary 182 | // if length == 1 add 16 to distance index 183 | cpy backref_length_high 184 | bne !+ 185 | ldx backref_length_low 186 | dex 187 | bne !+ 188 | clc 189 | adc #$10 190 | // Subtract table offset from current output pointer and store into backref_ptr 191 | !: tax 192 | lda decomp_write_instr + 1 193 | sec 194 | sbc distance_table_offset_low, x 195 | sta backref_ptr_low 196 | lda decomp_write_instr + 2 197 | sbc distance_table_offset_high, x 198 | sta backref_ptr_high 199 | 200 | // Read additional bits 201 | lda distance_table_additional_bits, x 202 | jsr decomp_read_n_bit_value 203 | // Subtract additional bits from backref_ptr 204 | lda backref_ptr_low 205 | sec 206 | sbc read_n_bit_value_low 207 | sta backref_ptr_low 208 | lda backref_ptr_high 209 | sbc read_n_bit_value_high 210 | sta backref_ptr_high 211 | 212 | // Copy backref_length bytes from backref_ptr to output 213 | ldx backref_length_low 214 | decomp_copy_bytes: 215 | // Copy and output byte 216 | lda (backref_ptr_low), y 217 | jsr decomp_write_byte 218 | 219 | // Increment backref_ptr 220 | inc backref_ptr_low 221 | bne !+ 222 | inc backref_ptr_high 223 | 224 | // Decrement backref_length 225 | // Unfortunately, dex doesn't set carry, so we have to detect it ourselves manually 226 | !: cpx #$00 227 | bne !+ 228 | dec backref_length_high 229 | !: dex 230 | 231 | // Check backref_length for 0. If we're not 0, copy some more bytes 232 | bne decomp_copy_bytes 233 | cpy backref_length_high 234 | bne decomp_copy_bytes 235 | beq decomp_next // Logically this should be jmp, but beq is equivalent here (due to cpy above) and 1 byte smaller 236 | 237 | decomp_literal: 238 | // Literal 239 | // Read byte 240 | tya 241 | ldx #$08 242 | !: asl 243 | sta read_bit_temp 244 | jsr decomp_read_bit 245 | ora read_bit_temp 246 | dex 247 | bne !- 248 | 249 | jsr decomp_write_byte 250 | 251 | decomp_next: 252 | lda decomp_write_instr + 1 253 | cmp #uncompressed_demo_end 257 | bne !+ 258 | jmp demo_entry 259 | 260 | !: jmp decomp_packet 261 | 262 | // Ensures n and z flags are set along with returning the bit in a 263 | decomp_read_bit: 264 | lda bit_index 265 | and #$07 266 | bne !+ 267 | decomp_read_instr: 268 | lda packed_demo_start 269 | sta bit_buffer 270 | 271 | inc decomp_read_instr + 1 272 | bne !+ 273 | inc decomp_read_instr + 2 274 | !: lda bit_buffer 275 | lsr bit_buffer 276 | inc bit_index 277 | and #$01 278 | rts 279 | 280 | // Reads unary value into a and x 281 | decomp_read_unary: 282 | ldx #$00 283 | !: jsr decomp_read_bit 284 | bne !+ 285 | inx 286 | bne !- // Logically this should be jmp, but bne is equivalent here (due to inx before) and 1 byte smaller 287 | !: txa 288 | rts 289 | 290 | // Expects number of bits to read in a 291 | // Reads into read_n_bit_value_low/high, and leaves read_n_bit_value_low in a 292 | // Does not _expect_ y to be zero since it's also called when decoding the table, but doesn't touch y either 293 | decomp_read_n_bit_value: 294 | ldx #$00 295 | stx read_n_bit_value_low 296 | stx read_n_bit_value_high 297 | tax 298 | beq decomp_read_n_bit_value_done 299 | !: asl read_n_bit_value_low 300 | rol read_n_bit_value_high 301 | jsr decomp_read_bit 302 | ora read_n_bit_value_low 303 | sta read_n_bit_value_low 304 | dex 305 | bne !- 306 | decomp_read_n_bit_value_done: 307 | rts 308 | 309 | decomp_write_byte: 310 | decomp_write_instr: 311 | sta uncompressed_start_addr 312 | 313 | inc decomp_write_instr + 1 314 | bne !+ 315 | inc decomp_write_instr + 2 316 | // Progress indicator 317 | inc $d800 + 999 318 | !: rts 319 | 320 | packed_demo_start: 321 | .import binary "build/packed-demo.bin" 322 | packed_demo_end: 323 | } 324 | 325 | .const target_size = $3000 326 | .const max_size = $3000 327 | 328 | .var total_size = * - start_addr + 2 // + 2 to include the program load address 329 | .if(total_size > max_size) { 330 | .error "Total size (" + total_size + " bytes) is more than the max size (" + max_size + " bytes)!" 331 | } 332 | .print "Total size: " + total_size + " bytes" 333 | .if(total_size > target_size) { 334 | .print "WARNING: Total size greater than target size (" + target_size + " bytes) by " + (total_size - target_size) + " bytes!" 335 | } --------------------------------------------------------------------------------