diff --git a/Cargo.lock b/Cargo.lock index 4cde1876a8..4dce7db1a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -96,6 +105,15 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -162,11 +180,26 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.2", "shlex 1.3.0", "syn 2.0.119", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -178,6 +211,9 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] [[package]] name = "blake3" @@ -193,6 +229,12 @@ dependencies = [ "cpufeatures 0.3.0", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -309,6 +351,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "cipher" version = "0.5.2" @@ -385,6 +433,22 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width 0.1.14", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" + [[package]] name = "color_quant" version = "1.1.0" @@ -425,6 +489,33 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "core_maths" version = "0.1.1" @@ -587,7 +678,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror", + "thiserror 2.0.19", ] [[package]] @@ -631,6 +722,15 @@ dependencies = [ "ctutils", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -785,6 +885,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "font-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7" +dependencies = [ + "bytemuck", +] + [[package]] name = "font-types" version = "0.11.3" @@ -817,12 +932,50 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.1", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-task" version = "0.3.32" @@ -895,12 +1048,105 @@ dependencies = [ "weezl", ] +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.11.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.11.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + [[package]] name = "half" version = "2.7.1" @@ -912,6 +1158,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -924,6 +1179,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hmac" version = "0.13.0" @@ -952,6 +1213,7 @@ dependencies = [ "byteorder-lite", "color_quant", "gif 0.14.1", + "image-webp", "moxcms", "num-traits", "png 0.18.1", @@ -989,7 +1251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] @@ -1059,6 +1321,34 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + [[package]] name = "js-sys" version = "0.3.102" @@ -1070,6 +1360,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "kurbo" version = "0.11.3" @@ -1123,21 +1430,51 @@ dependencies = [ "bitflags 2.11.0", "libc", "plain", - "redox_syscall", + "redox_syscall 0.7.5", ] +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.8.3" @@ -1162,6 +1499,21 @@ dependencies = [ "libc", ] +[[package]] +name = "metal" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + [[package]] name = "minicov" version = "0.3.8" @@ -1198,6 +1550,37 @@ dependencies = [ "pxfm", ] +[[package]] +name = "naga" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.0", + "cfg_aliases", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "strum 0.26.3", + "termcolor", + "thiserror 2.0.19", + "unicode-xid", +] + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "nom" version = "7.1.3" @@ -1227,6 +1610,15 @@ dependencies = [ "libm", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1246,18 +1638,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] -name = "paste" -version = "1.0.15" +name = "ordered-float" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] [[package]] -name = "pbkdf2" -version = "0.13.0" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "digest 0.11.3", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", "hmac", ] @@ -1282,6 +1706,18 @@ dependencies = [ "ryu", ] +[[package]] +name = "peniko" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b44f9ddd2f480176b34278eb653ec1c8062f3b143a4e16eeff5ffac3334e288" +dependencies = [ + "color", + "kurbo 0.11.3", + "linebender_resource_handle", + "smallvec", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -1342,6 +1778,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "portable-atomic" version = "1.14.0" @@ -1357,6 +1799,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "prettyplease" version = "0.2.37" @@ -1376,6 +1824,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + [[package]] name = "pxfm" version = "0.1.28" @@ -1421,6 +1875,18 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "rayon" version = "1.12.0" @@ -1441,6 +1907,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ea612a55c08586a1d15134be8a776186c440c312ebda3b9e8efbfe4255b7f4" +dependencies = [ + "bytemuck", + "font-types 0.9.0", +] + [[package]] name = "read-fonts" version = "0.39.2" @@ -1448,7 +1924,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" dependencies = [ "bytemuck", - "font-types", + "font-types 0.11.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -1489,6 +1974,12 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "resvg" version = "0.45.1" @@ -1559,7 +2050,9 @@ dependencies = [ "pbkdf2", "pcx", "pdf-writer", + "pollster", "quick-xml", + "resvg 0.45.1", "resvg 0.47.0", "roxmltree 0.21.1", "serde", @@ -1568,7 +2061,7 @@ dependencies = [ "sha2 0.11.0", "skia-safe", "snafu", - "strum", + "strum 0.28.0", "subsecond", "subsetter", "svg2pdf", @@ -1576,8 +2069,10 @@ dependencies = [ "ttf-parser", "unicode-properties", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.2", "usvg 0.45.1", + "vello", + "vello_svg", "wasm-bindgen", "wasm-bindgen-test", "web-sys", @@ -1613,6 +2108,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -1680,6 +2181,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.28" @@ -1840,6 +2347,16 @@ dependencies = [ "skia-bindings", ] +[[package]] +name = "skrifa" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576e60c7de4bb6a803a0312f9bef17e78cf1e8d25a80e1ade76770d7a0237955" +dependencies = [ + "bytemuck", + "read-fonts 0.33.1", +] + [[package]] name = "skrifa" version = "0.42.1" @@ -1847,7 +2364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.39.2", ] [[package]] @@ -1892,6 +2409,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "spki" version = "0.7.3" @@ -1902,6 +2428,12 @@ dependencies = [ "der", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strict-num" version = "0.1.1" @@ -1917,13 +2449,35 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ - "strum_macros", + "strum_macros 0.28.0", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", ] [[package]] @@ -1951,7 +2505,7 @@ dependencies = [ "memmap2", "serde", "subsecond-types", - "thiserror", + "thiserror 2.0.19", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1973,8 +2527,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38803281d1c23166c5ebcb455439a5d2afe711cc909cf88af72448c297756ad6" dependencies = [ "kurbo 0.13.0", - "rustc-hash", - "skrifa", + "rustc-hash 2.1.2", + "skrifa 0.42.1", "write-fonts", ] @@ -2003,6 +2557,12 @@ dependencies = [ "usvg 0.45.1", ] +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + [[package]] name = "svgtypes" version = "0.15.3" @@ -2056,13 +2616,42 @@ dependencies = [ "xattr", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -2265,12 +2854,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "usvg" version = "0.45.1" @@ -2342,6 +2943,62 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vello" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3f8a53870a2ee699ce05b738a3f9974c92c35ed4874de86052ac68d214811c" +dependencies = [ + "bytemuck", + "futures-intrusive", + "log", + "peniko", + "png 0.17.16", + "skrifa 0.35.0", + "static_assertions", + "thiserror 2.0.19", + "vello_encoding", + "vello_shaders", + "wgpu", +] + +[[package]] +name = "vello_encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c69b0fe94b0ac7e47619c504ee2c377355174f5c46353c46d03fa5f7e435922b" +dependencies = [ + "bytemuck", + "guillotiere", + "peniko", + "skrifa 0.35.0", + "smallvec", +] + +[[package]] +name = "vello_shaders" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ebea426bb2f95b7610bca09178b03d809ede1d3c500a9acf6eca43e8f200be" +dependencies = [ + "bytemuck", + "naga", + "thiserror 2.0.19", + "vello_encoding", +] + +[[package]] +name = "vello_svg" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffa961ac8d1a194997faf051f34cdc590f761a70dc4543f54b293411f766fd1e" +dependencies = [ + "image", + "thiserror 2.0.19", + "usvg 0.45.1", + "vello", +] + [[package]] name = "version_check" version = "0.9.5" @@ -2484,6 +3141,115 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wgpu" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +dependencies = [ + "arrayvec", + "bitflags 2.11.0", + "cfg_aliases", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.11.0", + "cfg_aliases", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.19", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "24.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.11.0", + "block", + "bytemuck", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.19", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows", + "windows-core", +] + +[[package]] +name = "wgpu-types" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +dependencies = [ + "bitflags 2.11.0", + "js-sys", + "log", + "web-sys", +] + [[package]] name = "which" version = "8.0.5" @@ -2502,12 +3268,76 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2517,6 +3347,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "1.0.2" @@ -2529,11 +3423,11 @@ version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb731d4c4d93eacc69a1ad2f270f905788a98e4a3438267bcafbe08d3431c8d8" dependencies = [ - "font-types", + "font-types 0.11.3", "indexmap", "kurbo 0.13.0", "log", - "read-fonts", + "read-fonts 0.39.2", ] [[package]] @@ -2546,6 +3440,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + [[package]] name = "xmlwriter" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f52b86e9eb..ff2780f093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,11 @@ subsecond = { version = "=0.7.10", optional = true } default = ["console_error_panic_hook"] native-skia = ["dep:resvg", "dep:skia-safe"] subsecond-dev = ["dep:subsecond"] +# [gym_gpu_raster] GPU 가속 SVG 래스터화 경로 (네이티브 전용). native-skia 와 같은 방식으로 +# 선택 게이팅한다 — CI 는 GPU 없이도 컴파일되고, 실제 GPU 실행은 로컬에서만 일어난다. +# vello(+vello_svg)로 SVG 를 GPU 래스터화하고, resvg(CPU)를 같은 usvg 트리로 돌려 정직한 +# 벤치마크 기준선으로 삼는다. +gpu = ["dep:vello", "dep:vello_svg", "dep:pollster", "dep:resvg-gpu"] # PDF 내보내기 (네이티브 전용, Task #21) [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -84,6 +89,17 @@ pdf-writer = "0.12" subsetter = "0.2" skia-safe = { version = "0.99.0", optional = true, default-features = false, features = ["binary-cache", "embed-icudtl", "pdf", "textlayout"] } +# [gym_gpu_raster] GPU 래스터화 스택 (feature = "gpu", 네이티브 전용). +# vello: Linebender 의 GPU 2D 렌더러(컴퓨트 파이프라인). vello_svg: usvg 트리 → vello Scene. +# pollster: 헤드리스라 이벤트 루프가 없으므로 wgpu 의 async(adapter/device/버퍼매핑)를 블로킹. +# resvg-gpu: CPU 기준선. vello_svg 0.7 과 usvg 0.45 를 공유하도록 0.45 로 핀했다(같은 트리를 +# 두 래스터라이저에 동일 입력으로 넣어 벤치마크·픽셀비교를 성립). native-skia 의 resvg 0.47 과 +# 별개 버전으로 공존한다. +vello = { version = "0.5", optional = true } +vello_svg = { version = "0.7", optional = true } +pollster = { version = "0.4", optional = true } +resvg-gpu = { package = "resvg", version = "0.45", optional = true } + [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = [ "CanvasRenderingContext2d", diff --git "a/mydocs/manual/agent_codex/10_\354\241\260\355\232\214.md" "b/mydocs/manual/agent_codex/10_\354\241\260\355\232\214.md" index e6af2539a2..c9ca60ba5b 100644 --- "a/mydocs/manual/agent_codex/10_\354\241\260\355\232\214.md" +++ "b/mydocs/manual/agent_codex/10_\354\241\260\355\232\214.md" @@ -2,7 +2,7 @@ kind: guide status: active canonical: mydocs/manual/agent_codex/10_조회.md -last_verified: 2026-08-11 +last_verified: 2026-08-15 generated: tools/gen_agent_codex.py — 수기 수정 금지, 재생성으로 갱신 --- @@ -31,7 +31,7 @@ rhwp info samples/basic/issue2007_nested_cell_pagination_42065.hwp --json "fonts": [ "굴림", "굴림체", - "… (12개 중 2개 표시)" + "… (82개 중 2개 표시)" ], "format": "hwp5", "pageCount": 17, diff --git "a/mydocs/manual/agent_codex/40_\353\263\200\355\231\230\352\263\274_\353\240\214\353\215\224.md" "b/mydocs/manual/agent_codex/40_\353\263\200\355\231\230\352\263\274_\353\240\214\353\215\224.md" index e957c9811b..30a2432fb7 100644 --- "a/mydocs/manual/agent_codex/40_\353\263\200\355\231\230\352\263\274_\353\240\214\353\215\224.md" +++ "b/mydocs/manual/agent_codex/40_\353\263\200\355\231\230\352\263\274_\353\240\214\353\215\224.md" @@ -2,7 +2,7 @@ kind: guide status: active canonical: mydocs/manual/agent_codex/40_변환과_렌더.md -last_verified: 2026-08-11 +last_verified: 2026-08-15 generated: tools/gen_agent_codex.py — 수기 수정 금지, 재생성으로 갱신 --- @@ -112,9 +112,9 @@ rhwp export-svg samples/field-01.hwp -o {tmp}/svg ```json (비 JSON 출력 — 앞 160자) 문서 로드 완료: samples/field-01.hwp (3페이지) - → /svg/field-01_001.svg - → /svg/field-01_002.svg - → /svg/field-01_003.svg + → /svg\field-01_001.svg + → /svg\field-01_002.svg + → /svg\field-01_003.svg 내보내기 완료: 3개 SVG 파일 → /sv ``` @@ -147,3 +147,17 @@ rhwp export-svg samples/field-01.hwp -o {tmp}/svg - **출처 표지**: 문서 파생 필드 없음 (엔진·에코 값뿐) > **계약만** — 입력 합성 비용 또는 산출 부피 때문에 표본 실행을 싣지 않는다 — 계약(플래그·봉투 필드·출처)은 아래가 전부이며 자기서술에서 생성됐다. + +### `export-png-gpu` — SVG를 GPU(vello/wgpu)로 래스터화해 페이지별 PNG로 렌더 (--benchmark 로 CPU 대비 실측) + +- 종류: `export` · exit 규약: 0 성공 / 1 IO / 2 사용법 +- 사용법: `export-png-gpu <파일.hwp|파일.hwpx> [옵션] (gpu feature 필요)` + +> **계약만** — 입력 합성 비용 또는 산출 부피 때문에 표본 실행을 싣지 않는다 — 계약(플래그·봉투 필드·출처)은 아래가 전부이며 자기서술에서 생성됐다. + +### `gpu-info` — 사용 가능한 GPU 어댑터 열거 (export-png-gpu 가 쓸 백엔드 확인) + +- 종류: `export` · exit 규약: 0 성공 / 1 IO / 2 사용법 +- 사용법: `gpu-info (gpu feature 필요)` + +> **계약만** — 입력 합성 비용 또는 산출 부피 때문에 표본 실행을 싣지 않는다 — 계약(플래그·봉투 필드·출처)은 아래가 전부이며 자기서술에서 생성됐다. diff --git "a/mydocs/manual/agent_codex/70_\354\236\220\352\270\260\354\204\234\354\210\240.md" "b/mydocs/manual/agent_codex/70_\354\236\220\352\270\260\354\204\234\354\210\240.md" index 2d1063a6aa..2d41e97e46 100644 --- "a/mydocs/manual/agent_codex/70_\354\236\220\352\270\260\354\204\234\354\210\240.md" +++ "b/mydocs/manual/agent_codex/70_\354\236\220\352\270\260\354\204\234\354\210\240.md" @@ -2,7 +2,7 @@ kind: guide status: active canonical: mydocs/manual/agent_codex/70_자기서술.md -last_verified: 2026-08-12 +last_verified: 2026-08-15 generated: tools/gen_agent_codex.py — 수기 수정 금지, 재생성으로 갱신 --- @@ -931,7 +931,7 @@ rhwp export-agent-manifest --json ], "summary": "페이지별 텍스트 추출 (TXT 파일 또는 --json stdout)" }, - "… (85개 중 2개 표시)" + "… (87개 중 2개 표시)" ], "exitCodes": { "0": "성공", diff --git a/src/main.rs b/src/main.rs index 385afcb84d..15f4bb1e35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -303,6 +303,10 @@ fn main() { Some("export-render-tree") => exit_with(export_render_tree(&args[2..])), Some("export-structure") => exit_with(export_structure(&args[2..])), Some("export-png") => exit_with(export_png(&args[2..])), + // [gym_gpu_raster] GPU 가속 PNG 래스터화 (feature = "gpu"). export-png(native-skia)과 + // 같은 방식으로 feature 게이팅 — 미빌드 바이너리는 사용법 오류(exit 2)로 안내한다. + Some("export-png-gpu") => exit_with(export_png_gpu(&args[2..])), + Some("gpu-info") => exit_with(gpu_info(&args[2..])), Some("export-pdf") => exit_with(export_pdf(&args[2..])), Some("export-text") => exit_with(export_text(&args[2..])), Some("export-markdown") => exit_with(export_markdown(&args[2..])), @@ -2750,6 +2754,20 @@ fn capabilities_command_entries() -> Vec { "native-skia", cfg!(feature = "native-skia"), ), + cmd_gated( + "export-png-gpu", + "export", + "SVG를 GPU(vello/wgpu)로 래스터화해 페이지별 PNG로 렌더 (--benchmark 로 CPU 대비 실측)", + "gpu", + cfg!(feature = "gpu"), + ), + cmd_gated( + "gpu-info", + "export", + "사용 가능한 GPU 어댑터 열거 (export-png-gpu 가 쓸 백엔드 확인)", + "gpu", + cfg!(feature = "gpu"), + ), cmd_json( "export-pdf", "export", @@ -3805,6 +3823,23 @@ fn print_help() { println!(" qwen-vl: 2240 px (Qwen-VL, 별칭: qwen)"); println!(" llava: 672 px (LLaVA / OSS CLIP)"); println!(); + println!(" export-png-gpu <파일.hwp|파일.hwpx> [옵션] (gpu feature 필요)"); + println!(" 기존 SVG 산출을 GPU(vello/wgpu)로 래스터화해 PNG로 내보내기"); + println!(" 대량 문서 코퍼스를 VLM 입력 이미지로 굽는 파이프라인용. 파싱·레이아웃은"); + println!(" GPU 대상이 아니다(분기 지배적) — 래스터화 단계만 GPU로 옮긴다."); + println!(); + println!(" -o, --output <폴더> 출력 폴더 (기본: output/)"); + println!(" -p, --page <번호> 특정 페이지만 내보내기 (0부터 시작)"); + println!(" --scale <배율> 렌더링 배율 (기본: 2.0)"); + println!(" --font-path <경로> 폰트 파일/디렉터리 탐색 경로 (여러 번 지정 가능)"); + println!( + " --benchmark 같은 벡터를 CPU(resvg)로도 굽고 시간·픽셀차를 정직 보고" + ); + println!(" --repeat 각 페이지 래스터화 반복 후 최솟값 (기본: 1)"); + println!(); + println!(" gpu-info (gpu feature 필요)"); + println!(" 사용 가능한 GPU 어댑터를 열거 (export-png-gpu 가 쓸 백엔드 확인)"); + println!(); println!(" export-text <파일.hwp> [옵션]"); println!(" 페이지별 텍스트를 TXT로 내보내기"); println!(); @@ -5361,6 +5396,487 @@ fn export_png(args: &[String]) -> i32 { } } +// ============================================================================ +// [gym_gpu_raster] export-png-gpu — GPU 가속 SVG→PNG 래스터화 (feature = "gpu") +// +// 파싱·레이아웃은 GPU로 가속되지 않는다(분기 지배적). 이 명령은 그 경계를 넘지 않고, 기존 +// SVG 산출(render_page_svg_native)이 만든 벡터를 **픽셀로 굽는 단계만** GPU(vello/wgpu)로 +// 옮긴다. 대량 문서 코퍼스를 VLM 입력 이미지로 굽는 에이전트 파이프라인이 대상이다. +// ============================================================================ + +/// gpu feature 없이 빌드된 바이너리 — export-png 의 native-skia 스텁과 같은 계약. +#[cfg(not(feature = "gpu"))] +fn export_png_gpu(_args: &[String]) -> i32 { + eprintln!("오류: export-png-gpu 명령은 gpu feature 가 활성화되어야 합니다."); + eprintln!(" cargo build --release --features gpu"); + // 기능이 아예 빌드되지 않은 바이너리다. 0으로 끝내면 스크립트가 성공으로 오인한다(#2707). + EXIT_USAGE +} + +#[cfg(feature = "gpu")] +fn export_png_gpu(args: &[String]) -> i32 { + use rhwp::renderer::gpu; + use std::time::Instant; + + let mut file_path: Option<&str> = None; + let mut output_dir = "output".to_string(); + let mut target_page: Option = None; + let mut scale: f64 = 2.0; + let mut font_paths: Vec = Vec::new(); + let mut benchmark = false; + let mut repeat: u32 = 1; + + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--help" | "-h" => { + print_export_png_gpu_usage(); + return EXIT_OK; + } + "--output" | "-o" => { + if i + 1 < args.len() { + output_dir = args[i + 1].clone(); + i += 2; + } else { + eprintln!("오류: --output 뒤에 폴더 경로가 필요합니다."); + return EXIT_USAGE; + } + } + "--page" | "-p" => { + if i + 1 < args.len() { + match args[i + 1].parse::() { + Ok(n) => target_page = Some(n), + Err(_) => { + eprintln!("오류: 페이지 번호가 올바르지 않습니다."); + return EXIT_USAGE; + } + } + i += 2; + } else { + eprintln!("오류: --page 뒤에 페이지 번호가 필요합니다."); + return EXIT_USAGE; + } + } + "--scale" => { + if i + 1 < args.len() { + match args[i + 1].parse::() { + Ok(s) if s.is_finite() && s > 0.0 => scale = s, + _ => { + eprintln!("오류: --scale 값이 올바르지 않습니다 (양수 실수 필요)."); + return EXIT_USAGE; + } + } + i += 2; + } else { + eprintln!("오류: --scale 뒤에 배율 값이 필요합니다."); + return EXIT_USAGE; + } + } + "--font-path" => { + if i + 1 < args.len() { + font_paths.push(std::path::PathBuf::from(&args[i + 1])); + i += 2; + } else { + eprintln!("오류: --font-path 뒤에 경로가 필요합니다."); + return EXIT_USAGE; + } + } + "--benchmark" => { + benchmark = true; + i += 1; + } + "--repeat" => { + if i + 1 < args.len() { + match args[i + 1].parse::() { + Ok(n) if n >= 1 => repeat = n, + _ => { + eprintln!("오류: --repeat 값이 올바르지 않습니다 (1 이상 정수 필요)."); + return EXIT_USAGE; + } + } + i += 2; + } else { + eprintln!("오류: --repeat 뒤에 반복 횟수가 필요합니다."); + return EXIT_USAGE; + } + } + other if other.starts_with('-') => { + eprintln!("알 수 없는 옵션: {other}"); + return EXIT_USAGE; + } + other => { + if file_path.replace(other).is_some() { + eprintln!("오류: 입력 파일은 하나만 지정할 수 있습니다: {other}"); + return EXIT_USAGE; + } + i += 1; + } + } + } + + let Some(file_path) = file_path else { + eprintln!("오류: HWP 파일 경로를 지정해주세요."); + eprintln!("사용법: rhwp export-png-gpu <파일.hwp|파일.hwpx> [옵션] (rhwp export-png-gpu --help 참조)"); + return EXIT_USAGE; + }; + + let data = match fs::read(file_path) { + Ok(d) => d, + Err(e) => { + eprintln!("오류: 파일을 읽을 수 없습니다 - {}: {}", file_path, e); + return EXIT_RUNTIME; + } + }; + + let mut core = match load_document_core(&data) { + Ok(c) => c, + Err(e) => return e.report(), + }; + + // 외부 연결 그림 자동 적재 — export-svg/export-png 와 동일 규칙(#3302). + if allows_implicit_sibling_resources(rhwp::parser::detect_format(&data)) { + if let Some(parent) = Path::new(file_path).parent() { + let _loaded = core.populate_external_images_from_dir(parent); + } + } + + let page_count = core.page_count(); + println!("문서 로드 완료: {} ({}페이지)", file_path, page_count); + + let output_path = Path::new(&output_dir); + if !output_path.exists() { + if let Err(e) = fs::create_dir_all(output_path) { + eprintln!( + "오류: 출력 폴더를 생성할 수 없습니다 - {}: {}", + output_dir, e + ); + return EXIT_RUNTIME; + } + } + + let pages: Vec = match target_page { + Some(p) => { + if p >= page_count as u32 { + eprintln!( + "오류: 페이지 번호가 범위를 벗어났습니다 (0~{})", + page_count - 1 + ); + return EXIT_USAGE; + } + vec![p] + } + None => (0..page_count as u32).collect(), + }; + + let file_stem = Path::new(file_path) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("page"); + + // ── GPU 컨텍스트: 배치 전체에서 재사용할 단 하나. 생성 비용(일회성)을 측정해 둔다. ── + let init_start = Instant::now(); + let mut ctx = match gpu::GpuContext::new() { + Ok(c) => c, + Err(e) => { + eprintln!("오류: GPU 컨텍스트 생성 실패 - {e}"); + eprintln!(" (헤드리스 Vulkan/DX12/Metal 어댑터가 필요합니다. `rhwp gpu-info` 로 확인하세요.)"); + return EXIT_RUNTIME; + } + }; + let init_ms = init_start.elapsed().as_secs_f64() * 1000.0; + println!("GPU 어댑터: {}", ctx.adapter_summary()); + println!("GPU 컨텍스트 초기화(일회성): {:.1} ms", init_ms); + if benchmark { + println!( + "벤치마크 모드: 각 페이지 래스터화를 {}회 반복해 최솟값(노이즈 최소)을 취합니다.\n", + repeat + ); + } + + let total_pages = pages.len(); + let mut success = 0usize; + let mut total_bytes = 0usize; + + // 벤치마크 누적기(래스터화 단계만 — 파싱·인코딩은 두 경로 공통이라 별도 집계). + let mut sum_svg_ms = 0.0f64; // 레이아웃+SVG 생성(CPU, 두 경로 공통 입력) + let mut sum_parse_ms = 0.0f64; // usvg 파싱+텍스트 셰이핑(두 경로 공통) + let mut sum_gpu_ms = 0.0f64; // vello: scene 빌드+GPU 래스터+리드백 + let mut sum_cpu_ms = 0.0f64; // resvg: tiny-skia CPU 래스터 + let mut sum_encode_ms = 0.0f64; // PNG 인코딩(두 경로 공통 코드) + let mut worst_mean_abs = 0.0f64; + let mut worst_pct = 0.0f64; + let mut dims_all_match = true; + + for page_num in &pages { + // 1) 레이아웃+SVG 생성 (CPU, 두 경로 공통 입력) + let t = Instant::now(); + let svg = match core.render_page_svg_native(*page_num) { + Ok(s) => s, + Err(e) => { + eprintln!("오류: 페이지 {} SVG 생성 실패 - {:?}", page_num + 1, e); + continue; + } + }; + sum_svg_ms += t.elapsed().as_secs_f64() * 1000.0; + + // 2) usvg 파싱 (두 경로 공통 벡터 트리) + let t = Instant::now(); + let tree = match gpu::parse_svg(&svg, &font_paths) { + Ok(t) => t, + Err(e) => { + eprintln!("오류: 페이지 {} SVG 파싱 실패 - {e}", page_num + 1); + continue; + } + }; + sum_parse_ms += t.elapsed().as_secs_f64() * 1000.0; + + // 3) GPU 래스터화 (repeat 회 중 최솟값) + let mut gpu_best = f64::INFINITY; + let mut gpu_img = None; + for _ in 0..repeat { + let t = Instant::now(); + match ctx.rasterize(&tree, scale) { + Ok(img) => { + let ms = t.elapsed().as_secs_f64() * 1000.0; + gpu_best = gpu_best.min(ms); + gpu_img = Some(img); + } + Err(e) => { + eprintln!("오류: 페이지 {} GPU 래스터화 실패 - {e}", page_num + 1); + break; + } + } + } + let Some(gpu_img) = gpu_img else { continue }; + sum_gpu_ms += gpu_best; + + // 4) PNG 인코딩 (공통 코드) + let t = Instant::now(); + let png_bytes = match gpu_img.encode_png() { + Ok(b) => b, + Err(e) => { + eprintln!("오류: 페이지 {} PNG 인코딩 실패 - {e}", page_num + 1); + continue; + } + }; + sum_encode_ms += t.elapsed().as_secs_f64() * 1000.0; + + let png_filename = if total_pages == 1 { + format!("{}.png", file_stem) + } else { + format!("{}_{:03}.png", file_stem, page_num + 1) + }; + let png_path = output_path.join(&png_filename); + if let Err(e) = fs::write(&png_path, &png_bytes) { + eprintln!("오류: 페이지 {} PNG 저장 실패 - {}", page_num + 1, e); + continue; + } + println!( + " → {} ({}x{}, {} bytes, GPU {:.1} ms)", + png_path.display(), + gpu_img.width, + gpu_img.height, + png_bytes.len(), + gpu_best + ); + total_bytes += png_bytes.len(); + success += 1; + + // 5) 벤치마크: 같은 트리를 CPU(resvg)로도 굽고, 시간·픽셀차를 잰다. + if benchmark { + let mut cpu_best = f64::INFINITY; + let mut cpu_img = None; + for _ in 0..repeat { + let t = Instant::now(); + match gpu::cpu_rasterize(&tree, scale) { + Ok(img) => { + cpu_best = cpu_best.min(t.elapsed().as_secs_f64() * 1000.0); + cpu_img = Some(img); + } + Err(e) => { + eprintln!("경고: 페이지 {} CPU 래스터화 실패 - {e}", page_num + 1); + break; + } + } + } + if let Some(cpu_img) = cpu_img { + sum_cpu_ms += cpu_best; + // CPU PNG 도 저장(눈 검증용). + if let Ok(cpu_png) = cpu_img.encode_png() { + let cpu_name = format!("{}_{:03}.cpu.png", file_stem, page_num + 1); + let _ = fs::write(output_path.join(cpu_name), &cpu_png); + } + let d = gpu::diff(&gpu_img, &cpu_img); + if !d.dims_match { + dims_all_match = false; + println!( + " [벤치] p{}: GPU {:.1}ms / CPU {:.1}ms · 치수 불일치 GPU {}x{} vs CPU {}x{}", + page_num + 1, + gpu_best, + cpu_best, + d.width_a, + d.height_a, + d.width_b, + d.height_b + ); + } else { + worst_mean_abs = worst_mean_abs.max(d.mean_abs); + worst_pct = worst_pct.max(d.pct_pixels_over_thresh); + println!( + " [벤치] p{}: GPU {:.1}ms / CPU {:.1}ms (x{:.2}) · 픽셀차 평균 {:.2}/255, |Δ|≥16 {:.2}%", + page_num + 1, + gpu_best, + cpu_best, + cpu_best / gpu_best, + d.mean_abs, + d.pct_pixels_over_thresh * 100.0 + ); + } + } + } + } + + println!( + "\n내보내기 완료: {}개 PNG → {}/ ({:.1} MB), 배율 {}x", + success, + output_dir, + total_bytes as f64 / 1024.0 / 1024.0, + scale + ); + + if benchmark && success > 0 { + let n = success as f64; + println!("\n==================== 정직한 벤치마크 요약 ===================="); + println!( + "표본: {} 페이지, 배율 {}x, 반복 {}회(최솟값), 어댑터 {}", + success, + scale, + repeat, + ctx.adapter_summary() + ); + println!("공통 단계(두 경로 동일 입력, 가속 대상 아님):"); + println!( + " 레이아웃+SVG 생성 : 합계 {:8.1} ms (페이지당 {:6.2} ms)", + sum_svg_ms, + sum_svg_ms / n + ); + println!( + " usvg 파싱+셰이핑 : 합계 {:8.1} ms (페이지당 {:6.2} ms)", + sum_parse_ms, + sum_parse_ms / n + ); + println!( + " PNG 인코딩 : 합계 {:8.1} ms (페이지당 {:6.2} ms)", + sum_encode_ms, + sum_encode_ms / n + ); + println!("래스터화 단계(비교 대상):"); + println!( + " CPU (resvg/tiny-skia) : 합계 {:8.1} ms (페이지당 {:6.2} ms)", + sum_cpu_ms, + sum_cpu_ms / n + ); + println!( + " GPU (vello/wgpu) : 합계 {:8.1} ms (페이지당 {:6.2} ms)", + sum_gpu_ms, + sum_gpu_ms / n + ); + if sum_gpu_ms > 0.0 { + println!( + " → 래스터화만: GPU가 CPU 대비 {:.2}x", + sum_cpu_ms / sum_gpu_ms + ); + } + // 엔드투엔드(초기화 포함/제외) — 소규모에서 GPU가 손해 보는 구간을 정직하게 보인다. + let e2e_common = sum_svg_ms + sum_parse_ms + sum_encode_ms; + let e2e_cpu = e2e_common + sum_cpu_ms; + let e2e_gpu_no_init = e2e_common + sum_gpu_ms; + let e2e_gpu_with_init = e2e_gpu_no_init + init_ms; + println!("엔드투엔드(공통 단계 포함):"); + println!(" CPU 경로 : {:8.1} ms", e2e_cpu); + println!( + " GPU 경로(초기화 제외) : {:8.1} ms → {:.2}x", + e2e_gpu_no_init, + e2e_cpu / e2e_gpu_no_init + ); + println!( + " GPU 경로(초기화 포함) : {:8.1} ms (일회성 {:.1} ms 포함) → {:.2}x", + e2e_gpu_with_init, + init_ms, + e2e_cpu / e2e_gpu_with_init + ); + println!("시각 일치(GPU vs CPU, 같은 벡터 입력):"); + if dims_all_match { + println!( + " 치수 전 페이지 일치 · 최악 평균 픽셀차 {:.2}/255 · 최악 |Δ|≥16 비율 {:.2}%", + worst_mean_abs, + worst_pct * 100.0 + ); + println!(" (차이는 레이아웃이 아니라 두 래스터라이저의 안티에일리어싱 방식 차이다.)"); + } else { + println!(" 일부 페이지 치수 불일치 — 위 로그 참조."); + } + println!("============================================================="); + } + + if success == total_pages { + EXIT_OK + } else { + EXIT_RUNTIME + } +} + +#[cfg(feature = "gpu")] +fn print_export_png_gpu_usage() { + println!("rhwp export-png-gpu <파일.hwp|파일.hwpx> [옵션]"); + println!(" 기존 SVG 산출을 GPU(vello/wgpu)로 래스터화해 페이지별 PNG로 내보낸다."); + println!(" 파싱·레이아웃은 GPU 대상이 아니다(분기 지배적) — 래스터화 단계만 GPU로 옮긴다."); + println!(); + println!(" -o, --output <폴더> 출력 폴더 (기본: output/)"); + println!(" -p, --page <번호> 특정 페이지만 (0부터)"); + println!(" --scale <배율> 렌더 배율 (기본: 2.0)"); + println!(" --font-path <경로> 폰트 파일/디렉터리 탐색 경로 (여러 번 지정 가능)"); + println!(" --benchmark 같은 벡터를 CPU(resvg)로도 굽고 시간·픽셀차를 보고"); + println!(" --repeat 각 페이지 래스터화 반복 후 최솟값 (기본: 1)"); +} + +/// gpu feature 없이 빌드된 바이너리 — gpu-info 스텁. +#[cfg(not(feature = "gpu"))] +fn gpu_info(_args: &[String]) -> i32 { + eprintln!("오류: gpu-info 명령은 gpu feature 가 활성화되어야 합니다."); + eprintln!(" cargo build --release --features gpu"); + EXIT_USAGE +} + +/// 사용 가능한 GPU 어댑터를 열거한다 — export-png-gpu 가 어떤 GPU를 쓸지 확인용. +#[cfg(feature = "gpu")] +fn gpu_info(_args: &[String]) -> i32 { + use rhwp::renderer::gpu; + let adapters = gpu::probe_adapters(); + if adapters.is_empty() { + println!("사용 가능한 GPU 어댑터가 없습니다."); + return EXIT_RUNTIME; + } + println!("사용 가능한 GPU 어댑터 ({}개):", adapters.len()); + for (idx, a) in adapters.iter().enumerate() { + println!(" [{}] {}", idx, a); + } + println!(); + match gpu::GpuContext::new() { + Ok(ctx) => { + println!( + "export-png-gpu 가 선택할 어댑터(HighPerformance): {}", + ctx.adapter_summary() + ); + EXIT_OK + } + Err(e) => { + eprintln!("경고: 헤드리스 렌더 컨텍스트 생성 실패 - {e}"); + EXIT_RUNTIME + } + } +} + fn export_pdf(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help" || a == "-h") { print_export_pdf_usage(); diff --git a/src/renderer/gpu.rs b/src/renderer/gpu.rs new file mode 100644 index 0000000000..6d696d26ee --- /dev/null +++ b/src/renderer/gpu.rs @@ -0,0 +1,398 @@ +//! GPU 가속 SVG 래스터화 경로 (`feature = "gpu"`, 네이티브 전용). +//! +//! # 무엇을 GPU로 옮기는가 — 그리고 무엇은 아닌가 +//! +//! rhwp의 파싱·레이아웃은 분기 지배적(branch-bound) 이라 GPU로 가속되지 않는다. 이 모듈은 +//! 그 경계를 넘지 않는다 — 레이아웃은 손대지 않고, 기존 SVG 산출(`render_page_svg_native`)이 +//! 이미 만들어 놓은 벡터 표현을 **픽셀로 굽는 단계**(rasterization) 만 GPU로 옮긴다. 이 단계는 +//! 픽셀마다 독립적이라 데이터 병렬성이 크고, 문서 코퍼스를 대량으로 이미지화해 비전 모델 +//! (VLM)에 먹이는 에이전트 파이프라인에서 실제 병목이 되는 곳이다. +//! +//! # 파이프라인 +//! +//! ```text +//! HWP/HWPX ──(파싱·레이아웃, CPU)──▶ SVG 문자열 ──(usvg 파싱)──▶ usvg::Tree +//! │ +//! ┌────────────────────────────────────────────────┤ +//! ▼ ▼ +//! vello_svg → vello::Scene resvg (CPU 기준선) +//! → wgpu 컴퓨트 래스터 → 텍스처 → 리드백 → RGBA → tiny_skia Pixmap → RGBA +//! ``` +//! +//! `usvg::Tree` 는 한 번만 파싱해 GPU·CPU 두 경로에 **동일 입력**으로 넣는다. resvg 0.45 와 +//! vello_svg 0.7 이 같은 usvg 0.45 를 공유하므로(카고가 단일 노드로 통합) 벤치마크가 +//! 순수 래스터화 단계만 비교하게 되고, 픽셀 비교도 의미를 가진다(같은 벡터 → 두 래스터라이저). +//! +//! # 정직성 +//! +//! GPU 컨텍스트(어댑터·디바이스·셰이더 컴파일) 생성은 수백 ms가 드는 **일회성 비용**이다. +//! 따라서 [`GpuContext`] 는 한 번만 만들어 배치 전체에서 재사용해야 하며, 벤치마크는 이 +//! 일회성 비용을 별도로 보고한다. 소규모 문서 한두 장이라면 이 초기화 비용이 지배해 GPU가 +//! 오히려 느릴 수 있다 — 이득은 대량 배치·고해상도에서 나온다. 이 모듈은 그 사실을 숨기지 +//! 않고 측정해 드러낸다. + +use std::path::PathBuf; + +// Cargo 에서 `resvg-gpu`(package = resvg, 0.45)로 이름을 바꿔 가져온다 — native-skia 의 +// resvg 0.47 과 버전이 다르기 때문이다. 0.45 로 핀하는 이유는 vello_svg 0.7 과 usvg 0.45 를 +// **공유**하기 위함이다(카고가 단일 노드로 통합). 그래야 하나의 `usvg::Tree` 를 GPU·CPU 두 +// 래스터라이저에 동일 입력으로 넣을 수 있다. `tiny_skia`·`usvg` 는 resvg 가 재수출한다. +use resvg_gpu::{tiny_skia, usvg}; +use vello::kurbo::Affine; +use vello::peniko::Color; +use vello::wgpu; +use vello::{AaConfig, AaSupport, Renderer, RendererOptions, Scene}; + +/// 래스터화 결과 한 장 — straight(비-프리멀티플라이) RGBA8, 행 우선, `width*height*4` 바이트. +/// +/// 페이지 배경을 불투명 흰색으로 채우므로 전 픽셀의 alpha 는 255 이고, 이 경우 +/// premultiplied 와 straight 표현이 동일해 두 래스터라이저의 출력을 바로 비교할 수 있다. +pub struct RasterImage { + pub width: u32, + pub height: u32, + pub rgba: Vec, +} + +impl RasterImage { + /// PNG 로 인코딩한다(저장소 공용 `image` 크레이트, png feature). + pub fn encode_png(&self) -> Result, String> { + let buf = image::RgbaImage::from_raw(self.width, self.height, self.rgba.clone()) + .ok_or_else(|| "RGBA 버퍼 크기가 이미지 치수와 맞지 않습니다".to_string())?; + let mut out = std::io::Cursor::new(Vec::new()); + buf.write_to(&mut out, image::ImageFormat::Png) + .map_err(|e| format!("PNG 인코딩 실패: {e}"))?; + Ok(out.into_inner()) + } +} + +/// SVG 문자열을 하나의 `usvg::Tree` 로 파싱한다. 텍스트→글리프 변환에 시스템 폰트와 +/// (선택적으로) 지정 폰트 경로를 쓴다. 이 트리를 GPU·CPU 두 경로가 공유한다. +pub fn parse_svg(svg: &str, font_paths: &[PathBuf]) -> Result { + let mut opt = usvg::Options::default(); + { + let db = opt.fontdb_mut(); + db.load_system_fonts(); + for path in font_paths { + if path.is_dir() { + db.load_fonts_dir(path); + } else if let Err(e) = db.load_font_file(path) { + eprintln!( + "경고: 폰트 파일을 불러오지 못했습니다 - {}: {e}", + path.display() + ); + } + } + } + usvg::Tree::from_str(svg, &opt).map_err(|e| format!("SVG 파싱 실패: {e}")) +} + +/// usvg 트리의 픽셀 치수를 배율에 맞춰 계산한다(각 변 최소 1px). +fn scaled_dims(tree: &usvg::Tree, scale: f64) -> (u32, u32) { + let size = tree.size(); + let w = ((size.width() as f64) * scale).ceil().max(1.0) as u32; + let h = ((size.height() as f64) * scale).ceil().max(1.0) as u32; + (w, h) +} + +/// CPU 기준선: 동일한 `usvg::Tree` 를 resvg(tiny-skia 백엔드)로 래스터화한다. +/// +/// GPU 경로와 완전히 같은 벡터 입력을 소비하므로, 두 결과의 픽셀 차이는 레이아웃 차이가 +/// 아니라 **래스터라이저 차이**(안티에일리어싱 방식 등)만 반영한다. +pub fn cpu_rasterize(tree: &usvg::Tree, scale: f64) -> Result { + let (width, height) = scaled_dims(tree, scale); + let mut pixmap = tiny_skia::Pixmap::new(width, height) + .ok_or_else(|| format!("tiny-skia Pixmap 생성 실패 ({width}x{height})"))?; + // 페이지 배경을 불투명 흰색으로 — GPU 경로(base_color=WHITE)와 동일 조건. + pixmap.fill(tiny_skia::Color::WHITE); + resvg_gpu::render( + tree, + tiny_skia::Transform::from_scale(scale as f32, scale as f32), + &mut pixmap.as_mut(), + ); + Ok(RasterImage { + width, + height, + rgba: pixmap.data().to_vec(), + }) +} + +/// GPU 래스터화 컨텍스트 — wgpu 디바이스/큐 + vello 렌더러를 **한 번** 만들어 배치 전체에서 +/// 재사용한다. 헤드리스(서피스 없음)로 동작하며, 기본 어댑터를 자동 선택한다. +pub struct GpuContext { + device: wgpu::Device, + queue: wgpu::Queue, + renderer: Renderer, + /// 선택된 어댑터 정보(백엔드·디바이스명) — 보고·진단용. + pub adapter_info: wgpu::AdapterInfo, +} + +impl GpuContext { + /// 헤드리스 GPU 컨텍스트를 만든다. 어댑터가 없거나 vello 초기화가 실패하면 `Err`. + /// + /// 이 함수 호출 비용(수백 ms 수준)이 곧 "일회성 초기화 비용" 이다 — 배치가 클수록 + /// 페이지당으로 분할 상각된다. + pub fn new() -> Result { + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + // GL 백엔드는 vello 의 컴퓨트 파이프라인을 지원하지 않으므로 PRIMARY(Vulkan/DX12/Metal)만. + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }); + + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + force_fallback_adapter: false, + compatible_surface: None, + })) + .ok_or_else(|| { + "GPU 어댑터를 찾을 수 없습니다 (Vulkan/DX12/Metal 지원 GPU·드라이버 필요)".to_string() + })?; + + let adapter_info = adapter.get_info(); + + let (device, queue) = pollster::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: Some("rhwp-gpu-raster"), + required_features: wgpu::Features::empty(), + // 어댑터가 실제로 지원하는 한도를 그대로 요청 — vello 의 스토리지 텍스처/버퍼 + // 요구를 다운레벨 기본값이 못 맞추는 경우를 피한다. + required_limits: adapter.limits(), + memory_hints: wgpu::MemoryHints::default(), + }, + None, + )) + .map_err(|e| format!("GPU 디바이스 요청 실패: {e}"))?; + + let renderer = Renderer::new( + &device, + RendererOptions { + use_cpu: false, + // Area AA만 초기화 — 셰이더 순열 컴파일을 줄여 초기화를 빠르게. + antialiasing_support: AaSupport::area_only(), + num_init_threads: None, + pipeline_cache: None, + }, + ) + .map_err(|e| format!("vello 렌더러 생성 실패: {e:?}"))?; + + Ok(Self { + device, + queue, + renderer, + adapter_info, + }) + } + + /// 사람이 읽는 어댑터 요약("DX12 / NVIDIA GeForce ... (DiscreteGpu)"). + pub fn adapter_summary(&self) -> String { + format!( + "{:?} / {} ({:?})", + self.adapter_info.backend, self.adapter_info.name, self.adapter_info.device_type + ) + } + + /// 공유 `usvg::Tree` 를 GPU에서 래스터화해 straight RGBA8 로 리드백한다. + pub fn rasterize(&mut self, tree: &usvg::Tree, scale: f64) -> Result { + let (width, height) = scaled_dims(tree, scale); + + // vello_svg 로 트리를 Scene 으로 변환하고, 배율은 Affine 으로 전체에 적용한다. + let svg_scene = vello_svg::render_tree(tree); + let mut scene = Scene::new(); + scene.append(&svg_scene, Some(Affine::scale(scale))); + + // vello 는 STORAGE_BINDING 텍스처(Rgba8Unorm)에 컴퓨트로 쓴다. 리드백을 위해 + // COPY_SRC 도 함께 요구한다. + let target = self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("rhwp-gpu-target"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + let view = target.create_view(&wgpu::TextureViewDescriptor::default()); + + self.renderer + .render_to_texture( + &self.device, + &self.queue, + &scene, + &view, + &vello::RenderParams { + // 불투명 흰 배경 — CPU 기준선과 동일 조건, alpha 전부 255. + base_color: Color::WHITE, + width, + height, + antialiasing_method: AaConfig::Area, + }, + ) + .map_err(|e| format!("vello 렌더 실패: {e:?}"))?; + + self.read_back(&target, width, height) + } + + /// 텍스처 → 매핑 버퍼로 복사하고, 256바이트 정렬 패딩을 제거해 조밀 RGBA 로 되돌린다. + fn read_back( + &self, + texture: &wgpu::Texture, + width: u32, + height: u32, + ) -> Result { + let unpadded_bpr = width * 4; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; + // wgpu 는 copy_texture_to_buffer 의 bytes_per_row 가 256의 배수이길 요구한다. + let padded_bpr = unpadded_bpr.div_ceil(align) * align; + + let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("rhwp-gpu-readback"), + size: (padded_bpr as u64) * (height as u64), + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + encoder.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: &buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(padded_bpr), + rows_per_image: Some(height), + }, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + self.queue.submit(Some(encoder.finish())); + + let slice = buffer.slice(..); + slice.map_async(wgpu::MapMode::Read, |_| {}); + // 헤드리스라 이벤트 루프가 없으므로 블로킹 폴로 매핑 완료를 기다린다. + self.device.poll(wgpu::Maintain::Wait); + + let mapped = slice.get_mapped_range(); + let mut rgba = vec![0u8; (unpadded_bpr as usize) * (height as usize)]; + for y in 0..height as usize { + let src = y * padded_bpr as usize; + let dst = y * unpadded_bpr as usize; + rgba[dst..dst + unpadded_bpr as usize] + .copy_from_slice(&mapped[src..src + unpadded_bpr as usize]); + } + drop(mapped); + buffer.unmap(); + + Ok(RasterImage { + width, + height, + rgba, + }) + } +} + +/// 두 래스터 결과의 저비용 픽셀 비교 통계. +#[derive(Debug, Clone, Copy)] +pub struct DiffStats { + pub dims_match: bool, + pub width_a: u32, + pub height_a: u32, + pub width_b: u32, + pub height_b: u32, + /// 채널당 평균 절대 차이(0..255). + pub mean_abs: f64, + /// 채널당 최대 절대 차이(0..255). + pub max_abs: u8, + /// 어떤 채널이든 16 이상 차이 나는 픽셀의 비율(0..1). + pub pct_pixels_over_thresh: f64, +} + +/// 같은 치수의 두 RGBA 이미지를 비교한다. 치수가 다르면 `dims_match=false` 만 채워 돌려준다. +pub fn diff(a: &RasterImage, b: &RasterImage) -> DiffStats { + let dims_match = a.width == b.width && a.height == b.height; + if !dims_match { + return DiffStats { + dims_match: false, + width_a: a.width, + height_a: a.height, + width_b: b.width, + height_b: b.height, + mean_abs: f64::NAN, + max_abs: 255, + pct_pixels_over_thresh: f64::NAN, + }; + } + + let n = a.rgba.len().min(b.rgba.len()); + let mut sum_abs: u64 = 0; + let mut max_abs: u8 = 0; + let mut pixels_over: u64 = 0; + let px_count = (a.width as u64) * (a.height as u64); + + for px in 0..(n / 4) { + let mut over = false; + for c in 0..4 { + let i = px * 4 + c; + let d = a.rgba[i].abs_diff(b.rgba[i]); + sum_abs += d as u64; + if d > max_abs { + max_abs = d; + } + if d >= 16 { + over = true; + } + } + if over { + pixels_over += 1; + } + } + + DiffStats { + dims_match: true, + width_a: a.width, + height_a: a.height, + width_b: b.width, + height_b: b.height, + mean_abs: sum_abs as f64 / n as f64, + max_abs, + pct_pixels_over_thresh: if px_count == 0 { + 0.0 + } else { + pixels_over as f64 / px_count as f64 + }, + } +} + +/// 사용 가능한 GPU 어댑터를 열거한다(`gpu-info` 하위명령용). 각 원소는 +/// "(backend) name — type" 요약 문자열이다. +pub fn probe_adapters() -> Vec { + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + ..Default::default() + }); + instance + .enumerate_adapters(wgpu::Backends::all()) + .into_iter() + .map(|adapter| { + let info = adapter.get_info(); + format!( + "({:?}) {} — {:?} [driver: {}]", + info.backend, info.name, info.device_type, info.driver + ) + }) + .collect() +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 4e62a7d7e7..e3e6491082 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -18,6 +18,10 @@ pub mod font_metrics_data; #[cfg(not(target_arch = "wasm32"))] pub mod font_paths; pub(crate) mod form_caption; +// [gym_gpu_raster] GPU 가속 SVG 래스터화(vello/wgpu). 네이티브 + gpu feature 전용 — +// native-skia 와 같은 방식으로 선택적 게이팅해 CI는 GPU 없이도 컴파일된다. +#[cfg(all(not(target_arch = "wasm32"), feature = "gpu"))] +pub mod gpu; pub(crate) mod hancom_pua; pub mod height_cursor; pub mod height_measurer; diff --git a/tools/gen_agent_codex.py b/tools/gen_agent_codex.py index 8466d0c3db..da7348a66c 100644 --- a/tools/gen_agent_codex.py +++ b/tools/gen_agent_codex.py @@ -133,7 +133,8 @@ def find_bin(): ("30_편집과_계획", "편집·계획 — 원본 무훼손 변경", ["edit", "edit replace-text", "edit set-cell", "edit fill-fields", "edit insert-image", "edit redact", "edit sanitize", "run"]), ("40_변환과_렌더", "변환·렌더 — 형식을 넘나든다", - ["convert", "export-hwpx", "export-hml", "export-markdown", "export-doclang", "export-pdf", "export-svg", "thumbnail", "render-diff", "build-from-ingest", "split-document"]), + ["convert", "export-hwpx", "export-hml", "export-markdown", "export-doclang", "export-pdf", "export-svg", "thumbnail", "render-diff", "build-from-ingest", "split-document", + "export-png-gpu", "gpu-info"]), ("50_검증_사다리", "검증 사다리 — 판정은 데이터다", ["verify", "ir-diff", "replay", "audit", "lineage", "hwpx-roundtrip", "keygen", "verify-signature", "harness",