diff --git a/.bazelrc b/.bazelrc index 76b362a465e..8d74239fe21 100644 --- a/.bazelrc +++ b/.bazelrc @@ -15,6 +15,7 @@ test --test_timeout=300,1800,1800,9600 # bazel 7 is somewhat forgiving for glob patterns that don't # match, but bazel 8 will be strict. So start now. common --incompatible_disallow_empty_glob +common --experimental_isolated_extension_usages build --cxxopt "-std=c++20" --host_cxxopt "-std=c++20" build --cxxopt "-xc++" --host_cxxopt "-xc++" diff --git a/BUILD.bazel b/BUILD.bazel index 2414be6bd1a..6fb6bd336b1 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -126,6 +126,8 @@ OPENROAD_LIBRARY_DEPS = [ "//src/upf:ui", "//src/utl", "//src/utl:ui", + "//src/web", + "//src/web:ui", "@abc", ":ord", ] + select( diff --git a/MODULE.bazel b/MODULE.bazel index a2a13e22cc2..86d3a4e0168 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -79,6 +79,10 @@ bazel_dep(name = "readline", version = "8.2.bcr.3") bazel_dep(name = "bant", version = "0.2.4", dev_dependency = True) bazel_dep(name = "googletest", version = "1.17.0.bcr.2", dev_dependency = True) +# JavaScript unit tests +bazel_dep(name = "aspect_rules_js", version = "3.0.2", dev_dependency = True) +bazel_dep(name = "rules_nodejs", version = "6.7.3", dev_dependency = True) + # A from source build of QT that allows it to link into OpenROAD. # Building like any other bazel project. scripts in the docker folder # of this project generate stubs .ifso for things like X11 that will @@ -123,6 +127,25 @@ pip.parse( ) use_repo(pip, "openroad-pip") +node = use_extension( + "@rules_nodejs//nodejs:extensions.bzl", + "node", + dev_dependency = True, +) +node.toolchain(node_version = "22.14.0") + +npm = use_extension( + "@aspect_rules_js//npm:extensions.bzl", + "npm", + dev_dependency = True, + isolate = True, +) +npm.npm_translate_lock( + name = "npm", + pnpm_lock = "//src/web:pnpm-lock.yaml", +) +use_repo(npm, "npm") + bazel_dep(name = "bazel-orfs", dev_dependency = True) # To bump version, run: bazelisk run @bazel-orfs//:bump diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 1d0226ac98f..ccf35f7cfb6 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -36,9 +36,18 @@ "https://bcr.bazel.build/modules/aspect_bazel_lib/1.31.2/MODULE.bazel": "7bee702b4862612f29333590f4b658a5832d433d6f8e4395f090e8f4e85d442f", "https://bcr.bazel.build/modules/aspect_bazel_lib/1.38.0/MODULE.bazel": "6307fec451ba9962c1c969eb516ebfe1e46528f7fa92e1c9ac8646bef4cdaa3f", "https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/MODULE.bazel": "004ba890363d05372a97248c37205ae64b6fa31047629cd2c0895a9d0c7779e8", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.5/source.json": "ac2c3213df8f985785f1d0aeb7f0f73d5324e6e67d593d9b9470fb74a25d4a9b", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", "https://bcr.bazel.build/modules/aspect_rules_js/1.33.1/MODULE.bazel": "db3e7f16e471cf6827059d03af7c21859e7a0d2bc65429a3a11f005d46fc501b", "https://bcr.bazel.build/modules/aspect_rules_js/1.39.0/MODULE.bazel": "aece421d479e3c31dc3e5f6d49a12acc2700457c03c556650ec7a0ff23fc0d95", + "https://bcr.bazel.build/modules/aspect_rules_js/3.0.2/MODULE.bazel": "c5d22d2db2a2f0cf41ec2028ded2e2543d7ff1ea9f6faf5d6b2791546ee1d6a9", + "https://bcr.bazel.build/modules/aspect_rules_js/3.0.2/source.json": "8a8642e6869ead1b37c5442b60b59912bb35ae265d94e4115d3e1a2598fdd849", "https://bcr.bazel.build/modules/aspect_rules_lint/0.12.0/MODULE.bazel": "e767c5dbfeb254ec03275a7701b5cfde2c4d2873676804bc7cb27ddff3728fed", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/MODULE.bazel": "37c764292861c2f70314efa9846bb6dbb44fc0308903b3285da6528305450183", + "https://bcr.bazel.build/modules/aspect_tools_telemetry/0.3.3/source.json": "605086bbc197743a0d360f7ddc550a1d4dfa0441bc807236e17170f636153348", "https://bcr.bazel.build/modules/bant/0.2.4/MODULE.bazel": "244ee96132d5f3c2dd5398b1dd5c485dfec54463ae0c965660a1cbaa24de3f74", "https://bcr.bazel.build/modules/bant/0.2.4/source.json": "a5ede60507444975054551e3298fc20890c6ce756feaa8ed2d883eab6cfc79f0", "https://bcr.bazel.build/modules/bazel_features/0.1.0/MODULE.bazel": "47011d645b0f949f42ee67f2e8775188a9cf4a0a1528aa2fa4952f2fd00906fd", @@ -55,9 +64,17 @@ "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", - "https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc", + "https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e", + "https://bcr.bazel.build/modules/bazel_features/1.39.0/MODULE.bazel": "28739425c1fc283c91931619749c832b555e60bcd1010b40d8441ce0a5cf726d", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.41.0/MODULE.bazel": "6e0f87fafed801273c371d41e22a15a6f8abf83fdd7f87d5e44ad317b94433d0", + "https://bcr.bazel.build/modules/bazel_features/1.41.0/source.json": "8fd525b31b0883c47e0593443cdd10219b94a7556b3195fc02d75c86c66cfe30", + "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18", + "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", + "https://bcr.bazel.build/modules/bazel_lib/3.2.2/MODULE.bazel": "e2c890c8a515d6bca9c66d47718aa9e44b458fde64ec7204b8030bf2d349058c", + "https://bcr.bazel.build/modules/bazel_lib/3.2.2/source.json": "9e84e115c20e14652c5c21401ae85ff4daa8702e265b5c0b3bf89353f17aa212", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", @@ -300,6 +317,7 @@ "https://bcr.bazel.build/modules/freetype/2.13.3/source.json": "a051388a7fa6b0e2ccf8e70bc30ecb00d9708fa98e5c2adac1d67514d8332cc3", "https://bcr.bazel.build/modules/fuzztest/20250214.0/MODULE.bazel": "b65b66befece838904a4e1a8bf5bb8d610b0e7519f0e4654fae6e61f3b9a54e8", "https://bcr.bazel.build/modules/fuzztest/20250214.0/source.json": "ab2a3ca04d8e6a606cbc688f2f18bdfa7ef2fedd83fef3b4ff8778df26fea6df", + "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.2/MODULE.bazel": "ae318680f31d1960f1d102db3b7e04cfa6fb38ae9ba54319b6b9b104b49e7c65", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.2/source.json": "004aeff692d2e12debb1105c5c332a95db9dfd7fe68be60c6f9cf7e1f18613bf", "https://bcr.bazel.build/modules/gazelle/0.27.0/MODULE.bazel": "3446abd608295de6d90b4a8a118ed64a9ce11dcb3dda2dc3290a22056bd20996", @@ -353,6 +371,9 @@ "https://bcr.bazel.build/modules/highwayhash/0.0.0-20240305-5ad3bf8/source.json": "211c0937ef5f537da6c3c135d12e60927c71b380642e207e4a02b86d29c55e85", "https://bcr.bazel.build/modules/icu/76.1.bcr.3/MODULE.bazel": "0af631e5c94380fcf75cc779cd4d66352fb23deba224f6ac0d3f32933b5698d4", "https://bcr.bazel.build/modules/icu/76.1.bcr.3/source.json": "a8e28c19f6bee73132069a28c8fb3830dff126c1faaab493fbd90cc2d694c1a3", + "https://bcr.bazel.build/modules/jq.bzl/0.1.0/MODULE.bazel": "2ce69b1af49952cd4121a9c3055faa679e748ce774c7f1fda9657f936cae902f", + "https://bcr.bazel.build/modules/jq.bzl/0.4.0/MODULE.bazel": "a7b39b37589f2b0dad53fd6c1ccaabbdb290330caa920d7ef3e6aad068cd4ab2", + "https://bcr.bazel.build/modules/jq.bzl/0.4.0/source.json": "52ec7530c4618e03f634b30ff719814a68d7d39c235938b7aa2abbfe1eb1c52c", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", @@ -394,6 +415,8 @@ "https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed", "https://bcr.bazel.build/modules/or-tools/9.14.bcr.1/MODULE.bazel": "0020eaeb58ab76ce4f7d670982d6fcdc83789055a0188f122c716b1e6983b93b", "https://bcr.bazel.build/modules/or-tools/9.14.bcr.1/source.json": "d38406b820de7a7fda5f56c6a74dcdcf29e8267979e3f598798e0988c03c6f82", + "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", + "https://bcr.bazel.build/modules/package_metadata/0.0.2/source.json": "e53a759a72488d2c0576f57491ef2da0cf4aab05ac0997314012495935531b73", "https://bcr.bazel.build/modules/pcre2/10.43/MODULE.bazel": "08eaa025111bd0fedc14a8187c2905fa6ee4501fbe558193e9bf6cc3e2cdf23c", "https://bcr.bazel.build/modules/pcre2/10.46-DEV/MODULE.bazel": "c3c40175cd5e383f02bcfb2d484560461c5ef11e5d52bfc09c7398486c2cbaa6", "https://bcr.bazel.build/modules/pcre2/10.46-DEV/source.json": "7d4a1e758cbef4b68046813147f3b031ed06d226a6925d64079b8d8ecdb20008", @@ -491,6 +514,7 @@ "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", "https://bcr.bazel.build/modules/rules_cc/0.2.11/MODULE.bazel": "e94f24f065bf2191dba2dace951814378b66a94bb3bcc48077492fe0508059b5", "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", + "https://bcr.bazel.build/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4", "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", "https://bcr.bazel.build/modules/rules_cc/0.2.2/MODULE.bazel": "a0656c5a8ff7f76bb1319ebf301bab9d94da5b48894cac25a14ed115f9dd0884", @@ -564,6 +588,8 @@ "https://bcr.bazel.build/modules/rules_m4/0.3/MODULE.bazel": "8793c3117300f7add1cd757d07eb7d6652e0c9259624379b84d891a6bf6e11e0", "https://bcr.bazel.build/modules/rules_m4/0.3/source.json": "9db752727b32753fa382a52ac0da80f01559ecb5f3ba8f6799d6e5b4582b45ac", "https://bcr.bazel.build/modules/rules_nodejs/5.8.2/MODULE.bazel": "6bc03c8f37f69401b888023bf511cb6ee4781433b0cb56236b2e55a21e3a026a", + "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056", + "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09", "https://bcr.bazel.build/modules/rules_perl/0.5.0/MODULE.bazel": "1bff473031644dfb23bd57abe3befe9780f003f0d2156b082527a5c477008792", "https://bcr.bazel.build/modules/rules_perl/0.5.0/source.json": "0076e22051d1b8aedf6d1bf3655bd9e895e9b01dbd3ccc82d80d3bbcae863c34", "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", @@ -606,6 +632,7 @@ "https://bcr.bazel.build/modules/rules_scala/7.1.5/source.json": "cd45f7bf50c69e7c2f621ce2a1b199797502ebee994b181818169683e912c651", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", + "https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", @@ -641,6 +668,10 @@ "https://bcr.bazel.build/modules/swig/4.3.0/MODULE.bazel": "51619e147172c5380869cc90460b1c7fecfe21d6f566e97bc7ecf61244bdc7b8", "https://bcr.bazel.build/modules/systemc/3.0.2/MODULE.bazel": "7c4283e827307eb8eedfcba74f500f114e248916d948a2cdc45971818f3fde0d", "https://bcr.bazel.build/modules/systemc/3.0.2/source.json": "47370abd3ed04ce3c1a65a5b87c973e3e80e7e10e6b1dca2f15076f612d86d4c", + "https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", + "https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351", + "https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c", + "https://bcr.bazel.build/modules/tar.bzl/0.6.0/source.json": "4a620381df075a16cb3a7ed57bd1d05f7480222394c64a20fa51bdb636fda658", "https://bcr.bazel.build/modules/tcl_lang/8.6.16.bcr.1/MODULE.bazel": "1fc27ececc903378b88ad5a0b92d2675b54fe3add9bcc27d612195bd823c2f2d", "https://bcr.bazel.build/modules/tcl_lang/8.6.16.bcr.1/source.json": "f755e3b14f4d08e68f03ef0e1a7ac1aebe993b73ff05ee55f69ceb4e1c36b7e6", "https://bcr.bazel.build/modules/tcmalloc/0.0.0-20250927-12f2552/MODULE.bazel": "b702a6b6806b1041d84918c5098b765b204261647f8cb3e75e0f439106b65ddd", @@ -663,6 +694,9 @@ "https://bcr.bazel.build/modules/xz/5.4.5.bcr.1/source.json": "766f28499a16fa9ed8dc94382d50e80ceda0d0ab80b79b7b104a67074ab10e1f", "https://bcr.bazel.build/modules/yaml-cpp/0.9.0/MODULE.bazel": "d0841e12e92973d7e4c97557198335788890dafa9487d6dc0f9b852053a6c5c0", "https://bcr.bazel.build/modules/yaml-cpp/0.9.0/source.json": "07a9973d6cee81c8bdb1902e8f90064a0ef9aa2262bffc4df2ed577956c08e1b", + "https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072", + "https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6", + "https://bcr.bazel.build/modules/yq.bzl/0.3.2/source.json": "c4ec3e192477e154f08769e29d69e8fd36e8a4f0f623997f3e1f6f7d328f7d7d", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", "https://bcr.bazel.build/modules/zlib/1.2.13/MODULE.bazel": "aa6deb1b83c18ffecd940c4119aff9567cd0a671d7bba756741cb2ef043a29d5", @@ -700,6 +734,38 @@ "recordedRepoMappingEntries": [] } }, + "@@aspect_tools_telemetry+//:extension.bzl%telemetry": { + "general": { + "bzlTransitiveDigest": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=", + "usagesDigest": "I5uCfnurOPHwIjCmdeR4dKRx7L0kxC/bdZUfjabCtCw=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "aspect_tools_telemetry_report": { + "repoRuleId": "@@aspect_tools_telemetry+//:extension.bzl%tel_repository", + "attributes": { + "deps": { + "aspect_rules_js": "3.0.2", + "aspect_tools_telemetry": "0.3.3" + } + } + } + }, + "recordedRepoMappingEntries": [ + [ + "aspect_tools_telemetry+", + "bazel_lib", + "bazel_lib+" + ], + [ + "aspect_tools_telemetry+", + "bazel_skylib", + "bazel_skylib+" + ] + ] + } + }, "@@bazel-orfs+//:extension.bzl%orfs_repositories": { "general": { "bzlTransitiveDigest": "QYKsLwaOfoRUMu1KyupRbCxEnHtHuIAr4YE3u1WzHko=", @@ -1407,7 +1473,7 @@ }, "@@rules_chisel+//chisel:extensions.bzl%chisel": { "general": { - "bzlTransitiveDigest": "aCXBew5jYa9d5TkWJLI5A8nWpTdanPY+PbldpTQLXr8=", + "bzlTransitiveDigest": "8YiJLOvTGeBWOqC6gILmYNgsck3pSfcT1RyCktl4Jzs=", "usagesDigest": "cyt2rWGIn4nNGvnFMJCaFYLV5TJb2XL/KUof4xYvBYY=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -1712,6 +1778,140 @@ "recordedRepoMappingEntries": [] } }, + "@@rules_nodejs+//nodejs:extensions.bzl%node": { + "general": { + "bzlTransitiveDigest": "4pUxCNc22K4I+6+4Nxu52Hur12tFRfa1JMsN5mdDv60=", + "usagesDigest": "2/WrxRCEcTRlp/R9YNdf2FCycgT32EsVVJqIe9crUIg=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "nodejs_linux_amd64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.14.0", + "include_headers": false, + "platform": "linux_amd64" + } + }, + "nodejs_linux_arm64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.14.0", + "include_headers": false, + "platform": "linux_arm64" + } + }, + "nodejs_linux_s390x": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.14.0", + "include_headers": false, + "platform": "linux_s390x" + } + }, + "nodejs_linux_ppc64le": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.14.0", + "include_headers": false, + "platform": "linux_ppc64le" + } + }, + "nodejs_darwin_amd64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.14.0", + "include_headers": false, + "platform": "darwin_amd64" + } + }, + "nodejs_darwin_arm64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.14.0", + "include_headers": false, + "platform": "darwin_arm64" + } + }, + "nodejs_windows_amd64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.14.0", + "include_headers": false, + "platform": "windows_amd64" + } + }, + "nodejs_windows_arm64": { + "repoRuleId": "@@rules_nodejs+//nodejs:repositories.bzl%_nodejs_repositories", + "attributes": { + "node_download_auth": {}, + "node_repositories": {}, + "node_urls": [ + "https://nodejs.org/dist/v{version}/{filename}" + ], + "node_version": "22.14.0", + "include_headers": false, + "platform": "windows_arm64" + } + }, + "nodejs": { + "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "nodejs" + } + }, + "nodejs_host": { + "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_repo_host_os_alias.bzl%nodejs_repo_host_os_alias", + "attributes": { + "user_node_repository_name": "nodejs" + } + }, + "nodejs_toolchains": { + "repoRuleId": "@@rules_nodejs+//nodejs/private:nodejs_toolchains_repo.bzl%nodejs_toolchains_repo", + "attributes": { + "user_node_repository_name": "nodejs" + } + } + }, + "recordedRepoMappingEntries": [] + } + }, "@@rules_python+//python/extensions:config.bzl%config": { "general": { "bzlTransitiveDigest": "TRGIl0CDmorwyNiblOYyhWuyKzi/kWFHT2uIofq7o9Y=", @@ -2225,6 +2425,80 @@ ] ] } + }, + "@@yq.bzl+//yq:extensions.bzl%yq": { + "general": { + "bzlTransitiveDigest": "61Uz+o5PnlY0jJfPZEUNqsKxnM/UCLeWsn5VVCc8u5Y=", + "usagesDigest": "StSqjtWqzKAwXWT5ulrZfkU6ouX05lxDRtKkeIn1bYE=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "yq_darwin_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "darwin_amd64", + "version": "4.45.1" + } + }, + "yq_darwin_arm64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "darwin_arm64", + "version": "4.45.1" + } + }, + "yq_linux_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_amd64", + "version": "4.45.1" + } + }, + "yq_linux_arm64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_arm64", + "version": "4.45.1" + } + }, + "yq_linux_s390x": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_s390x", + "version": "4.45.1" + } + }, + "yq_linux_riscv64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_riscv64", + "version": "4.45.1" + } + }, + "yq_linux_ppc64le": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "linux_ppc64le", + "version": "4.45.1" + } + }, + "yq_windows_amd64": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:platforms.bzl%yq_platform_repo", + "attributes": { + "platform": "windows_amd64", + "version": "4.45.1" + } + }, + "yq_toolchains": { + "repoRuleId": "@@yq.bzl+//yq/toolchain:toolchain.bzl%yq_toolchains_repo", + "attributes": { + "user_repository_name": "yq" + } + } + }, + "recordedRepoMappingEntries": [] + } } }, "facts": {} diff --git a/etc/Build.sh b/etc/Build.sh index f52f0b229ff..7f57a82bd67 100755 --- a/etc/Build.sh +++ b/etc/Build.sh @@ -265,4 +265,4 @@ if [[ "$isNinja" == "yes" ]]; then exit 0 fi eval cmake "${cmakeOptions}" -B "${buildDir}" . -eval time cmake --build "${buildDir}" -j "${numThreads}" \ No newline at end of file +eval time cmake --build "${buildDir}" -j "${numThreads}" diff --git a/include/ord/OpenRoad.hh b/include/ord/OpenRoad.hh index 62ecb50221c..546d4803432 100644 --- a/include/ord/OpenRoad.hh +++ b/include/ord/OpenRoad.hh @@ -128,6 +128,10 @@ namespace est { class EstimateParasitics; } +namespace web { +class WebServer; +} + namespace ord { class dbVerilogNetwork; @@ -181,6 +185,7 @@ class OpenRoad { return estimate_parasitics_; } + web::WebServer* getWebServer() { return web_server_; } // Return the bounding box of the db rows. odb::Rect getCore(); @@ -284,6 +289,7 @@ class OpenRoad stt::SteinerTreeBuilder* stt_builder_ = nullptr; dft::Dft* dft_ = nullptr; est::EstimateParasitics* estimate_parasitics_ = nullptr; + web::WebServer* web_server_ = nullptr; utl::CallBackHandler* callback_handler_ = nullptr; int threads_ = 1; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 268fc023c58..c9f9758686b 100755 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -288,6 +288,7 @@ add_subdirectory(dft) add_subdirectory(mpl) add_subdirectory(par) add_subdirectory(est) +add_subdirectory(web) ################################################################ @@ -358,6 +359,7 @@ target_link_libraries(openroad mpl par est + web absl::synchronization ${ABC_LIBRARY} ${TCL_LIBRARY} @@ -437,6 +439,7 @@ if (Python3_FOUND AND BUILD_PYTHON) pdn_py dft_py par_py + web_py ) else() diff --git a/src/Main.cc b/src/Main.cc index 87e973320dc..4128cf433cc 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -64,6 +64,7 @@ using std::string; X(gpl) \ X(dpl) \ X(exa) \ + X(web) \ X(ppl) \ X(tap) \ X(cts) \ diff --git a/src/OpenRoad.cc b/src/OpenRoad.cc index d4d92bb59a1..7485feaf540 100644 --- a/src/OpenRoad.cc +++ b/src/OpenRoad.cc @@ -90,6 +90,8 @@ #include "utl/Progress.h" #include "utl/ScopedTemporaryFile.h" #include "utl/decode.h" +#include "web/MakeWeb.h" +#include "web/web.h" namespace ord { extern const char* ord_tcl_inits[]; @@ -146,6 +148,7 @@ OpenRoad::~OpenRoad() delete stt_builder_; delete dft_; delete estimate_parasitics_; + delete web_server_; delete logger_; delete verilog_reader_; delete callback_handler_; @@ -256,6 +259,7 @@ void OpenRoad::init(Tcl_Interp* tcl_interp, icewall_ = new pad::ICeWall(db_, logger_); dft_ = new dft::Dft(db_, sta_, logger_); example_ = new exa::Example(db_, logger_); + web_server_ = new web::WebServer(db_, sta_, logger_, tcl_interp); // Init components. Ord_Init(tcl_interp); @@ -295,6 +299,7 @@ void OpenRoad::init(Tcl_Interp* tcl_interp, stt::initSteinerTreeBuilder(tcl_interp); dft::initDft(tcl_interp); est::initTcl(tcl_interp); + web::initWeb(tcl_interp); // Import exported commands to global namespace. Tcl_Eval(tcl_interp, "sta::define_sta_cmds"); diff --git a/src/grt/CMakeLists.txt b/src/grt/CMakeLists.txt index ee4ae574158..26324df3f52 100644 --- a/src/grt/CMakeLists.txt +++ b/src/grt/CMakeLists.txt @@ -39,6 +39,7 @@ target_link_libraries(grt_lib ant_lib dpl_lib dbSta_lib + gui_heatmap_core stt_lib rsz_lib OpenSTA @@ -94,6 +95,7 @@ if (Python3_FOUND AND BUILD_PYTHON) dpl_lib dbSta gui + gui_heatmap_core stt OpenSTA Boost::boost diff --git a/src/grt/include/grt/GlobalRouter.h b/src/grt/include/grt/GlobalRouter.h index 7fecba98699..7ab4dcd04ce 100644 --- a/src/grt/include/grt/GlobalRouter.h +++ b/src/grt/include/grt/GlobalRouter.h @@ -55,6 +55,11 @@ class dbNetwork; class SpefWriter; } // namespace sta +namespace gui { +class HeatMapSourceRegistration; +using HeatMapSourceHandle = std::shared_ptr; +} // namespace gui + namespace grt { class FastRouteCore; @@ -69,7 +74,6 @@ class RoutePt; class AbstractGrouteRenderer; class AbstractFastRouteRenderer; class GlobalRouter; -class AbstractRoutingCongestionDataSource; class GRouteDbCbk; class Rudy; @@ -122,10 +126,8 @@ class GlobalRouter dpl::Opendp* opendp); ~GlobalRouter(); - void initGui(std::unique_ptr - routing_congestion_data_source, - std::unique_ptr - routing_congestion_data_source_rudy); + void initGui(gui::HeatMapSourceHandle routing_congestion_data_source, + gui::HeatMapSourceHandle routing_congestion_data_source_rudy); void clear(); @@ -564,8 +566,8 @@ class GlobalRouter RepairAntennas* repair_antennas_; Rudy* rudy_; - std::unique_ptr heatmap_; - std::unique_ptr heatmap_rudy_; + gui::HeatMapSourceHandle heatmap_; + gui::HeatMapSourceHandle heatmap_rudy_; // variables congestion report file const char* congestion_file_name_; diff --git a/src/grt/src/GlobalRouter.cpp b/src/grt/src/GlobalRouter.cpp index 1414a746290..e4c03b778bc 100644 --- a/src/grt/src/GlobalRouter.cpp +++ b/src/grt/src/GlobalRouter.cpp @@ -26,7 +26,6 @@ #include "AbstractFastRouteRenderer.h" #include "AbstractGrouteRenderer.h" -#include "AbstractRoutingCongestionDataSource.h" #include "CUGR.h" #include "DataType.h" #include "FastRoute.h" @@ -42,6 +41,7 @@ #include "grt/GRoute.h" #include "grt/PinGridLocation.h" #include "grt/Rudy.h" +#include "gui/heatMap.h" #include "odb/db.h" #include "odb/dbObject.h" #include "odb/dbSet.h" @@ -109,15 +109,12 @@ GlobalRouter::GlobalRouter(utl::Logger* logger, cugr_ = new CUGR(db_, logger_, callback_handler_, stt_builder_, sta_); } -void GlobalRouter::initGui(std::unique_ptr - routing_congestion_data_source, - std::unique_ptr - routing_congestion_data_source_rudy) +void GlobalRouter::initGui( + gui::HeatMapSourceHandle routing_congestion_data_source, + gui::HeatMapSourceHandle routing_congestion_data_source_rudy) { heatmap_ = std::move(routing_congestion_data_source); - heatmap_->registerHeatMap(); heatmap_rudy_ = std::move(routing_congestion_data_source_rudy); - heatmap_rudy_->registerHeatMap(); } void GlobalRouter::clear() @@ -479,7 +476,12 @@ void GlobalRouter::updateDbCongestion() } else { fastroute_->updateDbCongestion(min_layer, max_layer); } - heatmap_->update(); + if (heatmap_) { + heatmap_->invalidateInstances(); + } + if (heatmap_rudy_) { + heatmap_rudy_->invalidateInstances(); + } } int GlobalRouter::repairAntennas(odb::dbMTerm* diode_mterm, @@ -2433,7 +2435,12 @@ void GlobalRouter::readGuides(const char* file_name) updateEdgesUsage(); computeGCellGridPatternFromGuides(guides); updateDbCongestionFromGuides(); - heatmap_->update(); + if (heatmap_) { + heatmap_->invalidateInstances(); + } + if (heatmap_rudy_) { + heatmap_rudy_->invalidateInstances(); + } saveGuidesFromFile(guides); } @@ -2472,7 +2479,12 @@ void GlobalRouter::loadGuidesFromDB() if (block_->getGCellGrid() == nullptr) { updateDbCongestion(); } - heatmap_->update(); + if (heatmap_) { + heatmap_->invalidateInstances(); + } + if (heatmap_rudy_) { + heatmap_rudy_->invalidateInstances(); + } } void GlobalRouter::ensurePinsPositions(odb::dbNet* db_net) diff --git a/src/grt/src/MakeGlobalRouter.cpp b/src/grt/src/MakeGlobalRouter.cpp index ed777146be6..a9788b85a3b 100644 --- a/src/grt/src/MakeGlobalRouter.cpp +++ b/src/grt/src/MakeGlobalRouter.cpp @@ -23,8 +23,8 @@ extern const char* grt_tcl_inits[]; void initGui(grt::GlobalRouter* grt, odb::dbDatabase* db, utl::Logger* logger) { - grt->initGui(std::make_unique(logger, db), - std::make_unique(logger, grt, db)); + grt->initGui(grt::registerRoutingCongestionHeatMapSource(logger, db), + grt::registerRudyHeatMapSource(logger, grt, db)); } void initTcl(Tcl_Interp* tcl_interp) diff --git a/src/grt/src/heatMap.cpp b/src/grt/src/heatMap.cpp index 795dec667be..1c6afe214b7 100644 --- a/src/grt/src/heatMap.cpp +++ b/src/grt/src/heatMap.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include "gui/heatMap.h" @@ -14,6 +15,16 @@ namespace grt { +gui::HeatMapSourceHandle registerRoutingCongestionHeatMapSource( + utl::Logger* logger, + odb::dbDatabase* db) +{ + return gui::registerHeatMapSource( + "Routing Congestion", "Routing", "RoutingCongestion", [logger, db]() { + return std::make_shared(logger, db); + }); +} + RoutingCongestionDataSource::RoutingCongestionDataSource(utl::Logger* logger, odb::dbDatabase* db) : gui::GlobalRoutingDataSource(logger, diff --git a/src/grt/src/heatMap.h b/src/grt/src/heatMap.h index cab6be65c37..49bd81a7db2 100644 --- a/src/grt/src/heatMap.h +++ b/src/grt/src/heatMap.h @@ -5,21 +5,16 @@ #include -#include "AbstractRoutingCongestionDataSource.h" #include "gui/heatMap.h" #include "odb/db.h" namespace grt { -class RoutingCongestionDataSource : public gui::GlobalRoutingDataSource, - public AbstractRoutingCongestionDataSource +class RoutingCongestionDataSource : public gui::GlobalRoutingDataSource { public: RoutingCongestionDataSource(utl::Logger* logger, odb::dbDatabase* db); - void registerHeatMap() override { gui::HeatMapDataSource::registerHeatMap(); } - void update() override { gui::HeatMapDataSource::update(); } - protected: bool populateMap() override; void combineMapData(bool base_has_value, @@ -65,4 +60,8 @@ class RoutingCongestionDataSource : public gui::GlobalRoutingDataSource, double max_; }; +gui::HeatMapSourceHandle registerRoutingCongestionHeatMapSource( + utl::Logger* logger, + odb::dbDatabase* db); + } // namespace grt diff --git a/src/grt/src/heatMapRudy.cpp b/src/grt/src/heatMapRudy.cpp index 8d9d4269a9f..0902e272bdd 100644 --- a/src/grt/src/heatMapRudy.cpp +++ b/src/grt/src/heatMapRudy.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,16 @@ namespace grt { +gui::HeatMapSourceHandle registerRudyHeatMapSource(utl::Logger* logger, + grt::GlobalRouter* grouter, + odb::dbDatabase* db) +{ + return gui::registerHeatMapSource( + "Estimated Congestion (RUDY)", "RUDY", "RUDY", [logger, grouter, db]() { + return std::make_shared(logger, grouter, db); + }); +} + RUDYDataSource::RUDYDataSource(utl::Logger* logger, grt::GlobalRouter* grouter, odb::dbDatabase* db) diff --git a/src/grt/src/heatMapRudy.h b/src/grt/src/heatMapRudy.h index 226d45cdc04..3527d4a7aae 100644 --- a/src/grt/src/heatMapRudy.h +++ b/src/grt/src/heatMapRudy.h @@ -3,7 +3,6 @@ #pragma once -#include "AbstractRoutingCongestionDataSource.h" #include "grt/GlobalRouter.h" #include "grt/Rudy.h" #include "gui/heatMap.h" @@ -17,7 +16,6 @@ class dbDatabase; namespace grt { class RUDYDataSource : public gui::GlobalRoutingDataSource, - public AbstractRoutingCongestionDataSource, public odb::dbBlockCallBackObj { public: @@ -25,9 +23,6 @@ class RUDYDataSource : public gui::GlobalRoutingDataSource, grt::GlobalRouter* grouter, odb::dbDatabase* db); - void registerHeatMap() override { gui::HeatMapDataSource::registerHeatMap(); } - void update() override { gui::HeatMapDataSource::update(); } - void onShow() override; void onHide() override; @@ -60,4 +55,8 @@ class RUDYDataSource : public gui::GlobalRoutingDataSource, bool selection_only_; }; +gui::HeatMapSourceHandle registerRudyHeatMapSource(utl::Logger* logger, + grt::GlobalRouter* grouter, + odb::dbDatabase* db); + } // namespace grt diff --git a/src/gui/BUILD b/src/gui/BUILD index 9cf40e3845c..fbff148020c 100644 --- a/src/gui/BUILD +++ b/src/gui/BUILD @@ -17,20 +17,39 @@ package( cc_library( name = "gui", srcs = [ + "src/bufferTreeDescriptor.h", + "src/dbDescriptors.cpp", + "src/dbDescriptors.h", + "src/descriptor_registry.cpp", + "src/heatMapCore.cpp", + "src/heatMapPinDensity.cpp", + "src/heatMapPinDensity.h", + "src/heatMapPlacementDensity.cpp", + "src/heatMapPlacementDensity.h", + "src/init_descriptors.cpp", + "src/staDescriptors.cpp", + "src/staDescriptors.h", "src/stub.cpp", - "src/stub_heatMap.cpp", ":tcl", ], hdrs = [ "include/gui/MakeGui.h", + "include/gui/descriptor_registry.h", "include/gui/gui.h", "include/gui/heatMap.h", ], + copts = ["-Isrc/gui/src"], includes = ["include"], deps = [ + "//src/dbSta", + "//src/dbSta:dbNetwork", "//src/odb", + "//src/sta:opensta_lib", + "//src/utl", "@abseil-cpp//absl/synchronization", + "@boost.algorithm", "@boost.multi_array", + "@spdlog", "@tcl_lang//:tcl", ], ) @@ -60,6 +79,7 @@ QT_GUI_PLATFORMS = { ], hdrs = [ "include/gui/MakeGui.h", + "include/gui/descriptor_registry.h", "include/gui/gui.h", "include/gui/heatMap.h", ], diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 84f1d72801a..3fcffba00c6 100755 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -12,6 +12,50 @@ find_package(OpenGL REQUIRED) include("openroad") set(CMAKE_INCLUDE_CURRENT_DIR ON) +# Lightweight library containing only DescriptorRegistry, Property::toString, +# and Selected methods. Has no Qt, no dbDescriptors, no ord dependency. +# Used by the web module so its test doesn't pull in the full GUI. +add_library(gui_descriptors + src/descriptor_registry.cpp +) +target_link_libraries(gui_descriptors + PUBLIC + odb + PRIVATE + utl_lib +) +target_include_directories(gui_descriptors + PUBLIC + include + PRIVATE + ${OPENSTA_HOME}/include + ${DBSTA_HOME}/include +) + +add_library(gui_heatmap_core + src/heatMapCore.cpp + src/heatMapPinDensity.cpp + src/heatMapPlacementDensity.cpp +) +target_link_libraries(gui_heatmap_core + PUBLIC + odb + PRIVATE + absl::synchronization + dbSta_lib + utl_lib + Boost::boost + OpenSTA +) +target_include_directories(gui_heatmap_core + PUBLIC + include + PRIVATE + src + ${OPENSTA_HOME}/include + ${DBSTA_HOME}/include +) + if (Qt5_FOUND AND BUILD_GUI) message(STATUS "GUI is enabled") set(CMAKE_AUTOMOC ON) @@ -47,6 +91,7 @@ if (Qt5_FOUND AND BUILD_GUI) src/findDialog.cpp src/gotoDialog.cpp src/inspector.cpp + src/init_descriptors.cpp src/dbDescriptors.cpp src/staDescriptors.cpp src/highlightGroupDialog.cpp @@ -60,8 +105,6 @@ if (Qt5_FOUND AND BUILD_GUI) src/label.cpp src/heatMap.cpp src/heatMapSetup.cpp - src/heatMapPinDensity.cpp - src/heatMapPlacementDensity.cpp src/browserWidget.cpp src/globalConnectDialog.cpp src/insertBufferDialog.cpp @@ -76,6 +119,8 @@ if (Qt5_FOUND AND BUILD_GUI) resources/resource.qrc ) + target_compile_definitions(gui PRIVATE ENABLE_QT) + target_link_libraries(gui PUBLIC odb @@ -85,6 +130,9 @@ if (Qt5_FOUND AND BUILD_GUI) Qt5::Widgets Qt5::Charts OpenGL::GL + gui_descriptors + gui_heatmap_core + dbSta utl_lib Boost::boost OpenSTA @@ -110,11 +158,19 @@ else() endif() add_library(gui src/stub.cpp - src/stub_heatMap.cpp + src/init_descriptors.cpp + src/dbDescriptors.cpp + src/staDescriptors.cpp ) target_link_libraries(gui PUBLIC odb + PRIVATE + gui_descriptors + gui_heatmap_core + dbSta + utl_lib + OpenSTA ) target_include_directories(gui PRIVATE diff --git a/src/gui/include/gui/descriptor_registry.h b/src/gui/include/gui/descriptor_registry.h new file mode 100644 index 00000000000..ebcf119be6c --- /dev/null +++ b/src/gui/include/gui/descriptor_registry.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace odb { +class dbDatabase; +} + +namespace sta { +class dbSta; +} + +namespace utl { +class Logger; +} + +namespace gui { + +class Descriptor; +class Selected; + +class DescriptorRegistry +{ + public: + static DescriptorRegistry* instance(); + + void setLogger(utl::Logger* logger); + + // Register all standard descriptors (db + sta). + // Must be called after db/sta are initialized. + void initDescriptors(odb::dbDatabase* db, sta::dbSta* sta); + + template + void registerDescriptor(const Descriptor* descriptor) + { + registerDescriptor(typeid(T), descriptor); + } + + template + void unregisterDescriptor() + { + unregisterDescriptor(typeid(T)); + } + + template + const Descriptor* getDescriptor() const + { + return getDescriptor(typeid(T)); + } + + Selected makeSelected(const std::any& object); + + void registerDescriptor(const std::type_info& type, + const Descriptor* descriptor); + void unregisterDescriptor(const std::type_info& type); + const Descriptor* getDescriptor(const std::type_info& type) const; + + // Iterate over all registered descriptors (used by Gui::select) + template + void forEachDescriptor(Func&& func) const + { + for (auto& [type, descriptor] : descriptors_) { + func(descriptor.get()); + } + } + + private: + DescriptorRegistry() = default; + + // RTTI-safe hasher/comparator for std::type_index. + // Handles libstdc++ vs libc++ differences where the latter can + // produce different type_index values for the same type across + // compilation units. + struct TypeInfoHasher + { + std::size_t operator()(const std::type_index& x) const; + }; + struct TypeInfoComparator + { + bool operator()(const std::type_index& a, const std::type_index& b) const; + }; + + std::unordered_map, + TypeInfoHasher, + TypeInfoComparator> + descriptors_; + + utl::Logger* logger_ = nullptr; +}; + +} // namespace gui diff --git a/src/gui/include/gui/gui.h b/src/gui/include/gui/gui.h index c051ad4a618..03ea8e04173 100644 --- a/src/gui/include/gui/gui.h +++ b/src/gui/include/gui/gui.h @@ -39,9 +39,6 @@ class Logger; namespace gui { class HeatMapDataSource; -class PinDensityDataSource; -class PlacementDensityDataSource; -class PowerDensityDataSource; class Painter; class Selected; class Options; @@ -292,7 +289,7 @@ class Painter } double getPixelsPerDBU() { return pixels_per_dbu_; } - Options* getOptions() { return options_; } + Options* getOptions(); const odb::Rect& getBounds() { return bounds_; } protected: @@ -1033,45 +1030,16 @@ class Gui utl::Logger* logger_; odb::dbDatabase* db_; - // There are RTTI implementation differences between libstdc++ and libc++, - // where the latter seems to generate multiple typeids for classes including - // but not limited to sta::Instance* in different compile units. We have been - // unable to remedy this. - // - // These classes are a workaround such that unless __GLIBCXX__ is set, hashing - // and comparing are done on the type's name instead, which adds a negligible - // performance penalty but has the distinct advantage of not crashing when an - // Instance is clicked in the GUI. - // - // In the event the RTTI issue is ever resolved, the following two structs may - // be removed. - struct TypeInfoHasher - { - std::size_t operator()(const std::type_index& x) const; - }; - struct TypeInfoComparator - { - bool operator()(const std::type_index& a, const std::type_index& b) const; - }; - - // Maps types to descriptors - std::unordered_map, - TypeInfoHasher, - TypeInfoComparator> - descriptors_; // Heatmaps std::set heat_maps_; + std::map> heat_map_renderers_; + std::vector> owned_heat_maps_; // tcl commands needed to restore state std::vector tcl_state_commands_; std::set renderers_; - std::unique_ptr pin_density_heat_map_; - std::unique_ptr placement_density_heat_map_; - std::unique_ptr power_density_heat_map_; - std::vector> gifs_; static constexpr int kDefaultGifDelay = 250; diff --git a/src/gui/include/gui/heatMap.h b/src/gui/include/gui/heatMap.h index 41796b40873..e6175716579 100644 --- a/src/gui/include/gui/heatMap.h +++ b/src/gui/include/gui/heatMap.h @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -33,8 +35,7 @@ class Logger; } // namespace utl namespace gui { -class HeatMapRenderer; -class HeatMapSetup; +class HeatMapSourceRegistration; class HeatMapDataSource { @@ -92,8 +93,7 @@ class HeatMapDataSource virtual void setBlock(odb::dbBlock* block) { block_ = block; } void setUseDBU(bool use_dbu) { use_dbu_ = use_dbu; } - - HeatMapRenderer* getRenderer() { return renderer_.get(); } + bool getUseDBU() const { return use_dbu_; } const std::string& getName() const { return name_; } const std::string& getShortName() const { return short_name_; } @@ -164,6 +164,9 @@ class HeatMapDataSource void destroyMap(); const Map& getMap() const { return map_; } MapView getMapView(const odb::Rect& bounds); + std::vector getVisibleMap(const odb::Rect& bounds, + double pixels_per_dbu, + double min_pixels_per_bin = 2.0); bool isPopulated() const { return populated_; } bool hasData() const; @@ -179,6 +182,9 @@ class HeatMapDataSource virtual void onHide(); void redraw(); + void setRedrawCallback(std::function callback); + void setSetupCallback(std::function callback); + void setUnregisterCallback(std::function callback); protected: void addBooleanSetting(const std::string& name, @@ -256,37 +262,17 @@ class HeatMapDataSource std::vector map_x_grid_; std::vector map_y_grid_; - std::unique_ptr renderer_; - HeatMapSetup* setup_; - SpectrumGenerator color_generator_; std::vector color_lower_bounds_; std::vector settings_; - absl::Mutex ensure_mutex_; -}; - -class HeatMapRenderer : public Renderer -{ - public: - HeatMapRenderer(HeatMapDataSource& datasource); - - const char* getDisplayControlGroupName() override { return "Heat Maps"; } - - void drawObjects(Painter& painter) override; + std::function redraw_callback_; + std::function setup_callback_; + std::function unregister_callback_; - std::string getSettingsGroupName() override; - Settings getSettings() override; - void setSettings(const Settings& settings) override; - - private: - HeatMapDataSource& datasource_; - bool first_paint_; - - static constexpr char kDatasourcePrefix[] = "data#"; - static constexpr char kGroupnamePrefix[] = "HeatMap#"; + absl::Mutex ensure_mutex_; }; class RealValueHeatMapDataSource : public HeatMapDataSource @@ -382,4 +368,41 @@ class PowerDensityDataSource : public RealValueHeatMapDataSource sta::Scene* getScene() const; }; +class HeatMapSourceRegistration +{ + public: + using Factory = std::function()>; + + HeatMapSourceRegistration(std::string name, + std::string short_name, + std::string settings_group, + Factory factory); + + const std::string& getName() const { return name_; } + const std::string& getShortName() const { return short_name_; } + const std::string& getSettingsGroupName() const { return settings_group_; } + + std::shared_ptr createInstance() const; + void invalidateInstances() const; + + private: + std::string name_; + std::string short_name_; + std::string settings_group_; + Factory factory_; + mutable std::vector> instances_; + mutable std::mutex instances_mutex_; +}; + +using HeatMapSourceHandle = std::shared_ptr; + +HeatMapSourceHandle registerHeatMapSource( + const std::string& name, + const std::string& short_name, + const std::string& settings_group, + const HeatMapSourceRegistration::Factory& factory); +const std::vector& getRegisteredHeatMapSources(); +HeatMapSourceHandle findRegisteredHeatMapSource(const std::string& short_name); +void registerBuiltinHeatMapSources(sta::dbSta* sta, utl::Logger* logger); + } // namespace gui diff --git a/src/gui/src/dbDescriptors.cpp b/src/gui/src/dbDescriptors.cpp index dfc591e1cfd..f3689bb97a8 100644 --- a/src/gui/src/dbDescriptors.cpp +++ b/src/gui/src/dbDescriptors.cpp @@ -3,10 +3,12 @@ #include "dbDescriptors.h" +#ifdef ENABLE_QT #include #include #include #include +#endif #include #include #include @@ -32,7 +34,9 @@ #include "db_sta/dbNetwork.hh" #include "db_sta/dbSta.hh" #include "gui/gui.h" +#ifdef ENABLE_QT #include "insertBufferDialog.h" +#endif #include "odb/db.h" #include "odb/dbObject.h" #include "odb/dbShape.h" @@ -40,7 +44,9 @@ #include "odb/dbTypes.h" #include "odb/dbWireGraph.h" #include "odb/geom.h" +#ifdef ENABLE_QT #include "options.h" +#endif #include "sta/Liberty.hh" #include "sta/LibertyClass.hh" #include "sta/NetworkClass.hh" @@ -232,6 +238,7 @@ static void addTimingActions(T obj, }}); } +#ifdef ENABLE_QT // get list of tech layers as EditorOption list static void addLayersToOptions(odb::dbTech* tech, std::vector& options) @@ -274,6 +281,7 @@ static odb::dbTechLayer* getLayerSelection(odb::dbTech* tech, } return current; } +#endif ////////////////////////////////////////////////// @@ -1699,7 +1707,11 @@ void DbNetDescriptor::highlight(const std::any& object, Painter& painter) const bool draw_flywires = true; +#ifdef ENABLE_QT if (!painter.getOptions()->isFlywireHighlightOnly()) { +#else + { +#endif odb::dbWire* wire = net->getWire(); if (wire) { draw_flywires = false; @@ -2010,6 +2022,7 @@ Descriptor::Actions DbNetDescriptor::getActions(const std::any& object) const } } +#ifdef ENABLE_QT if (drivers <= 1) { actions.push_back( {"Insert Buffer", [this, net]() { @@ -2057,6 +2070,7 @@ Descriptor::Actions DbNetDescriptor::getActions(const std::any& object) const return makeSelected(net); }}); } +#endif return actions; } @@ -2895,30 +2909,31 @@ Descriptor::Actions DbObstructionDescriptor::getActions( const std::any& object) const { auto obs = std::any_cast(object); - return Actions( - {{"Copy to layer", - [obs]() { - odb::dbBox* box = obs->getBBox(); - odb::dbTechLayer* layer = getLayerSelection( - obs->getBlock()->getDataBase()->getTech(), box->getTechLayer()); - auto gui = gui::Gui::get(); - if (layer == nullptr) { - // select old layer again - return gui->makeSelected(obs); - } - auto new_obs = odb::dbObstruction::create(obs->getBlock(), - layer, - box->xMin(), - box->yMin(), - box->xMax(), - box->yMax()); - // does not copy other parameters - return gui->makeSelected(new_obs); - }}, - {"Delete", [obs]() { - odb::dbObstruction::destroy(obs); - return Selected(); // unselect since this object is now gone - }}}); + Actions actions; +#ifdef ENABLE_QT + actions.push_back( + {"Copy to layer", [obs]() { + odb::dbBox* box = obs->getBBox(); + odb::dbTechLayer* layer = getLayerSelection( + obs->getBlock()->getDataBase()->getTech(), box->getTechLayer()); + auto gui = gui::Gui::get(); + if (layer == nullptr) { + return gui->makeSelected(obs); + } + auto new_obs = odb::dbObstruction::create(obs->getBlock(), + layer, + box->xMin(), + box->yMin(), + box->xMax(), + box->yMax()); + return gui->makeSelected(new_obs); + }}); +#endif + actions.push_back({"Delete", [obs]() { + odb::dbObstruction::destroy(obs); + return Selected(); + }}); + return actions; } void DbObstructionDescriptor::visitAllObjects( diff --git a/src/gui/src/descriptor_registry.cpp b/src/gui/src/descriptor_registry.cpp new file mode 100644 index 00000000000..e508878a259 --- /dev/null +++ b/src/gui/src/descriptor_registry.cpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025, The OpenROAD Authors + +#include "gui/descriptor_registry.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __GNUC__ +#include +#endif + +#include + +#include "gui/gui.h" +#include "odb/geom.h" +#include "utl/Logger.h" + +namespace gui { + +// Helper to format doubles with %g (equivalent to fmt "{:g}") without +// requiring a dependency on the fmt library. +static std::string format_g(double v) +{ + char buf[64]; + std::snprintf(buf, sizeof(buf), "%g", v); + return buf; +} + +// Static member definitions for Descriptor::Property. +// These provide raw-integer defaults; the full GUI build overrides +// convert_dbu with a proper micron converter in MainWindow::postReadDb(). +DBUToString Descriptor::Property::convert_dbu + = [](int value, bool) { return std::to_string(value); }; +StringToDBU Descriptor::Property::convert_string + = [](const std::string& value, bool*) { return 0; }; + +DescriptorRegistry* DescriptorRegistry::instance() +{ + static DescriptorRegistry* registry = new DescriptorRegistry(); + return registry; +} + +void DescriptorRegistry::setLogger(utl::Logger* logger) +{ + logger_ = logger; +} + +void DescriptorRegistry::registerDescriptor(const std::type_info& type, + const Descriptor* descriptor) +{ + descriptors_[type] = std::unique_ptr(descriptor); +} + +const Descriptor* DescriptorRegistry::getDescriptor( + const std::type_info& type) const +{ + auto it = descriptors_.find(type); + if (it == descriptors_.end()) { + if (logger_) { + logger_->error( + utl::GUI, 53, "Unable to find descriptor for: {}", type.name()); + } + return nullptr; + } + return it->second.get(); +} + +void DescriptorRegistry::unregisterDescriptor(const std::type_info& type) +{ + descriptors_.erase(type); +} + +Selected DescriptorRegistry::makeSelected(const std::any& object) +{ + if (!object.has_value()) { + return Selected(); + } + + auto it = descriptors_.find(object.type()); + if (it != descriptors_.end()) { + return it->second->makeSelected(object); + } + +#ifdef __GNUC__ + char* type_name + = abi::__cxa_demangle(object.type().name(), nullptr, nullptr, nullptr); + if (logger_) { + logger_->warn( + utl::GUI, 33, "No descriptor is registered for type {}.", type_name); + } + free(type_name); +#else + if (logger_) { + logger_->warn(utl::GUI, + 112, + "No descriptor is registered for type {}.", + object.type().name()); + } +#endif + return Selected(); +} + +// See class header for documentation on why these exist. +std::size_t DescriptorRegistry::TypeInfoHasher::operator()( + const std::type_index& x) const +{ +#ifdef __GLIBCXX__ + return std::hash{}(x); +#else + return std::hash{}(std::string_view(x.name())); +#endif +} + +bool DescriptorRegistry::TypeInfoComparator::operator()( + const std::type_index& a, + const std::type_index& b) const +{ +#ifdef __GLIBCXX__ + return a == b; +#else + return strcmp(a.name(), b.name()) == 0; +#endif +} + +// ---- Property::toString (Qt-free) ---- + +std::string Descriptor::Property::toString(const std::any& value) +{ + if (auto v = std::any_cast(&value)) { + if (*v) { + return v->getName(); + } + } else if (auto v = std::any_cast(&value)) { + return *v; + } else if (auto v = std::any_cast(&value)) { + return *v; + } else if (auto v = std::any_cast(&value)) { + return std::to_string(*v); + } else if (auto v = std::any_cast(&value)) { + return std::to_string(*v); + } else if (auto v = std::any_cast(&value)) { + return format_g(*v); + } else if (auto v = std::any_cast(&value)) { + return format_g(*v); + } else if (auto v = std::any_cast(&value)) { + return *v ? "True" : "False"; + } else if (auto v = std::any_cast(&value)) { + std::string text = "("; + text += convert_dbu(v->xMin(), false) + ", "; + text += convert_dbu(v->yMin(), false) + "), ("; + text += convert_dbu(v->xMax(), false) + ", "; + text += convert_dbu(v->yMax(), false) + ")"; + return text; + } else if (auto v = std::any_cast(&value)) { + return "(" + convert_dbu(v->x(), false) + "," + convert_dbu(v->y(), false) + + ")"; + } + + return ""; +} + +// ---- Selected methods ---- + +Descriptor::Properties Selected::getProperties() const +{ + Descriptor::Properties props = descriptor_->getProperties(object_); + props.insert(props.begin(), {"Name", getName()}); + props.insert(props.begin(), {"Type", getTypeName()}); + odb::Rect bbox; + if (getBBox(bbox)) { + props.push_back({"BBox", bbox}); + props.push_back( + {"BBox Width, Height", + std::string("(") + Descriptor::Property::convert_dbu(bbox.dx(), false) + + ", " + Descriptor::Property::convert_dbu(bbox.dy(), false) + + ")"}); + } + + return props; +} + +void Selected::highlight(Painter& painter, + const Painter::Color& pen, + int pen_width, + const Painter::Color& brush, + const Painter::Brush& brush_style) const +{ + painter.setPen(pen, true, pen_width); + painter.setBrush(brush, brush_style); + descriptor_->highlight(object_, painter); +} + +// Selected::getActions() is defined in init_descriptors.cpp because it +// references Gui::get() / Gui::zoomTo(), which are not available in the +// lightweight gui_descriptors library. + +} // namespace gui diff --git a/src/gui/src/gui.cpp b/src/gui/src/gui.cpp index 3896d856350..5d62206e934 100644 --- a/src/gui/src/gui.cpp +++ b/src/gui/src/gui.cpp @@ -19,6 +19,8 @@ #include #include #include + +#include "gui/descriptor_registry.h" #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include #else @@ -36,8 +38,7 @@ #include "displayControls.h" #include "drcWidget.h" #include "gif.h" -#include "heatMapPinDensity.h" -#include "heatMapPlacementDensity.h" +#include "heatMapGui.h" #include "helpWidget.h" #include "inspector.h" #include "layoutViewer.h" @@ -59,6 +60,89 @@ extern char** cmd_argv; namespace gui { +// Default Options implementation for Painters without a real Options +// (e.g. the web viewer's ShapeCollector). Returns sensible defaults: +// everything visible, no exclusive modes. +class DefaultOptions : public Options +{ + public: + QColor background() override { return Qt::black; } + QColor color(const odb::dbTechLayer*) override { return Qt::white; } + Qt::BrushStyle pattern(const odb::dbTechLayer*) override + { + return Qt::SolidPattern; + } + QColor placementBlockageColor() override { return Qt::darkGray; } + Qt::BrushStyle placementBlockagePattern() override + { + return Qt::SolidPattern; + } + QColor regionColor() override { return Qt::darkGray; } + Qt::BrushStyle regionPattern() override { return Qt::SolidPattern; } + QColor instanceNameColor() override { return Qt::white; } + QFont instanceNameFont() override { return {}; } + QColor itermLabelColor() override { return Qt::white; } + QFont itermLabelFont() override { return {}; } + QColor siteColor(odb::dbSite*) override { return Qt::darkGray; } + bool isVisible(const odb::dbTechLayer*) override { return true; } + bool isSelectable(const odb::dbTechLayer*) override { return true; } + bool isNetVisible(odb::dbNet*) override { return true; } + bool isNetSelectable(odb::dbNet*) override { return true; } + bool isInstanceVisible(odb::dbInst*) override { return true; } + bool isInstanceSelectable(odb::dbInst*) override { return true; } + bool areInstanceNamesVisible() override { return true; } + bool areInstancePinsVisible() override { return true; } + bool areInstancePinsSelectable() override { return true; } + bool areInstancePinNamesVisible() override { return true; } + bool areInstanceBlockagesVisible() override { return true; } + bool areBlockagesVisible() override { return true; } + bool areBlockagesSelectable() override { return true; } + bool areObstructionsVisible() override { return true; } + bool areObstructionsSelectable() override { return true; } + bool areSitesVisible() override { return false; } + bool areSitesSelectable() override { return false; } + bool isSiteSelectable(odb::dbSite*) override { return false; } + bool isSiteVisible(odb::dbSite*) override { return false; } + bool arePrefTracksVisible() override { return false; } + bool areNonPrefTracksVisible() override { return false; } + bool areIOPinsVisible() const override { return true; } + bool areIOPinsSelectable() const override { return true; } + bool areIOPinNamesVisible() const override { return true; } + QFont ioPinMarkersFont() const override { return {}; } + bool areRoutingSegmentsVisible() const override { return true; } + bool areRoutingViasVisible() const override { return true; } + bool areSpecialRoutingSegmentsVisible() const override { return true; } + bool areSpecialRoutingViasVisible() const override { return true; } + bool areFillsVisible() const override { return true; } + QColor rulerColor() override { return Qt::cyan; } + QFont rulerFont() override { return {}; } + bool areRulersVisible() override { return true; } + bool areRulersSelectable() override { return true; } + QFont labelFont() override { return {}; } + bool areLabelsVisible() override { return true; } + bool areLabelsSelectable() override { return true; } + bool isDetailedVisibility() override { return false; } + bool areSelectedVisible() override { return true; } + bool isScaleBarVisible() const override { return false; } + bool areAccessPointsVisible() const override { return false; } + bool areRegionsVisible() const override { return true; } + bool areRegionsSelectable() const override { return true; } + bool isManufacturingGridVisible() const override { return false; } + bool isModuleView() const override { return false; } + bool isGCellGridVisible() const override { return false; } + bool isFlywireHighlightOnly() const override { return false; } + bool areFocusedNetsGuidesVisible() const override { return false; } +}; + +Options* Painter::getOptions() +{ + if (!options_) { + static DefaultOptions defaults; + return &defaults; + } + return options_; +} + static QApplication* application = nullptr; static void message_handler(QtMsgType type, const QMessageLogContext& context, @@ -126,80 +210,6 @@ static odb::dbBlock* getBlock(odb::dbDatabase* db) // This provides the link for Gui::redraw to the widget static gui::MainWindow* main_window = nullptr; -// Used by toString to convert dbu to microns (and back), will be set in -// main_window -DBUToString Descriptor::Property::convert_dbu; -StringToDBU Descriptor::Property::convert_string; - -// Heatmap / Spectrum colors -// https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html -// https://gist.github.com/mikhailov-work/6a308c20e494d9e0ccc29036b28faa7a -const unsigned char SpectrumGenerator::kSpectrum[256][3] - = {{48, 18, 59}, {50, 21, 67}, {51, 24, 74}, {52, 27, 81}, - {53, 30, 88}, {54, 33, 95}, {55, 36, 102}, {56, 39, 109}, - {57, 42, 115}, {58, 45, 121}, {59, 47, 128}, {60, 50, 134}, - {61, 53, 139}, {62, 56, 145}, {63, 59, 151}, {63, 62, 156}, - {64, 64, 162}, {65, 67, 167}, {65, 70, 172}, {66, 73, 177}, - {66, 75, 181}, {67, 78, 186}, {68, 81, 191}, {68, 84, 195}, - {68, 86, 199}, {69, 89, 203}, {69, 92, 207}, {69, 94, 211}, - {70, 97, 214}, {70, 100, 218}, {70, 102, 221}, {70, 105, 224}, - {70, 107, 227}, {71, 110, 230}, {71, 113, 233}, {71, 115, 235}, - {71, 118, 238}, {71, 120, 240}, {71, 123, 242}, {70, 125, 244}, - {70, 128, 246}, {70, 130, 248}, {70, 133, 250}, {70, 135, 251}, - {69, 138, 252}, {69, 140, 253}, {68, 143, 254}, {67, 145, 254}, - {66, 148, 255}, {65, 150, 255}, {64, 153, 255}, {62, 155, 254}, - {61, 158, 254}, {59, 160, 253}, {58, 163, 252}, {56, 165, 251}, - {55, 168, 250}, {53, 171, 248}, {51, 173, 247}, {49, 175, 245}, - {47, 178, 244}, {46, 180, 242}, {44, 183, 240}, {42, 185, 238}, - {40, 188, 235}, {39, 190, 233}, {37, 192, 231}, {35, 195, 228}, - {34, 197, 226}, {32, 199, 223}, {31, 201, 221}, {30, 203, 218}, - {28, 205, 216}, {27, 208, 213}, {26, 210, 210}, {26, 212, 208}, - {25, 213, 205}, {24, 215, 202}, {24, 217, 200}, {24, 219, 197}, - {24, 221, 194}, {24, 222, 192}, {24, 224, 189}, {25, 226, 187}, - {25, 227, 185}, {26, 228, 182}, {28, 230, 180}, {29, 231, 178}, - {31, 233, 175}, {32, 234, 172}, {34, 235, 170}, {37, 236, 167}, - {39, 238, 164}, {42, 239, 161}, {44, 240, 158}, {47, 241, 155}, - {50, 242, 152}, {53, 243, 148}, {56, 244, 145}, {60, 245, 142}, - {63, 246, 138}, {67, 247, 135}, {70, 248, 132}, {74, 248, 128}, - {78, 249, 125}, {82, 250, 122}, {85, 250, 118}, {89, 251, 115}, - {93, 252, 111}, {97, 252, 108}, {101, 253, 105}, {105, 253, 102}, - {109, 254, 98}, {113, 254, 95}, {117, 254, 92}, {121, 254, 89}, - {125, 255, 86}, {128, 255, 83}, {132, 255, 81}, {136, 255, 78}, - {139, 255, 75}, {143, 255, 73}, {146, 255, 71}, {150, 254, 68}, - {153, 254, 66}, {156, 254, 64}, {159, 253, 63}, {161, 253, 61}, - {164, 252, 60}, {167, 252, 58}, {169, 251, 57}, {172, 251, 56}, - {175, 250, 55}, {177, 249, 54}, {180, 248, 54}, {183, 247, 53}, - {185, 246, 53}, {188, 245, 52}, {190, 244, 52}, {193, 243, 52}, - {195, 241, 52}, {198, 240, 52}, {200, 239, 52}, {203, 237, 52}, - {205, 236, 52}, {208, 234, 52}, {210, 233, 53}, {212, 231, 53}, - {215, 229, 53}, {217, 228, 54}, {219, 226, 54}, {221, 224, 55}, - {223, 223, 55}, {225, 221, 55}, {227, 219, 56}, {229, 217, 56}, - {231, 215, 57}, {233, 213, 57}, {235, 211, 57}, {236, 209, 58}, - {238, 207, 58}, {239, 205, 58}, {241, 203, 58}, {242, 201, 58}, - {244, 199, 58}, {245, 197, 58}, {246, 195, 58}, {247, 193, 58}, - {248, 190, 57}, {249, 188, 57}, {250, 186, 57}, {251, 184, 56}, - {251, 182, 55}, {252, 179, 54}, {252, 177, 54}, {253, 174, 53}, - {253, 172, 52}, {254, 169, 51}, {254, 167, 50}, {254, 164, 49}, - {254, 161, 48}, {254, 158, 47}, {254, 155, 45}, {254, 153, 44}, - {254, 150, 43}, {254, 147, 42}, {254, 144, 41}, {253, 141, 39}, - {253, 138, 38}, {252, 135, 37}, {252, 132, 35}, {251, 129, 34}, - {251, 126, 33}, {250, 123, 31}, {249, 120, 30}, {249, 117, 29}, - {248, 114, 28}, {247, 111, 26}, {246, 108, 25}, {245, 105, 24}, - {244, 102, 23}, {243, 99, 21}, {242, 96, 20}, {241, 93, 19}, - {240, 91, 18}, {239, 88, 17}, {237, 85, 16}, {236, 83, 15}, - {235, 80, 14}, {234, 78, 13}, {232, 75, 12}, {231, 73, 12}, - {229, 71, 11}, {228, 69, 10}, {226, 67, 10}, {225, 65, 9}, - {223, 63, 8}, {221, 61, 8}, {220, 59, 7}, {218, 57, 7}, - {216, 55, 6}, {214, 53, 6}, {212, 51, 5}, {210, 49, 5}, - {208, 47, 5}, {206, 45, 4}, {204, 43, 4}, {202, 42, 4}, - {200, 40, 3}, {197, 38, 3}, {195, 37, 3}, {193, 35, 2}, - {190, 33, 2}, {188, 32, 2}, {185, 30, 2}, {183, 29, 2}, - {180, 27, 1}, {178, 26, 1}, {175, 24, 1}, {172, 23, 1}, - {169, 22, 1}, {167, 20, 1}, {164, 19, 1}, {161, 18, 1}, - {158, 16, 1}, {155, 15, 1}, {152, 14, 1}, {149, 13, 1}, - {146, 11, 1}, {142, 10, 1}, {139, 9, 2}, {136, 8, 2}, - {133, 7, 2}, {129, 6, 2}, {126, 5, 2}, {122, 4, 3}}; - static void resetConversions() { Descriptor::Property::convert_dbu @@ -215,12 +225,7 @@ Gui* Gui::get() return singleton; } -Gui::Gui() - : continue_after_close_(false), - logger_(nullptr), - db_(nullptr), - pin_density_heat_map_(nullptr), - placement_density_heat_map_(nullptr) +Gui::Gui() : continue_after_close_(false), logger_(nullptr), db_(nullptr) { resetConversions(); } @@ -274,20 +279,7 @@ void Gui::pause(int timeout) Selected Gui::makeSelected(const std::any& object) { - if (!object.has_value()) { - return Selected(); - } - - auto it = descriptors_.find(object.type()); - if (it != descriptors_.end()) { - return it->second->makeSelected(object); - } - char* type_name - = abi::__cxa_demangle(object.type().name(), nullptr, nullptr, nullptr); - logger_->warn( - utl::GUI, 33, "No descriptor is registered for type {}.", type_name); - free(type_name); - return Selected(); // FIXME: null descriptor + return DescriptorRegistry::instance()->makeSelected(object); } void Gui::setSelected(const Selected& selection) @@ -504,10 +496,14 @@ int Gui::select(const std::string& type, QRegExp::WildcardUnix); #endif const bool is_simple = isSimpleStringPattern(name_filter); - for (auto& [object_type, descriptor] : descriptors_) { - if (descriptor->getTypeName() != type) { - continue; + bool found = false; + int result = 0; + auto* registry = DescriptorRegistry::instance(); + registry->forEachDescriptor([&](const Descriptor* descriptor) { + if (found || descriptor->getTypeName() != type) { + return; } + found = true; SelectionSet selected_set; descriptor->visitAllObjects([&](const Selected& sel) { if (!name_filter.empty()) { @@ -551,11 +547,13 @@ int Gui::select(const std::string& type, main_window->addHighlighted(selected_set, highlight_group); } - // already found the descriptor, so return to exit loop - return selected_set.size(); - } + result = selected_set.size(); + }); - logger_->error(utl::GUI, 35, "Unable to find descriptor for: {}", type); + if (!found) { + logger_->error(utl::GUI, 35, "Unable to find descriptor for: {}", type); + } + return result; } bool Gui::filterSelectionProperties(const Descriptor::Properties& properties, @@ -952,8 +950,18 @@ void Gui::triggerAction(const std::string& name) void Gui::registerHeatMap(HeatMapDataSource* heatmap) { + if (heat_maps_.contains(heatmap)) { + return; + } heat_maps_.insert(heatmap); - registerRenderer(heatmap->getRenderer()); + auto renderer = makeHeatMapRenderer(*heatmap); + heatmap->setRedrawCallback( + [renderer_ptr = renderer.get()]() { renderer_ptr->redraw(); }); + heatmap->setSetupCallback([heatmap]() { showHeatMapSetupDialog(heatmap); }); + heatmap->setUnregisterCallback( + [this](HeatMapDataSource* source) { unregisterHeatMap(source); }); + registerRenderer(renderer.get()); + heat_map_renderers_[heatmap] = std::move(renderer); if (Gui::enabled()) { main_window->registerHeatMap(heatmap); } @@ -965,7 +973,14 @@ void Gui::unregisterHeatMap(HeatMapDataSource* heatmap) return; } - unregisterRenderer(heatmap->getRenderer()); + heatmap->setRedrawCallback({}); + heatmap->setSetupCallback({}); + heatmap->setUnregisterCallback({}); + auto renderer_itr = heat_map_renderers_.find(heatmap); + if (renderer_itr != heat_map_renderers_.end()) { + unregisterRenderer(renderer_itr->second.get()); + heat_map_renderers_.erase(renderer_itr); + } if (Gui::enabled()) { main_window->unregisterHeatMap(heatmap); } @@ -1067,7 +1082,7 @@ void Gui::setHeatMapSetting(const std::string& name, source->setSettings(settings); } - source->getRenderer()->redraw(); + source->redraw(); } Renderer::Setting Gui::getHeatMapSetting(const std::string& name, @@ -1180,29 +1195,6 @@ void Renderer::setSettings(const Renderer::Settings& settings) ////////////////////////////////////////////////////////////////// -SpectrumGenerator::SpectrumGenerator(double max_value) : scale_(1.0 / max_value) -{ -} - -int SpectrumGenerator::getColorCount() const -{ - return 256; -} - -Painter::Color SpectrumGenerator::getColor(double value, int alpha) const -{ - const int max_index = getColorCount() - 1; - int index = std::round(scale_ * value * max_index); - if (index < 0) { - index = 0; - } else if (index > max_index) { - index = max_index; - } - - return Painter::Color( - kSpectrum[index][0], kSpectrum[index][1], kSpectrum[index][2], alpha); -} - void SpectrumGenerator::drawLegend( Painter& painter, const std::vector>& legend_key) const @@ -1373,23 +1365,17 @@ void Gui::fit() void Gui::registerDescriptor(const std::type_info& type, const Descriptor* descriptor) { - descriptors_[type] = std::unique_ptr(descriptor); + DescriptorRegistry::instance()->registerDescriptor(type, descriptor); } const Descriptor* Gui::getDescriptor(const std::type_info& type) const { - auto find_descriptor = descriptors_.find(type); - if (find_descriptor == descriptors_.end()) { - logger_->error( - utl::GUI, 53, "Unable to find descriptor for: {}", type.name()); - } - - return find_descriptor->second.get(); + return DescriptorRegistry::instance()->getDescriptor(type); } void Gui::unregisterDescriptor(const std::type_info& type) { - descriptors_.erase(type); + DescriptorRegistry::instance()->unregisterDescriptor(type); } const Selected& Gui::getInspectorSelection() @@ -1510,16 +1496,23 @@ void Gui::init(odb::dbDatabase* db, sta::dbSta* sta, utl::Logger* logger) db_ = db; setLogger(logger); - pin_density_heat_map_ = std::make_unique(logger); - pin_density_heat_map_->registerHeatMap(); - - placement_density_heat_map_ - = std::make_unique(logger); - placement_density_heat_map_->registerHeatMap(); - - power_density_heat_map_ - = std::make_unique(sta, logger); - power_density_heat_map_->registerHeatMap(); + auto* registry = DescriptorRegistry::instance(); + registry->setLogger(logger); + registry->initDescriptors(db, sta); + registerBuiltinHeatMapSources(sta, logger); + for (const auto& source : getRegisteredHeatMapSources()) { + const bool already_registered = std::ranges::any_of( + heat_maps_, [&source](HeatMapDataSource* heatmap) { + return heatmap->getShortName() == source->getShortName(); + }); + if (already_registered) { + continue; + } + auto instance = source->createInstance(); + instance->setBlock(db->getChip() ? db->getChip()->getBlock() : nullptr); + registerHeatMap(instance.get()); + owned_heat_maps_.push_back(std::move(instance)); + } } void Gui::selectHelp(const std::string& item) @@ -1547,26 +1540,6 @@ void Gui::updateTimingReport() main_window->getTimingWidget()->populatePaths(); } -// See class header for documentation. -std::size_t Gui::TypeInfoHasher::operator()(const std::type_index& x) const -{ -#ifdef __GLIBCXX__ - return std::hash{}(x); -#else - return std::hash{}(std::string_view(x.name())); -#endif -} -// See class header for documentation. -bool Gui::TypeInfoComparator::operator()(const std::type_index& a, - const std::type_index& b) const -{ -#ifdef __GLIBCXX__ - return a == b; -#else - return strcmp(a.name(), b.name()) == 0; -#endif -} - int Gui::gifStart(const std::string& filename) { if (!enabled()) { @@ -1907,89 +1880,6 @@ int startGui(int& argc, return exit_code; } -void Selected::highlight(Painter& painter, - const Painter::Color& pen, - int pen_width, - const Painter::Color& brush, - const Painter::Brush& brush_style) const -{ - painter.setPen(pen, true, pen_width); - painter.setBrush(brush, brush_style); - - descriptor_->highlight(object_, painter); -} - -Descriptor::Properties Selected::getProperties() const -{ - Descriptor::Properties props = descriptor_->getProperties(object_); - props.insert(props.begin(), {"Name", getName()}); - props.insert(props.begin(), {"Type", getTypeName()}); - odb::Rect bbox; - if (getBBox(bbox)) { - props.push_back({"BBox", bbox}); - // convenience; the user may want to know the dimensions - props.push_back( - {"BBox Width, Height", - std::string("(") + Descriptor::Property::convert_dbu(bbox.dx(), false) - + ", " + Descriptor::Property::convert_dbu(bbox.dy(), false) - + ")"}); - } - - return props; -} - -Descriptor::Actions Selected::getActions() const -{ - auto actions = descriptor_->getActions(object_); - - odb::Rect bbox; - if (getBBox(bbox)) { - actions.push_back({"Zoom to", [this, bbox]() -> Selected { - auto gui = Gui::get(); - gui->zoomTo(bbox); - return *this; - }}); - } - - return actions; -} - -std::string Descriptor::Property::toString(const std::any& value) -{ - if (auto v = std::any_cast(&value)) { - if (*v) { - return v->getName(); - } - } else if (auto v = std::any_cast(&value)) { - return *v; - } else if (auto v = std::any_cast(&value)) { - return *v; - } else if (auto v = std::any_cast(&value)) { - return std::to_string(*v); - } else if (auto v = std::any_cast(&value)) { - return std::to_string(*v); - } else if (auto v = std::any_cast(&value)) { - return QString::number(*v).toStdString(); - } else if (auto v = std::any_cast(&value)) { - return QString::number(*v).toStdString(); - } else if (auto v = std::any_cast(&value)) { - return *v ? "True" : "False"; - } else if (auto v = std::any_cast(&value)) { - std::string text = "("; - text += convert_dbu(v->xMin(), false) + ", "; - text += convert_dbu(v->yMin(), false) + "), ("; - text += convert_dbu(v->xMax(), false) + ", "; - text += convert_dbu(v->yMax(), false) + ")"; - return text; - } else if (auto v = std::any_cast(&value)) { - std::string text = fmt::format( - "({},{})", convert_dbu(v->x(), false), convert_dbu(v->y(), false)); - return text; - } - - return ""; -} - // Tcl files encoded into strings. extern const char* gui_tcl_inits[]; diff --git a/src/gui/src/heatMap.cpp b/src/gui/src/heatMap.cpp index e74e3865c61..1a0e660abb6 100644 --- a/src/gui/src/heatMap.cpp +++ b/src/gui/src/heatMap.cpp @@ -1,1206 +1,168 @@ // SPDX-License-Identifier: BSD-3-Clause -// Copyright (c) 2021-2025, The OpenROAD Authors +// Copyright (c) 2021-2026, The OpenROAD Authors #include "gui/heatMap.h" -#include #include +#include #include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include #include #include -#include #include #include -#include #include -#include "absl/synchronization/mutex.h" -#include "db_sta/dbNetwork.hh" -#include "db_sta/dbSta.hh" #include "gui/gui.h" +#include "heatMapGui.h" #include "heatMapSetup.h" -#include "odb/db.h" -#include "sta/PowerClass.hh" -#include "utl/Logger.h" namespace gui { -HeatMapDataSource::HeatMapDataSource(utl::Logger* logger, - const std::string& name, - const std::string& short_name, - const std::string& settings_group) - : name_(name), - short_name_(short_name), - settings_group_(settings_group), - destroy_map_(true), - use_dbu_(false), - populated_(false), - colors_correct_(false), - issue_redraw_(true), - block_(nullptr), - logger_(logger), - grid_x_size_(10.0), - grid_y_size_(10.0), - display_range_min_(0.0), - display_range_max_(100.0), - draw_below_min_display_range_(false), - draw_above_max_display_range_(true), - color_alpha_(150), - log_scale_(false), - reverse_log_(false), - show_numbers_(false), - show_legend_(false), - use_selected_only_(false), - renderer_(std::make_unique(*this)), - setup_(nullptr), - color_generator_(SpectrumGenerator(100.0)) -{ - clearMap(); - - // ensure color map is initialized - updateMapColors(); -} - -HeatMapDataSource::~HeatMapDataSource() -{ - Gui::get()->unregisterHeatMap(this); -} +namespace { -void HeatMapDataSource::registerHeatMap() -{ - Gui::get()->registerHeatMap(this); -} - -void HeatMapDataSource::dumpToFile(const std::string& file) +class HeatMapRenderer : public Renderer { - ensureMap(); - - if (!isPopulated()) { - logger_->error(utl::GUI, 72, "\"{}\" is not populated with data.", name_); + public: + explicit HeatMapRenderer(HeatMapDataSource& datasource) + : datasource_(datasource), first_paint_(true) + { + addDisplayControl(datasource_.getName(), + false, + [this]() { datasource_.showSetup(); }, + {""}); } - std::ofstream csv(file); - if (!csv.is_open()) { - logger_->error(utl::GUI, 73, "Unable to open {}", file); - } - - const double dbu_to_micron = block_->getDbUnitsPerMicron(); + const char* getDisplayControlGroupName() override { return "Heat Maps"; } - csv << "x0,y0,x1,y1,value (" << getValueUnits() << ")\n"; - for (const auto& map_col : map_) { - for (const auto& map_value : map_col) { - if (!map_value->has_value) { - continue; + void drawObjects(Painter& painter) override + { + if (!checkDisplayControl(datasource_.getName())) { + if (!first_paint_) { + first_paint_ = true; + datasource_.onHide(); } - const odb::Rect& box_rect = map_value->rect; - const double scaled_value = convertPercentToValue(map_value->value); - - csv << std::defaultfloat << std::setprecision(4); - csv << box_rect.xMin() / dbu_to_micron << ","; - csv << box_rect.yMin() / dbu_to_micron << ","; - csv << box_rect.xMax() / dbu_to_micron << ","; - csv << box_rect.yMax() / dbu_to_micron << ","; - csv << std::scientific << std::setprecision(6); - csv << scaled_value << '\n'; - } - } - - csv.close(); -} - -void HeatMapDataSource::redraw() -{ - if (issue_redraw_) { - renderer_->redraw(); - } -} - -void HeatMapDataSource::setColorAlpha(int alpha) -{ - color_alpha_ - = boundValue(alpha, getColorAlphaMinimum(), getColorAlphaMaximum()); - updateMapColors(); - - redraw(); -} - -void HeatMapDataSource::setDisplayRange(double min, double max) -{ - if (max < min) { - std::swap(min, max); - } - - display_range_min_ = boundValue( - min, getDisplayRangeMinimumValue(), getDisplayRangeMaximumValue()); - display_range_max_ = boundValue( - max, getDisplayRangeMinimumValue(), getDisplayRangeMaximumValue()); - - updateMapColors(); - - redraw(); -} - -void HeatMapDataSource::setDrawBelowRangeMin(bool show) -{ - draw_below_min_display_range_ = show; - - redraw(); -} - -void HeatMapDataSource::setDrawAboveRangeMax(bool show) -{ - draw_above_max_display_range_ = show; - - redraw(); -} - -void HeatMapDataSource::setGridSizes(double x, double y) -{ - bool changed = false; - if (grid_x_size_ != x) { - grid_x_size_ = boundValue( - x, getGridSizeMinimumValue(), getGridSizeMaximumValue()); - changed = true; - } - if (grid_y_size_ != y) { - grid_y_size_ = boundValue( - y, getGridSizeMinimumValue(), getGridSizeMaximumValue()); - changed = true; - } - - if (changed) { - destroyMap(); - } -} - -void HeatMapDataSource::setLogScale(bool scale) -{ - log_scale_ = scale; - updateMapColors(); - - redraw(); -} - -void HeatMapDataSource::setReverseLogScale(bool reverse) -{ - reverse_log_ = reverse; - updateMapColors(); - - redraw(); -} - -void HeatMapDataSource::setShowNumbers(bool numbers) -{ - show_numbers_ = numbers; - - redraw(); -} - -void HeatMapDataSource::setShowLegend(bool legend) -{ - show_legend_ = legend; - - redraw(); -} - -Painter::Color HeatMapDataSource::getColor(double value) const -{ - auto find_val = std::ranges::find_if( - color_lower_bounds_, - - [value](const double other) { return other >= value; }); - const double color_index - = std::distance(color_lower_bounds_.begin(), find_val); - return color_generator_.getColor( - 100.0 * color_index / color_generator_.getColorCount(), color_alpha_); -} - -void HeatMapDataSource::showSetup() -{ - if (block_ == nullptr) { - return; - } - - if (setup_ == nullptr) { - setup_ = new HeatMapSetup(*this, - QString::fromStdString(name_), - use_dbu_, - block_->getDbUnitsPerMicron()); - - QObject::connect(setup_, &QDialog::finished, &QObject::deleteLater); - QObject::connect( - setup_, &QObject::destroyed, [this]() { setup_ = nullptr; }); - - setup_->show(); - } else { - setup_->raise(); - } -} - -std::string HeatMapDataSource::formatValue(double value, bool legend) const -{ - QString text; - text.setNum(value, 'f', 2); - if (legend) { - text += "%"; - } - return text.toStdString(); -} - -void HeatMapDataSource::addBooleanSetting( - const std::string& name, - const std::string& label, - const std::function& getter, - const std::function& setter) -{ - settings_.emplace_back(MapSettingBoolean{name, label, getter, setter}); -} - -void HeatMapDataSource::addMultipleChoiceSetting( - const std::string& name, - const std::string& label, - const std::function()>& choices, - const std::function& getter, - const std::function& setter) -{ - settings_.emplace_back( - MapSettingMultiChoice{name, label, choices, getter, setter}); -} - -Renderer::Settings HeatMapDataSource::getSettings() const -{ - Renderer::Settings settings{{"DisplayMin", display_range_min_}, - {"DisplayMax", display_range_max_}, - {"GridX", grid_x_size_}, - {"GridY", grid_y_size_}, - {"Alpha", color_alpha_}, - {"LogScale", log_scale_}, - {"ReverseLog", reverse_log_}, - {"ShowNumbers", show_numbers_}, - {"ShowLegend", show_legend_}, - {"UseSelectedOnly", use_selected_only_}}; - - for (const auto& setting : settings_) { - if (std::holds_alternative(setting)) { - const auto& set = std::get(setting); - settings[set.name] = set.getter(); - } else if (std::holds_alternative(setting)) { - const auto& set = std::get(setting); - settings[set.name] = set.getter(); - } - } - - return settings; -} - -void HeatMapDataSource::setSettings(const Renderer::Settings& settings) -{ - Renderer::setSetting(settings, "DisplayMin", display_range_min_); - Renderer::setSetting(settings, "DisplayMax", display_range_max_); - Renderer::setSetting(settings, "GridX", grid_x_size_); - Renderer::setSetting(settings, "GridY", grid_y_size_); - Renderer::setSetting(settings, "Alpha", color_alpha_); - Renderer::setSetting(settings, "LogScale", log_scale_); - Renderer::setSetting(settings, "ReverseLog", reverse_log_); - Renderer::setSetting(settings, "ShowNumbers", show_numbers_); - Renderer::setSetting(settings, "ShowLegend", show_legend_); - Renderer::setSetting(settings, "UseSelectedOnly", use_selected_only_); - - for (const auto& setting : settings_) { - if (std::holds_alternative(setting)) { - const auto& set = std::get(setting); - bool temp_value = set.getter(); - Renderer::setSetting(settings, set.name, temp_value); - set.setter(temp_value); - } else if (std::holds_alternative(setting)) { - const auto& set = std::get(setting); - std::string temp_value = set.getter(); - Renderer::setSetting(settings, set.name, temp_value); - set.setter(temp_value); - } - } - - // only reapply bounded value settings - setDisplayRange(display_range_min_, display_range_max_); - setGridSizes(grid_x_size_, grid_y_size_); - setColorAlpha(color_alpha_); -} - -std::set HeatMapDataSource::getSelectedInsts() const -{ - std::set selected_insts; - if (!useSelectedOnly() || !gui::Gui::enabled()) { - return selected_insts; - } - for (const gui::Selected& item : gui::Gui::get()->selection()) { - if (item.isInst()) { - selected_insts.insert(std::any_cast(item.getObject())); - } - } - return selected_insts; -} - -HeatMapDataSource::MapView HeatMapDataSource::getMapView( - const odb::Rect& bounds) -{ - const auto x_low_find = std::ranges::lower_bound(map_x_grid_, bounds.xMin()); - const auto x_high_find - = std::upper_bound(x_low_find, map_x_grid_.end(), bounds.xMax()); - const auto y_low_find = std::ranges::lower_bound(map_y_grid_, bounds.yMin()); - const auto y_high_find - = std::upper_bound(y_low_find, map_y_grid_.end(), bounds.yMax()); - - const int shape_x = static_cast(map_.shape()[0]); - const int shape_y = static_cast(map_.shape()[1]); - - const int x_low = std::max( - static_cast(std::distance(map_x_grid_.begin(), x_low_find)) - 1, 0); - const int x_high = std::min( - static_cast(std::distance(map_x_grid_.begin(), x_high_find)), - shape_x); - const int y_low = std::max( - static_cast(std::distance(map_y_grid_.begin(), y_low_find)) - 1, 0); - const int y_high = std::min( - static_cast(std::distance(map_y_grid_.begin(), y_high_find)), - shape_y); - - return map_[boost::indices[Map::index_range(x_low, x_high)] - [Map::index_range(y_low, y_high)]]; -} - -void HeatMapDataSource::addToMap(const odb::Rect& region, double value) -{ - for (const auto& map_col : getMapView(region)) { - for (const auto& map_pt : map_col) { - if (map_pt == nullptr) { - continue; - } - - odb::Rect intersection; - map_pt->rect.intersection(region, intersection); - - const double intersect_area = intersection.area(); - const double value_area = region.area(); - const double region_area = map_pt->rect.area(); - - combineMapData(map_pt->has_value, - map_pt->value, - value, - value_area, - intersect_area, - region_area); - map_pt->has_value = true; - - markColorsInvalid(); - } - } -} - -odb::Rect HeatMapDataSource::getBounds() const -{ - return getBlock()->getDieArea(); -} - -void HeatMapDataSource::clearMap() -{ - map_.resize(boost::extents[1][1]); - map_[0][0] = nullptr; - populated_ = false; -} - -bool HeatMapDataSource::setupMap() -{ - if (getBlock() == nullptr || getBlock()->getDieArea().area() == 0) { - return false; - } - - populateXYGrid(); - - const size_t x_grid_size = map_x_grid_.size() - 1; - const size_t y_grid_size = map_y_grid_.size() - 1; - - debugPrint(logger_, - utl::GUI, - "HeatMap", - 1, - "{} - Generating {}x{} map", - name_, - x_grid_size, - y_grid_size); - map_.resize(boost::extents[x_grid_size][y_grid_size]); - - const Painter::Color default_color = getColor(0); - for (size_t x = 0; x < x_grid_size; x++) { - const int x_min = map_x_grid_[x]; - const int x_max = map_x_grid_[x + 1]; - - for (size_t y = 0; y < y_grid_size; y++) { - const int y_min = map_y_grid_[y]; - const int y_max = map_y_grid_[y + 1]; - - auto map_pt = std::make_shared(); - map_pt->rect = odb::Rect(x_min, y_min, x_max, y_max); - map_pt->has_value = false; - map_pt->value = 0.0; - map_pt->color = default_color; - - map_[x][y] = std::move(map_pt); - } - } - - return true; -} - -void HeatMapDataSource::populateXYGrid() -{ - const int dx = getGridXSize() * getBlock()->getDbUnitsPerMicron(); - const int dy = getGridYSize() * getBlock()->getDbUnitsPerMicron(); - - const odb::Rect bounds = getBounds(); - - const int x_grid = std::ceil(bounds.dx() / static_cast(dx)); - const int y_grid = std::ceil(bounds.dy() / static_cast(dy)); - - std::vector x_grid_set, y_grid_set; - for (int x = 0; x < x_grid; x++) { - const int x_min = bounds.xMin() + x * dx; - const int x_max = std::min(x_min + dx, bounds.xMax()); - if (x == 0) { - x_grid_set.push_back(x_min); - } - x_grid_set.push_back(x_max); - } - for (int y = 0; y < y_grid; y++) { - const int y_min = bounds.yMin() + y * dy; - const int y_max = std::min(y_min + dy, bounds.yMax()); - if (y == 0) { - y_grid_set.push_back(y_min); - } - y_grid_set.push_back(y_max); - } - - setXYMapGrid(x_grid_set, y_grid_set); -} - -void HeatMapDataSource::setXYMapGrid(const std::vector& x_grid, - const std::vector& y_grid) -{ - // ensure sorted and uniqueness - const std::set x_grid_set(x_grid.begin(), x_grid.end()); - const std::set y_grid_set(y_grid.begin(), y_grid.end()); - - map_x_grid_.clear(); - map_y_grid_.clear(); - - map_x_grid_.insert(map_x_grid_.end(), x_grid_set.begin(), x_grid_set.end()); - map_y_grid_.insert(map_y_grid_.end(), y_grid_set.begin(), y_grid_set.end()); -} - -void HeatMapDataSource::destroyMap() -{ - if (destroy_map_) { - return; - } - - debugPrint( - logger_, utl::GUI, "HeatMap", 1, "{} - destroy map requested", name_); - - destroy_map_ = true; - - redraw(); -} - -bool HeatMapDataSource::hasData() const -{ - if (!populated_) { - return false; - } - - for (const auto& map_col : map_) { - for (const auto& map_pt : map_col) { - if (map_pt->has_value) { - return true; - } - } - } - - return false; -} - -void HeatMapDataSource::ensureMap() -{ - absl::MutexLock lock(&ensure_mutex_); - - if (destroy_map_) { - debugPrint(logger_, utl::GUI, "HeatMap", 1, "{} - Destroying map", name_); - clearMap(); - destroy_map_ = false; - } - - const bool build_map = map_[0][0] == nullptr; - if (build_map) { - debugPrint(logger_, utl::GUI, "HeatMap", 1, "{} - Setting up map", name_); - if (!setupMap()) { - debugPrint( - logger_, utl::GUI, "HeatMap", 1, "{} - No map available", name_); return; } - } - - if (build_map || !isPopulated()) { - debugPrint(logger_, utl::GUI, "HeatMap", 1, "{} - Populating map", name_); - - const bool update_cursor - = gui::Gui::enabled() - && QApplication::instance()->thread() == QThread::currentThread(); - - if (update_cursor) { - QApplication::setOverrideCursor(Qt::WaitCursor); - } - populated_ = populateMap(); - if (update_cursor) { - QApplication::restoreOverrideCursor(); - } - - if (isPopulated()) { - debugPrint( - logger_, utl::GUI, "HeatMap", 1, "{} - Correcting map scale", name_); - correctMapScale(map_); - } - - if (setup_ != nullptr) { - // announce changes - setIssueRedraw(false); - setup_->changed(); - setIssueRedraw(true); - } - } - - if (!colors_correct_ && isPopulated()) { - debugPrint( - logger_, utl::GUI, "HeatMap", 1, "{} - Assigning map colors", name_); - assignMapColors(); - } -} - -void HeatMapDataSource::updateMapColors() -{ - const int color_count = color_generator_.getColorCount(); - color_lower_bounds_.clear(); - color_lower_bounds_.resize(color_count + 1); - // generate ranges for colors - if (log_scale_) { - double range = display_range_max_; - if (display_range_min_ != 0.0) { - range = display_range_max_ / display_range_min_; - } - - const double step = std::pow(range, 1.0 / color_count); - - for (int i = 0; i <= color_count; i++) { - double start = display_range_max_ / std::pow(step, i); - if (i == color_generator_.getColorCount()) { - start = display_range_min_; - } - color_lower_bounds_[i] = start; - } - - if (reverse_log_) { - for (auto& lower_bound : color_lower_bounds_) { - lower_bound = display_range_max_ - lower_bound + display_range_min_; - } - } else { - std::ranges::reverse(color_lower_bounds_); - } - } else { - const double step = (display_range_max_ - display_range_min_) / color_count; - for (int i = 0; i <= color_count; i++) { - color_lower_bounds_[i] = display_range_min_ + i * step; - } - } - - markColorsInvalid(); -} - -void HeatMapDataSource::assignMapColors() -{ - for (const auto& map_col : map_) { - for (const auto& map_pt : map_col) { - map_pt->color = getColor(map_pt->value); - } - } - colors_correct_ = true; -} - -double HeatMapDataSource::getRealRangeMinimumValue() const -{ - return color_lower_bounds_[0]; -} - -double HeatMapDataSource::getRealRangeMaximumValue() const -{ - return color_lower_bounds_[color_lower_bounds_.size() - 1]; -} -std::vector> HeatMapDataSource::getLegendValues() const -{ - const int color_count = color_generator_.getColorCount(); - const int count = 6; - std::vector> values; - const double index_incr = static_cast(color_count) / (count - 1); - const double linear_start = getRealRangeMinimumValue(); - const double linear_step - = (getRealRangeMaximumValue() - linear_start) / (count - 1); - for (int i = 0; i < count; i++) { - int idx = std::round(i * index_incr); - idx = std::min(idx, color_count); - double value = color_lower_bounds_[idx]; - if (!log_scale_) { - value = linear_step * i + linear_start; + if (first_paint_) { + first_paint_ = false; + datasource_.onShow(); } - values.emplace_back(idx, value); - } - return values; -} - -void HeatMapDataSource::onShow() -{ - if (!isPopulated()) { - logger_->warn(utl::GUI, - 66, - "Heat map \"{}\" has not been populated with data.", - getName()); - } -} - -void HeatMapDataSource::onHide() -{ - if (destroyMapOnNotVisible()) { - setIssueRedraw(false); - destroyMap(); - setIssueRedraw(true); - } -} - -/////////// - -HeatMapRenderer::HeatMapRenderer(HeatMapDataSource& datasource) - : datasource_(datasource), first_paint_(true) -{ - addDisplayControl(datasource_.getName(), - false, - [this]() { datasource_.showSetup(); }, - {""}); // mutually exclusive to all -} + for (const auto& map_point : datasource_.getVisibleMap( + painter.getBounds(), painter.getPixelsPerDBU())) { + painter.setPen(map_point.color, true); + painter.setBrush(map_point.color); + painter.drawRect(map_point.rect); -void HeatMapRenderer::drawObjects(Painter& painter) -{ - if (!checkDisplayControl(datasource_.getName())) { - if (!first_paint_) { - first_paint_ = true; // reset check - // first time so announce onHide - datasource_.onHide(); - } - return; - } - - datasource_.ensureMap(); - - if (first_paint_) { - first_paint_ = false; - // first time so announce onShow - datasource_.onShow(); - } - - if (!datasource_.isPopulated()) { - // nothing to paint - return; - } - - const bool show_numbers = datasource_.getShowNumbers(); - const double min_value = datasource_.getRealRangeMinimumValue(); - const double max_value = datasource_.getRealRangeMaximumValue(); - const bool show_mins = datasource_.getDrawBelowRangeMin(); - const bool show_maxs = datasource_.getDrawAboveRangeMax(); - - const odb::Rect& bounds = painter.getBounds(); - - // minimum box size is 2 pixels - const double min_dbu = 2.0 / painter.getPixelsPerDBU(); - - const double dbu_per_micron = datasource_.getBlock()->getDbUnitsPerMicron(); - const int x_scale - = std::ceil(min_dbu / (datasource_.getGridXSize() * dbu_per_micron)); - const int y_scale - = std::ceil(min_dbu / (datasource_.getGridYSize() * dbu_per_micron)); - - const HeatMapDataSource::MapView map_view = datasource_.getMapView(bounds); - const int x_size = map_view.shape()[0]; - const int y_size = map_view.shape()[1]; - - for (int x = 0; x < x_size; x += x_scale) { - for (int y = 0; y < y_size; y += y_scale) { - HeatMapDataSource::MapColor draw_pt; - draw_pt.rect.mergeInit(); - draw_pt.has_value = false; - draw_pt.value = std::numeric_limits::lowest(); - - for (int x_sub = 0; x_sub < x_scale; x_sub++) { - const int x_idx = x + x_sub; - if (x_idx >= x_size) { - continue; - } - - for (int y_sub = 0; y_sub < y_scale; y_sub++) { - const int y_idx = y + y_sub; - if (y_idx >= y_size) { - continue; - } - - const auto& map_pt = map_view[x_idx][y_idx]; - draw_pt.rect.merge(map_pt->rect); - if (!map_pt->has_value) { // value not set so nothing to do - continue; - } - - if (draw_pt.value < map_pt->value) { - draw_pt.has_value = true; - draw_pt.value = map_pt->value; - draw_pt.color = map_pt->color; - } - } - } - - if (!draw_pt.has_value) { // value not set so nothing to draw - continue; - } - if (!show_mins && draw_pt.value < min_value) { - continue; - } - if (!show_maxs && draw_pt.value > max_value) { - continue; - } - - painter.setPen(draw_pt.color, true); - painter.setBrush(draw_pt.color); - - painter.drawRect(draw_pt.rect); - - if (show_numbers) { - const int x = draw_pt.rect.xCenter(); - const int y = draw_pt.rect.yCenter(); + if (datasource_.getShowNumbers()) { + const int x = map_point.rect.xCenter(); + const int y = map_point.rect.yCenter(); const Painter::Anchor text_anchor = Painter::Anchor::kCenter; const double text_rect_margin = 0.8; - const std::string text = datasource_.formatValue(draw_pt.value, false); + const std::string text + = datasource_.formatValue(map_point.value, false); const odb::Rect text_bound = painter.stringBoundaries(x, y, text_anchor, text); - bool draw = true; - if (text_bound.dx() >= text_rect_margin * draw_pt.rect.dx() - || text_bound.dy() >= text_rect_margin * draw_pt.rect.dy()) { - // don't draw if text will be too small - draw = false; - } - - if (draw) { + if (text_bound.dx() < text_rect_margin * map_point.rect.dx() + && text_bound.dy() < text_rect_margin * map_point.rect.dy()) { painter.setPen(Painter::kWhite, true); painter.drawString(x, y, text_anchor, text); } } } - } - // legend - if (datasource_.getShowLegend()) { - std::vector> legend; - for (const auto& [color_index, color_value] : - datasource_.getLegendValues()) { - legend.emplace_back(color_index, - datasource_.formatValue(color_value, true)); + if (datasource_.getShowLegend()) { + std::vector> legend; + for (const auto& [color_index, color_value] : + datasource_.getLegendValues()) { + legend.emplace_back(color_index, + datasource_.formatValue(color_value, true)); + } + datasource_.getColorGenerator().drawLegend(painter, legend); } - - datasource_.getColorGenerator().drawLegend(painter, legend); } -} -std::string HeatMapRenderer::getSettingsGroupName() -{ - return kGroupnamePrefix + datasource_.getSettingsGroupName(); -} - -Renderer::Settings HeatMapRenderer::getSettings() -{ - Renderer::Settings settings = Renderer::getSettings(); - for (const auto& [name, value] : datasource_.getSettings()) { - settings[kDatasourcePrefix + name] = value; + std::string getSettingsGroupName() override + { + return kGroupnamePrefix + datasource_.getSettingsGroupName(); } - return settings; -} -void HeatMapRenderer::setSettings(const Settings& settings) -{ - Renderer::setSettings(settings); - Renderer::Settings data_settings; - for (const auto& [name, value] : settings) { - if (name.starts_with(kDatasourcePrefix)) { - data_settings[name.substr(strlen(kDatasourcePrefix))] = value; + Settings getSettings() override + { + Settings settings = Renderer::getSettings(); + for (const auto& [name, value] : datasource_.getSettings()) { + settings[kDatasourcePrefix + name] = value; } + return settings; } - datasource_.setSettings(data_settings); -} -////////// - -RealValueHeatMapDataSource::RealValueHeatMapDataSource( - utl::Logger* logger, - const std::string& unit_suffix, - const std::string& name, - const std::string& short_name, - const std::string& settings_group) - : HeatMapDataSource(logger, name, short_name, settings_group), - unit_suffix_(unit_suffix), - units_(unit_suffix_), - min_(0.0), - max_(0.0), - scale_(1.0) -{ -} - -void RealValueHeatMapDataSource::correctMapScale(HeatMapDataSource::Map& map) -{ - determineMinMax(map); - determineUnits(); - - for (const auto& map_col : map) { - for (const auto& map_pt : map_col) { - map_pt->value = convertValueToPercent(map_pt->value); - } - } - - min_ = roundData(min_ * scale_); - max_ = roundData(max_ * scale_); - - // reset since all data has been scaled by the appropriate amount - scale_ = 1.0; -} - -double RealValueHeatMapDataSource::roundData(double value) const -{ - const double precision = 1000.0; - return std::round(value * precision) / precision; -} - -void RealValueHeatMapDataSource::determineMinMax( - const HeatMapDataSource::Map& map) -{ - min_ = std::numeric_limits::max(); - max_ = std::numeric_limits::lowest(); - - for (const auto& map_col : map) { - for (const auto& map_pt : map_col) { - min_ = std::min(min_, map_pt->value); - max_ = std::max(max_, map_pt->value); + void setSettings(const Settings& settings) override + { + Renderer::setSettings(settings); + Settings data_settings; + for (const auto& [name, value] : settings) { + if (name.starts_with(kDatasourcePrefix)) { + data_settings[name.substr(strlen(kDatasourcePrefix))] = value; + } } + datasource_.setSettings(data_settings); } -} - -void RealValueHeatMapDataSource::determineUnits() -{ - const double range = getValueRange(); - if (range >= 1.0 || range == 0) { - units_ = ""; - scale_ = 1.0; - } else if (range > 1e-3) { - units_ = "m"; - scale_ = 1e3; - } else if (range > 1e-6) { - units_ = "μ"; - scale_ = 1e6; - } else if (range > 1e-9) { - units_ = "n"; - scale_ = 1e9; - } else if (range > 1e-12) { - units_ = "p"; - scale_ = 1e12; - } else { - units_ = "f"; - scale_ = 1e15; - } - - units_ += unit_suffix_; -} -std::string RealValueHeatMapDataSource::formatValue(double value, - bool legend) const -{ - int digits = legend ? 3 : 2; + private: + HeatMapDataSource& datasource_; + bool first_paint_; - QString text; - text.setNum(convertPercentToValue(value), 'f', digits); - if (legend) { - text += QString::fromStdString(getValueUnits()); - } - return text.toStdString(); -} + static constexpr char kDatasourcePrefix[] = "data#"; + static constexpr char kGroupnamePrefix[] = "HeatMap#"; +}; -std::string RealValueHeatMapDataSource::getValueUnits() const -{ - return units_; -} +using SetupMap = std::map; -double RealValueHeatMapDataSource::getValueRange() const +SetupMap& activeSetups() { - double range = max_ - min_; - if (range == 0.0) { - range = 1.0; // dummy numbers until drops has been populated - } - return range; -} - -double RealValueHeatMapDataSource::convertValueToPercent(double value) const -{ - const double range = getValueRange(); - const double offset = min_; - - return roundData(100.0 * (value - offset) / range); -} - -double RealValueHeatMapDataSource::convertPercentToValue(double percent) const -{ - const double range = getValueRange(); - const double offset = min_; - - return roundData(percent * range / 100.0 + offset); -} - -double RealValueHeatMapDataSource::getDisplayRangeIncrement() const -{ - return getValueRange() / 100.0; -} - -/////////////////////////// - -GlobalRoutingDataSource::GlobalRoutingDataSource( - utl::Logger* logger, - const std::string& name, - const std::string& short_name, - const std::string& settings_group) - : HeatMapDataSource(logger, name, short_name, settings_group) -{ -} - -std::pair GlobalRoutingDataSource::getReportableXYGrid() const -{ - if (getBlock() == nullptr) { - return {kDefaultGrid, kDefaultGrid}; - } - - auto* gcell_grid = getBlock()->getGCellGrid(); - if (gcell_grid == nullptr) { - return {kDefaultGrid, kDefaultGrid}; - } - - auto grid_mode = [gcell_grid](int num_grids, - void (odb::dbGCellGrid::*get_grid)( - int, int&, int&, int&)) -> int { - std::map grid_pitch_count; - for (int i = 0; i < num_grids; i++) { - int origin, count, step; - (gcell_grid->*get_grid)(i, origin, count, step); - grid_pitch_count[step] += count; - } - - if (grid_pitch_count.empty()) { - return kDefaultGrid; - } - - auto mode = grid_pitch_count.begin(); - for (auto check_mode = grid_pitch_count.begin(); - check_mode != grid_pitch_count.end(); - check_mode++) { - if (mode->second < check_mode->second) { - mode = check_mode; - } - } - return mode->first; - }; - - const double x_grid = grid_mode(gcell_grid->getNumGridPatternsX(), - &odb::dbGCellGrid::getGridPatternX); - const double y_grid = grid_mode(gcell_grid->getNumGridPatternsY(), - &odb::dbGCellGrid::getGridPatternY); - - const double dbus = getBlock()->getDbUnitsPerMicron(); - return {x_grid / dbus, y_grid / dbus}; + static SetupMap setups; + return setups; } -double GlobalRoutingDataSource::getGridXSize() const -{ - const auto& [x, y] = getReportableXYGrid(); - return x; -} +} // namespace -double GlobalRoutingDataSource::getGridYSize() const +std::unique_ptr makeHeatMapRenderer(HeatMapDataSource& datasource) { - const auto& [x, y] = getReportableXYGrid(); - return y; + return std::make_unique(datasource); } -void GlobalRoutingDataSource::populateXYGrid() +void showHeatMapSetupDialog(HeatMapDataSource* source) { - if (getBlock() == nullptr) { - HeatMapDataSource::populateXYGrid(); + if (source == nullptr || source->getBlock() == nullptr) { return; } - auto* gcell_grid = getBlock()->getGCellGrid(); - if (gcell_grid == nullptr) { - HeatMapDataSource::populateXYGrid(); + auto& setups = activeSetups(); + if (auto found = setups.find(source); + found != setups.end() && found->second != nullptr) { + found->second->raise(); return; } - std::vector gcell_xgrid, gcell_ygrid; - gcell_grid->getGridX(gcell_xgrid); - gcell_grid->getGridY(gcell_ygrid); - - const auto die_area = getBlock()->getDieArea(); - gcell_xgrid.push_back(die_area.xMax()); - gcell_ygrid.push_back(die_area.yMax()); - - setXYMapGrid(gcell_xgrid, gcell_ygrid); -} - -PowerDensityDataSource::PowerDensityDataSource(sta::dbSta* sta, - utl::Logger* logger) - : gui::RealValueHeatMapDataSource(logger, - "W", - "Power Density", - "Power", - "PowerDensity"), - sta_(sta) -{ - setIssueRedraw(false); // disable during initial setup - setLogScale(true); - setIssueRedraw(true); + auto* setup = new HeatMapSetup(*source, + QString::fromStdString(source->getName()), + source->getUseDBU(), + source->getBlock()->getDbUnitsPerMicron()); + setups[source] = setup; - addMultipleChoiceSetting( - "Scene", - "Scene:", - [this]() { - std::vector scenes; - for (auto* scene : sta_->scenes()) { - scenes.emplace_back(scene->name()); - } - return scenes; - }, - [this]() -> std::string { return scene_; }, - [this](const std::string& value) { scene_ = value; }); - addBooleanSetting( - "Internal", - "Internal power:", - [this]() { return include_internal_; }, - [this](bool value) { include_internal_ = value; }); - addBooleanSetting( - "Leakage", - "Leakage power:", - [this]() { return include_leakage_; }, - [this](bool value) { include_leakage_ = value; }); - addBooleanSetting( - "Switching", - "Switching power:", - [this]() { return include_switching_; }, - [this](bool value) { include_switching_ = value; }); - - registerHeatMap(); + QObject::connect(setup, &QDialog::finished, setup, &QObject::deleteLater); + QObject::connect( + setup, &QObject::destroyed, [source]() { activeSetups().erase(source); }); + setup->show(); } -bool PowerDensityDataSource::populateMap() -{ - if (getBlock() == nullptr || sta_ == nullptr) { - return false; - } - - if (sta_->cmdNetwork() == nullptr) { - return false; - } - - auto* network = sta_->getDbNetwork(); - - // Collect selected instances if filter is enabled - const std::set selected_insts = getSelectedInsts(); - const bool filter = !selected_insts.empty(); - - const bool include_all - = include_internal_ && include_leakage_ && include_switching_; - for (auto* inst : getBlock()->getInsts()) { - if (!inst->getPlacementStatus().isPlaced()) { - continue; - } - if (filter && selected_insts.find(inst) == selected_insts.end()) { - continue; - } - - sta::PowerResult power = sta_->power(network->dbToSta(inst), getScene()); - - float pwr = 0.0; - if (include_all) { - pwr = power.total(); - } else { - if (include_internal_) { - pwr += power.internal(); - } - if (include_leakage_) { - pwr += power.switching(); - } - if (include_switching_) { - pwr += power.leakage(); - } - } - - odb::Rect inst_box = inst->getBBox()->getBox(); - - addToMap(inst_box, pwr); - } - - return true; -} - -void PowerDensityDataSource::combineMapData(bool base_has_value, - double& base, - const double new_data, - const double data_area, - const double intersection_area, - const double rect_area) -{ - base += (new_data / data_area) * intersection_area; -} - -sta::Scene* PowerDensityDataSource::getScene() const +void HeatMapDataSource::registerHeatMap() { - auto* scene = sta_->findScene(scene_); - if (scene != nullptr) { - return scene; - } - - auto scenes = sta_->scenes(); - if (!scenes.empty()) { - return scenes[0]; - } - - return nullptr; + Gui::get()->registerHeatMap(this); } } // namespace gui diff --git a/src/gui/src/heatMapCore.cpp b/src/gui/src/heatMapCore.cpp new file mode 100644 index 00000000000..3a5a21ca09f --- /dev/null +++ b/src/gui/src/heatMapCore.cpp @@ -0,0 +1,1300 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2021-2026, The OpenROAD Authors + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/synchronization/mutex.h" +#include "db_sta/dbNetwork.hh" +#include "db_sta/dbSta.hh" +#include "gui/heatMap.h" +#include "heatMapPinDensity.h" +#include "heatMapPlacementDensity.h" +#include "odb/db.h" +#include "sta/PowerClass.hh" +#include "utl/Logger.h" + +namespace gui { + +// Heatmap / Spectrum colors +// https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html +// https://gist.github.com/mikhailov-work/6a308c20e494d9e0ccc29036b28faa7a +const unsigned char SpectrumGenerator::kSpectrum[256][3] + = {{48, 18, 59}, {50, 21, 67}, {51, 24, 74}, {52, 27, 81}, + {53, 30, 88}, {54, 33, 95}, {55, 36, 102}, {56, 39, 109}, + {57, 42, 115}, {58, 45, 121}, {59, 47, 128}, {60, 50, 134}, + {61, 53, 139}, {62, 56, 145}, {63, 59, 151}, {63, 62, 156}, + {64, 64, 162}, {65, 67, 167}, {65, 70, 172}, {66, 73, 177}, + {66, 75, 181}, {67, 78, 186}, {68, 81, 191}, {68, 84, 195}, + {68, 86, 199}, {69, 89, 203}, {69, 92, 207}, {69, 94, 211}, + {70, 97, 214}, {70, 100, 218}, {70, 102, 221}, {70, 105, 224}, + {70, 107, 227}, {71, 110, 230}, {71, 113, 233}, {71, 115, 235}, + {71, 118, 238}, {71, 120, 240}, {71, 123, 242}, {70, 125, 244}, + {70, 128, 246}, {70, 130, 248}, {70, 133, 250}, {70, 135, 251}, + {69, 138, 252}, {69, 140, 253}, {68, 143, 254}, {67, 145, 254}, + {66, 148, 255}, {65, 150, 255}, {64, 153, 255}, {62, 155, 254}, + {61, 158, 254}, {59, 160, 253}, {58, 163, 252}, {56, 165, 251}, + {55, 168, 250}, {53, 171, 248}, {51, 173, 247}, {49, 175, 245}, + {47, 178, 244}, {46, 180, 242}, {44, 183, 240}, {42, 185, 238}, + {40, 188, 235}, {39, 190, 233}, {37, 192, 231}, {35, 195, 228}, + {34, 197, 226}, {32, 199, 223}, {31, 201, 221}, {30, 203, 218}, + {28, 205, 216}, {27, 208, 213}, {26, 210, 210}, {26, 212, 208}, + {25, 213, 205}, {24, 215, 202}, {24, 217, 200}, {24, 219, 197}, + {24, 221, 194}, {24, 222, 192}, {24, 224, 189}, {25, 226, 187}, + {25, 227, 185}, {26, 228, 182}, {28, 230, 180}, {29, 231, 178}, + {31, 233, 175}, {32, 234, 172}, {34, 235, 170}, {37, 236, 167}, + {39, 238, 164}, {42, 239, 161}, {44, 240, 158}, {47, 241, 155}, + {50, 242, 152}, {53, 243, 148}, {56, 244, 145}, {60, 245, 142}, + {63, 246, 138}, {67, 247, 135}, {70, 248, 132}, {74, 248, 128}, + {78, 249, 125}, {82, 250, 122}, {85, 250, 118}, {89, 251, 115}, + {93, 252, 111}, {97, 252, 108}, {101, 253, 105}, {105, 253, 102}, + {109, 254, 98}, {113, 254, 95}, {117, 254, 92}, {121, 254, 89}, + {125, 255, 86}, {128, 255, 83}, {132, 255, 81}, {136, 255, 78}, + {139, 255, 75}, {143, 255, 73}, {146, 255, 71}, {150, 254, 68}, + {153, 254, 66}, {156, 254, 64}, {159, 253, 63}, {161, 253, 61}, + {164, 252, 60}, {167, 252, 58}, {169, 251, 57}, {172, 251, 56}, + {175, 250, 55}, {177, 249, 54}, {180, 248, 54}, {183, 247, 53}, + {185, 246, 53}, {188, 245, 52}, {190, 244, 52}, {193, 243, 52}, + {195, 241, 52}, {198, 240, 52}, {200, 239, 52}, {203, 237, 52}, + {205, 236, 52}, {208, 234, 52}, {210, 233, 53}, {212, 231, 53}, + {215, 229, 53}, {217, 228, 54}, {219, 226, 54}, {221, 224, 55}, + {223, 223, 55}, {225, 221, 55}, {227, 219, 56}, {229, 217, 56}, + {231, 215, 57}, {233, 213, 57}, {235, 211, 57}, {236, 209, 58}, + {238, 207, 58}, {239, 205, 58}, {241, 203, 58}, {242, 201, 58}, + {244, 199, 58}, {245, 197, 58}, {246, 195, 58}, {247, 193, 58}, + {248, 190, 57}, {249, 188, 57}, {250, 186, 57}, {251, 184, 56}, + {251, 182, 55}, {252, 179, 54}, {252, 177, 54}, {253, 174, 53}, + {253, 172, 52}, {254, 169, 51}, {254, 167, 50}, {254, 164, 49}, + {254, 161, 48}, {254, 158, 47}, {254, 155, 45}, {254, 153, 44}, + {254, 150, 43}, {254, 147, 42}, {254, 144, 41}, {253, 141, 39}, + {253, 138, 38}, {252, 135, 37}, {252, 132, 35}, {251, 129, 34}, + {251, 126, 33}, {250, 123, 31}, {249, 120, 30}, {249, 117, 29}, + {248, 114, 28}, {247, 111, 26}, {246, 108, 25}, {245, 105, 24}, + {244, 102, 23}, {243, 99, 21}, {242, 96, 20}, {241, 93, 19}, + {240, 91, 18}, {239, 88, 17}, {237, 85, 16}, {236, 83, 15}, + {235, 80, 14}, {234, 78, 13}, {232, 75, 12}, {231, 73, 12}, + {229, 71, 11}, {228, 69, 10}, {226, 67, 10}, {225, 65, 9}, + {223, 63, 8}, {221, 61, 8}, {220, 59, 7}, {218, 57, 7}, + {216, 55, 6}, {214, 53, 6}, {212, 51, 5}, {210, 49, 5}, + {208, 47, 5}, {206, 45, 4}, {204, 43, 4}, {202, 42, 4}, + {200, 40, 3}, {197, 38, 3}, {195, 37, 3}, {193, 35, 2}, + {190, 33, 2}, {188, 32, 2}, {185, 30, 2}, {183, 29, 2}, + {180, 27, 1}, {178, 26, 1}, {175, 24, 1}, {172, 23, 1}, + {169, 22, 1}, {167, 20, 1}, {164, 19, 1}, {161, 18, 1}, + {158, 16, 1}, {155, 15, 1}, {152, 14, 1}, {149, 13, 1}, + {146, 11, 1}, {142, 10, 1}, {139, 9, 2}, {136, 8, 2}, + {133, 7, 2}, {129, 6, 2}, {126, 5, 2}, {122, 4, 3}}; + +SpectrumGenerator::SpectrumGenerator(double max_value) : scale_(1.0 / max_value) +{ +} + +int SpectrumGenerator::getColorCount() const +{ + return 256; +} + +Painter::Color SpectrumGenerator::getColor(double value, int alpha) const +{ + const int max_index = getColorCount() - 1; + int index = std::round(scale_ * value * max_index); + if (index < 0) { + index = 0; + } else if (index > max_index) { + index = max_index; + } + + return Painter::Color( + kSpectrum[index][0], kSpectrum[index][1], kSpectrum[index][2], alpha); +} + +namespace { + +std::mutex& heatMapSourceMutex() +{ + static std::mutex mutex; + return mutex; +} + +std::vector& heatMapSources() +{ + static std::vector sources; + return sources; +} + +std::string formatFixed(double value, int digits) +{ + std::ostringstream out; + out << std::fixed << std::setprecision(digits) << value; + return out.str(); +} + +} // namespace + +HeatMapDataSource::HeatMapDataSource(utl::Logger* logger, + const std::string& name, + const std::string& short_name, + const std::string& settings_group) + : name_(name), + short_name_(short_name), + settings_group_(settings_group), + destroy_map_(true), + use_dbu_(false), + populated_(false), + colors_correct_(false), + issue_redraw_(true), + block_(nullptr), + logger_(logger), + grid_x_size_(10.0), + grid_y_size_(10.0), + display_range_min_(0.0), + display_range_max_(100.0), + draw_below_min_display_range_(false), + draw_above_max_display_range_(true), + color_alpha_(150), + log_scale_(false), + reverse_log_(false), + show_numbers_(false), + show_legend_(false), + use_selected_only_(false), + color_generator_(SpectrumGenerator(100.0)) +{ + clearMap(); + updateMapColors(); +} + +HeatMapDataSource::~HeatMapDataSource() +{ + if (unregister_callback_) { + unregister_callback_(this); + } +} + +void HeatMapDataSource::dumpToFile(const std::string& file) +{ + ensureMap(); + + if (!isPopulated()) { + logger_->error(utl::GUI, 72, "\"{}\" is not populated with data.", name_); + } + + std::ofstream csv(file); + if (!csv.is_open()) { + logger_->error(utl::GUI, 73, "Unable to open {}", file); + } + + const double dbu_to_micron = block_->getDbUnitsPerMicron(); + + csv << "x0,y0,x1,y1,value (" << getValueUnits() << ")\n"; + for (const auto& map_col : map_) { + for (const auto& map_value : map_col) { + if (!map_value->has_value) { + continue; + } + const odb::Rect& box_rect = map_value->rect; + const double scaled_value = convertPercentToValue(map_value->value); + + csv << std::defaultfloat << std::setprecision(4); + csv << box_rect.xMin() / dbu_to_micron << ","; + csv << box_rect.yMin() / dbu_to_micron << ","; + csv << box_rect.xMax() / dbu_to_micron << ","; + csv << box_rect.yMax() / dbu_to_micron << ","; + csv << std::scientific << std::setprecision(6); + csv << scaled_value << '\n'; + } + } +} + +void HeatMapDataSource::redraw() +{ + if (issue_redraw_ && redraw_callback_) { + redraw_callback_(); + } +} + +void HeatMapDataSource::showSetup() +{ + if (setup_callback_) { + setup_callback_(); + } +} + +void HeatMapDataSource::setRedrawCallback(std::function callback) +{ + redraw_callback_ = std::move(callback); +} + +void HeatMapDataSource::setSetupCallback(std::function callback) +{ + setup_callback_ = std::move(callback); +} + +void HeatMapDataSource::setUnregisterCallback( + std::function callback) +{ + unregister_callback_ = std::move(callback); +} + +void HeatMapDataSource::setColorAlpha(int alpha) +{ + color_alpha_ + = boundValue(alpha, getColorAlphaMinimum(), getColorAlphaMaximum()); + updateMapColors(); + redraw(); +} + +void HeatMapDataSource::setDisplayRange(double min, double max) +{ + if (max < min) { + std::swap(min, max); + } + + display_range_min_ = boundValue( + min, getDisplayRangeMinimumValue(), getDisplayRangeMaximumValue()); + display_range_max_ = boundValue( + max, getDisplayRangeMinimumValue(), getDisplayRangeMaximumValue()); + + updateMapColors(); + redraw(); +} + +void HeatMapDataSource::setDrawBelowRangeMin(bool show) +{ + draw_below_min_display_range_ = show; + redraw(); +} + +void HeatMapDataSource::setDrawAboveRangeMax(bool show) +{ + draw_above_max_display_range_ = show; + redraw(); +} + +void HeatMapDataSource::setGridSizes(double x, double y) +{ + bool changed = false; + if (grid_x_size_ != x) { + grid_x_size_ = boundValue( + x, getGridSizeMinimumValue(), getGridSizeMaximumValue()); + changed = true; + } + if (grid_y_size_ != y) { + grid_y_size_ = boundValue( + y, getGridSizeMinimumValue(), getGridSizeMaximumValue()); + changed = true; + } + + if (changed) { + destroyMap(); + } +} + +void HeatMapDataSource::setLogScale(bool scale) +{ + log_scale_ = scale; + updateMapColors(); + redraw(); +} + +void HeatMapDataSource::setReverseLogScale(bool reverse) +{ + reverse_log_ = reverse; + updateMapColors(); + redraw(); +} + +void HeatMapDataSource::setShowNumbers(bool numbers) +{ + show_numbers_ = numbers; + redraw(); +} + +void HeatMapDataSource::setShowLegend(bool legend) +{ + show_legend_ = legend; + redraw(); +} + +Painter::Color HeatMapDataSource::getColor(double value) const +{ + auto find_val = std::ranges::find_if( + color_lower_bounds_, + [value](const double other) { return other >= value; }); + const double color_index + = std::distance(color_lower_bounds_.begin(), find_val); + return color_generator_.getColor( + 100.0 * color_index / color_generator_.getColorCount(), color_alpha_); +} + +std::string HeatMapDataSource::formatValue(double value, bool legend) const +{ + std::string text = formatFixed(value, 2); + if (legend) { + text += "%"; + } + return text; +} + +void HeatMapDataSource::addBooleanSetting( + const std::string& name, + const std::string& label, + const std::function& getter, + const std::function& setter) +{ + settings_.emplace_back(MapSettingBoolean{name, label, getter, setter}); +} + +void HeatMapDataSource::addMultipleChoiceSetting( + const std::string& name, + const std::string& label, + const std::function()>& choices, + const std::function& getter, + const std::function& setter) +{ + settings_.emplace_back( + MapSettingMultiChoice{name, label, choices, getter, setter}); +} + +Renderer::Settings HeatMapDataSource::getSettings() const +{ + Renderer::Settings settings{{"DisplayMin", display_range_min_}, + {"DisplayMax", display_range_max_}, + {"GridX", grid_x_size_}, + {"GridY", grid_y_size_}, + {"Alpha", color_alpha_}, + {"LogScale", log_scale_}, + {"ReverseLog", reverse_log_}, + {"ShowNumbers", show_numbers_}, + {"ShowLegend", show_legend_}, + {"UseSelectedOnly", use_selected_only_}, + {"ShowMin", draw_below_min_display_range_}, + {"ShowMax", draw_above_max_display_range_}}; + + for (const auto& setting : settings_) { + if (std::holds_alternative(setting)) { + const auto& set = std::get(setting); + settings[set.name] = set.getter(); + } else if (std::holds_alternative(setting)) { + const auto& set = std::get(setting); + settings[set.name] = set.getter(); + } + } + + return settings; +} + +void HeatMapDataSource::setSettings(const Renderer::Settings& settings) +{ + Renderer::setSetting(settings, "DisplayMin", display_range_min_); + Renderer::setSetting(settings, "DisplayMax", display_range_max_); + Renderer::setSetting(settings, "GridX", grid_x_size_); + Renderer::setSetting(settings, "GridY", grid_y_size_); + Renderer::setSetting(settings, "Alpha", color_alpha_); + Renderer::setSetting(settings, "LogScale", log_scale_); + Renderer::setSetting(settings, "ReverseLog", reverse_log_); + Renderer::setSetting(settings, "ShowNumbers", show_numbers_); + Renderer::setSetting(settings, "ShowLegend", show_legend_); + Renderer::setSetting(settings, "UseSelectedOnly", use_selected_only_); + Renderer::setSetting( + settings, "ShowMin", draw_below_min_display_range_); + Renderer::setSetting( + settings, "ShowMax", draw_above_max_display_range_); + + for (const auto& setting : settings_) { + if (std::holds_alternative(setting)) { + const auto& set = std::get(setting); + bool temp_value = set.getter(); + Renderer::setSetting(settings, set.name, temp_value); + set.setter(temp_value); + } else if (std::holds_alternative(setting)) { + const auto& set = std::get(setting); + std::string temp_value = set.getter(); + Renderer::setSetting(settings, set.name, temp_value); + set.setter(temp_value); + } + } + + setDisplayRange(display_range_min_, display_range_max_); + setGridSizes(grid_x_size_, grid_y_size_); + setColorAlpha(color_alpha_); +} + +std::set HeatMapDataSource::getSelectedInsts() const +{ + std::set selected_insts; +#ifdef ENABLE_QT + if (!useSelectedOnly() || !gui::Gui::enabled()) { + return selected_insts; + } + for (const gui::Selected& item : gui::Gui::get()->selection()) { + if (item.isInst()) { + selected_insts.insert(std::any_cast(item.getObject())); + } + } +#endif + return selected_insts; +} + +HeatMapDataSource::MapView HeatMapDataSource::getMapView( + const odb::Rect& bounds) +{ + const auto x_low_find = std::ranges::lower_bound(map_x_grid_, bounds.xMin()); + const auto x_high_find + = std::upper_bound(x_low_find, map_x_grid_.end(), bounds.xMax()); + const auto y_low_find = std::ranges::lower_bound(map_y_grid_, bounds.yMin()); + const auto y_high_find + = std::upper_bound(y_low_find, map_y_grid_.end(), bounds.yMax()); + + const int shape_x = static_cast(map_.shape()[0]); + const int shape_y = static_cast(map_.shape()[1]); + + const int x_low = std::max( + static_cast(std::distance(map_x_grid_.begin(), x_low_find)) - 1, 0); + const int x_high = std::min( + static_cast(std::distance(map_x_grid_.begin(), x_high_find)), + shape_x); + const int y_low = std::max( + static_cast(std::distance(map_y_grid_.begin(), y_low_find)) - 1, 0); + const int y_high = std::min( + static_cast(std::distance(map_y_grid_.begin(), y_high_find)), + shape_y); + + return map_[boost::indices[Map::index_range(x_low, x_high)] + [Map::index_range(y_low, y_high)]]; +} + +std::vector HeatMapDataSource::getVisibleMap( + const odb::Rect& bounds, + const double pixels_per_dbu, + const double min_pixels_per_bin) +{ + ensureMap(); + + if (!isPopulated()) { + return {}; + } + + const double min_value = getRealRangeMinimumValue(); + const double max_value = getRealRangeMaximumValue(); + const bool show_mins = getDrawBelowRangeMin(); + const bool show_maxs = getDrawAboveRangeMax(); + + const double min_dbu = (pixels_per_dbu > 0.0 && min_pixels_per_bin > 0.0) + ? min_pixels_per_bin / pixels_per_dbu + : 0.0; + + const double dbu_per_micron = getBlock()->getDbUnitsPerMicron(); + const int x_scale + = min_dbu <= 0.0 + ? 1 + : std::max(1, + static_cast(std::ceil( + min_dbu / (getGridXSize() * dbu_per_micron)))); + const int y_scale + = min_dbu <= 0.0 + ? 1 + : std::max(1, + static_cast(std::ceil( + min_dbu / (getGridYSize() * dbu_per_micron)))); + + const HeatMapDataSource::MapView map_view = getMapView(bounds); + const int x_size = map_view.shape()[0]; + const int y_size = map_view.shape()[1]; + + std::vector visible_map; + for (int x = 0; x < x_size; x += x_scale) { + for (int y = 0; y < y_size; y += y_scale) { + MapColor draw_pt; + draw_pt.rect.mergeInit(); + draw_pt.has_value = false; + draw_pt.value = std::numeric_limits::lowest(); + + for (int x_sub = 0; x_sub < x_scale; x_sub++) { + const int x_idx = x + x_sub; + if (x_idx >= x_size) { + continue; + } + + for (int y_sub = 0; y_sub < y_scale; y_sub++) { + const int y_idx = y + y_sub; + if (y_idx >= y_size) { + continue; + } + + const auto& map_pt = map_view[x_idx][y_idx]; + draw_pt.rect.merge(map_pt->rect); + if (!map_pt->has_value) { + continue; + } + + if (draw_pt.value < map_pt->value) { + draw_pt.has_value = true; + draw_pt.value = map_pt->value; + draw_pt.color = map_pt->color; + } + } + } + + if (!draw_pt.has_value) { + continue; + } + if (!show_mins && draw_pt.value < min_value) { + continue; + } + if (!show_maxs && draw_pt.value > max_value) { + continue; + } + + visible_map.push_back(draw_pt); + } + } + + return visible_map; +} + +void HeatMapDataSource::addToMap(const odb::Rect& region, double value) +{ + for (const auto& map_col : getMapView(region)) { + for (const auto& map_pt : map_col) { + if (map_pt == nullptr) { + continue; + } + + odb::Rect intersection; + map_pt->rect.intersection(region, intersection); + + const double intersect_area = intersection.area(); + const double value_area = region.area(); + const double region_area = map_pt->rect.area(); + + combineMapData(map_pt->has_value, + map_pt->value, + value, + value_area, + intersect_area, + region_area); + map_pt->has_value = true; + + markColorsInvalid(); + } + } +} + +odb::Rect HeatMapDataSource::getBounds() const +{ + return getBlock()->getDieArea(); +} + +void HeatMapDataSource::clearMap() +{ + map_.resize(boost::extents[1][1]); + map_[0][0] = nullptr; + populated_ = false; +} + +bool HeatMapDataSource::setupMap() +{ + if (getBlock() == nullptr || getBlock()->getDieArea().area() == 0) { + return false; + } + + populateXYGrid(); + + const size_t x_grid_size = map_x_grid_.size() - 1; + const size_t y_grid_size = map_y_grid_.size() - 1; + + debugPrint(logger_, + utl::GUI, + "HeatMap", + 1, + "{} - Generating {}x{} map", + name_, + x_grid_size, + y_grid_size); + map_.resize(boost::extents[x_grid_size][y_grid_size]); + + const Painter::Color default_color = getColor(0); + for (size_t x = 0; x < x_grid_size; x++) { + const int x_min = map_x_grid_[x]; + const int x_max = map_x_grid_[x + 1]; + + for (size_t y = 0; y < y_grid_size; y++) { + const int y_min = map_y_grid_[y]; + const int y_max = map_y_grid_[y + 1]; + + auto map_pt = std::make_shared(); + map_pt->rect = odb::Rect(x_min, y_min, x_max, y_max); + map_pt->has_value = false; + map_pt->value = 0.0; + map_pt->color = default_color; + + map_[x][y] = std::move(map_pt); + } + } + + return true; +} + +void HeatMapDataSource::populateXYGrid() +{ + const int dx = getGridXSize() * getBlock()->getDbUnitsPerMicron(); + const int dy = getGridYSize() * getBlock()->getDbUnitsPerMicron(); + + const odb::Rect bounds = getBounds(); + + const int x_grid = std::ceil(bounds.dx() / static_cast(dx)); + const int y_grid = std::ceil(bounds.dy() / static_cast(dy)); + + std::vector x_grid_set, y_grid_set; + for (int x = 0; x < x_grid; x++) { + const int x_min = bounds.xMin() + x * dx; + const int x_max = std::min(x_min + dx, bounds.xMax()); + if (x == 0) { + x_grid_set.push_back(x_min); + } + x_grid_set.push_back(x_max); + } + for (int y = 0; y < y_grid; y++) { + const int y_min = bounds.yMin() + y * dy; + const int y_max = std::min(y_min + dy, bounds.yMax()); + if (y == 0) { + y_grid_set.push_back(y_min); + } + y_grid_set.push_back(y_max); + } + + setXYMapGrid(x_grid_set, y_grid_set); +} + +void HeatMapDataSource::setXYMapGrid(const std::vector& x_grid, + const std::vector& y_grid) +{ + const std::set x_grid_set(x_grid.begin(), x_grid.end()); + const std::set y_grid_set(y_grid.begin(), y_grid.end()); + + map_x_grid_.clear(); + map_y_grid_.clear(); + + map_x_grid_.insert(map_x_grid_.end(), x_grid_set.begin(), x_grid_set.end()); + map_y_grid_.insert(map_y_grid_.end(), y_grid_set.begin(), y_grid_set.end()); +} + +void HeatMapDataSource::destroyMap() +{ + if (destroy_map_) { + return; + } + + debugPrint( + logger_, utl::GUI, "HeatMap", 1, "{} - destroy map requested", name_); + + destroy_map_ = true; + redraw(); +} + +bool HeatMapDataSource::hasData() const +{ + if (!populated_) { + return false; + } + + for (const auto& map_col : map_) { + for (const auto& map_pt : map_col) { + if (map_pt->has_value) { + return true; + } + } + } + + return false; +} + +void HeatMapDataSource::ensureMap() +{ + absl::MutexLock lock(&ensure_mutex_); + + if (destroy_map_) { + debugPrint(logger_, utl::GUI, "HeatMap", 1, "{} - Destroying map", name_); + clearMap(); + destroy_map_ = false; + } + + const bool build_map = map_[0][0] == nullptr; + if (build_map) { + debugPrint(logger_, utl::GUI, "HeatMap", 1, "{} - Setting up map", name_); + if (!setupMap()) { + debugPrint( + logger_, utl::GUI, "HeatMap", 1, "{} - No map available", name_); + return; + } + } + + if (build_map || !isPopulated()) { + debugPrint(logger_, utl::GUI, "HeatMap", 1, "{} - Populating map", name_); + populated_ = populateMap(); + + if (isPopulated()) { + debugPrint( + logger_, utl::GUI, "HeatMap", 1, "{} - Correcting map scale", name_); + correctMapScale(map_); + } + } + + if (!colors_correct_ && isPopulated()) { + debugPrint( + logger_, utl::GUI, "HeatMap", 1, "{} - Assigning map colors", name_); + assignMapColors(); + } +} + +void HeatMapDataSource::updateMapColors() +{ + const int color_count = color_generator_.getColorCount(); + color_lower_bounds_.clear(); + color_lower_bounds_.resize(color_count + 1); + if (log_scale_) { + double range = display_range_max_; + if (display_range_min_ != 0.0) { + range = display_range_max_ / display_range_min_; + } + + const double step = std::pow(range, 1.0 / color_count); + + for (int i = 0; i <= color_count; i++) { + double start = display_range_max_ / std::pow(step, i); + if (i == color_generator_.getColorCount()) { + start = display_range_min_; + } + color_lower_bounds_[i] = start; + } + + if (reverse_log_) { + for (auto& lower_bound : color_lower_bounds_) { + lower_bound = display_range_max_ - lower_bound + display_range_min_; + } + } else { + std::ranges::reverse(color_lower_bounds_); + } + } else { + const double step = (display_range_max_ - display_range_min_) / color_count; + for (int i = 0; i <= color_count; i++) { + color_lower_bounds_[i] = display_range_min_ + i * step; + } + } + + markColorsInvalid(); +} + +void HeatMapDataSource::assignMapColors() +{ + for (const auto& map_col : map_) { + for (const auto& map_pt : map_col) { + map_pt->color = getColor(map_pt->value); + } + } + colors_correct_ = true; +} + +double HeatMapDataSource::getRealRangeMinimumValue() const +{ + return color_lower_bounds_[0]; +} + +double HeatMapDataSource::getRealRangeMaximumValue() const +{ + return color_lower_bounds_[color_lower_bounds_.size() - 1]; +} + +std::vector> HeatMapDataSource::getLegendValues() const +{ + const int color_count = color_generator_.getColorCount(); + const int count = 6; + std::vector> values; + const double index_incr = static_cast(color_count) / (count - 1); + const double linear_start = getRealRangeMinimumValue(); + const double linear_step + = (getRealRangeMaximumValue() - linear_start) / (count - 1); + for (int i = 0; i < count; i++) { + int idx = std::round(i * index_incr); + idx = std::min(idx, color_count); + double value = color_lower_bounds_[idx]; + if (!log_scale_) { + value = linear_step * i + linear_start; + } + + values.emplace_back(idx, value); + } + return values; +} + +void HeatMapDataSource::onShow() +{ + if (!isPopulated()) { + logger_->warn(utl::GUI, + 66, + "Heat map \"{}\" has not been populated with data.", + getName()); + } +} + +void HeatMapDataSource::onHide() +{ + if (destroyMapOnNotVisible()) { + setIssueRedraw(false); + destroyMap(); + setIssueRedraw(true); + } +} + +RealValueHeatMapDataSource::RealValueHeatMapDataSource( + utl::Logger* logger, + const std::string& unit_suffix, + const std::string& name, + const std::string& short_name, + const std::string& settings_group) + : HeatMapDataSource(logger, name, short_name, settings_group), + unit_suffix_(unit_suffix), + units_(unit_suffix_), + min_(0.0), + max_(0.0), + scale_(1.0) +{ +} + +void RealValueHeatMapDataSource::correctMapScale(HeatMapDataSource::Map& map) +{ + determineMinMax(map); + determineUnits(); + + for (const auto& map_col : map) { + for (const auto& map_pt : map_col) { + map_pt->value = convertValueToPercent(map_pt->value); + } + } + + min_ = roundData(min_ * scale_); + max_ = roundData(max_ * scale_); + scale_ = 1.0; +} + +double RealValueHeatMapDataSource::roundData(double value) const +{ + const double precision = 1000.0; + return std::round(value * precision) / precision; +} + +void RealValueHeatMapDataSource::determineMinMax( + const HeatMapDataSource::Map& map) +{ + min_ = std::numeric_limits::max(); + max_ = std::numeric_limits::lowest(); + + for (const auto& map_col : map) { + for (const auto& map_pt : map_col) { + min_ = std::min(min_, map_pt->value); + max_ = std::max(max_, map_pt->value); + } + } +} + +void RealValueHeatMapDataSource::determineUnits() +{ + const double range = getValueRange(); + if (range >= 1.0 || range == 0) { + units_ = ""; + scale_ = 1.0; + } else if (range > 1e-3) { + units_ = "m"; + scale_ = 1e3; + } else if (range > 1e-6) { + units_ = "u"; + scale_ = 1e6; + } else if (range > 1e-9) { + units_ = "n"; + scale_ = 1e9; + } else if (range > 1e-12) { + units_ = "p"; + scale_ = 1e12; + } else { + units_ = "f"; + scale_ = 1e15; + } + + units_ += unit_suffix_; +} + +std::string RealValueHeatMapDataSource::formatValue(double value, + bool legend) const +{ + const int digits = legend ? 3 : 2; + std::string text = formatFixed(convertPercentToValue(value), digits); + if (legend) { + text += getValueUnits(); + } + return text; +} + +std::string RealValueHeatMapDataSource::getValueUnits() const +{ + return units_; +} + +double RealValueHeatMapDataSource::getValueRange() const +{ + double range = max_ - min_; + if (range == 0.0) { + range = 1.0; + } + return range; +} + +double RealValueHeatMapDataSource::convertValueToPercent(double value) const +{ + const double range = getValueRange(); + const double offset = min_; + + return roundData(100.0 * (value - offset) / range); +} + +double RealValueHeatMapDataSource::convertPercentToValue(double percent) const +{ + const double range = getValueRange(); + const double offset = min_; + + return roundData(percent * range / 100.0 + offset); +} + +double RealValueHeatMapDataSource::getDisplayRangeIncrement() const +{ + return getValueRange() / 100.0; +} + +GlobalRoutingDataSource::GlobalRoutingDataSource( + utl::Logger* logger, + const std::string& name, + const std::string& short_name, + const std::string& settings_group) + : HeatMapDataSource(logger, name, short_name, settings_group) +{ +} + +std::pair GlobalRoutingDataSource::getReportableXYGrid() const +{ + if (getBlock() == nullptr) { + return {kDefaultGrid, kDefaultGrid}; + } + + auto* gcell_grid = getBlock()->getGCellGrid(); + if (gcell_grid == nullptr) { + return {kDefaultGrid, kDefaultGrid}; + } + + auto grid_mode = [gcell_grid](int num_grids, + void (odb::dbGCellGrid::*get_grid)( + int, int&, int&, int&)) -> int { + std::map grid_pitch_count; + for (int i = 0; i < num_grids; i++) { + int origin; + int count; + int step; + (gcell_grid->*get_grid)(i, origin, count, step); + grid_pitch_count[step] += count; + } + + if (grid_pitch_count.empty()) { + return kDefaultGrid; + } + + auto mode = grid_pitch_count.begin(); + for (auto check_mode = grid_pitch_count.begin(); + check_mode != grid_pitch_count.end(); + check_mode++) { + if (mode->second < check_mode->second) { + mode = check_mode; + } + } + return mode->first; + }; + + const double x_grid = grid_mode(gcell_grid->getNumGridPatternsX(), + &odb::dbGCellGrid::getGridPatternX); + const double y_grid = grid_mode(gcell_grid->getNumGridPatternsY(), + &odb::dbGCellGrid::getGridPatternY); + + const double dbus = getBlock()->getDbUnitsPerMicron(); + return {x_grid / dbus, y_grid / dbus}; +} + +double GlobalRoutingDataSource::getGridXSize() const +{ + const auto& [x, y] = getReportableXYGrid(); + return x; +} + +double GlobalRoutingDataSource::getGridYSize() const +{ + const auto& [x, y] = getReportableXYGrid(); + return y; +} + +void GlobalRoutingDataSource::populateXYGrid() +{ + if (getBlock() == nullptr) { + HeatMapDataSource::populateXYGrid(); + return; + } + + auto* gcell_grid = getBlock()->getGCellGrid(); + if (gcell_grid == nullptr) { + HeatMapDataSource::populateXYGrid(); + return; + } + + std::vector gcell_xgrid; + std::vector gcell_ygrid; + gcell_grid->getGridX(gcell_xgrid); + gcell_grid->getGridY(gcell_ygrid); + + const auto die_area = getBlock()->getDieArea(); + gcell_xgrid.push_back(die_area.xMax()); + gcell_ygrid.push_back(die_area.yMax()); + + setXYMapGrid(gcell_xgrid, gcell_ygrid); +} + +PowerDensityDataSource::PowerDensityDataSource(sta::dbSta* sta, + utl::Logger* logger) + : gui::RealValueHeatMapDataSource(logger, + "W", + "Power Density", + "Power", + "PowerDensity"), + sta_(sta) +{ + setIssueRedraw(false); + setLogScale(true); + setIssueRedraw(true); + + addMultipleChoiceSetting( + "Scene", + "Scene:", + [this]() { + std::vector scenes; + for (auto* scene : sta_->scenes()) { + scenes.emplace_back(scene->name()); + } + return scenes; + }, + [this]() -> std::string { return scene_; }, + [this](const std::string& value) { scene_ = value; }); + addBooleanSetting( + "Internal", + "Internal power:", + [this]() { return include_internal_; }, + [this](bool value) { include_internal_ = value; }); + addBooleanSetting( + "Leakage", + "Leakage power:", + [this]() { return include_leakage_; }, + [this](bool value) { include_leakage_ = value; }); + addBooleanSetting( + "Switching", + "Switching power:", + [this]() { return include_switching_; }, + [this](bool value) { include_switching_ = value; }); + + if (sta_ != nullptr) { + for (auto* scene : sta_->scenes()) { + if (scene != nullptr) { + scene_ = scene->name(); + break; + } + } + } +} + +bool PowerDensityDataSource::populateMap() +{ + if (getBlock() == nullptr || sta_ == nullptr) { + return false; + } + + if (sta_->cmdNetwork() == nullptr) { + return false; + } + + auto* network = sta_->getDbNetwork(); + auto* scene = getScene(); + if (scene == nullptr) { + return false; + } + + // Collect selected instances if filter is enabled + const std::set selected_insts = getSelectedInsts(); + const bool filter = !selected_insts.empty(); + + const bool include_all + = include_internal_ && include_leakage_ && include_switching_; + for (auto* inst : getBlock()->getInsts()) { + if (!inst->getPlacementStatus().isPlaced()) { + continue; + } + if (filter && selected_insts.find(inst) == selected_insts.end()) { + continue; + } + + sta::PowerResult power = sta_->power(network->dbToSta(inst), scene); + + float pwr = 0.0; + if (include_all) { + pwr = power.total(); + } else { + if (include_internal_) { + pwr += power.internal(); + } + if (include_leakage_) { + pwr += power.switching(); + } + if (include_switching_) { + pwr += power.leakage(); + } + } + + odb::Rect inst_box = inst->getBBox()->getBox(); + addToMap(inst_box, pwr); + } + + return true; +} + +void PowerDensityDataSource::combineMapData(bool, + double& base, + const double new_data, + const double data_area, + const double intersection_area, + const double) +{ + base += (new_data / data_area) * intersection_area; +} + +sta::Scene* PowerDensityDataSource::getScene() const +{ + if (scene_.empty()) { + return nullptr; + } + for (auto* scene : sta_->scenes()) { + if (scene->name() == scene_) { + return scene; + } + } + return nullptr; +} + +HeatMapSourceRegistration::HeatMapSourceRegistration(std::string name, + std::string short_name, + std::string settings_group, + Factory factory) + : name_(std::move(name)), + short_name_(std::move(short_name)), + settings_group_(std::move(settings_group)), + factory_(std::move(factory)) +{ +} + +std::shared_ptr HeatMapSourceRegistration::createInstance() + const +{ + std::lock_guard lock(instances_mutex_); + auto instance = factory_(); + instances_.erase( + std::remove_if(instances_.begin(), + instances_.end(), + [](const std::weak_ptr& candidate) { + return candidate.expired(); + }), + instances_.end()); + instances_.push_back(instance); + return instance; +} + +void HeatMapSourceRegistration::invalidateInstances() const +{ + std::lock_guard lock(instances_mutex_); + auto keep = instances_.begin(); + for (auto it = instances_.begin(); it != instances_.end(); ++it) { + if (auto instance = it->lock()) { + instance->update(); + *keep++ = *it; + } + } + instances_.erase(keep, instances_.end()); +} + +HeatMapSourceHandle registerHeatMapSource( + const std::string& name, + const std::string& short_name, + const std::string& settings_group, + const HeatMapSourceRegistration::Factory& factory) +{ + std::lock_guard lock(heatMapSourceMutex()); + for (const auto& source : heatMapSources()) { + if (source->getShortName() == short_name) { + return source; + } + } + + auto source = std::make_shared( + name, short_name, settings_group, factory); + heatMapSources().push_back(source); + return source; +} + +const std::vector& getRegisteredHeatMapSources() +{ + return heatMapSources(); +} + +HeatMapSourceHandle findRegisteredHeatMapSource(const std::string& short_name) +{ + std::lock_guard lock(heatMapSourceMutex()); + for (const auto& source : heatMapSources()) { + if (source->getShortName() == short_name) { + return source; + } + } + return nullptr; +} + +void registerBuiltinHeatMapSources(sta::dbSta* sta, utl::Logger* logger) +{ + registerHeatMapSource("Pin Density", "Pin", "PinDensity", [logger]() { + return std::make_shared(logger); + }); + registerHeatMapSource( + "Placement Density", "Placement", "PlacementDensity", [logger]() { + return std::make_shared(logger); + }); + if (sta != nullptr) { + registerHeatMapSource( + "Power Density", "Power", "PowerDensity", [sta, logger]() { + return std::make_shared(sta, logger); + }); + } +} + +} // namespace gui diff --git a/src/gui/src/heatMapGui.h b/src/gui/src/heatMapGui.h new file mode 100644 index 00000000000..e505dc61ed0 --- /dev/null +++ b/src/gui/src/heatMapGui.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include + +#include "gui/gui.h" +#include "gui/heatMap.h" + +namespace gui { + +std::unique_ptr makeHeatMapRenderer(HeatMapDataSource& datasource); +void showHeatMapSetupDialog(HeatMapDataSource* source); + +} // namespace gui diff --git a/src/gui/src/init_descriptors.cpp b/src/gui/src/init_descriptors.cpp new file mode 100644 index 00000000000..3ffcf2b4045 --- /dev/null +++ b/src/gui/src/init_descriptors.cpp @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2025, The OpenROAD Authors + +// Implements DescriptorRegistry::initDescriptors(). +// This is in a separate file from descriptor_registry.cpp so that +// gui_descriptors (the lightweight CMake library used by web) does not +// pull in dbDescriptors / staDescriptors and their heavy dependencies. + +#include + +#include "bufferTreeDescriptor.h" +#include "dbDescriptors.h" +#include "db_sta/dbSta.hh" +#include "gui/descriptor_registry.h" +#include "gui/gui.h" +#include "odb/db.h" +#include "odb/geom.h" +#include "sta/Liberty.hh" +#include "sta/Scene.hh" +#include "staDescriptors.h" + +namespace gui { + +// Defined here (rather than in descriptor_registry.cpp) because the "Zoom to" +// action references Gui::get() / Gui::zoomTo(), which are not available in the +// lightweight gui_descriptors CMake library. +Descriptor::Actions Selected::getActions() const +{ + auto actions = descriptor_->getActions(object_); + + odb::Rect bbox; + if (getBBox(bbox)) { + actions.push_back({"Zoom to", [this, bbox]() -> Selected { + auto gui = Gui::get(); + if (gui) { + gui->zoomTo(bbox); + } + return *this; + }}); + } + + return actions; +} + +void DescriptorRegistry::initDescriptors(odb::dbDatabase* db, sta::dbSta* sta) +{ + // Initialize BufferTree with STA so that DbNetDescriptor::getDBProperties() + // can call BufferTree::isAggregate() safely. + BufferTree::setSTA(sta); + + // Static empty sets for DbNetDescriptor — in the full GUI build, + // MainWindow::init() re-registers with real widget-owned sets. + static const std::set empty_net_set; + + registerDescriptor(new DbInstDescriptor(db, sta)); + registerDescriptor(new DbMasterDescriptor(db, sta)); + registerDescriptor(new DbNetDescriptor( + db, sta, empty_net_set, empty_net_set, empty_net_set)); + registerDescriptor(new DbNetDescriptor( + db, sta, empty_net_set, empty_net_set, empty_net_set)); + registerDescriptor(new DbWireDescriptor(db)); + registerDescriptor(new DbSWireDescriptor(db)); + registerDescriptor( + new DbITermDescriptor(db, []() { return false; })); + registerDescriptor( + new DbMTermDescriptor(db, []() { return false; })); + registerDescriptor(new DbBTermDescriptor(db)); + registerDescriptor(new DbBPinDescriptor(db)); + registerDescriptor(new DbViaDescriptor(db)); + registerDescriptor(new DbBlockageDescriptor(db)); + registerDescriptor(new DbObstructionDescriptor(db)); + registerDescriptor(new DbTechLayerDescriptor(db)); + registerDescriptor(new DbTermAccessPointDescriptor(db)); + registerDescriptor(new DbGroupDescriptor(db)); + registerDescriptor(new DbRegionDescriptor(db)); + registerDescriptor(new DbModuleDescriptor(db)); + registerDescriptor(new DbModBTermDescriptor(db)); + registerDescriptor(new DbModITermDescriptor(db)); + registerDescriptor(new DbModInstDescriptor(db)); + registerDescriptor(new DbModNetDescriptor(db)); + registerDescriptor(new DbTechViaDescriptor(db)); + registerDescriptor(new DbTechViaRuleDescriptor(db)); + registerDescriptor( + new DbTechViaLayerRuleDescriptor(db)); + registerDescriptor( + new DbGenerateViaDescriptor(db)); + registerDescriptor( + new DbNonDefaultRuleDescriptor(db)); + registerDescriptor(new DbTechLayerRuleDescriptor(db)); + registerDescriptor( + new DbTechSameNetRuleDescriptor(db)); + registerDescriptor(new DbSiteDescriptor(db)); + registerDescriptor(new DbSiteDescriptor(db)); + registerDescriptor(new DbRowDescriptor(db)); + registerDescriptor(new DbBlockDescriptor(db)); + registerDescriptor(new DbTechDescriptor(db)); + registerDescriptor( + new DbMetalWidthViaMapDescriptor(db)); + registerDescriptor( + new DbMarkerCategoryDescriptor(db)); + registerDescriptor(new DbMarkerDescriptor(db)); + registerDescriptor(new DbScanInstDescriptor(db)); + registerDescriptor(new DbScanListDescriptor(db)); + registerDescriptor(new DbScanPartitionDescriptor(db)); + registerDescriptor(new DbScanChainDescriptor(db)); + registerDescriptor(new DbBoxDescriptor(db)); + registerDescriptor(new DbSBoxDescriptor(db)); + registerDescriptor( + new DbBoxDescriptor(db)); + registerDescriptor( + new DbMasterEdgeTypeDescriptor(db)); + registerDescriptor( + new DbCellEdgeSpacingDescriptor(db)); + + // STA descriptors + registerDescriptor(new SceneDescriptor(sta)); + registerDescriptor(new LibertyLibraryDescriptor(sta)); + registerDescriptor(new LibertyCellDescriptor(sta)); + registerDescriptor(new LibertyPortDescriptor(sta)); + registerDescriptor(new StaInstanceDescriptor(sta)); + registerDescriptor(new ClockDescriptor(sta)); + + // Note: RulerDescriptor, LabelDescriptor, and BufferTreeDescriptor are + // GUI-only and are registered in MainWindow::init(). +} + +} // namespace gui diff --git a/src/gui/src/staDescriptors.cpp b/src/gui/src/staDescriptors.cpp index 5028be6ecc7..e1090478ecd 100644 --- a/src/gui/src/staDescriptors.cpp +++ b/src/gui/src/staDescriptors.cpp @@ -3,8 +3,10 @@ #include "staDescriptors.h" +#ifdef ENABLE_QT #include #include +#endif #include #include #include @@ -707,7 +709,8 @@ Descriptor::Properties StaInstanceDescriptor::getProperties( port_id = network->name(port); } - if (is_lib_port) { + if (is_lib_port + && !network->libertyPort(port)->direction()->isPowerGround()) { const std::string freq = Descriptor::convertUnits(power.density(), false, kFloatPrecision); const std::string activity_info = fmt::format("{:.2f}% at {}Hz from {}", @@ -717,8 +720,8 @@ Descriptor::Properties StaInstanceDescriptor::getProperties( port_power_activity.emplace_back(port_id, activity_info); const sta::Unit* timeunit = sta_->units()->timeUnit(); - const auto setup_arrival - = sta_->arrival(pin, nullptr, sta::MinMax::max()); + const auto setup_arrival = sta_->arrival( + pin, sta::RiseFallBoth::riseFall(), sta::MinMax::max()); const std::string setup_text = is_inf(setup_arrival) ? "None" diff --git a/src/gui/src/stub.cpp b/src/gui/src/stub.cpp index d0bcdc2bd40..26f7d0e360f 100644 --- a/src/gui/src/stub.cpp +++ b/src/gui/src/stub.cpp @@ -8,11 +8,15 @@ #include #include #include +#include #include #include #include +#include "bufferTreeDescriptor.h" +#include "gui/descriptor_registry.h" #include "gui/gui.h" +#include "gui/heatMap.h" #include "odb/db.h" #include "odb/geom.h" #include "tcl.h" @@ -24,25 +28,10 @@ struct GifWriter namespace gui { -// Used by toString to convert dbu to microns -DBUToString Descriptor::Property::convert_dbu - = [](int value, bool) { return std::to_string(value); }; -StringToDBU Descriptor::Property::convert_string - = [](const std::string& value, bool*) { return 0; }; - -// empty heat map class -class PinDensityDataSource -{ -}; - -// empty heat map class -class PlacementDensityDataSource -{ -}; - -class PowerDensityDataSource +Options* Painter::getOptions() { -}; + return options_; +} //// @@ -52,7 +41,8 @@ Gui::Gui() : continue_after_close_(false), logger_(nullptr), db_(nullptr) Gui* gui::Gui::get() { - return nullptr; + static Gui* singleton = new Gui(); + return singleton; } bool gui::Gui::enabled() @@ -64,6 +54,10 @@ void gui::Gui::registerRenderer(gui::Renderer*) { } +void HeatMapDataSource::registerHeatMap() +{ +} + void gui::Gui::unregisterRenderer(gui::Renderer*) { } @@ -94,10 +88,6 @@ void Renderer::redraw() Renderer::~Renderer() = default; -SpectrumGenerator::SpectrumGenerator(double scale) : scale_(scale) -{ -} - void DiscreteLegend::addLegendKey(const Painter::Color& color, const std::string& text) { @@ -129,9 +119,9 @@ void Renderer::setSettings(const Renderer::Settings& /* settings */) { } -Selected Gui::makeSelected(const std::any& /* object */) +Selected Gui::makeSelected(const std::any& object) { - return Selected(); + return DescriptorRegistry::instance()->makeSelected(object); } void Gui::setSelected(const Selected& selection) @@ -147,26 +137,23 @@ const SelectionSet& Gui::selection() void Gui::registerDescriptor(const std::type_info& type, const Descriptor* descriptor) { + DescriptorRegistry::instance()->registerDescriptor(type, descriptor); } void Gui::unregisterDescriptor(const std::type_info& type) { + DescriptorRegistry::instance()->unregisterDescriptor(type); } -const Descriptor* Gui::getDescriptor(const std::type_info& /* type */) const +const Descriptor* Gui::getDescriptor(const std::type_info& type) const { - return nullptr; + return DescriptorRegistry::instance()->getDescriptor(type); } void Gui::removeSelectedByType(const std::string& /* type */) { } -std::string Descriptor::Property::toString(const std::any& /* value */) -{ - return ""; -} - // using namespace odb; int startGui(int& argc, char* argv[], @@ -188,6 +175,13 @@ void initGui(Tcl_Interp* interp, sta::dbSta* sta, utl::Logger* logger) { + // Initialize the descriptor registry so that descriptors are available + // for the web viewer and other non-GUI consumers. + auto* registry = DescriptorRegistry::instance(); + registry->setLogger(logger); + registry->initDescriptors(db, sta); + registerBuiltinHeatMapSources(sta, logger); + // Tcl requires this to be a writable string std::string cmd_save_image( "proc save_image { args } {" @@ -284,4 +278,49 @@ void Gui::addNetToHighlightSet(const char* name, int highlight_group) { } +void Gui::addFocusNet(odb::dbNet* net) +{ +} + +void Gui::removeFocusNet(odb::dbNet* net) +{ +} + +void Gui::addRouteGuides(odb::dbNet* net) +{ +} + +void Gui::removeRouteGuides(odb::dbNet* net) +{ +} + +void Gui::addNetTracks(odb::dbNet* net) +{ +} + +void Gui::removeNetTracks(odb::dbNet* net) +{ +} + +void Gui::timingCone(Term term, bool fanin, bool fanout) +{ +} + +void Gui::timingPathsThrough(const std::set& terms) +{ +} + +// BufferTree stubs — the real implementation is in bufferTreeDescriptor.cpp +// which is only compiled in the Qt build. +sta::dbSta* BufferTree::sta_ = nullptr; + +BufferTree::BufferTree(odb::dbNet* /* net */) +{ +} + +bool BufferTree::isAggregate(odb::dbNet* /* net */) +{ + return false; +} + } // namespace gui diff --git a/src/psm/include/psm/pdnsim.h b/src/psm/include/psm/pdnsim.h index 6d318ee3a84..c2613596b0d 100644 --- a/src/psm/include/psm/pdnsim.h +++ b/src/psm/include/psm/pdnsim.h @@ -31,6 +31,10 @@ class EstimateParasitics; namespace dpl { class Opendp; } +namespace gui { +class HeatMapSourceRegistration; +using HeatMapSourceHandle = std::shared_ptr; +} // namespace gui namespace psm { class IRDropDataSource; @@ -122,6 +126,9 @@ class PDNSim : public odb::dbBlockCallBackObj void addDecapMaster(odb::dbMaster* decap_master, double decap_cap); void insertDecapCells(double target, const char* net_name); + odb::dbNet* getLastAnalyzedNet() const { return last_net_; } + sta::Scene* getLastAnalyzedCorner() const { return last_corner_; } + private: // Functions of decap cells odb::dbTechLayer* getLowestLayer(odb::dbNet* db_net); @@ -135,7 +142,7 @@ class PDNSim : public odb::dbBlockCallBackObj dpl::Opendp* opendp_ = nullptr; utl::Logger* logger_ = nullptr; - std::unique_ptr heatmap_; + gui::HeatMapSourceHandle heatmap_source_; bool debug_gui_enabled_ = false; @@ -145,6 +152,7 @@ class PDNSim : public odb::dbBlockCallBackObj std::map> user_voltages_; std::map> user_powers_; + odb::dbNet* last_net_ = nullptr; sta::Scene* last_corner_ = nullptr; }; } // namespace psm diff --git a/src/psm/src/CMakeLists.txt b/src/psm/src/CMakeLists.txt index 396ce69d05b..c63218bac63 100644 --- a/src/psm/src/CMakeLists.txt +++ b/src/psm/src/CMakeLists.txt @@ -45,6 +45,7 @@ target_link_libraries(psm est_lib Eigen3::Eigen gui + gui_heatmap_core Boost::boost ) diff --git a/src/psm/src/heatMap.cpp b/src/psm/src/heatMap.cpp index b1590ed4562..b4a8e65880d 100644 --- a/src/psm/src/heatMap.cpp +++ b/src/psm/src/heatMap.cpp @@ -206,6 +206,11 @@ void IRDropDataSource::ensureNet() return; } + if (psm_ != nullptr && psm_->getLastAnalyzedNet() != nullptr) { + net_ = psm_->getLastAnalyzedNet(); + return; + } + if (getBlock() == nullptr) { return; } @@ -238,6 +243,11 @@ void IRDropDataSource::ensureCorner() return; } + if (psm_ != nullptr && psm_->getLastAnalyzedCorner() != nullptr) { + corner_ = psm_->getLastAnalyzedCorner(); + return; + } + corner_ = sta_->cmdScene(); } diff --git a/src/psm/src/pdnsim.cpp b/src/psm/src/pdnsim.cpp index d7c774860d8..b149f5f5bd8 100644 --- a/src/psm/src/pdnsim.cpp +++ b/src/psm/src/pdnsim.cpp @@ -39,8 +39,10 @@ PDNSim::PDNSim(utl::Logger* logger, estimate_parasitics_ = estimate_parasitics; opendp_ = opendp; logger_ = logger; - heatmap_ = std::make_unique(this, sta, logger_); - heatmap_->registerHeatMap(); + heatmap_source_ = gui::registerHeatMapSource( + "IR Drop", "IRDrop", "IRDrop", [this, sta, logger]() { + return std::make_shared(this, sta, logger); + }); } PDNSim::~PDNSim() = default; @@ -88,6 +90,7 @@ void PDNSim::analyzePowerGrid(odb::dbNet* net, return; } + last_net_ = net; last_corner_ = corner; auto* solver = getIRSolver(net, false); if (!use_prev_solution || !solver->hasSolution(corner)) { @@ -97,9 +100,9 @@ void PDNSim::analyzePowerGrid(odb::dbNet* net, } solver->report(corner); - heatmap_->setNet(net); - heatmap_->setCorner(corner); - heatmap_->update(); + if (heatmap_source_) { + heatmap_source_->invalidateInstances(); + } if (enable_em) { solver->reportEM(corner); diff --git a/src/utl/include/utl/Logger.h b/src/utl/include/utl/Logger.h index e186216f732..8592bea6785 100644 --- a/src/utl/include/utl/Logger.h +++ b/src/utl/include/utl/Logger.h @@ -79,7 +79,8 @@ class Progress; X(TST) \ X(UKN) \ X(UPF) \ - X(UTL) + X(UTL) \ + X(WEB) #define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_STRING(STRING) #STRING, diff --git a/src/web/.gitignore b/src/web/.gitignore new file mode 100644 index 00000000000..f5ed9f7c4f0 --- /dev/null +++ b/src/web/.gitignore @@ -0,0 +1,4 @@ +*~ +test/results +TAGS +node_modules diff --git a/src/web/BUILD b/src/web/BUILD new file mode 100644 index 00000000000..ad325929f91 --- /dev/null +++ b/src/web/BUILD @@ -0,0 +1,118 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors + +load("@aspect_rules_js//js:defs.bzl", "js_library") +load("@npm//:defs.bzl", "npm_link_all_packages") +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("//bazel:tcl_encode_or.bzl", "tcl_encode") +load("//bazel:tcl_wrap_cc.bzl", "tcl_wrap_cc") + +package( + default_visibility = ["//:__subpackages__"], + features = ["layering_check"], +) + +npm_link_all_packages(name = "node_modules") + +cc_library( + name = "web", + srcs = [ + "src/clock_tree_report.cpp", + "src/clock_tree_report.h", + "src/color.cpp", + "src/color.h", + "src/hierarchy_report.cpp", + "src/hierarchy_report.h", + "src/json_builder.h", + "src/request_handler.cpp", + "src/request_handler.h", + "src/search.cpp", + "src/search.h", + "src/tile_generator.cpp", + "src/tile_generator.h", + "src/timing_report.cpp", + "src/timing_report.h", + "src/web.cpp", + ], + hdrs = [ + "include/web/web.h", + ], + copts = [ + "-DBOOST_ASIO_NO_DEPRECATED", + "-Isrc/web/src", + ], + includes = [ + "include", + ], + deps = [ + "//src/dbSta", + "//src/dbSta:dbNetwork", + "//src/gui", + "//src/odb", + "//src/sta:opensta_lib", + "//src/utl", + "//third-party/lodepng", + "@boost.asio", + "@boost.beast", + "@boost.geometry", + "@tcl_lang//:tcl", + ], +) + +cc_library( + name = "ui", + srcs = [ + "src/MakeWeb.cpp", + ":swig", + ":tcl", + ], + hdrs = [ + "include/web/MakeWeb.h", + ], + copts = [ + "-Isrc/web/src", + ], + includes = [ + "include", + ], + deps = [ + ":web", + "//:ord", + "//src/odb", + "//src/utl", + "@boost.stacktrace", + "@tcl_lang//:tcl", + ], +) + +tcl_encode( + name = "tcl", + srcs = [ + "src/web.tcl", + ], + char_array_name = "web_tcl_inits", + namespace = "web", +) + +tcl_wrap_cc( + name = "swig", + srcs = [ + "src/web.i", + "//:error_swig", + ], + module = "web", + namespace_prefix = "web", + root_swig_src = "src/web.i", + swig_includes = [ + "src/web/src", + ], + deps = [ + "//src/odb:swig", + ], +) + +js_library( + name = "js_sources", + srcs = glob(["src/*.js"]), + visibility = ["//src/web:__subpackages__"], +) diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt new file mode 100644 index 00000000000..418e0c97967 --- /dev/null +++ b/src/web/CMakeLists.txt @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors + +include("openroad") +swig_lib(NAME web + NAMESPACE web + I_FILE src/web.i + SCRIPTS src/web.tcl + SWIG_INCLUDES ${ODB_HOME}/src/swig/common + ${ODB_HOME}/src/swig/tcl + ${ODB_HOME}/include +) + +target_sources(web + PRIVATE + src/clock_tree_report.cpp + src/color.cpp + src/hierarchy_report.cpp + src/request_handler.cpp + src/search.cpp + src/tile_generator.cpp + src/timing_report.cpp + src/web.cpp + src/MakeWeb.cpp +) + +target_link_libraries(web + PUBLIC + odb + PRIVATE + gui_descriptors + gui_heatmap_core + dbSta_lib + lodepng + utl_lib +) + +target_include_directories(web + PUBLIC + include + PRIVATE + src + ${TCL_INCLUDE_PATH} + ${OPENSTA_HOME}/include + ${DBSTA_HOME}/include + ${OPENROAD_HOME}/third-party +) + +target_compile_definitions(web PRIVATE BOOST_ASIO_NO_DEPRECATED) + +messages( + TARGET web +) + +################################################################ + +if (Python3_FOUND AND BUILD_PYTHON) + swig_lib(NAME web_py + NAMESPACE web + LANGUAGE python + I_FILE src/web-py.i + SWIG_INCLUDES ${PROJECT_SOURCE_DIR}/include/dpl + ${ODB_HOME}/src/swig/common + ${ODB_HOME}/src/swig/python + SCRIPTS ${CMAKE_CURRENT_BINARY_DIR}/web_py.py + ) + + target_link_libraries(web_py + PUBLIC + web + ) + +endif() + +if(ENABLE_TESTS) + add_subdirectory(test) +endif() diff --git a/src/web/README.md b/src/web/README.md new file mode 100644 index 00000000000..47153f7dc38 --- /dev/null +++ b/src/web/README.md @@ -0,0 +1,87 @@ +# Web Viewer + +The web viewer module in OpenROAD (`web`) provides a browser-based interface +for exploring chip layouts and performing design analysis. It renders the design +as PNG tiles served over WebSocket, enabling smooth zoom and pan of large +designs without a native GUI. + +## Commands + +```{note} +- Parameters in square brackets `[-param param]` are optional. +- Parameters without square brackets `-param2 param2` are required. +``` + +### Web Server + +Start the web viewer server. This opens a WebSocket server and launches the +viewer in the default browser. + +```tcl +web_server + [-dir dir] +``` + +#### Options + +| Switch Name | Description | +| ----- | ----- | +| `[-dir]` | Path to a custom document root directory for serving static files. When omitted, the built-in HTML/JS/CSS assets are used. | + +## Features + +- **Tile-based rendering** — The server renders 256x256 PNG tiles on demand, + supporting smooth zoom and pan of designs with millions of instances. +- **Object inspection** — Click on instances, nets, pins, or other objects to + view their properties in an inspector panel. Hover highlights are rendered + server-side in tiles. +- **Timing analysis** — View timing paths with slack, delay, and arrival time + metrics. Highlight critical paths on the layout. Slack histogram charts with + filtering by path group and clock domain. +- **Clock tree visualization** — Browse clock tree hierarchy, highlight clock + paths, and view per-level statistics. +- **Hierarchy browser** — Navigate the module tree with instance counts and area + statistics. Toggle visibility and assign colors per module using a 31-color + palette. +- **Display controls** — Toggle visibility of cell types (stdcells, macros, + pads), net types (signal, power, clock), and shapes (routing, pins, blockages, + rows, tracks). +- **Focus nets** — Isolate specific nets for inspection, dimming all other + routing. +- **Tcl console** — Execute Tcl commands interactively from the browser. + +## Architecture + +The module has two parts: + +- **C++ server** (`src/web.cpp`, `src/request_handler.cpp`, + `src/tile_generator.cpp`) — A Boost Beast WebSocket server that handles tile + rendering, object selection, timing/clock-tree queries, and Tcl evaluation. + Tiles are rendered from ODB geometry and encoded as PNG using lodepng. + +- **JavaScript frontend** (`src/main.js`, `src/index.html`, `src/style.css`) — + A single-page application using Leaflet.js for the map and GoldenLayout for + resizable panels. Communicates with the server over a binary WebSocket + protocol. + +## Example scripts + +```tcl +# Start the web viewer with default settings +web_server + +# Start with a custom document root (for development) +web_server -dir /path/to/custom/html +``` + +## Regression tests + +There are a set of regression tests in `./test`. + +```shell +bazel test //src/web/test/... +``` + +## License + +BSD 3-Clause License. See [LICENSE](../../LICENSE) file. diff --git a/src/web/include/web/MakeWeb.h b/src/web/include/web/MakeWeb.h new file mode 100644 index 00000000000..364a04a20c7 --- /dev/null +++ b/src/web/include/web/MakeWeb.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include "tcl.h" + +namespace web { + +void initWeb(Tcl_Interp* tcl_interp); + +} // namespace web diff --git a/src/web/include/web/web.h b/src/web/include/web/web.h new file mode 100644 index 00000000000..1dcaeddf867 --- /dev/null +++ b/src/web/include/web/web.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include +#include + +#include "odb/db.h" +#include "tcl.h" +#include "utl/Logger.h" + +namespace sta { +class dbSta; +} + +namespace web { + +struct Color; +class Search; + +class TileGenerator; +struct TclEvaluator; + +// A layout web server + +class WebServer +{ + public: + WebServer(odb::dbDatabase* db, + sta::dbSta* sta, + utl::Logger* logger, + Tcl_Interp* interp); + ~WebServer(); + + void serve(const std::string& doc_root = ""); + + private: + odb::dbDatabase* db_ = nullptr; + sta::dbSta* sta_ = nullptr; + utl::Logger* logger_ = nullptr; + Tcl_Interp* interp_ = nullptr; + std::shared_ptr generator_; +}; + +} // namespace web diff --git a/src/web/package.json b/src/web/package.json new file mode 100644 index 00000000000..8159eb8c8d5 --- /dev/null +++ b/src/web/package.json @@ -0,0 +1,10 @@ +{ + "private": true, + "type": "module", + "pnpm": { + "onlyBuiltDependencies": [] + }, + "devDependencies": { + "jsdom": "^26.1.0" + } +} diff --git a/src/web/pnpm-lock.yaml b/src/web/pnpm-lock.yaml new file mode 100644 index 00000000000..bc187b15b07 --- /dev/null +++ b/src/web/pnpm-lock.yaml @@ -0,0 +1,346 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + +packages: + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + +snapshots: + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + agent-base@7.1.4: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + entities@6.0.1: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + is-potential-custom-element-name@1.0.1: {} + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + lru-cache@10.4.3: {} + + ms@2.1.3: {} + + nwsapi@2.2.23: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + punycode@2.3.1: {} + + rrweb-cssom@0.8.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + symbol-tree@3.2.4: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + ws@8.19.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} diff --git a/src/web/src/MakeWeb.cpp b/src/web/src/MakeWeb.cpp new file mode 100644 index 00000000000..6586b5d08a8 --- /dev/null +++ b/src/web/src/MakeWeb.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include "web/MakeWeb.h" + +#include "tcl.h" +#include "utl/decode.h" + +// Generated by swig from the web.i +extern "C" { +extern int Web_Init(Tcl_Interp* interp); +} + +namespace web { + +// example.tcl encoded as strings. +extern const char* web_tcl_inits[]; + +void initWeb(Tcl_Interp* tcl_interp) +{ + // Define swig TCL commands. + Web_Init(tcl_interp); + // Eval encoded TCL sources. + utl::evalTclInit(tcl_interp, web_tcl_inits); +} + +} // namespace web diff --git a/src/web/src/charts-widget.js b/src/web/src/charts-widget.js new file mode 100644 index 00000000000..d704d40630a --- /dev/null +++ b/src/web/src/charts-widget.js @@ -0,0 +1,514 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Canvas-based slack histogram widget. + +// Layout margins (pixels) +export const kLeftMargin = 60; +export const kRightMargin = 20; +export const kTopMargin = 30; +export const kBottomMargin = 50; +export const kBarGap = 2; + +// Colors matching the Qt GUI (chartsWidget.cpp:798-803) +const kNegativeFill = '#f08080'; // lightcoral +const kNegativeBorder = '#8b0000'; // darkred +const kPositiveFill = '#90ee90'; // lightgreen +const kPositiveBorder = '#006400'; // darkgreen + +// Pure layout computation — extracted for testability. +export function computeHistogramLayout(histogramData, canvasWidth, canvasHeight) { + const bins = histogramData?.bins; + if (!bins || bins.length === 0) { + return { bars: [], yMax: 0, yTicks: [], chartArea: null }; + } + + const chartLeft = kLeftMargin; + const chartRight = canvasWidth - kRightMargin; + const chartTop = kTopMargin; + const chartBottom = canvasHeight - kBottomMargin; + const chartWidth = chartRight - chartLeft; + const chartHeight = chartBottom - chartTop; + + if (chartWidth <= 0 || chartHeight <= 0) { + return { bars: [], yMax: 0, yTicks: [], chartArea: null }; + } + + const chartArea = { left: chartLeft, right: chartRight, + top: chartTop, bottom: chartBottom }; + + // Find max count for Y scale + let maxCount = 0; + for (const bin of bins) { + if (bin.count > maxCount) maxCount = bin.count; + } + if (maxCount === 0) maxCount = 1; + + // Compute nice Y-axis max and ticks + const { yMax, yTicks } = computeYAxis(maxCount); + + // Compute bar rectangles + const barWidth = (chartWidth - kBarGap * (bins.length - 1)) / bins.length; + const bars = []; + for (let i = 0; i < bins.length; i++) { + const bin = bins[i]; + const barHeight = (bin.count / yMax) * chartHeight; + bars.push({ + x: chartLeft + i * (barWidth + kBarGap), + y: chartBottom - barHeight, + width: barWidth, + height: barHeight, + count: bin.count, + lower: bin.lower, + upper: bin.upper, + negative: bin.negative, + }); + } + + return { bars, yMax, yTicks, chartArea }; +} + +// Compute nice Y-axis max and tick values. +function computeYAxis(maxCount) { + if (maxCount <= 10) { + const ticks = []; + for (let i = 0; i <= maxCount; i++) ticks.push(i); + return { yMax: maxCount, yTicks: ticks }; + } + + // Snap to a nice ceiling + const digits = Math.floor(Math.log10(maxCount)) + 1; + const firstDigit = Math.floor(maxCount / Math.pow(10, digits - 1)); + const snapMax = (firstDigit + 1) * Math.pow(10, digits - 1); + + let total = Math.pow(10, digits); + if (firstDigit < 5) total /= 2; + const interval = Math.ceil(total / 10); + + const ticks = []; + for (let v = 0; v <= snapMax; v += interval) { + ticks.push(v); + } + + return { yMax: snapMax, yTicks: ticks }; +} + +export class ChartsWidget { + constructor(container, app, redrawAllLayers) { + this._app = app; + this._redrawAllLayers = redrawAllLayers; + this._currentTab = 'setup'; + this._histogramData = null; + this._bars = []; + this._yMax = 0; + this._yTicks = []; + this._chartArea = null; + this._hoveredBar = null; + + this._build(container); + } + + // ---- DOM construction ---- + + _build(container) { + const el = document.createElement('div'); + el.className = 'charts-widget'; + + // Toolbar + const toolbar = document.createElement('div'); + toolbar.className = 'charts-toolbar'; + + this._updateBtn = document.createElement('button'); + this._updateBtn.className = 'timing-btn'; + this._updateBtn.textContent = 'Update'; + + this._statusLabel = document.createElement('span'); + this._statusLabel.className = 'timing-path-count'; + + toolbar.appendChild(this._updateBtn); + toolbar.appendChild(this._statusLabel); + el.appendChild(toolbar); + + // Tab bar (Setup / Hold) + const tabBar = document.createElement('div'); + tabBar.className = 'timing-tab-bar'; + + this._setupTab = document.createElement('div'); + this._setupTab.className = 'timing-tab active'; + this._setupTab.textContent = 'Setup Slack'; + + this._holdTab = document.createElement('div'); + this._holdTab.className = 'timing-tab'; + this._holdTab.textContent = 'Hold Slack'; + + tabBar.appendChild(this._setupTab); + tabBar.appendChild(this._holdTab); + el.appendChild(tabBar); + + // Filter row + const filterRow = document.createElement('div'); + filterRow.className = 'charts-toolbar'; + + const pgLabel = document.createElement('span'); + pgLabel.textContent = 'Path Group:'; + pgLabel.style.color = '#888'; + pgLabel.style.fontSize = '12px'; + this._pathGroupSelect = document.createElement('select'); + this._pathGroupSelect.className = 'charts-select'; + this._pathGroupSelect.innerHTML = ''; + + const clkLabel = document.createElement('span'); + clkLabel.textContent = 'Clock:'; + clkLabel.style.color = '#888'; + clkLabel.style.fontSize = '12px'; + this._clockSelect = document.createElement('select'); + this._clockSelect.className = 'charts-select'; + this._clockSelect.innerHTML = ''; + + filterRow.appendChild(pgLabel); + filterRow.appendChild(this._pathGroupSelect); + filterRow.appendChild(clkLabel); + filterRow.appendChild(this._clockSelect); + el.appendChild(filterRow); + + // Canvas + this._canvas = document.createElement('canvas'); + this._canvas.className = 'charts-canvas'; + el.appendChild(this._canvas); + + // Tooltip + this._tooltip = document.createElement('div'); + this._tooltip.className = 'charts-tooltip'; + this._tooltip.style.display = 'none'; + el.appendChild(this._tooltip); + + container.element.appendChild(el); + this._el = el; + + this._ctx = this._canvas.getContext('2d'); + this._bindEvents(); + } + + _bindEvents() { + this._updateBtn.addEventListener('click', () => this.update()); + + this._setupTab.addEventListener('click', () => { + if (this._currentTab === 'setup') return; + this._currentTab = 'setup'; + this._setupTab.classList.add('active'); + this._holdTab.classList.remove('active'); + this.update(); + }); + + this._holdTab.addEventListener('click', () => { + if (this._currentTab === 'hold') return; + this._currentTab = 'hold'; + this._holdTab.classList.add('active'); + this._setupTab.classList.remove('active'); + this.update(); + }); + + this._pathGroupSelect.addEventListener('change', () => this._fetchHistogram()); + this._clockSelect.addEventListener('change', () => this._fetchHistogram()); + + this._canvas.addEventListener('mousemove', (e) => this._handleHover(e)); + this._canvas.addEventListener('mouseleave', () => { + this._hoveredBar = null; + this._tooltip.style.display = 'none'; + this._render(); + }); + this._canvas.addEventListener('click', (e) => this._handleClick(e)); + + // Re-render on resize + const ro = new ResizeObserver(() => { + this._sizeCanvas(); + this._computeLayout(); + this._render(); + }); + ro.observe(this._canvas); + } + + _sizeCanvas() { + const rect = this._canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + this._canvas.width = rect.width * dpr; + this._canvas.height = rect.height * dpr; + this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + + async update() { + this._updateBtn.disabled = true; + this._updateBtn.textContent = 'Loading...'; + this._statusLabel.textContent = ''; + try { + await this._fetchFilters(); + await this._fetchHistogram(); + } catch (err) { + this._statusLabel.textContent = 'Error: ' + err.message; + } + this._updateBtn.disabled = false; + this._updateBtn.textContent = 'Update'; + } + + async _fetchFilters() { + const filters = await this._app.websocketManager.request({ + type: 'chart_filters', + }); + this._populateSelect(this._pathGroupSelect, filters.path_groups || []); + this._populateSelect(this._clockSelect, filters.clocks || []); + } + + _populateSelect(select, items) { + const prev = select.value; + select.innerHTML = ''; + for (const name of items) { + const opt = document.createElement('option'); + opt.value = name; + opt.textContent = name; + select.appendChild(opt); + } + // Restore previous selection if still valid + if (items.includes(prev)) { + select.value = prev; + } + } + + async _fetchHistogram() { + try { + const req = { + type: 'slack_histogram', + is_setup: this._currentTab === 'setup' ? 1 : 0, + }; + if (this._pathGroupSelect.value) { + req.path_group = this._pathGroupSelect.value; + } + if (this._clockSelect.value) { + req.clock_name = this._clockSelect.value; + } + const data = await this._app.websocketManager.request(req); + this._histogramData = data; + this._sizeCanvas(); + this._computeLayout(); + this._render(); + + const total = data.total_endpoints || 0; + const unconstrained = data.unconstrained_count || 0; + const constrained = total - unconstrained; + this._statusLabel.textContent = `${constrained} endpoints` + + (unconstrained > 0 ? `, ${unconstrained} unconstrained` : ''); + } catch (err) { + this._statusLabel.textContent = 'Error: ' + err.message; + } + } + + _computeLayout() { + if (!this._histogramData) return; + const rect = this._canvas.getBoundingClientRect(); + const result = computeHistogramLayout( + this._histogramData, rect.width, rect.height); + this._bars = result.bars; + this._yMax = result.yMax; + this._yTicks = result.yTicks; + this._chartArea = result.chartArea; + } + + _render() { + const rect = this._canvas.getBoundingClientRect(); + const w = rect.width; + const h = rect.height; + const ctx = this._ctx; + + ctx.clearRect(0, 0, w, h); + + // Background + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, w, h); + + if (!this._bars || this._bars.length === 0) { + ctx.fillStyle = '#666'; + ctx.font = '14px monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('Click "Update" to load histogram', w / 2, h / 2); + return; + } + + this._drawAxes(ctx); + this._drawBars(ctx); + this._drawTitle(ctx, w); + } + + _drawAxes(ctx) { + const ca = this._chartArea; + if (!ca) return; + + ctx.strokeStyle = '#555'; + ctx.lineWidth = 1; + + // Y axis line + ctx.beginPath(); + ctx.moveTo(ca.left, ca.top); + ctx.lineTo(ca.left, ca.bottom); + ctx.stroke(); + + // X axis line + ctx.beginPath(); + ctx.moveTo(ca.left, ca.bottom); + ctx.lineTo(ca.right, ca.bottom); + ctx.stroke(); + + // Y axis ticks and labels + ctx.fillStyle = '#aaa'; + ctx.font = '11px monospace'; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + const chartHeight = ca.bottom - ca.top; + for (const tick of this._yTicks) { + const y = ca.bottom - (tick / this._yMax) * chartHeight; + ctx.fillText(String(tick), ca.left - 6, y); + if (tick > 0) { + ctx.strokeStyle = '#333'; + ctx.beginPath(); + ctx.moveTo(ca.left, y); + ctx.lineTo(ca.right, y); + ctx.stroke(); + ctx.strokeStyle = '#555'; + } + } + + // Y axis title + ctx.save(); + ctx.translate(14, (ca.top + ca.bottom) / 2); + ctx.rotate(-Math.PI / 2); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#888'; + ctx.font = '11px monospace'; + ctx.fillText('Endpoints', 0, 0); + ctx.restore(); + + // X axis labels — show bin boundaries + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillStyle = '#aaa'; + ctx.font = '10px monospace'; + + const bins = this._histogramData.bins; + const unit = this._histogramData.time_unit || ''; + + // Determine precision from bin width + const binWidth = bins.length > 0 ? bins[0].upper - bins[0].lower : 1; + const precision = Math.max(0, -Math.floor(Math.log10(binWidth))); + + // Label at each bar boundary + for (let i = 0; i <= this._bars.length; i++) { + const val = i < bins.length ? bins[i].lower : bins[bins.length - 1].upper; + const x = i < this._bars.length + ? this._bars[i].x + : this._bars[this._bars.length - 1].x + this._bars[this._bars.length - 1].width; + ctx.fillText(val.toFixed(precision), x, ca.bottom + 4); + } + + // X axis title + ctx.fillStyle = '#888'; + ctx.font = '11px monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillText(`Slack [${unit}]`, (ca.left + ca.right) / 2, ca.bottom + 22); + } + + _drawBars(ctx) { + for (const bar of this._bars) { + if (bar.height <= 0) continue; + + const isHovered = (this._hoveredBar === bar); + + // Fill + ctx.fillStyle = bar.negative ? kNegativeFill : kPositiveFill; + if (isHovered) { + // Lighten on hover + ctx.fillStyle = bar.negative ? '#ff9999' : '#b0ffb0'; + } + ctx.fillRect(bar.x, bar.y, bar.width, bar.height); + + // Border + ctx.strokeStyle = bar.negative ? kNegativeBorder : kPositiveBorder; + ctx.lineWidth = 1; + ctx.strokeRect(bar.x, bar.y, bar.width, bar.height); + } + } + + _drawTitle(ctx, canvasWidth) { + const mode = this._currentTab === 'setup' ? 'Setup' : 'Hold'; + ctx.fillStyle = '#ccc'; + ctx.font = '13px monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillText(`${mode} Endpoint Slack`, canvasWidth / 2, 8); + } + + _hitTestBar(e) { + if (!this._bars) return null; + const rect = this._canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + for (const bar of this._bars) { + if (mx >= bar.x && mx <= bar.x + bar.width && + my >= bar.y && my <= bar.y + bar.height) { + return bar; + } + } + return null; + } + + _handleHover(e) { + const bar = this._hitTestBar(e); + if (bar !== this._hoveredBar) { + this._hoveredBar = bar; + this._render(); + } + + if (bar) { + const unit = this._histogramData?.time_unit || ''; + const binWidth = bar.upper - bar.lower; + const precision = Math.max(0, -Math.floor(Math.log10(binWidth))); + this._tooltip.textContent = + `Endpoints: ${bar.count}\n` + + `Slack: [${bar.lower.toFixed(precision)}, ${bar.upper.toFixed(precision)}) ${unit}`; + this._tooltip.style.display = 'block'; + + const rect = this._el.getBoundingClientRect(); + const tx = e.clientX - rect.left + 12; + const ty = e.clientY - rect.top - 10; + this._tooltip.style.left = tx + 'px'; + this._tooltip.style.top = ty + 'px'; + } else { + this._tooltip.style.display = 'none'; + } + } + + async _handleClick(e) { + const bar = this._hitTestBar(e); + if (!bar || bar.count === 0) return; + + try { + const resp = await this._app.websocketManager.request({ + type: 'timing_report', + is_setup: this._currentTab === 'setup' ? 1 : 0, + max_paths: 50, + slack_min: bar.lower, + slack_max: bar.upper, + }); + + if (this._app.timingWidget) { + this._app.timingWidget.showPaths( + this._currentTab, resp.paths || []); + if (this._app.focusComponent) { + this._app.focusComponent('TimingWidget'); + } + } + } catch (err) { + console.error('Charts bar click error:', err); + } + } +} diff --git a/src/web/src/checkbox-tree-model.js b/src/web/src/checkbox-tree-model.js new file mode 100644 index 00000000000..f28f181f607 --- /dev/null +++ b/src/web/src/checkbox-tree-model.js @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Pure-model checkbox tree with tri-state propagation. +// No DOM dependency — consumers handle their own rendering. +// +// Nodes with hasCheckbox:false are structural: they are traversed through +// but skipped during tri-state computation. + +export class CheckboxTreeModel { + constructor(onChange) { + this.onChange = onChange; + this.roots = []; + this._nodeMap = new Map(); + } + + // Build from a declarative spec. + // Spec: { id, label?, checked?, hasCheckbox?, data?, children?: [...] } + // Returns the root node. + addFromSpec(spec, parent = null) { + const node = this._makeNode(spec.id, parent, spec); + node.checked = spec.checked !== false; + if (spec.children) { + for (const child of spec.children) { + node.children.push(this.addFromSpec(child, node)); + } + if (node.hasCheckbox) { + this._computeParent(node); + } + } + if (!parent) { + this.roots.push(node); + } + return node; + } + + // Build from a flat list of { id, parentId, hasCheckbox, checked, data }. + // Returns array of root nodes. + buildFromNodes(flatNodes) { + this._nodeMap.clear(); + this.roots = []; + + for (const item of flatNodes) { + this._makeNode(item.id, null, item); + } + + for (const item of flatNodes) { + const node = this._nodeMap.get(item.id); + if (item.parentId != null && item.parentId >= 0 + && this._nodeMap.has(item.parentId)) { + const parent = this._nodeMap.get(item.parentId); + node.parent = parent; + parent.children.push(node); + } else { + this.roots.push(node); + } + } + + // Compute parent states bottom-up. + for (let i = flatNodes.length - 1; i >= 0; i--) { + const node = this._nodeMap.get(flatNodes[i].id); + if (node.hasCheckbox && node.children.length > 0) { + this._computeParent(node); + } + } + + return this.roots; + } + + get(id) { + return this._nodeMap.get(id); + } + + // Handle a user check/uncheck. Propagates down then up. + check(id, checked) { + const node = this._nodeMap.get(id); + if (!node) return; + node.checked = checked; + node.indeterminate = false; + for (const c of node.children) { + this._setSubtree(c, checked); + } + for (let n = node.parent; n; n = n.parent) { + if (n.hasCheckbox) { + this._computeParent(n); + } + } + this.onChange(); + } + + // Bulk check/uncheck. Single onChange call at the end. + // idToChecked: object { id: boolean, ... } + checkSet(idToChecked) { + for (const [id, checked] of Object.entries(idToChecked)) { + const node = this._nodeMap.get(id); + if (!node) continue; + node.checked = checked; + node.indeterminate = false; + } + this._recomputeAllParents(); + this.onChange(); + } + + // DFS iteration over every node. + forEach(fn) { + const walk = (node) => { + fn(node); + for (const c of node.children) walk(c); + }; + for (const r of this.roots) walk(r); + } + + // -- internal -- + + _makeNode(id, parent, spec) { + const node = { + id, + parent, + children: [], + hasCheckbox: spec.hasCheckbox !== false, + checked: spec.checked !== false, + indeterminate: false, + data: spec.data != null ? spec.data : spec, + }; + this._nodeMap.set(id, node); + return node; + } + + // Gather the nearest checkbox-bearing descendants of a node. + // Non-checkbox children are traversed through. + _checkboxDescendants(node) { + const result = []; + for (const child of node.children) { + if (child.hasCheckbox) { + result.push(child); + } else { + result.push(...this._checkboxDescendants(child)); + } + } + return result; + } + + _computeParent(node) { + const leaves = this._checkboxDescendants(node); + if (leaves.length === 0) return; + const all = leaves.every(c => c.checked && !c.indeterminate); + const none = leaves.every(c => !c.checked && !c.indeterminate); + node.checked = all; + node.indeterminate = !all && !none; + } + + _setSubtree(node, checked) { + if (node.hasCheckbox) { + node.checked = checked; + node.indeterminate = false; + } + for (const c of node.children) { + this._setSubtree(c, checked); + } + } + + _recomputeAllParents() { + const recompute = (node) => { + for (const c of node.children) recompute(c); + if (node.hasCheckbox && node.children.length > 0) { + this._computeParent(node); + } + }; + for (const r of this.roots) recompute(r); + } +} diff --git a/src/web/src/clock-tree-widget.js b/src/web/src/clock-tree-widget.js new file mode 100644 index 00000000000..dfac231df1e --- /dev/null +++ b/src/web/src/clock-tree-widget.js @@ -0,0 +1,642 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Canvas-based clock tree viewer widget. + +export const kNodeSpacing = 24; // pixels between adjacent leaf bins +export const kNodeSize = 10; // base node shape size in pixels +export const kTopMargin = 40; // pixels above the first node +export const kBottomMargin = 40; // pixels below the last node +export const kLeftMargin = 70; // room for time-axis labels +export const kRightMargin = 20; + +// Node type → fill colour +const kTypeColors = { + root: '#ff4444', + buffer: '#4488ff', + inverter: '#00b0b0', + clock_gate: '#cc44cc', + register: '#ff4444', + macro: '#00b0b0', + unknown: '#888888', +}; + +// Pure layout computation — extracted for testability. +export function computeClockTreeLayout(clockData) { + const nodes = clockData.nodes; + if (!nodes || nodes.length === 0) { + return { + layout: [], + layoutWidth: 0, + layoutHeight: 0, + timeMin: 0, + timeMax: 0, + timeUnit: '', + sceneHeight: 0, + layoutById: new Map(), + }; + } + + // Build children map + const childrenMap = new Map(); + for (const n of nodes) { + childrenMap.set(n.id, []); + } + for (const n of nodes) { + if (n.parent_id >= 0 && childrenMap.has(n.parent_id)) { + childrenMap.get(n.parent_id).push(n.id); + } + } + const nodeById = new Map(); + for (const n of nodes) { + nodeById.set(n.id, n); + } + + // Compute total leaf-equivalent width for each subtree + const widthOf = new Map(); + const computeWidth = (id) => { + const kids = childrenMap.get(id) || []; + if (kids.length === 0) { + widthOf.set(id, 1); + return 1; + } + let w = 0; + for (const kid of kids) { + w += computeWidth(kid); + } + widthOf.set(id, w); + return w; + }; + + // Find root nodes (parent_id == -1) + const roots = nodes.filter(n => n.parent_id === -1); + let totalWidth = 0; + for (const r of roots) { + totalWidth += computeWidth(r.id); + } + + // Time range + const minArr = clockData.min_arrival; + const maxArr = clockData.max_arrival; + const timeRange = maxArr - minArr || 1; + const sceneHeight = Math.max(200, totalWidth * kNodeSpacing * 0.6); + + // Assign x positions: each node gets the centre of its allocated bins + const layout = []; + const assignPositions = (id, binStart) => { + const n = nodeById.get(id); + const w = widthOf.get(id); + const x = kLeftMargin + (binStart + w / 2) * kNodeSpacing; + const y = kTopMargin + + ((n.arrival - minArr) / timeRange) * sceneHeight; + + layout.push({ + id: n.id, + x, + y, + type: n.type, + name: n.name, + pin_name: n.pin_name, + arrival: n.arrival, + delay: n.delay, + fanout: n.fanout, + level: n.level, + parent_id: n.parent_id, + }); + + let offset = binStart; + const kids = childrenMap.get(id) || []; + for (const kid of kids) { + assignPositions(kid, offset); + offset += widthOf.get(kid); + } + }; + + let rootOffset = 0; + for (const r of roots) { + assignPositions(r.id, rootOffset); + rootOffset += widthOf.get(r.id); + } + + const layoutWidth = kLeftMargin + totalWidth * kNodeSpacing + kRightMargin; + const layoutHeight = kTopMargin + sceneHeight + kBottomMargin; + + const layoutById = new Map(); + for (const item of layout) { + layoutById.set(item.id, item); + } + + return { + layout, + layoutWidth, + layoutHeight, + timeMin: minArr, + timeMax: maxArr, + timeUnit: clockData.time_unit || '', + sceneHeight, + layoutById, + }; +} + +export class ClockTreeWidget { + constructor(container, app, redrawAllLayers) { + this._app = app; + this._redrawAllLayers = redrawAllLayers; + this._clockData = []; + this._selectedClockIdx = 0; + this._selectedNodeId = -1; + + // Canvas transform (pan / zoom) + this._tx = 0; + this._ty = 0; + this._scale = 1; + + // Computed layout: array of {id, x, y, type, name, ...} + this._layout = []; + + // Drag state + this._dragging = false; + this._dragStartX = 0; + this._dragStartY = 0; + this._dragStartTx = 0; + this._dragStartTy = 0; + + this._build(container); + } + + // ---- DOM construction ---- + + _build(container) { + const el = document.createElement('div'); + el.className = 'clock-tree-widget'; + + // Toolbar + const toolbar = document.createElement('div'); + toolbar.className = 'clock-tree-toolbar'; + this._updateBtn = document.createElement('button'); + this._updateBtn.className = 'timing-btn'; + this._updateBtn.textContent = 'Update'; + this._fitBtn = document.createElement('button'); + this._fitBtn.className = 'timing-btn'; + this._fitBtn.textContent = 'Fit'; + this._statusLabel = document.createElement('span'); + this._statusLabel.className = 'timing-path-count'; + toolbar.appendChild(this._updateBtn); + toolbar.appendChild(this._fitBtn); + toolbar.appendChild(this._statusLabel); + el.appendChild(toolbar); + + // Tab bar (one tab per clock) + this._tabBar = document.createElement('div'); + this._tabBar.className = 'timing-tab-bar'; + el.appendChild(this._tabBar); + + // Canvas + this._canvas = document.createElement('canvas'); + this._canvas.className = 'clock-tree-canvas'; + el.appendChild(this._canvas); + + // Tooltip + this._tooltip = document.createElement('div'); + this._tooltip.className = 'clock-tree-tooltip'; + this._tooltip.style.display = 'none'; + el.appendChild(this._tooltip); + + container.element.appendChild(el); + this._el = el; + + this._ctx = this._canvas.getContext('2d'); + this._bindEvents(); + } + + _fit() { + this._sizeCanvas(); + this._fitToView(); + this._render(); + } + + _bindEvents() { + this._updateBtn.addEventListener('click', () => this.update()); + this._fitBtn.addEventListener('click', () => this._fit()); + + this._canvas.addEventListener('keydown', (e) => { + if (e.key === 'f') { + this._fit(); + } + }); + this._canvas.tabIndex = 0; // make canvas focusable for key events + + this._canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + const rect = this._canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15; + // Zoom centred on cursor + this._tx = mx - (mx - this._tx) * factor; + this._ty = my - (my - this._ty) * factor; + this._scale *= factor; + this._render(); + }); + + this._canvas.addEventListener('mousedown', (e) => { + this._dragging = true; + this._dragStartX = e.clientX; + this._dragStartY = e.clientY; + this._dragStartTx = this._tx; + this._dragStartTy = this._ty; + this._canvas.style.cursor = 'grabbing'; + }); + + window.addEventListener('mousemove', (e) => { + if (this._dragging) { + this._tx = this._dragStartTx + (e.clientX - this._dragStartX); + this._ty = this._dragStartTy + (e.clientY - this._dragStartY); + this._render(); + } else { + this._handleHover(e); + } + }); + + window.addEventListener('mouseup', () => { + if (this._dragging) { + this._dragging = false; + this._canvas.style.cursor = 'grab'; + } + }); + + this._canvas.addEventListener('click', (e) => this._handleClick(e)); + + // Resize observer to keep canvas resolution matched to layout + this._resizeObserver = new ResizeObserver(() => { + this._sizeCanvas(); + this._render(); + }); + this._resizeObserver.observe(this._canvas); + } + + _sizeCanvas() { + const rect = this._canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + this._canvas.width = rect.width * dpr; + this._canvas.height = rect.height * dpr; + this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + + // ---- Data fetching ---- + + async update() { + this._statusLabel.textContent = 'Loading...'; + try { + const data = await this._app.websocketManager.request( + { type: 'clock_tree' }); + this._clockData = data.clocks || []; + this._buildTabs(); + this._selectClock(0); + this._statusLabel.textContent = + this._clockData.length + ' clock' + + (this._clockData.length !== 1 ? 's' : ''); + } catch (err) { + console.error('Clock tree error:', err); + this._statusLabel.textContent = 'Error: ' + err.message; + } + } + + _buildTabs() { + this._tabBar.innerHTML = ''; + this._clockData.forEach((clk, idx) => { + const btn = document.createElement('button'); + btn.className = 'timing-tab' + + (idx === this._selectedClockIdx ? ' active' : ''); + btn.textContent = clk.name; + btn.addEventListener('click', () => this._selectClock(idx)); + this._tabBar.appendChild(btn); + }); + } + + _selectClock(idx) { + if (idx < 0 || idx >= this._clockData.length) return; + this._selectedClockIdx = idx; + this._selectedNodeId = -1; + + // Update tab highlights + const tabs = this._tabBar.querySelectorAll('.timing-tab'); + tabs.forEach((t, i) => { + t.classList.toggle('active', i === idx); + }); + + this._computeLayout(this._clockData[idx]); + // Defer fit+render to ensure canvas has been laid out + requestAnimationFrame(() => { + this._sizeCanvas(); + this._fitToView(); + this._render(); + }); + } + + // ---- Layout computation ---- + + _computeLayout(clockData) { + const result = computeClockTreeLayout(clockData); + this._layout = result.layout; + this._layoutWidth = result.layoutWidth; + this._layoutHeight = result.layoutHeight; + this._timeMin = result.timeMin; + this._timeMax = result.timeMax; + this._timeUnit = result.timeUnit; + this._sceneHeight = result.sceneHeight; + this._layoutById = result.layoutById; + } + + _fitToView() { + if (!this._layout.length) return; + const rect = this._canvas.getBoundingClientRect(); + const cw = rect.width; + const ch = rect.height; + const sx = cw / this._layoutWidth; + const sy = ch / this._layoutHeight; + this._scale = Math.min(sx, sy) * 0.9; + this._tx = (cw - this._layoutWidth * this._scale) / 2; + this._ty = (ch - this._layoutHeight * this._scale) / 2; + } + + // ---- Rendering ---- + + _render() { + const ctx = this._ctx; + const rect = this._canvas.getBoundingClientRect(); + const w = rect.width; + const h = rect.height; + ctx.clearRect(0, 0, w, h); + + // Background + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, w, h); + + if (!this._layout.length) { + ctx.fillStyle = '#666'; + ctx.font = '14px monospace'; + ctx.textAlign = 'center'; + ctx.fillText('Click "Update" to load clock tree data', + w / 2, h / 2); + return; + } + + ctx.save(); + ctx.translate(this._tx, this._ty); + ctx.scale(this._scale, this._scale); + + // Draw connections first (behind nodes) + this._drawConnections(ctx); + + // Draw nodes + for (const item of this._layout) { + this._drawNode(ctx, item, item.id === this._selectedNodeId); + } + + ctx.restore(); + + // Draw time scale (in screen coordinates, on the left) + this._drawTimeScale(ctx, w, h); + } + + _drawConnections(ctx) { + const lw = 1.5 / this._scale; + const timeRange = this._timeMax - this._timeMin || 1; + + for (const item of this._layout) { + if (item.parent_id < 0) continue; + const parent = this._layoutById.get(item.parent_id); + if (!parent) continue; + + const px = parent.x; + const py = parent.y + kNodeSize / 2; // bottom of parent node + const cx = item.x; + const cy = item.y - kNodeSize / 2; // top of child node + + // Cell delay: vertical segment below parent node + const cellDelayPx = (parent.delay || 0) / timeRange + * this._sceneHeight; + const splitY = py + cellDelayPx; + + // Cell delay segment (orange) + if (cellDelayPx > 0) { + ctx.strokeStyle = '#886644'; + ctx.lineWidth = lw; + ctx.beginPath(); + ctx.moveTo(px, py); + ctx.lineTo(px, splitY); + ctx.stroke(); + } + + // Wire delay segment (green Bezier from splitY to child) + ctx.strokeStyle = '#338833'; + ctx.lineWidth = lw; + const midY = (splitY + cy) / 2; + ctx.beginPath(); + ctx.moveTo(px, splitY); + ctx.bezierCurveTo(px, midY, cx, midY, cx, cy); + ctx.stroke(); + } + } + + _drawNode(ctx, item, selected) { + const x = item.x; + const y = item.y; + const s = kNodeSize; + const color = kTypeColors[item.type] || kTypeColors.unknown; + + ctx.fillStyle = color; + + switch (item.type) { + case 'root': + case 'buffer': + // Triangle pointing down + ctx.beginPath(); + ctx.moveTo(x - s / 2, y - s / 2); + ctx.lineTo(x + s / 2, y - s / 2); + ctx.lineTo(x, y + s / 2); + ctx.closePath(); + ctx.fill(); + break; + case 'inverter': + // Triangle with small circle at tip + ctx.beginPath(); + ctx.moveTo(x - s / 2, y - s / 2); + ctx.lineTo(x + s / 2, y - s / 2); + ctx.lineTo(x, y + s / 2); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.arc(x, y + s / 2 + 2, 2, 0, Math.PI * 2); + ctx.fill(); + break; + case 'clock_gate': + // Circle + ctx.beginPath(); + ctx.arc(x, y, s / 2, 0, Math.PI * 2); + ctx.fill(); + break; + case 'register': + case 'macro': + // Rectangle + ctx.fillRect(x - s / 2, y - s / 4, s, s / 2); + break; + default: + // Diamond + ctx.beginPath(); + ctx.moveTo(x, y - s / 2); + ctx.lineTo(x + s / 2, y); + ctx.lineTo(x, y + s / 2); + ctx.lineTo(x - s / 2, y); + ctx.closePath(); + ctx.fill(); + break; + } + + if (selected) { + ctx.strokeStyle = '#ffff00'; + ctx.lineWidth = 2 / this._scale; + ctx.beginPath(); + ctx.arc(x, y, s / 2 + 3, 0, Math.PI * 2); + ctx.stroke(); + } + } + + _drawTimeScale(ctx, canvasW, canvasH) { + if (!this._sceneHeight) return; + + // Map layout Y to screen Y + const yToScreen = (layoutY) => layoutY * this._scale + this._ty; + + const topY = yToScreen(kTopMargin); + const botY = yToScreen(kTopMargin + this._sceneHeight); + + // Only draw if visible + if (botY < 0 || topY > canvasH) return; + + ctx.save(); + ctx.fillStyle = '#aaa'; + ctx.strokeStyle = '#555'; + ctx.font = '11px monospace'; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + + const range = this._timeMax - this._timeMin; + if (range <= 0) { + ctx.restore(); + return; + } + + // Choose nice tick interval + const pixelRange = botY - topY; + const targetTicks = Math.max(2, Math.floor(pixelRange / 40)); + const rawInterval = range / targetTicks; + const mag = Math.pow(10, Math.floor(Math.log10(rawInterval))); + let interval = mag; + if (rawInterval / mag > 5) interval = 10 * mag; + else if (rawInterval / mag > 2) interval = 5 * mag; + else if (rawInterval / mag > 1) interval = 2 * mag; + + const startVal = Math.ceil(this._timeMin / interval) * interval; + + ctx.beginPath(); + for (let val = startVal; val <= this._timeMax; val += interval) { + const frac = (val - this._timeMin) / range; + const sy = topY + frac * (botY - topY); + ctx.moveTo(0, sy); + ctx.lineTo(canvasW, sy); + + const label = val.toFixed( + interval < 0.01 ? 3 : interval < 0.1 ? 2 : 1) + + ' ' + this._timeUnit; + ctx.fillText(label, kLeftMargin - 4, sy); + } + ctx.stroke(); + ctx.restore(); + } + + // ---- Interaction ---- + + _screenToLayout(screenX, screenY) { + return { + x: (screenX - this._tx) / this._scale, + y: (screenY - this._ty) / this._scale, + }; + } + + _hitTest(screenX, screenY) { + const p = this._screenToLayout(screenX, screenY); + const hitRadius = kNodeSize; + for (const item of this._layout) { + const dx = p.x - item.x; + const dy = p.y - item.y; + if (dx * dx + dy * dy < hitRadius * hitRadius) { + return item; + } + } + return null; + } + + _handleClick(e) { + const rect = this._canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + const hit = this._hitTest(mx, my); + + if (hit) { + this._selectedNodeId = hit.id; + this._render(); + // Highlight on the layout map + this._app.websocketManager.request({ + type: 'clock_tree_highlight', + inst_name: hit.name, + }).then(() => this._redrawAllLayers()); + } else { + if (this._selectedNodeId >= 0) { + this._selectedNodeId = -1; + this._render(); + this._app.websocketManager.request({ + type: 'clock_tree_highlight', + inst_name: '', + }).then(() => this._redrawAllLayers()); + } + } + } + + _handleHover(e) { + const rect = this._canvas.getBoundingClientRect(); + const mx = e.clientX - rect.left; + const my = e.clientY - rect.top; + + // Only handle if mouse is over our canvas + if (mx < 0 || my < 0 || + mx > rect.width || my > rect.height) { + this._tooltip.style.display = 'none'; + return; + } + + const hit = this._hitTest(mx, my); + if (hit) { + const lines = [ + hit.name, + 'Type: ' + hit.type, + 'Arrival: ' + hit.arrival.toFixed(3) + + ' ' + (this._timeUnit || ''), + ]; + if (hit.delay) { + lines.push('Delay: ' + hit.delay.toFixed(3) + + ' ' + (this._timeUnit || '')); + } + if (hit.fanout) { + lines.push('Fanout: ' + hit.fanout); + } + this._tooltip.textContent = lines.join('\n'); + this._tooltip.style.display = 'block'; + this._tooltip.style.left = (e.clientX + 12) + 'px'; + this._tooltip.style.top = (e.clientY + 12) + 'px'; + } else { + this._tooltip.style.display = 'none'; + } + } +} diff --git a/src/web/src/clock_tree_report.cpp b/src/web/src/clock_tree_report.cpp new file mode 100644 index 00000000000..7a32d0894dc --- /dev/null +++ b/src/web/src/clock_tree_report.cpp @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include "clock_tree_report.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "db_sta/dbNetwork.hh" +#include "db_sta/dbSta.hh" +#include "odb/db.h" +#include "odb/geom.h" +#include "sta/Clock.hh" +#include "sta/Delay.hh" +#include "sta/Graph.hh" +#include "sta/Liberty.hh" +#include "sta/MinMax.hh" +#include "sta/Mode.hh" +#include "sta/NetworkClass.hh" +#include "sta/Path.hh" +#include "sta/PathExpanded.hh" +#include "sta/Scene.hh" +#include "sta/Sdc.hh" +#include "sta/Search.hh" +#include "sta/Transition.hh" +#include "sta/Units.hh" + +namespace web { + +const char* ClockTreeNode::typeToString(Type t) +{ + switch (t) { + case ROOT: + return "root"; + case BUFFER: + return "buffer"; + case INVERTER: + return "inverter"; + case CLOCK_GATE: + return "clock_gate"; + case REGISTER: + return "register"; + case MACRO: + return "macro"; + default: + return "unknown"; + } +} + +// ---- Internal tree structure (mirrors gui::ClockTree) ---- + +namespace { + +struct TreeNode +{ + TreeNode* parent = nullptr; + sta::Net* net = nullptr; + sta::dbNetwork* network = nullptr; + int level = 0; + + std::map drivers; + std::map child_sinks; + std::map leaves; + std::vector> fanout; + + sta::Net* getNet(const sta::Pin* pin) const + { + sta::Term* term = network->term(pin); + sta::Net* n = term ? network->net(term) : network->net(pin); + return network->findFlatNet(n); + } + + bool isLeaf(const sta::Pin* pin) const + { + return network->isRegClkPin(pin) || network->isLatchData(pin); + } + + bool addVertex(sta::Vertex* vertex, sta::Delay delay) + { + const sta::Pin* pin = vertex->pin(); + if (isLeaf(pin)) { + leaves[pin] = delay; + return false; + } + if (vertex->isDriver(network)) { + drivers[pin] = delay; + } else { + child_sinks[pin] = delay; + } + return true; + } + + TreeNode* findChild(sta::Net* target) + { + for (auto& child : fanout) { + if (child->net == target) { + return child.get(); + } + } + return nullptr; + } + + TreeNode* getOrCreateChild(sta::Net* target) + { + TreeNode* existing = findChild(target); + if (existing) { + return existing; + } + auto child = std::make_unique(); + child->parent = this; + child->net = target; + child->network = network; + child->level = level + 1; + TreeNode* ptr = child.get(); + fanout.push_back(std::move(child)); + return ptr; + } + + void addPath(sta::PathExpanded& path, int idx, const sta::StaState* sta) + { + if (idx == static_cast(path.size())) { + return; + } + const sta::Path* ref = path.path(idx); + sta::Vertex* vertex = ref->vertex(sta); + sta::Pin* pin = vertex->pin(); + sta::Net* pin_net = getNet(pin); + + TreeNode* target = getOrCreateChild(pin_net); + if (target->addVertex(vertex, ref->arrival())) { + target->addPath(path, idx + 1, sta); + } + } + + void addPath(sta::PathExpanded& path, const sta::StaState* sta) + { + const sta::Path* start = path.startPath(); + if (start->clkEdge(sta)->transition() != sta::RiseFall::rise()) { + return; + } + if (start->minMax(sta) != sta::MinMax::max()) { + return; + } + if (getNet(start->pin(sta)) != net) { + return; + } + addPath(path, 0, sta); + } + + int getTotalFanout() const + { + int total = 0; + if (!leaves.empty()) { + total = 1; + } + for (const auto& child : fanout) { + total += child->getTotalFanout(); + } + return total; + } + + sta::Delay getMinArrival() const + { + sta::Delay minimum = std::numeric_limits::max(); + for (const auto& [pin, arr] : drivers) { + minimum = std::min(minimum, arr); + } + for (const auto& [pin, arr] : leaves) { + minimum = std::min(minimum, arr); + } + for (const auto& child : fanout) { + minimum = std::min(minimum, child->getMinArrival()); + } + return minimum; + } + + sta::Delay getMaxArrival() const + { + sta::Delay maximum = std::numeric_limits::lowest(); + for (const auto& [pin, arr] : drivers) { + maximum = std::max(maximum, arr); + } + for (const auto& [pin, arr] : leaves) { + maximum = std::max(maximum, arr); + } + for (const auto& child : fanout) { + maximum = std::max(maximum, child->getMaxArrival()); + } + return maximum; + } +}; + +ClockTreeNode::Type classifyDriver(const sta::Pin* pin, sta::dbNetwork* network) +{ + sta::Instance* inst = network->instance(pin); + if (!inst) { + return ClockTreeNode::ROOT; + } + sta::LibertyCell* cell = network->libertyCell(inst); + if (!cell) { + return ClockTreeNode::UNKNOWN; + } + if (cell->isClockGate()) { + return ClockTreeNode::CLOCK_GATE; + } + if (cell->isInverter()) { + return ClockTreeNode::INVERTER; + } + if (cell->isBuffer()) { + return ClockTreeNode::BUFFER; + } + return ClockTreeNode::UNKNOWN; +} + +ClockTreeNode::Type classifyLeaf(const sta::Pin* pin, sta::dbNetwork* network) +{ + sta::Instance* inst = network->instance(pin); + if (!inst) { + return ClockTreeNode::UNKNOWN; + } + odb::dbITerm* iterm; + odb::dbBTerm* bterm; + odb::dbModITerm* moditerm; + network->staToDb(pin, iterm, bterm, moditerm); + if (iterm) { + odb::dbInst* db_inst = iterm->getInst(); + if (db_inst->getMaster()->getType().isBlock()) { + return ClockTreeNode::MACRO; + } + } + return ClockTreeNode::REGISTER; +} + +void getPinLocation(const sta::Pin* pin, + sta::dbNetwork* network, + int& x, + int& y) +{ + x = 0; + y = 0; + odb::dbITerm* iterm; + odb::dbBTerm* bterm; + odb::dbModITerm* moditerm; + network->staToDb(pin, iterm, bterm, moditerm); + if (iterm) { + odb::dbInst* inst = iterm->getInst(); + int lx, ly; + inst->getLocation(lx, ly); + odb::Rect bbox = inst->getBBox()->getBox(); + x = (bbox.xMin() + bbox.xMax()) / 2; + y = (bbox.yMin() + bbox.yMax()) / 2; + } else if (bterm) { + for (odb::dbBPin* bpin : bterm->getBPins()) { + odb::Rect bbox = bpin->getBBox(); + x = (bbox.xMin() + bbox.xMax()) / 2; + y = (bbox.yMin() + bbox.yMax()) / 2; + break; + } + } +} + +std::string getPinName(const sta::Pin* pin, sta::dbNetwork* network) +{ + odb::dbITerm* iterm; + odb::dbBTerm* bterm; + odb::dbModITerm* moditerm; + network->staToDb(pin, iterm, bterm, moditerm); + if (iterm) { + return iterm->getName(); + } + if (bterm) { + return bterm->getName(); + } + return ""; +} + +std::string getInstName(const sta::Pin* pin, sta::dbNetwork* network) +{ + odb::dbITerm* iterm; + odb::dbBTerm* bterm; + odb::dbModITerm* moditerm; + network->staToDb(pin, iterm, bterm, moditerm); + if (iterm) { + return iterm->getInst()->getName(); + } + if (bterm) { + return bterm->getName(); + } + return ""; +} + +// Flatten one TreeNode level into the output array. +// Each TreeNode can have multiple driver pins; we emit one output node +// per unique instance. The root TreeNode typically has no drivers +// (they live in the first child), so we handle that case by emitting +// a synthetic root node and recursing directly into children. +void flattenNode(const TreeNode* tree, + int parent_id, + sta::dbNetwork* network, + float time_scale, + const std::string& clock_name, + ClockTreeData& data) +{ + // Driverless TreeNode: the tree has alternating "sink" and "driver" + // levels per net hop. Sink-only levels are pass-throughs — skip them + // and recurse into children with the same parent_id. + // Exception: the absolute root (parent_id == -1) emits a synthetic node. + if (tree->drivers.empty()) { + int effective_parent = parent_id; + + if (parent_id == -1) { + // Top-level root — emit a synthetic root node + ClockTreeNode root_node; + root_node.id = static_cast(data.nodes.size()); + root_node.parent_id = -1; + root_node.name = clock_name; + root_node.type = ClockTreeNode::ROOT; + root_node.level = 0; + root_node.fanout + = static_cast(tree->fanout.size() + tree->leaves.size()); + effective_parent = root_node.id; + data.nodes.push_back(root_node); + } + + for (const auto& child : tree->fanout) { + flattenNode( + child.get(), effective_parent, network, time_scale, clock_name, data); + } + for (const auto& [leaf_pin, leaf_arrival] : tree->leaves) { + ClockTreeNode leaf; + leaf.id = static_cast(data.nodes.size()); + leaf.parent_id = effective_parent; + leaf.name = getInstName(leaf_pin, network); + leaf.pin_name = getPinName(leaf_pin, network); + leaf.type = classifyLeaf(leaf_pin, network); + leaf.arrival = static_cast(leaf_arrival / time_scale); + leaf.level = tree->level + 1; + leaf.fanout = 0; + getPinLocation(leaf_pin, network, leaf.dbu_x, leaf.dbu_y); + data.nodes.push_back(leaf); + } + return; + } + + // Emit driver nodes + std::set emitted_insts; + for (const auto& [pin, arrival] : tree->drivers) { + std::string inst_name = getInstName(pin, network); + if (!emitted_insts.insert(inst_name).second) { + continue; + } + + ClockTreeNode node; + node.id = static_cast(data.nodes.size()); + node.parent_id = parent_id; + node.name = inst_name; + node.pin_name = getPinName(pin, network); + node.type = classifyDriver(pin, network); + node.fanout = static_cast(tree->fanout.size() + tree->leaves.size()); + node.level = tree->level; + getPinLocation(pin, network, node.dbu_x, node.dbu_y); + + // Position the node at its INPUT arrival (from parent's child_sinks) + // and compute delay as the cell delay (output - input). + // For the root (no parent), use the driver arrival directly. + node.arrival = static_cast(arrival / time_scale); + if (tree->parent) { + sta::Instance* inst = network->instance(pin); + if (inst) { + for (const auto& [sink_pin, sink_arr] : tree->parent->child_sinks) { + if (network->instance(sink_pin) == inst) { + node.arrival = static_cast(sink_arr / time_scale); + node.delay = static_cast((arrival - sink_arr) / time_scale); + break; + } + } + } + } + + int this_id = node.id; + data.nodes.push_back(node); + + // Recurse into children + for (const auto& child : tree->fanout) { + flattenNode(child.get(), this_id, network, time_scale, clock_name, data); + } + + // Emit leaf nodes + for (const auto& [leaf_pin, leaf_arrival] : tree->leaves) { + ClockTreeNode leaf; + leaf.id = static_cast(data.nodes.size()); + leaf.parent_id = this_id; + leaf.name = getInstName(leaf_pin, network); + leaf.pin_name = getPinName(leaf_pin, network); + leaf.type = classifyLeaf(leaf_pin, network); + leaf.arrival = static_cast(leaf_arrival / time_scale); + leaf.level = tree->level + 1; + leaf.fanout = 0; + getPinLocation(leaf_pin, network, leaf.dbu_x, leaf.dbu_y); + data.nodes.push_back(leaf); + } + } +} + +} // namespace + +ClockTreeReport::ClockTreeReport(sta::dbSta* sta) : sta_(sta) +{ +} + +std::vector ClockTreeReport::getReport() const +{ + std::vector result; + if (!sta_) { + return result; + } + + sta_->ensureGraph(); + sta_->searchPreamble(); + sta::Scene* scene = sta_->cmdScene(); + sta_->ensureClkNetwork(scene->mode()); + sta_->ensureClkArrivals(); + + sta::dbNetwork* network = sta_->getDbNetwork(); + const float time_scale = sta_->units()->timeUnit()->scale(); + const std::string time_suffix + = sta_->units()->timeUnit()->scaleAbbrevSuffix(); + + // Build internal trees per clock + std::map> roots; + std::vector clock_order; + + for (sta::Clock* clk : scene->sdc()->clocks()) { + auto root = std::make_unique(); + root->network = network; + root->level = 0; + // Get the root net from the clock's first pin + const sta::PinSet& pins = clk->pins(); + if (pins.empty()) { + continue; + } + const sta::Pin* first_pin = *pins.begin(); + sta::Term* term = network->term(first_pin); + sta::Net* net = term ? network->net(term) : network->net(first_pin); + root->net = network->findFlatNet(net); + + clock_order.push_back(clk); + roots[clk] = std::move(root); + } + + // Populate trees from STA clock paths + sta::Graph* graph = sta_->graph(); + for (sta::Vertex* src_vertex : graph->regClkVertices()) { + sta::VertexPathIterator path_iter(src_vertex, sta_); + while (path_iter.hasNext()) { + sta::Path* path = path_iter.next(); + if (path->scene(sta_) != scene) { + continue; + } + sta::PathExpanded expand(path, sta_); + const sta::Clock* clock = path->clock(sta_); + if (clock) { + auto it = roots.find(clock); + if (it != roots.end()) { + it->second->addPath(expand, sta_); + } + } + } + } + + // Flatten each tree into ClockTreeData + for (const sta::Clock* clk : clock_order) { + const auto& root = roots[clk]; + if (root->drivers.empty() && root->fanout.empty()) { + continue; // virtual clock or empty tree + } + + ClockTreeData data; + data.clock_name = clk->name(); + data.time_unit = time_suffix; + + flattenNode(root.get(), -1, network, time_scale, data.clock_name, data); + + if (!data.nodes.empty()) { + sta::Delay min_arr = root->getMinArrival(); + sta::Delay max_arr = root->getMaxArrival(); + data.min_arrival = static_cast(min_arr / time_scale); + data.max_arrival = static_cast(max_arr / time_scale); + } + + result.push_back(std::move(data)); + } + + return result; +} + +} // namespace web diff --git a/src/web/src/clock_tree_report.h b/src/web/src/clock_tree_report.h new file mode 100644 index 00000000000..79e35f54d79 --- /dev/null +++ b/src/web/src/clock_tree_report.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include +#include + +namespace sta { +class dbSta; +} // namespace sta + +namespace web { + +struct ClockTreeNode +{ + int id = 0; + int parent_id = -1; // -1 for root + std::string name; // instance or port name + std::string pin_name; + + enum Type + { + ROOT, + BUFFER, + INVERTER, + CLOCK_GATE, + REGISTER, + MACRO, + UNKNOWN + }; + Type type = UNKNOWN; + + float arrival = 0.0f; // input arrival (user time units) + float delay = 0.0f; // cell delay (user time units) + int fanout = 0; + int level = 0; + + int dbu_x = 0; + int dbu_y = 0; + + static const char* typeToString(Type t); +}; + +struct ClockTreeData +{ + std::string clock_name; + float min_arrival = 0.0f; + float max_arrival = 0.0f; + std::string time_unit; + std::vector nodes; +}; + +class ClockTreeReport +{ + public: + explicit ClockTreeReport(sta::dbSta* sta); + + std::vector getReport() const; + + private: + sta::dbSta* sta_; +}; + +} // namespace web diff --git a/src/web/src/color.cpp b/src/web/src/color.cpp new file mode 100644 index 00000000000..8d1d9942dee --- /dev/null +++ b/src/web/src/color.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include "color.h" + +#include // For std::min, std::max +#include + +namespace web { + +struct HSL +{ + double h; // Hue (0-360) + double s; // Saturation (0-1) + double l; // Lightness (0-1) +}; + +// Converts your existing Color struct (RGB) to HSL +static HSL rgb_to_hsl(const Color& rgb) +{ + // Normalize R, G, B to 0-1 + double r = rgb.r / 255.0; + double g = rgb.g / 255.0; + double b = rgb.b / 255.0; + + double vmin = std::min({r, g, b}); + double vmax = std::max({r, g, b}); + double delta = vmax - vmin; + + HSL hsl; + hsl.l = (vmax + vmin) / 2.0; // Lightness + + if (delta == 0.0) { + hsl.h = 0.0; // Achromatic (grey) + hsl.s = 0.0; // Saturation + } else { + hsl.s = (hsl.l < 0.5) ? (delta / (vmax + vmin)) + : (delta / (2.0 - vmax - vmin)); // Saturation + + // Hue + if (vmax == r) { + hsl.h = 60.0 * (g - b) / delta; + } else if (vmax == g) { + hsl.h = 60.0 * (2.0 + (b - r) / delta); + } else { // vmax == b + hsl.h = 60.0 * (4.0 + (r - g) / delta); + } + + if (hsl.h < 0.0) { + hsl.h += 360.0; // Ensure hue is positive + } + } + return hsl; +} + +// Helper for HSL to RGB conversion +static double hue_to_rgb(double p, double q, double t) +{ + if (t < 0.0) { + t += 1.0; + } + if (t > 1.0) { + t -= 1.0; + } + if (t < 1.0 / 6.0) { + return p + (q - p) * 6.0 * t; + } + if (t < 1.0 / 2.0) { + return q; + } + if (t < 2.0 / 3.0) { + return p + (q - p) * (2.0 / 3.0 - t) * 6.0; + } + return p; +} + +// Converts an HSL struct back to your Color struct (RGB) +static Color hsl_to_rgb(const HSL& hsl, const unsigned char a) +{ + Color rgb; + rgb.a = a; + + if (hsl.s == 0.0) { + // Achromatic (grey) + unsigned char l = (unsigned char) std::round(hsl.l * 255.0); + rgb.r = l; + rgb.g = l; + rgb.b = l; + } else { + double q = (hsl.l < 0.5) ? (hsl.l * (1.0 + hsl.s)) + : (hsl.l + hsl.s - hsl.l * hsl.s); + double p = 2.0 * hsl.l - q; + double h_norm = hsl.h / 360.0; + + rgb.r = (unsigned char) std::round(hue_to_rgb(p, q, h_norm + 1.0 / 3.0) + * 255.0); + rgb.g = (unsigned char) std::round(hue_to_rgb(p, q, h_norm) * 255.0); + rgb.b = (unsigned char) std::round(hue_to_rgb(p, q, h_norm - 1.0 / 3.0) + * 255.0); + } + return rgb; +} + +// factor is a value > 1.0. 1.5 is a 50% increase in lightness. +Color Color::lighter(double factor) const +{ + HSL hsl = rgb_to_hsl(*this); + hsl.l = std::min(1.0, hsl.l * factor); + return hsl_to_rgb(hsl, a); +} + +// Factor is 0.0 - 1.0, e.g., 0.8 is 20% darker +Color Color::darken(double factor) const +{ + HSL hsl = rgb_to_hsl(*this); + hsl.l = std::max(0.0, hsl.l * factor); + return hsl_to_rgb(hsl, a); +} + +} // namespace web diff --git a/src/web/src/color.h b/src/web/src/color.h new file mode 100644 index 00000000000..b1f0ea7deb3 --- /dev/null +++ b/src/web/src/color.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +namespace web { + +struct Color +{ + unsigned char r; // Red (0-255) + unsigned char g; // Green (0-255) + unsigned char b; // Blue (0-255) + unsigned char a; // Alpha (0-255) + + Color lighter(double factor = 1.5) const; + Color darken(double factor = 0.5) const; +}; + +} // namespace web diff --git a/src/web/src/coordinates.js b/src/web/src/coordinates.js new file mode 100644 index 00000000000..845a14032b2 --- /dev/null +++ b/src/web/src/coordinates.js @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// DBU <-> Leaflet coordinate conversion. +// +// The tile renderer uses two Y-flips (tile index + pixel row), so the +// correct mapping is: +// lat = (dbu_y - maxDXDY) * scale +// lng = dbu_x * scale +// +// `scale` = tileSize / max(designWidth, designHeight) (pixels-per-DBU) +// `maxDXDY` = max(designWidth, designHeight) in DBU + +export function dbuToLatLng(dbuX, dbuY, scale, maxDXDY) { + return [(dbuY - maxDXDY) * scale, dbuX * scale]; +} + +export function dbuRectToBounds(x1, y1, x2, y2, scale, maxDXDY) { + return [ + [(y1 - maxDXDY) * scale, x1 * scale], + [(y2 - maxDXDY) * scale, x2 * scale], + ]; +} + +export function latLngToDbu(lat, lng, scale, maxDXDY) { + return { + dbuX: Math.round(lng / scale), + dbuY: Math.round(maxDXDY + lat / scale), + }; +} diff --git a/src/web/src/display-controls.js b/src/web/src/display-controls.js new file mode 100644 index 00000000000..b458786739f --- /dev/null +++ b/src/web/src/display-controls.js @@ -0,0 +1,599 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Display controls — layer checkboxes and visibility tree. + +import { CheckboxTreeModel } from './checkbox-tree-model.js'; +import { VisTree } from './vis-tree.js'; + +// Compute a Set of layer indices around `center` within [0, count). +// `lower` layers below and `upper` layers above are included. +export function layerRangeSet(center, lower, upper, count) { + const indices = new Set(); + const lo = Math.max(0, center - lower); + const hi = Math.min(count - 1, center + upper); + for (let i = lo; i <= hi; i++) indices.add(i); + return indices; +} + +// Layer color palette (must match server-side palette in web.cpp) +const layerPalette = [ + [70, 130, 210], // moderate blue + [200, 50, 50], // red + [50, 180, 80], // green + [200, 160, 40], // amber + [160, 60, 200], // purple + [40, 190, 190], // teal + [220, 120, 50], // orange + [180, 70, 150], // magenta +]; + +// Populate display controls with layer checkboxes and visibility tree. +export function populateDisplayControls(app, visibility, WebSocketTileLayer, + techData, redrawAllLayers, + HeatMapTileLayer) { + if (!app.displayControlsEl) return; + app.displayControlsEl.innerHTML = ''; + app.allLayers = []; + + // Instance borders layer (always below routing layers) + const instancesLayer = new WebSocketTileLayer(app.websocketManager, '_instances', { + zIndex: 0, + }); + instancesLayer.addTo(app.map); + app.allLayers.push(instancesLayer); + + // Module coloring overlay layer (between instances and routing layers) + const modulesLayer = new WebSocketTileLayer(app.websocketManager, '_modules', { + zIndex: 1, + }); + // Don't add to map until "Module view" is enabled + app.modulesLayer = modulesLayer; + app.allLayers.push(modulesLayer); + + // --- Layers group (using CheckboxTreeModel) --- + + // Create Leaflet layers and build a model spec. + const leafletLayers = []; // index → WebSocketTileLayer + const layerIds = []; // index → model node id + + const layerSpec = { + id: 'layers_parent', + children: techData.layers.map((name, index) => { + const layer = new WebSocketTileLayer(app.websocketManager, name, { + opacity: 0.7, + zIndex: index + 2, + }); + layer.addTo(app.map); + app.allLayers.push(layer); + leafletLayers.push(layer); + app.visibleLayers.add(name); + + const id = `layer_${index}`; + layerIds.push(id); + return { id, data: { name, layer, colorIndex: index }, checked: true }; + }), + }; + + const layerModel = new CheckboxTreeModel(() => { + // Sync DOM and Leaflet layer visibility from model. + layerModel.forEach(node => { + if (node.cb) { + node.cb.checked = node.checked; + node.cb.indeterminate = node.indeterminate; + } + if (node.data && node.data.layer) { + if (node.checked) { + node.data.layer.addTo(app.map); + app.visibleLayers.add(node.data.name); + } else { + app.map.removeLayer(node.data.layer); + app.visibleLayers.delete(node.data.name); + } + } + }); + }); + layerModel.addFromSpec(layerSpec); + + // Build layer DOM. + const layerGroup = document.createElement('div'); + layerGroup.className = 'vis-group'; + + const layerHeader = document.createElement('label'); + layerHeader.className = 'vis-group-header'; + const layerArrow = document.createElement('span'); + layerArrow.className = 'vis-arrow'; + layerArrow.textContent = '▼'; + layerHeader.appendChild(layerArrow); + + const parentNode = layerModel.get('layers_parent'); + const parentCb = document.createElement('input'); + parentCb.type = 'checkbox'; + parentCb.checked = true; + parentNode.cb = parentCb; + parentCb.addEventListener('change', () => { + layerModel.check('layers_parent', parentCb.checked); + }); + layerHeader.appendChild(parentCb); + layerHeader.appendChild(document.createTextNode('Layers')); + layerGroup.appendChild(layerHeader); + + const layerChildren = document.createElement('div'); + layerChildren.className = 'vis-group-children'; + + techData.layers.forEach((name, index) => { + const id = layerIds[index]; + const modelNode = layerModel.get(id); + + const label = document.createElement('label'); + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = true; + modelNode.cb = checkbox; + checkbox.addEventListener('change', () => { + layerModel.check(id, checkbox.checked); + }); + label.appendChild(checkbox); + + const colorSwatch = document.createElement('span'); + colorSwatch.className = 'layer-color'; + const c = layerPalette[index % layerPalette.length]; + colorSwatch.style.backgroundColor = `rgb(${c[0]},${c[1]},${c[2]})`; + label.appendChild(colorSwatch); + + label.appendChild(document.createTextNode(name)); + layerChildren.appendChild(label); + }); + layerGroup.appendChild(layerChildren); + + // --- Layer context menu (right-click) --- + const contextMenu = document.createElement('div'); + contextMenu.className = 'context-menu'; + contextMenu.style.display = 'none'; + document.body.appendChild(contextMenu); + + function showOnlyLayers(indices) { + const updates = {}; + layerIds.forEach((id, i) => { + updates[id] = indices.has(i); + }); + layerModel.checkSet(updates); + } + + function hideContextMenu() { + contextMenu.style.display = 'none'; + } + + const n = techData.layers.length; + const menuItems = [ + { label: 'Show only this layer', fn: (i) => layerRangeSet(i, 0, 0, n) }, + { label: 'Show layer range \u2195', fn: (i) => layerRangeSet(i, 1, 1, n) }, + { label: 'Show layer range \u2195\u2195', fn: (i) => layerRangeSet(i, 2, 2, n) }, + { label: 'Show layer range \u2193', fn: (i) => layerRangeSet(i, 1, 0, n) }, + { label: 'Show layer range \u2191', fn: (i) => layerRangeSet(i, 0, 1, n) }, + ]; + + layerChildren.querySelectorAll('label').forEach((label, index) => { + label.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + contextMenu.innerHTML = ''; + for (const item of menuItems) { + const div = document.createElement('div'); + div.className = 'context-menu-item'; + div.textContent = item.label; + div.addEventListener('click', () => { + showOnlyLayers(item.fn(index)); + hideContextMenu(); + }); + contextMenu.appendChild(div); + } + contextMenu.style.left = e.clientX + 'px'; + contextMenu.style.top = e.clientY + 'px'; + contextMenu.style.display = 'block'; + }); + }); + + document.addEventListener('click', (e) => { + if (!contextMenu.contains(e.target)) hideContextMenu(); + }); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') hideContextMenu(); + }); + + // Toggle collapse + layerArrow.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const collapsed = layerChildren.classList.toggle('collapsed'); + layerArrow.textContent = collapsed ? '▶' : '▼'; + }); + + app.displayControlsEl.appendChild(layerGroup); + + // --- Visibility tree (Instances, Nets, Shapes, Debug) --- + const visTree = new VisTree(visibility, redrawAllLayers); + visTree.add({ label: 'Instances', children: [ + { label: 'Std Cells', visKey: 'stdcells', disabled: !app.hasLiberty, children: [ + { label: 'Bufs/Invs', children: [ + { key: 'std_bufinv_timing', label: 'Timing opt.' }, + { key: 'std_bufinv', label: 'Netlist' }, + ]}, + { key: 'std_combinational', label: 'Combinational' }, + { key: 'std_sequential', label: 'Sequential' }, + { label: 'Clock tree', children: [ + { key: 'std_clock_bufinv', label: 'Buffer/Inverter' }, + { key: 'std_clock_gate', label: 'Clock gate' }, + ]}, + { key: 'std_level_shift', label: 'Level shifter' }, + ]}, + { key: 'macros', label: 'Macros' }, + { label: 'Pads', children: [ + { key: 'pad_input', label: 'Input' }, + { key: 'pad_output', label: 'Output' }, + { key: 'pad_inout', label: 'Inout' }, + { key: 'pad_power', label: 'Power' }, + { key: 'pad_spacer', label: 'Spacer' }, + { key: 'pad_areaio', label: 'Area IO' }, + { key: 'pad_other', label: 'Other' }, + ]}, + { label: 'Physical', children: [ + { key: 'phys_fill', label: 'Fill' }, + { key: 'phys_endcap', label: 'Endcap' }, + { key: 'phys_welltap', label: 'Welltap' }, + { key: 'phys_tie', label: 'Tie Hi/Lo' }, + { key: 'phys_antenna', label: 'Antenna' }, + { key: 'phys_cover', label: 'Cover' }, + { key: 'phys_bump', label: 'Bump' }, + { key: 'phys_other', label: 'Other' }, + ]}, + ]}); + visTree.add({ label: 'Nets', children: [ + { key: 'net_signal', label: 'Signal' }, + { key: 'net_power', label: 'Power' }, + { key: 'net_ground', label: 'Ground' }, + { key: 'net_clock', label: 'Clock' }, + { key: 'net_reset', label: 'Reset' }, + { key: 'net_tieoff', label: 'Tie off' }, + { key: 'net_scan', label: 'Scan' }, + { key: 'net_analog', label: 'Analog' }, + ]}); + visTree.add({ label: 'Shapes', children: [ + { key: 'routing', label: 'Routing' }, + { key: 'special_nets', label: 'Special Nets' }, + { key: 'pins', label: 'Pins' }, + { key: 'blockages', label: 'Blockages' }, + ]}); + visTree.add({ label: 'Blockages', children: [ + { key: 'placement_blockages', label: 'Placement' }, + { key: 'routing_obstructions', label: 'Routing' }, + ]}); + if (techData.sites && techData.sites.length > 0) { + visTree.add({ label: 'Rows', visKey: 'rows', children: + techData.sites.map(name => ({ + key: 'site_' + name, label: name, + })), + }); + } + visTree.add({ label: 'Tracks', children: [ + { key: 'tracks_pref', label: 'Pref' }, + { key: 'tracks_non_pref', label: 'Non Pref' }, + ]}); + visTree.add({ key: 'module_view', label: 'Module view' }); + visTree.add({ key: 'debug', label: 'Debug tiles' }); + visTree.render(app.displayControlsEl); + + if (!app.heatMapLayer) { + app.heatMapLayer = new HeatMapTileLayer(app.websocketManager, app, { + zIndex: techData.layers.length + 10, + opacity: 1, + }); + } + + const heatMapGroup = document.createElement('div'); + heatMapGroup.className = 'vis-group heatmap-controls'; + app.displayControlsEl.appendChild(heatMapGroup); + + const heatMapHeader = document.createElement('label'); + heatMapHeader.className = 'vis-group-header heatmap-header'; + const heatMapArrow = document.createElement('span'); + heatMapArrow.className = 'vis-arrow'; + heatMapArrow.textContent = '▼'; + heatMapHeader.appendChild(heatMapArrow); + heatMapHeader.appendChild(document.createTextNode('Heat Maps')); + heatMapGroup.appendChild(heatMapHeader); + + const heatMapContainer = document.createElement('div'); + heatMapContainer.className = 'vis-group-children heatmap-group-children collapsed'; + heatMapGroup.appendChild(heatMapContainer); + + let heatMapCollapsed = true; + heatMapArrow.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + heatMapCollapsed = !heatMapCollapsed; + heatMapContainer.classList.toggle('collapsed', heatMapCollapsed); + heatMapArrow.textContent = heatMapCollapsed ? '▶' : '▼'; + }); + + function addCheckbox(parent, label, checked, onChange) { + const row = document.createElement('label'); + row.className = 'heatmap-setting'; + const input = document.createElement('input'); + input.type = 'checkbox'; + input.checked = checked; + input.addEventListener('change', () => onChange(input.checked)); + row.appendChild(input); + row.appendChild(document.createTextNode(label)); + parent.appendChild(row); + } + + function addNumber(parent, label, value, step, onChange) { + const row = document.createElement('label'); + row.className = 'heatmap-setting'; + const text = document.createElement('span'); + text.textContent = label; + const input = document.createElement('input'); + input.type = 'number'; + input.value = String(value); + input.step = String(step || 1); + input.addEventListener('change', () => onChange(parseFloat(input.value))); + row.appendChild(text); + row.appendChild(input); + parent.appendChild(row); + } + + function addSelect(parent, label, value, choices, onChange) { + const row = document.createElement('label'); + row.className = 'heatmap-setting'; + const text = document.createElement('span'); + text.textContent = label; + const select = document.createElement('select'); + for (const choice of choices) { + const option = document.createElement('option'); + option.value = choice; + option.textContent = choice; + if (choice === value) option.selected = true; + select.appendChild(option); + } + select.addEventListener('change', () => onChange(select.value)); + row.appendChild(text); + row.appendChild(select); + parent.appendChild(row); + } + + function sendHeatMapUpdate(message) { + app.websocketManager.request(message).then(data => { + if (app.updateHeatMaps) { + app.updateHeatMaps(data); + } + }).catch(err => console.error('Heat map update failed', err)); + } + + function renderMapLegend(active) { + if (!app.heatMapLegendEl) { + return; + } + + const legend = app.heatMapLegendEl; + legend.innerHTML = ''; + + if (!active || !active.show_legend || !active.legend || active.legend.length === 0) { + legend.classList.add('hidden'); + return; + } + + const title = document.createElement('div'); + title.className = 'heatmap-map-legend-title'; + title.textContent = active.title; + legend.appendChild(title); + + const units = document.createElement('div'); + units.className = 'heatmap-map-legend-units'; + units.textContent = active.units || ''; + legend.appendChild(units); + + const list = document.createElement('div'); + list.className = 'heatmap-map-legend-list'; + for (const entry of active.legend) { + const row = document.createElement('div'); + row.className = 'heatmap-legend-row'; + + const swatch = document.createElement('span'); + swatch.className = 'heatmap-legend-swatch'; + swatch.style.backgroundColor + = `rgba(${entry.color[0]}, ${entry.color[1]}, ${entry.color[2]}, ${entry.color[3] / 255})`; + + const text = document.createElement('span'); + text.textContent = entry.value; + + row.appendChild(swatch); + row.appendChild(text); + list.appendChild(row); + } + + legend.appendChild(list); + legend.classList.remove('hidden'); + } + + app.renderHeatMapControls = (data) => { + heatMapContainer.innerHTML = ''; + + const list = document.createElement('div'); + list.className = 'heatmap-list'; + heatMapContainer.appendChild(list); + + const noneLabel = document.createElement('label'); + noneLabel.className = 'heatmap-setting'; + const noneInput = document.createElement('input'); + noneInput.type = 'radio'; + noneInput.name = 'active-heatmap'; + noneInput.checked = !data.active; + noneInput.addEventListener('change', () => { + sendHeatMapUpdate({ type: 'set_active_heatmap', name: '' }); + }); + noneLabel.appendChild(noneInput); + noneLabel.appendChild(document.createTextNode('Off')); + list.appendChild(noneLabel); + + for (const heatMap of data.heatmaps || []) { + const label = document.createElement('label'); + label.className = 'heatmap-setting'; + const input = document.createElement('input'); + input.type = 'radio'; + input.name = 'active-heatmap'; + input.checked = heatMap.name === data.active; + input.addEventListener('change', () => { + sendHeatMapUpdate({ + type: 'set_active_heatmap', + name: heatMap.name, + }); + }); + label.appendChild(input); + label.appendChild(document.createTextNode(heatMap.title)); + list.appendChild(label); + } + + const active = (data.heatmaps || []).find(h => h.name === data.active); + renderMapLegend(active); + if (!active) { + return; + } + + const settings = document.createElement('div'); + settings.className = 'heatmap-settings'; + heatMapContainer.appendChild(settings); + + addNumber(settings, 'Display min', active.display_min, + active.display_range_increment, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'DisplayMin', + value, + }); + }); + addNumber(settings, 'Display max', active.display_max, + active.display_range_increment, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'DisplayMax', + value, + }); + }); + addCheckbox(settings, 'Show below min', active.draw_below_min, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'ShowMin', + value, + }); + }); + addCheckbox(settings, 'Show above max', active.draw_above_max, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'ShowMax', + value, + }); + }); + addCheckbox(settings, 'Log scale', active.log_scale, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'LogScale', + value, + }); + }); + addCheckbox(settings, 'Reverse log', active.reverse_log, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'ReverseLog', + value, + }); + }); + if (active.can_adjust_grid) { + addNumber(settings, 'Grid X', active.grid_x, 0.1, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'GridX', + value, + }); + }); + addNumber(settings, 'Grid Y', active.grid_y, 0.1, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'GridY', + value, + }); + }); + } + addNumber(settings, 'Alpha', active.alpha, 1, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'Alpha', + value, + }); + }); + addCheckbox(settings, 'Legend', active.show_legend, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'ShowLegend', + value, + }); + }); + if (active.supports_numbers) { + addCheckbox(settings, 'Show numbers', active.show_numbers, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'ShowNumbers', + value, + }); + }); + } + + for (const option of active.options || []) { + if (option.type === 'bool') { + addCheckbox(settings, option.label, option.value, value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: option.name, + value, + }); + }); + } else if (option.type === 'choice') { + addSelect(settings, option.label, option.value, + option.choices || [], value => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: option.name, + value, + }); + }); + } + } + + const rebuild = document.createElement('button'); + rebuild.className = 'heatmap-rebuild'; + rebuild.textContent = 'Rebuild data'; + rebuild.addEventListener('click', () => { + sendHeatMapUpdate({ + type: 'set_heatmap', + name: active.name, + option: 'rebuild', + value: 1, + }); + }); + settings.appendChild(rebuild); + }; +} diff --git a/src/web/src/hierarchy-browser.js b/src/web/src/hierarchy-browser.js new file mode 100644 index 00000000000..623747a509f --- /dev/null +++ b/src/web/src/hierarchy-browser.js @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Hierarchy browser widget — module tree with coloring. + +import { CheckboxTreeModel } from './checkbox-tree-model.js'; +import { makeResizableHeaders } from './ui-utils.js'; + +const COLS = [ + 'Instance', 'Module', 'Instances', 'Macros', 'Modules', + 'Area', 'Local Inst', 'Local Macros', 'Local Modules', +]; + +// Must match HierarchyNodeKind enum on the server. +const NODE_KIND = { MODULE: 0, LEAF_GROUP: 1, TYPE_GROUP: 2, INSTANCE: 3 }; + +// 31-color palette matching the Qt GUI's ColorGenerator. +const MODULE_COLORS = [ + [255,0,0], [255,140,0], [255,215,0], [0,255,0], [148,0,211], + [0,250,154], [220,20,60], [0,255,255], [0,191,255], [0,0,255], + [173,255,47], [218,112,214], [255,0,255], [30,144,255], [250,128,114], + [176,224,230], [255,20,147], [123,104,238], [255,250,205], [255,182,193], + [85,107,47], [139,69,19], [72,61,139], [0,128,0], [60,179,113], + [184,134,11], [0,139,139], [0,0,139], [50,205,50], [128,0,128], + [176,48,96], +]; + +export class HierarchyBrowser { + constructor(container, app, redrawAllLayers) { + this._app = app; + this._redrawAllLayers = redrawAllLayers; + this._nodes = []; // flat server response + this._rows = []; // DFS-ordered rows with depth + this._childrenMap = new Map(); // id → [child ids] + this._collapsed = new Set(); // collapsed node ids + + // Module coloring state: odb_id → {color, effectiveColor, visible} + this._moduleState = new Map(); + + // Checkbox tree model for module visibility (tri-state propagation). + this._checkModel = null; + + this._build(container); + + // Expose on app so display-controls can interact + app.hierarchyBrowser = this; + } + + _build(container) { + const el = document.createElement('div'); + el.className = 'hierarchy-widget'; + + // Toolbar + const toolbar = document.createElement('div'); + toolbar.className = 'timing-toolbar'; + + this._updateBtn = document.createElement('button'); + this._updateBtn.className = 'timing-btn'; + this._updateBtn.textContent = 'Update'; + + this._statusLabel = document.createElement('span'); + this._statusLabel.className = 'timing-path-count'; + + toolbar.appendChild(this._updateBtn); + toolbar.appendChild(this._statusLabel); + el.appendChild(toolbar); + + // Table container + this._tableContainer = document.createElement('div'); + this._tableContainer.className = 'hierarchy-table-container'; + this._table = document.createElement('table'); + this._table.className = 'timing-table'; + this._tableContainer.appendChild(this._table); + el.appendChild(this._tableContainer); + + container.element.appendChild(el); + this._bindEvents(); + } + + _bindEvents() { + this._updateBtn.addEventListener('click', () => this.update()); + } + + async update() { + this._updateBtn.disabled = true; + this._updateBtn.textContent = 'Loading...'; + this._statusLabel.textContent = ''; + try { + const data = await this._app.websocketManager.request({ + type: 'module_hierarchy', + }); + this._nodes = data.nodes || []; + this._buildTree(); + this._assignColors(); + this._computeEffectiveColors(); + this._render(); + const nMods = this._nodes.filter( + n => (n.node_kind || 0) === NODE_KIND.MODULE).length; + this._statusLabel.textContent = nMods + ' modules'; + await this._sendModuleColors(); + } catch (err) { + this._statusLabel.textContent = 'Error: ' + err.message; + } + this._updateBtn.disabled = false; + this._updateBtn.textContent = 'Update'; + } + + _buildTree() { + this._childrenMap.clear(); + this._collapsed.clear(); + + // Initialize children lists + for (const n of this._nodes) { + this._childrenMap.set(n.id, []); + } + for (const n of this._nodes) { + if (n.parent_id >= 0 && this._childrenMap.has(n.parent_id)) { + this._childrenMap.get(n.parent_id).push(n.id); + } + } + + // Build id → node lookup + this._nodeMap = new Map(); + for (const n of this._nodes) { + this._nodeMap.set(n.id, n); + } + + // Default collapse state: + // - Modules at depth > 1 are collapsed + // - Leaf groups and type groups are collapsed + for (const n of this._nodes) { + const children = this._childrenMap.get(n.id); + if (!children || children.length === 0) continue; + const kind = n.node_kind || 0; + if (kind === NODE_KIND.LEAF_GROUP || kind === NODE_KIND.TYPE_GROUP) { + this._collapsed.add(n.id); + } else if (kind === NODE_KIND.MODULE && n.parent_id >= 0) { + this._collapsed.add(n.id); + } + } + + // Build DFS-ordered rows + this._rows = []; + const roots = this._nodes.filter(n => n.parent_id < 0); + for (const root of roots) { + this._dfs(root.id, 0); + } + + // Build checkbox model for module visibility. + // Only MODULE nodes with odb_id get checkboxes; others are structural. + this._checkModel = new CheckboxTreeModel(() => { + this._checkModel.forEach(node => { + if (!node.hasCheckbox) return; + const st = this._moduleState.get(node.data.odb_id); + if (st) st.visible = node.checked; + }); + this._sendModuleColors(); + }); + this._checkModel.buildFromNodes(this._nodes.map(n => ({ + id: n.id, + parentId: n.parent_id, + hasCheckbox: (n.node_kind || 0) === NODE_KIND.MODULE + && n.odb_id != null, + checked: true, + data: n, + }))); + } + + // Assign a color from the palette to each MODULE node in DFS order. + _assignColors() { + this._moduleState.clear(); + let colorIdx = 0; + for (const row of this._rows) { + const node = this._nodeMap.get(row.id); + if (!node || (node.node_kind || 0) !== NODE_KIND.MODULE) continue; + if (node.odb_id == null) continue; + const c = MODULE_COLORS[colorIdx % MODULE_COLORS.length]; + this._moduleState.set(node.odb_id, { + color: c, + effectiveColor: c, + visible: true, + nodeId: node.id, + }); + colorIdx++; + } + } + + // DFS to compute effective colors based on collapse state. + // When a MODULE is collapsed, all descendant MODULEs inherit its effective color. + _computeEffectiveColors() { + for (const row of this._rows) { + const node = this._nodeMap.get(row.id); + if (!node || (node.node_kind || 0) !== NODE_KIND.MODULE) continue; + const st = this._moduleState.get(node.odb_id); + if (!st) continue; + + // Find nearest ancestor MODULE that is collapsed + let parentId = node.parent_id; + let inheritedColor = null; + while (parentId >= 0) { + const parent = this._nodeMap.get(parentId); + if (!parent) break; + if ((parent.node_kind || 0) === NODE_KIND.MODULE) { + const pst = this._moduleState.get(parent.odb_id); + if (pst && this._collapsed.has(parent.id)) { + inheritedColor = pst.effectiveColor; + // Don't break — keep walking up, the highest + // collapsed ancestor's effective color wins. + } + } + parentId = parent.parent_id; + } + st.effectiveColor = inheritedColor || st.color; + } + } + + // Check if a node has MODULE children (not just LEAF_GROUP/TYPE_GROUP). + _hasModuleChildren(nodeId) { + const children = this._childrenMap.get(nodeId) || []; + return children.some(cid => { + const c = this._nodeMap.get(cid); + return c && (c.node_kind || 0) === NODE_KIND.MODULE; + }); + } + + // Send the current effective color map to the server. + // Expanded modules with sub-modules are excluded — their "background" + // instances stay uncolored so child module colors are clearly visible. + async _sendModuleColors() { + const parts = []; + for (const [odbId, st] of this._moduleState) { + if (!st.visible) continue; + // Skip expanded modules that have child modules — only + // collapsed or leaf modules contribute to the color overlay. + const expanded = !this._collapsed.has(st.nodeId); + if (expanded && this._hasModuleChildren(st.nodeId)) continue; + const [r, g, b] = st.effectiveColor; + parts.push(`${odbId}:${r},${g},${b},100`); + } + const colors = parts.join(';'); + try { + const resp = await this._app.websocketManager.request({ + type: 'set_module_colors', + colors, + }); + console.log('set_module_colors:', resp.count, 'modules,', + parts.length, 'sent'); + } catch (err) { + console.error('set_module_colors failed:', err); + } + + // Refresh the modules layer if it exists + if (this._app.modulesLayer && this._app.map.hasLayer(this._app.modulesLayer)) { + this._app.modulesLayer.refreshTiles(); + } + } + + _dfs(id, depth) { + this._rows.push({ id, depth }); + const children = this._childrenMap.get(id) || []; + for (const childId of children) { + this._dfs(childId, depth + 1); + } + } + + _render() { + this._table.innerHTML = ''; + + // Header + const thead = document.createElement('thead'); + const hr = document.createElement('tr'); + for (const col of COLS) { + const th = document.createElement('th'); + th.textContent = col; + hr.appendChild(th); + } + thead.appendChild(hr); + this._table.appendChild(thead); + + // Body + const tbody = document.createElement('tbody'); + for (const row of this._rows) { + const node = this._nodeMap.get(row.id); + if (!node) continue; + + // Check if any ancestor is collapsed + if (this._isHidden(node)) continue; + + const tr = document.createElement('tr'); + const kind = node.node_kind || 0; + const children = this._childrenMap.get(row.id) || []; + const hasChildren = children.length > 0; + const isCollapsed = this._collapsed.has(row.id); + + // Style non-module rows + if (kind === NODE_KIND.LEAF_GROUP || kind === NODE_KIND.TYPE_GROUP) { + tr.style.fontStyle = 'italic'; + tr.style.color = '#999'; + } else if (kind === NODE_KIND.INSTANCE) { + tr.style.color = '#aaa'; + } + + // Column 0: Instance (with tree indent, color swatch, and arrow) + const tdInst = document.createElement('td'); + tdInst.style.paddingLeft = (8 + row.depth * 16) + 'px'; + tdInst.style.whiteSpace = 'nowrap'; + + // Module color swatch + visibility checkbox + if (kind === NODE_KIND.MODULE && node.odb_id != null) { + const st = this._moduleState.get(node.odb_id); + const modelNode = this._checkModel + ? this._checkModel.get(node.id) : null; + if (st && modelNode) { + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = modelNode.checked; + cb.indeterminate = modelNode.indeterminate; + cb.className = 'hierarchy-module-cb'; + modelNode.cb = cb; + cb.addEventListener('change', (e) => { + e.stopPropagation(); + this._checkModel.check(node.id, cb.checked); + this._render(); + }); + tdInst.appendChild(cb); + + const swatch = document.createElement('span'); + swatch.className = 'hierarchy-color-swatch'; + const [r, g, b] = st.effectiveColor; + swatch.style.backgroundColor = `rgb(${r},${g},${b})`; + tdInst.appendChild(swatch); + } + } + + if (hasChildren) { + const arrow = document.createElement('span'); + arrow.className = 'hierarchy-arrow'; + arrow.textContent = isCollapsed ? '▶' : '▼'; + arrow.addEventListener('click', (e) => { + e.stopPropagation(); + this._toggleNode(row.id); + }); + tdInst.appendChild(arrow); + } else { + const spacer = document.createElement('span'); + spacer.className = 'hierarchy-arrow'; + spacer.style.visibility = 'hidden'; + spacer.textContent = '▶'; + tdInst.appendChild(spacer); + } + + const nameSpan = document.createElement('span'); + nameSpan.textContent = node.inst_name; + tdInst.appendChild(nameSpan); + tr.appendChild(tdInst); + + // Columns 1-8 + const vals = [ + node.module_name, + fmtInt(node.insts), + fmtInt(node.macros), + fmtInt(node.modules), + fmtArea(node.area), + fmtInt(node.local_insts), + fmtInt(node.local_macros), + fmtInt(node.local_modules), + ]; + for (const v of vals) { + const td = document.createElement('td'); + td.textContent = v; + td.style.textAlign = 'right'; + tr.appendChild(td); + } + // Module name column should be left-aligned + tr.children[1].style.textAlign = 'left'; + + tbody.appendChild(tr); + } + this._table.appendChild(tbody); + makeResizableHeaders(this._table); + } + + _isHidden(node) { + // Walk up parents; if any ancestor is collapsed, this node is hidden + let parentId = node.parent_id; + while (parentId >= 0) { + if (this._collapsed.has(parentId)) return true; + const parent = this._nodeMap.get(parentId); + if (!parent) break; + parentId = parent.parent_id; + } + return false; + } + + _toggleNode(id) { + if (this._collapsed.has(id)) { + this._collapsed.delete(id); + } else { + this._collapsed.add(id); + } + // Recompute effective colors since collapse state changed + this._computeEffectiveColors(); + this._render(); + this._sendModuleColors(); + } +} + +function fmtInt(v) { + return v != null ? String(v) : ''; +} + +function fmtArea(v) { + if (v == null) return ''; + if (v >= 1e6) return (v / 1e6).toFixed(3) + ' mm²'; + return v.toFixed(3) + ' μm²'; +} diff --git a/src/web/src/hierarchy_report.cpp b/src/web/src/hierarchy_report.cpp new file mode 100644 index 00000000000..61464e9928c --- /dev/null +++ b/src/web/src/hierarchy_report.cpp @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include "hierarchy_report.h" + +#include +#include +#include +#include + +#include "db_sta/dbSta.hh" +#include "odb/db.h" + +namespace web { + +HierarchyReport::HierarchyReport(odb::dbBlock* block, sta::dbSta* sta) + : block_(block), sta_(sta) +{ +} + +struct ModuleStats +{ + int insts = 0; + int macros = 0; + int modules = 0; + int64_t area_dbu2 = 0; +}; + +// Instance types that the GUI excludes from the hierarchy by default. +static bool isPhysicalType(sta::dbSta::InstType type) +{ + using IT = sta::dbSta::InstType; + switch (type) { + case IT::ENDCAP: + case IT::FILL: + case IT::TAPCELL: + case IT::STD_PHYSICAL: + case IT::BUMP: + case IT::COVER: + case IT::ANTENNA: + return true; + default: + return false; + } +} + +// Map InstType enum to the display name used in the GUI. +static const char* typeBucketName(sta::dbSta::InstType type) +{ + using IT = sta::dbSta::InstType; + switch (type) { + case IT::BLOCK: + return "Macro"; + case IT::PAD: + return "Pad"; + case IT::PAD_INPUT: + return "Input pad"; + case IT::PAD_OUTPUT: + return "Output pad"; + case IT::PAD_INOUT: + return "Input/output pad"; + case IT::PAD_POWER: + return "Power pad"; + case IT::PAD_SPACER: + return "Pad spacer"; + case IT::PAD_AREAIO: + return "Area IO"; + case IT::TIE: + return "Tie cell"; + case IT::LEF_OTHER: + return "Other"; + case IT::STD_CELL: + return "Standard cell"; + case IT::STD_BUF: + return "Buffer"; + case IT::STD_INV: + return "Inverter"; + case IT::STD_BUF_CLK_TREE: + return "Clock buffer"; + case IT::STD_INV_CLK_TREE: + return "Clock inverter"; + case IT::STD_BUF_TIMING_REPAIR: + return "Timing Repair Buffer"; + case IT::STD_INV_TIMING_REPAIR: + return "Timing Repair Inverter"; + case IT::STD_CLOCK_GATE: + return "Clock gate cell"; + case IT::STD_LEVEL_SHIFT: + return "Level shifter cell"; + case IT::STD_SEQUENTIAL: + return "Sequential cell"; + case IT::STD_COMBINATIONAL: + return "Multi-Input combinational cell"; + case IT::STD_OTHER: + return "Other"; + default: + return "Standard cell"; + } +} + +// Info accumulated per instance-type group within a module. +struct TypeBucket +{ + int count = 0; + int64_t area_dbu2 = 0; + bool is_macro = false; + // Individual macro instances (only populated for BLOCK type) + std::vector macro_insts; +}; + +// Emit "Leaf instances" folder with type sub-groups for a module. +static void emitLeafNodes(odb::dbModule* module, + sta::dbSta* sta, + int parent_id, + int& next_id, + std::vector& nodes) +{ + // Group instances by type + std::map buckets; + for (odb::dbInst* inst : module->getInsts()) { + sta::dbSta::InstType inst_type = sta->getInstanceType(inst); + if (isPhysicalType(inst_type)) { + continue; + } + const char* type_name = typeBucketName(inst_type); + auto& bucket = buckets[type_name]; + bucket.count++; + auto* box = inst->getBBox(); + bucket.area_dbu2 += static_cast(box->getDX()) + * static_cast(box->getDY()); + if (inst_type == sta::dbSta::InstType::BLOCK) { + bucket.is_macro = true; + bucket.macro_insts.push_back(inst); + } + } + + if (buckets.empty()) { + return; + } + + // Emit "Leaf instances" parent node + const int leaf_id = next_id++; + nodes.emplace_back(); + HierarchyNode& leaf = nodes[leaf_id]; + leaf.id = leaf_id; + leaf.parent_id = parent_id; + leaf.inst_name = "Leaf instances"; + leaf.node_kind = HierarchyNodeKind::LEAF_GROUP; + + // Aggregate totals for the leaf group + for (const auto& [name, bucket] : buckets) { + leaf.area += static_cast(bucket.area_dbu2); + if (bucket.is_macro) { + leaf.macros += bucket.count; + } else { + leaf.insts += bucket.count; + } + } + leaf.local_insts = leaf.insts; + leaf.local_macros = leaf.macros; + + // Emit type sub-group nodes + for (const auto& [type_name, bucket] : buckets) { + const int type_id = next_id++; + nodes.emplace_back(); + HierarchyNode& type_node = nodes[type_id]; + type_node.id = type_id; + type_node.parent_id = leaf_id; + type_node.inst_name = type_name; + type_node.node_kind = HierarchyNodeKind::TYPE_GROUP; + type_node.area = static_cast(bucket.area_dbu2); + if (bucket.is_macro) { + type_node.macros = bucket.count; + type_node.local_macros = bucket.count; + } else { + type_node.insts = bucket.count; + type_node.local_insts = bucket.count; + } + + // For macros, emit individual instance rows + if (bucket.is_macro) { + for (odb::dbInst* inst : bucket.macro_insts) { + const int inst_id = next_id++; + nodes.emplace_back(); + HierarchyNode& inst_node = nodes[inst_id]; + inst_node.id = inst_id; + inst_node.parent_id = type_id; + inst_node.inst_name = inst->getConstName(); + inst_node.module_name = inst->getMaster()->getConstName(); + inst_node.node_kind = HierarchyNodeKind::INSTANCE; + inst_node.macros = 1; + inst_node.local_macros = 1; + auto* box = inst->getBBox(); + inst_node.area + = static_cast(static_cast(box->getDX()) + * static_cast(box->getDY())); + } + } + } +} + +// Recursive DFS: adds a node for the module, recurses into children, +// then writes back hierarchical totals. Returns hierarchical stats. +static ModuleStats addModule(odb::dbModule* module, + sta::dbSta* sta, + int parent_id, + const char* inst_name, + int& next_id, + std::vector& nodes) +{ + const int my_id = next_id++; + nodes.emplace_back(); + nodes[my_id].id = my_id; + nodes[my_id].parent_id = parent_id; + nodes[my_id].inst_name = inst_name; + nodes[my_id].module_name = module->getName(); + nodes[my_id].odb_id = module->getId(); + + // Count local instances and area (skip physical cell types to match GUI) + ModuleStats local; + for (odb::dbInst* inst : module->getInsts()) { + sta::dbSta::InstType inst_type = sta->getInstanceType(inst); + if (isPhysicalType(inst_type)) { + continue; + } + auto* box = inst->getBBox(); + local.area_dbu2 += static_cast(box->getDX()) + * static_cast(box->getDY()); + if (inst->isBlock()) { + local.macros++; + } else { + local.insts++; + } + } + local.modules = module->getModInstCount(); + + nodes[my_id].local_insts = local.insts; + nodes[my_id].local_macros = local.macros; + nodes[my_id].local_modules = local.modules; + + // Hierarchical = local + sum of children + ModuleStats hier = local; + + for (odb::dbModInst* child_mi : module->getChildren()) { + odb::dbModule* child_mod = child_mi->getMaster(); + ModuleStats child_stats + = addModule(child_mod, sta, my_id, child_mi->getName(), next_id, nodes); + hier.insts += child_stats.insts; + hier.macros += child_stats.macros; + hier.modules += child_stats.modules; + hier.area_dbu2 += child_stats.area_dbu2; + } + + // Emit "Leaf instances" folder with type sub-groups + emitLeafNodes(module, sta, my_id, next_id, nodes); + + // Write hierarchical totals (area stored as DBU², converted later) + nodes[my_id].insts = hier.insts; + nodes[my_id].macros = hier.macros; + nodes[my_id].modules = hier.modules; + nodes[my_id].area = static_cast(hier.area_dbu2); + + return hier; +} + +HierarchyResult HierarchyReport::getReport() const +{ + HierarchyResult result; + + if (!block_ || !sta_) { + return result; + } + + odb::dbModule* top = block_->getTopModule(); + if (!top) { + return result; + } + + int next_id = 0; + addModule(top, sta_, -1, top->getName(), next_id, result.nodes); + + // Convert area from DBU² to μm² + const int dbu_per_um = block_->getDbUnitsPerMicron(); + const double dbu_to_um_sq + = 1.0 / (static_cast(dbu_per_um) * dbu_per_um); + for (auto& node : result.nodes) { + node.area *= dbu_to_um_sq; + } + + return result; +} + +} // namespace web diff --git a/src/web/src/hierarchy_report.h b/src/web/src/hierarchy_report.h new file mode 100644 index 00000000000..550cc027505 --- /dev/null +++ b/src/web/src/hierarchy_report.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include +#include + +namespace odb { +class dbBlock; +} // namespace odb + +namespace sta { +class dbSta; +} // namespace sta + +namespace web { + +// Node types in the hierarchy tree +enum class HierarchyNodeKind +{ + MODULE = 0, // Module (default) + LEAF_GROUP = 1, // "Leaf instances" folder + TYPE_GROUP = 2, // Instance type sub-group (e.g. "Standard cell", "Macro") + INSTANCE = 3, // Individual instance row (only for macros) +}; + +struct HierarchyNode +{ + int id = 0; + int parent_id = -1; // -1 for root (top module) + std::string inst_name; + std::string module_name; // master name for TYPE_GROUP/INSTANCE nodes + int insts = 0; // hierarchical stdcell count + int macros = 0; // hierarchical macro count + int modules = 0; // hierarchical sub-module count + double area = 0.0; // hierarchical area (μm²) + int local_insts = 0; // direct stdcell count + int local_macros = 0; // direct macro count + int local_modules = 0; // direct child module count + HierarchyNodeKind node_kind = HierarchyNodeKind::MODULE; + unsigned int odb_id = 0; // dbModule::getId() for MODULE nodes +}; + +struct HierarchyResult +{ + std::vector nodes; +}; + +class HierarchyReport +{ + public: + HierarchyReport(odb::dbBlock* block, sta::dbSta* sta); + + HierarchyResult getReport() const; + + private: + odb::dbBlock* block_; + sta::dbSta* sta_; +}; + +} // namespace web diff --git a/src/web/src/index.html b/src/web/src/index.html new file mode 100644 index 00000000000..fdb30234fd6 --- /dev/null +++ b/src/web/src/index.html @@ -0,0 +1,34 @@ + + + + OpenROAD Web Viewer + + + + + + + + + + + + + + +
+
+
+
+
+ Loading shapes… +
+
+ + + + diff --git a/src/web/src/inspector.js b/src/web/src/inspector.js new file mode 100644 index 00000000000..d3674c29360 --- /dev/null +++ b/src/web/src/inspector.js @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Inspector panel — property tree, hover highlights, bbox display. + +import { dbuToLatLng, dbuRectToBounds } from './coordinates.js'; + +// SVG icons — distinct shapes so they're easy to tell apart at a glance. +// Zoom to: magnifying glass with "+" (Material "zoom_in") +const ZOOM_TO_SVG = + '' + + '' + + '' + + ''; + +// Back: left arrow (Material "arrow_back") +const BACK_SVG = + '' + + '' + + ''; + +// Focus net: eye open (Material "visibility") +const FOCUS_SVG = + '' + + '' + + ''; + +// De-focus net: eye off (Material "visibility_off") +const DEFOCUS_SVG = + '' + + '' + + ''; + +// Show route guides: Material "alt_route" +const ROUTE_GUIDE_SVG = + '' + + '' + + ''; + +// Hide route guides: Material "alt_route" with strikethrough +const HIDE_ROUTE_GUIDE_SVG = + '' + + '' + + '' + + ''; + +// Clear focus: X (Material "close") +const CLEAR_FOCUS_SVG = + '' + + '' + + ''; + +export function createInspectorPanel(app, redrawAllLayers) { + let lastInspectData = null; + let pendingInspectId = null; + const kMinHoverBoxPixels = 10; + + function showLoading() { + if (!app.inspectorEl) return; + app.inspectorEl.innerHTML = ''; + const loading = document.createElement('div'); + loading.className = 'stub-panel'; + loading.innerHTML = + '
Loading\u2026
' + + '
Fetching properties from server.
'; + app.inspectorEl.appendChild(loading); + } + + async function toggleFocusNet(name) { + const action = app.focusNets.has(name) ? 'remove' : 'add'; + if (action === 'add') app.focusNets.add(name); + else app.focusNets.delete(name); + try { + await app.websocketManager.request({ + type: 'set_focus_nets', action, net_name: name, + }); + } catch (err) { + console.error('set_focus_nets failed:', err); + } + redrawAllLayers(); + if (lastInspectData) updateInspector(lastInspectData); + } + + async function clearFocusNets() { + app.focusNets.clear(); + try { + await app.websocketManager.request({ + type: 'set_focus_nets', action: 'clear', net_name: '', + }); + } catch (err) { + console.error('set_focus_nets clear failed:', err); + } + redrawAllLayers(); + if (lastInspectData) updateInspector(lastInspectData); + } + + async function toggleRouteGuides(name) { + const action = app.routeGuideNets.has(name) ? 'remove' : 'add'; + if (action === 'add') app.routeGuideNets.add(name); + else app.routeGuideNets.delete(name); + try { + await app.websocketManager.request({ + type: 'set_route_guides', action, net_name: name, + }); + } catch (err) { + console.error('set_route_guides failed:', err); + } + redrawAllLayers(); + if (lastInspectData) updateInspector(lastInspectData); + } + + function clearClientHoverHighlight() { + if (app.hoverHighlightLayer) { + app.map.removeLayer(app.hoverHighlightLayer); + app.hoverHighlightLayer = null; + } + } + + function clearHoverHighlight() { + clearClientHoverHighlight(); + app.websocketManager.request({ type: 'hover', select_id: -1 }) + .then(() => {}) + .catch(() => {}); + } + + function boundsWithMinimumScreenSize(x1, y1, x2, y2) { + const baseBounds = L.latLngBounds( + dbuRectToBounds(x1, y1, x2, y2, app.designScale, app.designMaxDXDY) + ); + if (!app.map) { + return baseBounds; + } + + const southWest = baseBounds.getSouthWest(); + const northEast = baseBounds.getNorthEast(); + const swPoint = app.map.latLngToContainerPoint(southWest); + const nePoint = app.map.latLngToContainerPoint(northEast); + + const minX = Math.min(swPoint.x, nePoint.x); + const maxX = Math.max(swPoint.x, nePoint.x); + const minY = Math.min(swPoint.y, nePoint.y); + const maxY = Math.max(swPoint.y, nePoint.y); + + let expandedMinX = minX; + let expandedMaxX = maxX; + let expandedMinY = minY; + let expandedMaxY = maxY; + + if ((maxX - minX) < kMinHoverBoxPixels) { + const pad = (kMinHoverBoxPixels - (maxX - minX)) / 2; + expandedMinX -= pad; + expandedMaxX += pad; + } + if ((maxY - minY) < kMinHoverBoxPixels) { + const pad = (kMinHoverBoxPixels - (maxY - minY)) / 2; + expandedMinY -= pad; + expandedMaxY += pad; + } + + const topLeft = app.map.containerPointToLatLng(L.point(expandedMinX, expandedMinY)); + const bottomRight = app.map.containerPointToLatLng(L.point(expandedMaxX, expandedMaxY)); + return L.latLngBounds(topLeft, bottomRight); + } + + function renderHoverRects(rects) { + clearClientHoverHighlight(); + if (!app.map || !app.designScale || !rects || rects.length === 0) { + return; + } + + app.hoverHighlightLayer = L.layerGroup( + rects.map(([x1, y1, x2, y2]) => L.rectangle(boundsWithMinimumScreenSize(x1, y1, x2, y2), { + color: '#ffe85c', + weight: 3, + fill: true, + fillColor: '#fff27a', + fillOpacity: 0.14, + opacity: 1, + interactive: false, + className: 'hover-highlight', + pane: app.hoverHighlightPane, + })) + ).addTo(app.map); + } + + function makeClickable(el, selectId) { + el.classList.add('inspector-link'); + el.addEventListener('click', (e) => { + e.stopPropagation(); + navigateInspector(selectId); + }); + el.addEventListener('mouseenter', () => { + app.websocketManager.request({ type: 'hover', select_id: selectId }) + .then(data => { + renderHoverRects(data.rects || []); + redrawAllLayers(); + }) + .catch(() => {}); + }); + el.addEventListener('mouseleave', () => { + clearHoverHighlight(); + redrawAllLayers(); + }); + } + + function navigateInspector(selectId) { + // Cancel previous in-flight inspect request + if (pendingInspectId !== null) { + app.websocketManager.cancel(pendingInspectId); + pendingInspectId = null; + } + + // Show loading state immediately + showLoading(); + + const promise = app.websocketManager.request( + { type: 'inspect', select_id: selectId }); + pendingInspectId = promise.requestId; + + promise + .then(data => { + pendingInspectId = null; + if (data.error) { + console.error('Inspect error:', data.error); + return; + } + updateInspector(data); + + app.map.closePopup(); + clearClientHoverHighlight(); + if (app.highlightRect) { + app.map.removeLayer(app.highlightRect); + app.highlightRect = null; + } + if (data.bbox && app.map && app.designScale) { + const [x1, y1, x2, y2] = data.bbox; + // For non-instance objects, show dashed bbox outline + // (instances get the yellow tile-based highlight instead) + if (data.type !== 'Inst') { + highlightBBox(x1, y1, x2, y2); + } + } + // Redraw tiles to update instance highlight + redrawAllLayers(); + }) + .catch(err => { + pendingInspectId = null; + console.error('Inspect failed:', err); + }); + } + + function navigateBack() { + if (pendingInspectId !== null) { + app.websocketManager.cancel(pendingInspectId); + pendingInspectId = null; + } + + showLoading(); + + const promise = app.websocketManager.request({ type: 'inspect_back' }); + pendingInspectId = promise.requestId; + + promise + .then(data => { + pendingInspectId = null; + if (data.error) { + console.error('Inspect back error:', data.error); + return; + } + updateInspector(data); + + app.map.closePopup(); + clearClientHoverHighlight(); + if (app.highlightRect) { + app.map.removeLayer(app.highlightRect); + app.highlightRect = null; + } + if (data.bbox && app.map && app.designScale && data.type !== 'Inst') { + const [x1, y1, x2, y2] = data.bbox; + highlightBBox(x1, y1, x2, y2); + } + redrawAllLayers(); + }) + .catch(err => { + pendingInspectId = null; + console.error('Inspect back failed:', err); + }); + } + + function highlightBBox(x1, y1, x2, y2) { + if (app.highlightRect) { + app.map.removeLayer(app.highlightRect); + } + const bounds = dbuRectToBounds(x1, y1, x2, y2, app.designScale, app.designMaxDXDY); + app.highlightRect = L.rectangle(bounds, { + color: '#ff0', weight: 2, fill: false, dashArray: '6,4', + }).addTo(app.map); + } + + function renderProperty(prop) { + // Group with children (PropertyList or SelectionSet) + if (prop.children) { + const group = document.createElement('div'); + group.className = 'inspector-group'; + + const header = document.createElement('div'); + header.className = 'inspector-group-header'; + const arrow = document.createElement('span'); + arrow.className = 'vis-arrow'; + arrow.textContent = 'â–¶'; + const nameSpan = document.createElement('span'); + nameSpan.className = 'inspector-prop-name'; + nameSpan.textContent = prop.name; + const countSpan = document.createElement('span'); + countSpan.className = 'inspector-count'; + countSpan.textContent = `(${prop.children.length})`; + header.appendChild(arrow); + header.appendChild(nameSpan); + header.appendChild(countSpan); + group.appendChild(header); + + const kids = document.createElement('div'); + const autoExpand = prop.children.length < 10; + kids.className = 'inspector-group-children' + (autoExpand ? '' : ' collapsed'); + arrow.textContent = autoExpand ? 'â–¼' : 'â–¶'; + for (const child of prop.children) { + kids.appendChild(renderProperty(child)); + } + group.appendChild(kids); + + arrow.addEventListener('click', () => { + kids.classList.toggle('collapsed'); + arrow.textContent = kids.classList.contains('collapsed') + ? 'â–¶' : 'â–¼'; + }); + header.addEventListener('click', () => { + kids.classList.toggle('collapsed'); + arrow.textContent = kids.classList.contains('collapsed') + ? 'â–¶' : 'â–¼'; + }); + + return group; + } + + // Leaf property: name + value + const row = document.createElement('div'); + row.className = 'inspector-prop'; + const nameEl = document.createElement('span'); + nameEl.className = 'inspector-prop-name'; + nameEl.textContent = prop.name || ''; + const valEl = document.createElement('span'); + valEl.className = 'inspector-prop-value'; + valEl.textContent = prop.value || ''; + row.appendChild(nameEl); + row.appendChild(valEl); + + // For single-target rows like SelectionSet entries, make the whole row + // interactive so hover is easy to hit. + if (prop.name_select_id !== undefined && prop.value_select_id === undefined) { + makeClickable(row, prop.name_select_id); + nameEl.classList.add('inspector-link'); + } else if (prop.name_select_id !== undefined) { + makeClickable(nameEl, prop.name_select_id); + } + if (prop.value_select_id !== undefined) { + makeClickable(valEl, prop.value_select_id); + } + + return row; + } + + function zoomToBBox(bbox) { + if (!bbox || !app.map || !app.designScale) return; + const [x1, y1, x2, y2] = bbox; + app.map.fitBounds(dbuRectToBounds(x1, y1, x2, y2, app.designScale, app.designMaxDXDY), + { padding: [20, 20] }); + } + + function updateInspector(data) { + if (!app.inspectorEl) return; + lastInspectData = data; + app.inspectorEl.innerHTML = ''; + + if (!data || !data.properties || data.properties.length === 0) { + const placeholder = document.createElement('div'); + placeholder.className = 'stub-panel'; + placeholder.innerHTML = + '
Inspector
' + + '
Select an object in the layout to inspect its properties.
'; + app.inspectorEl.appendChild(placeholder); + return; + } + + // Toolbar with action buttons + if (data) { + const toolbar = document.createElement('div'); + toolbar.className = 'inspector-toolbar'; + + const backBtn = document.createElement('button'); + backBtn.className = 'inspector-btn'; + backBtn.title = 'Back'; + backBtn.innerHTML = BACK_SVG; + backBtn.disabled = !data.can_navigate_back; + backBtn.addEventListener('click', () => navigateBack()); + toolbar.appendChild(backBtn); + + if (data.bbox) { + const zoomBtn = document.createElement('button'); + zoomBtn.className = 'inspector-btn'; + zoomBtn.title = 'Zoom to'; + zoomBtn.innerHTML = ZOOM_TO_SVG; + zoomBtn.addEventListener('click', () => zoomToBBox(data.bbox)); + toolbar.appendChild(zoomBtn); + } + + // Focus/De-focus button for Net objects + if (data.type === 'Net' && data.name) { + const isFocused = app.focusNets.has(data.name); + const focusBtn = document.createElement('button'); + focusBtn.className = 'inspector-btn'; + focusBtn.title = isFocused ? 'De-focus net' : 'Focus net'; + focusBtn.innerHTML = isFocused ? DEFOCUS_SVG : FOCUS_SVG; + focusBtn.addEventListener('click', () => toggleFocusNet(data.name)); + toolbar.appendChild(focusBtn); + } + + // Show/Hide route guides button for Net objects with guides + if (data.type === 'Net' && data.name && data.has_guides) { + const isShowing = app.routeGuideNets.has(data.name); + const guideBtn = document.createElement('button'); + guideBtn.className = 'inspector-btn'; + guideBtn.title = isShowing ? 'Hide route guides' : 'Show route guides'; + guideBtn.innerHTML = isShowing ? HIDE_ROUTE_GUIDE_SVG : ROUTE_GUIDE_SVG; + guideBtn.addEventListener('click', () => toggleRouteGuides(data.name)); + toolbar.appendChild(guideBtn); + } + + // Clear all focus nets button + if (app.focusNets.size > 0) { + const clearBtn = document.createElement('button'); + clearBtn.className = 'inspector-btn'; + clearBtn.title = 'Clear focus nets'; + clearBtn.innerHTML = CLEAR_FOCUS_SVG; + clearBtn.addEventListener('click', () => clearFocusNets()); + toolbar.appendChild(clearBtn); + } + + app.inspectorEl.appendChild(toolbar); + } + + for (const prop of data.properties) { + app.inspectorEl.appendChild(renderProperty(prop)); + } + } + + function createInspector(container) { + const el = document.createElement('div'); + el.className = 'inspector'; + app.inspectorEl = el; + container.element.appendChild(el); + + // Show placeholder initially + updateInspector(null); + } + + return { createInspector, updateInspector, highlightBBox }; +} diff --git a/src/web/src/json_builder.h b/src/web/src/json_builder.h new file mode 100644 index 00000000000..0ecc08bbb17 --- /dev/null +++ b/src/web/src/json_builder.h @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include + +namespace web { + +// Escape a string for safe inclusion in a JSON string value. +// Handles: \\ \" \n \r \t and control characters below 0x20. +inline std::string json_escape(const std::string& s) +{ + std::string out; + out.reserve(s.size()); + for (char c : s) { + switch (c) { + case '\\': + out += "\\\\"; + break; + case '"': + out += "\\\""; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (static_cast(c) < 0x20) { + char buf[8]; + std::snprintf( + buf, sizeof(buf), "\\u%04x", static_cast(c)); + out += buf; + } else { + out += c; + } + break; + } + } + return out; +} + +// Lightweight streaming JSON builder. +// +// Usage: +// JsonBuilder builder; +// builder.beginObject(); +// builder.field("name", some_string); +// builder.field("count", 42); +// builder.field("active", true); +// builder.beginArray("items"); +// builder.beginObject(); +// builder.field("id", 1); +// builder.endObject(); +// builder.endArray(); +// builder.endObject(); +// std::string json = builder.str(); +// +class JsonBuilder +{ + public: + JsonBuilder() { buf_.reserve(256); } + + // -- Containers -- + + void beginObject() + { + maybeComma(); + buf_ += '{'; + pushContext(); + } + + void beginObject(const char* key) + { + writeKey(key); + buf_ += '{'; + pushContext(); + } + void beginObject(const std::string& key) { beginObject(key.c_str()); } + + void endObject() + { + buf_ += '}'; + popContext(); + } + + void beginArray() + { + maybeComma(); + buf_ += '['; + pushContext(); + } + + void beginArray(const char* key) + { + writeKey(key); + buf_ += '['; + pushContext(); + } + void beginArray(const std::string& key) { beginArray(key.c_str()); } + + void endArray() + { + buf_ += ']'; + popContext(); + } + + // -- Named fields (inside objects) -- + + void field(const char* key, const std::string& val) + { + writeKey(key); + writeString(val); + } + void field(const std::string& key, const std::string& val) + { + field(key.c_str(), val); + } + + void field(const char* key, const char* val) + { + writeKey(key); + writeString(val); + } + + void field(const char* key, int val) + { + writeKey(key); + buf_ += std::to_string(val); + } + + void field(const char* key, float val) + { + writeKey(key); + writeFloat(val); + } + + void field(const char* key, double val) + { + writeKey(key); + writeDouble(val); + } + + void field(const char* key, bool val) + { + writeKey(key); + buf_ += val ? "true" : "false"; + } + + void field(const std::string& key, int val) { field(key.c_str(), val); } + void field(const std::string& key, float val) { field(key.c_str(), val); } + void field(const std::string& key, double val) { field(key.c_str(), val); } + void field(const std::string& key, bool val) { field(key.c_str(), val); } + + // -- Unnamed values (inside arrays) -- + + void value(const std::string& val) + { + maybeComma(); + writeString(val); + } + + void value(const char* val) + { + maybeComma(); + writeString(val); + } + + void value(int val) + { + maybeComma(); + buf_ += std::to_string(val); + } + + void value(float val) + { + maybeComma(); + writeFloat(val); + } + + void value(double val) + { + maybeComma(); + writeDouble(val); + } + + void value(bool val) + { + maybeComma(); + buf_ += val ? "true" : "false"; + } + + // -- Output -- + + const std::string& str() const { return buf_; } + std::string&& take() { return std::move(buf_); } + + private: + static constexpr int kMaxDepth = 16; + bool need_comma_[kMaxDepth]; + int depth_ = 0; + + std::string buf_; + + void pushContext() + { + assert(depth_ < kMaxDepth); + need_comma_[depth_] = false; + depth_++; + } + + void popContext() + { + depth_--; + if (depth_ > 0) { + need_comma_[depth_ - 1] = true; + } + } + + void maybeComma() + { + if (depth_ > 0 && need_comma_[depth_ - 1]) { + buf_ += ", "; + } + if (depth_ > 0) { + need_comma_[depth_ - 1] = true; + } + } + + void writeKey(const char* key) + { + maybeComma(); + buf_ += '"'; + buf_ += key; + buf_ += "\": "; + } + + void writeString(const std::string& s) + { + buf_ += '"'; + buf_ += json_escape(s); + buf_ += '"'; + } + + void writeString(const char* s) + { + buf_ += '"'; + buf_ += json_escape(s); + buf_ += '"'; + } + + void writeFloat(float val) + { + char buf[32]; + int n = std::snprintf(buf, sizeof(buf), "%g", static_cast(val)); + buf_.append(buf, n); + } + + void writeDouble(double val) + { + char buf[32]; + int n = std::snprintf(buf, sizeof(buf), "%g", val); + buf_.append(buf, n); + } +}; + +} // namespace web diff --git a/src/web/src/main.js b/src/web/src/main.js new file mode 100644 index 00000000000..9ff732c74a7 --- /dev/null +++ b/src/web/src/main.js @@ -0,0 +1,696 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { GoldenLayout, LayoutConfig } from 'https://esm.sh/golden-layout@2.6.0'; +import { latLngToDbu } from './coordinates.js'; +import { WebSocketManager } from './websocket-manager.js'; +import { createWebSocketTileLayer } from './websocket-tile-layer.js'; +import { TimingWidget } from './timing-widget.js'; +import { ClockTreeWidget } from './clock-tree-widget.js'; +import { ChartsWidget } from './charts-widget.js'; +import { HierarchyBrowser } from './hierarchy-browser.js'; +import { createInspectorPanel } from './inspector.js'; +import { populateDisplayControls } from './display-controls.js'; +import { createMenuBar } from './menu-bar.js'; + +// ─── Status Indicator ─────────────────────────────────────────────────────── + +const statusDiv = document.getElementById('websocket-status'); + +function updateStatus() { + const n = app.websocketManager ? app.websocketManager.pending.size : 0; + if (n === 0) { + statusDiv.textContent = ''; + statusDiv.style.display = 'none'; + } else { + statusDiv.textContent = `pending: ${n}`; + statusDiv.style.display = ''; + statusDiv.style.color = n > 20 ? '#f88' : '#ff0'; + } +} + +// ─── Component Factories ──────────────────────────────────────────────────── + +// Shared application state — replaces scattered module-level globals. +// Components receive this via closure now; when extracted to separate files +// they'll receive it as an explicit parameter. +const app = { + map: null, + fitBounds: null, + displayControlsEl: null, + allLayers: [], + designScale: null, // pixels-per-DBU for coordinate conversion + designMaxDXDY: null, // max(width, height) in DBU for Y-axis mapping + websocketManager: null, // set after construction below + goldenLayout: null, // set after GL init below + hasLiberty: false, + techData: null, + inspectorEl: null, + tclOutputEl: null, + highlightRect: null, + hoverHighlightLayer: null, + hoverHighlightPane: 'hover-highlight-pane', + modulesLayer: null, + hierarchyBrowser: null, + focusNets: new Set(), + routeGuideNets: new Set(), + visibleLayers: new Set(), + heatMapData: null, + activeHeatMap: '', + heatMapLayer: null, + heatMapLegendEl: null, + renderHeatMapControls: null, +}; + +const visibility = { + stdcells: true, + macros: true, + // Pad sub-types + pad_input: true, + pad_output: true, + pad_inout: true, + pad_power: true, + pad_spacer: true, + pad_areaio: true, + pad_other: true, + // Physical sub-types + phys_fill: false, + phys_endcap: true, + phys_welltap: true, + phys_tie: true, + phys_antenna: true, + phys_cover: true, + phys_bump: true, + phys_other: true, + // Std cell sub-types + std_bufinv: true, + std_bufinv_timing: true, + std_clock_bufinv: true, + std_clock_gate: true, + std_level_shift: true, + std_sequential: true, + std_combinational: true, + // Net sub-types + net_signal: true, + net_power: true, + net_ground: true, + net_clock: true, + net_reset: true, + net_tieoff: true, + net_scan: true, + net_analog: true, + // Shapes + routing: true, + special_nets: true, + pins: true, + blockages: true, + // Blockages + placement_blockages: true, + routing_obstructions: true, + // Rows (off by default, matching GUI) + rows: false, + // Tracks (off by default, matching GUI) + tracks_pref: false, + tracks_non_pref: false, + // Module view + module_view: false, + // Debug + debug: false, +}; + +const WebSocketTileLayer = createWebSocketTileLayer(visibility); +const BLANK_TILE + = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; + +const HeatMapTileLayer = L.GridLayer.extend({ + initialize: function(websocketManager, appState, options) { + this._websocketManager = websocketManager; + this._appState = appState; + L.GridLayer.prototype.initialize.call(this, options); + }, + + createTile: function(coords, done) { + const tile = document.createElement('img'); + tile.alt = ''; + tile.setAttribute('role', 'presentation'); + tile._tileDone = false; + tile.onload = () => { + if (tile.src && tile.src.startsWith('blob:')) { + URL.revokeObjectURL(tile.src); + } + if (!tile._tileDone) { + tile._tileDone = true; + done(null, tile); + } + }; + tile.onerror = () => { + if (!tile._tileDone) { + tile._tileDone = true; + done(new Error('heat map tile load error'), tile); + } + }; + + const active = this._appState.activeHeatMap; + if (!active) { + tile.src = BLANK_TILE; + return tile; + } + + this._websocketManager.request({ + type: 'heatmap_tile', + name: active, + z: coords.z, + x: coords.x, + y: coords.y, + }).then(blob => { + tile.src = URL.createObjectURL(blob); + }).catch(() => { + tile.src = BLANK_TILE; + }); + + return tile; + }, + + refreshTiles: function() { + if (!this._map) return; + for (const key in this._tiles) { + const tileInfo = this._tiles[key]; + if (!tileInfo || !tileInfo.el) continue; + const tile = tileInfo.el; + const coords = tileInfo.coords; + const active = this._appState.activeHeatMap; + if (!active) { + tile.src = BLANK_TILE; + continue; + } + this._websocketManager.request({ + type: 'heatmap_tile', + name: active, + z: coords.z, + x: coords.x, + y: coords.y, + }).then(blob => { + if (tile.src && tile.src.startsWith('blob:')) { + URL.revokeObjectURL(tile.src); + } + tile.src = URL.createObjectURL(blob); + }).catch(() => { + tile.src = BLANK_TILE; + }); + } + }, +}); + +function updateHeatMaps(data) { + app.heatMapData = data; + app.activeHeatMap = data.active || ''; + if (app.heatMapLayer) { + if (app.activeHeatMap) { + if (!app.map.hasLayer(app.heatMapLayer)) { + app.heatMapLayer.addTo(app.map); + } + } else if (app.map.hasLayer(app.heatMapLayer)) { + app.map.removeLayer(app.heatMapLayer); + } + app.heatMapLayer.refreshTiles(); + } + if (app.renderHeatMapControls) { + app.renderHeatMapControls(data); + } +} +app.updateHeatMaps = updateHeatMaps; + +function redrawAllLayers() { + // Show/hide modules layer based on module_view visibility + if (app.modulesLayer) { + if (visibility.module_view && !app.map.hasLayer(app.modulesLayer)) { + app.modulesLayer.addTo(app.map); + } else if (!visibility.module_view && app.map.hasLayer(app.modulesLayer)) { + app.map.removeLayer(app.modulesLayer); + } + } + for (const layer of app.allLayers) { + layer.refreshTiles(); + } + if (app.heatMapLayer) { + app.heatMapLayer.refreshTiles(); + } +} + + +function createLayoutViewer(container) { + const mapDiv = document.createElement('div'); + mapDiv.className = 'layout-viewer'; + mapDiv.style.width = '100%'; + mapDiv.style.height = '100%'; + mapDiv.style.backgroundColor = '#111'; + container.element.appendChild(mapDiv); + + const heatMapLegend = document.createElement('div'); + heatMapLegend.className = 'heatmap-map-legend hidden'; + mapDiv.appendChild(heatMapLegend); + app.heatMapLegendEl = heatMapLegend; + + app.map = L.map(mapDiv, { + crs: L.CRS.Simple, + zoom: 1, + zoomSnap: 0, + fadeAnimation: false, + attributionControl: false, + }); + const hoverPane = app.map.createPane(app.hoverHighlightPane); + hoverPane.style.zIndex = '650'; + hoverPane.style.pointerEvents = 'none'; + + new ResizeObserver(() => { + app.map.invalidateSize({ animate: false }); + }).observe(mapDiv); +} + +function createDisplayControls(container) { + const el = document.createElement('div'); + el.className = 'display-controls'; + el.innerHTML = '
Loading layers...
'; + container.element.appendChild(el); + app.displayControlsEl = el; +} + +function tclAppend(text, className) { + if (!app.tclOutputEl) return; + const span = document.createElement('span'); + if (className) span.className = className; + span.textContent = text; + app.tclOutputEl.appendChild(span); + app.tclOutputEl.scrollTop = app.tclOutputEl.scrollHeight; +} + +function createTclConsole(container) { + const el = document.createElement('div'); + el.className = 'tcl-console'; + el.innerHTML = + '
' + + '
' + + ' %' + + ' ' + + '
'; + container.element.appendChild(el); + + app.tclOutputEl = el.querySelector('.tcl-output'); + const input = el.querySelector('.tcl-input'); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const cmd = input.value.trim(); + if (!cmd) return; + tclAppend(`>>> ${cmd}\n`, 'tcl-cmd'); + input.value = ''; + app.websocketManager.request({ type: 'tcl_eval', cmd }) + .then(data => { + if (data.output) { + tclAppend(data.output, + data.is_error ? 'tcl-error' : ''); + } + if (data.result) { + tclAppend(data.result + '\n', + data.is_error ? 'tcl-error' : ''); + } + }) + .catch(err => tclAppend(`Error: ${err}\n`, 'tcl-error')); + } + }); +} + +// ─── Inspector Panel ──────────────────────────────────────────────────────── + +const inspector = createInspectorPanel(app, redrawAllLayers); +const createInspector = inspector.createInspector; +const updateInspector = inspector.updateInspector; +const highlightBBox = inspector.highlightBBox; + +function createBrowser(container) { + new HierarchyBrowser(container, app, redrawAllLayers); +} + +function createTimingWidget(container) { + app.timingWidget = new TimingWidget(container, app, redrawAllLayers); +} + +function createDRCWidget(container) { + createStubPanel(container, 'DRC', + 'Design rule check violations viewer.'); +} + +function createClockWidget(container) { + new ClockTreeWidget(container, app, redrawAllLayers); +} + +function createChartsWidget(container) { + new ChartsWidget(container, app, redrawAllLayers); +} + +function createHelpWidget(container) { + const el = document.createElement('div'); + el.className = 'help-panel'; + el.innerHTML = + '

Keyboard Shortcuts

' + + '' + + '' + + '' + + '' + + '' + + '
fFit design to viewport
scrollZoom in/out
dragPan the view
right-dragRubber-band zoom
'; + container.element.appendChild(el); +} + +function createSelectHighlight(container) { + createStubPanel(container, 'Selection', + 'Selection and highlight browser.'); +} + +function createStubPanel(container, title, description) { + const el = document.createElement('div'); + el.className = 'stub-panel'; + el.innerHTML = + `
${title}
` + + `
${description}
`; + container.element.appendChild(el); +} + +// ─── Layout Configuration ─────────────────────────────────────────────────── + +const defaultLayoutConfig = { + root: { + type: 'row', + content: [ + { + type: 'component', + componentType: 'DisplayControls', + title: 'Display Controls', + width: 15, + }, + { + type: 'column', + width: 55, + content: [ + { + type: 'component', + componentType: 'LayoutViewer', + title: 'Layout', + height: 70, + isClosable: false, + }, + { + type: 'component', + componentType: 'TclConsole', + title: 'Tcl Console', + height: 30, + }, + ], + }, + { + type: 'stack', + width: 30, + content: [ + { + type: 'component', + componentType: 'Inspector', + title: 'Inspector', + }, + { + type: 'component', + componentType: 'Browser', + title: 'Hierarchy', + }, + { + type: 'component', + componentType: 'TimingWidget', + title: 'Timing', + }, + { + type: 'component', + componentType: 'DRCWidget', + title: 'DRC', + }, + { + type: 'component', + componentType: 'ClockWidget', + title: 'Clock Tree', + }, + { + type: 'component', + componentType: 'ChartsWidget', + title: 'Charts', + }, + { + type: 'component', + componentType: 'HelpWidget', + title: 'Help', + }, + ], + }, + ], + }, +}; + +// ─── Golden Layout Init ───────────────────────────────────────────────────── + +app.goldenLayout = new GoldenLayout(document.getElementById('gl-container')); + +app.goldenLayout.registerComponentFactoryFunction('LayoutViewer', createLayoutViewer); +app.goldenLayout.registerComponentFactoryFunction('DisplayControls', createDisplayControls); +app.goldenLayout.registerComponentFactoryFunction('TclConsole', createTclConsole); +app.goldenLayout.registerComponentFactoryFunction('Inspector', createInspector); +app.goldenLayout.registerComponentFactoryFunction('Browser', createBrowser); +app.goldenLayout.registerComponentFactoryFunction('TimingWidget', createTimingWidget); +app.goldenLayout.registerComponentFactoryFunction('DRCWidget', createDRCWidget); +app.goldenLayout.registerComponentFactoryFunction('ClockWidget', createClockWidget); +app.goldenLayout.registerComponentFactoryFunction('ChartsWidget', createChartsWidget); +app.goldenLayout.registerComponentFactoryFunction('HelpWidget', createHelpWidget); +app.goldenLayout.registerComponentFactoryFunction('SelectHighlight', createSelectHighlight); + +// Layout version — bump this to force a layout reset when components change. +const LAYOUT_VERSION = 2; + +// Restore saved layout or use default +const savedLayout = localStorage.getItem('gl-layout'); +const savedVersion = parseInt(localStorage.getItem('gl-layout-version'), 10); +if (savedLayout && savedVersion === LAYOUT_VERSION) { + try { + const resolved = JSON.parse(savedLayout); + app.goldenLayout.loadLayout(LayoutConfig.fromResolved(resolved)); + } catch (e) { + app.goldenLayout.loadLayout(defaultLayoutConfig); + } +} else { + app.goldenLayout.loadLayout(defaultLayoutConfig); +} +localStorage.setItem('gl-layout-version', LAYOUT_VERSION); + +// Persist layout on changes (drag, resize, close, etc.) +app.goldenLayout.on('stateChanged', () => { + localStorage.setItem('gl-layout', JSON.stringify(app.goldenLayout.saveLayout())); +}); + +// Handle window resize +window.addEventListener('resize', () => { + const menuBarHeight = document.getElementById('menu-bar').offsetHeight; + app.goldenLayout.setSize(window.innerWidth, window.innerHeight - menuBarHeight); +}); + +// Focus a Golden Layout component tab by its componentType name. +function focusComponent(componentType) { + function find(item) { + if (item.isComponent && item.componentType === componentType) return item; + if (item.contentItems) { + for (const child of item.contentItems) { + const found = find(child); + if (found) return found; + } + } + return null; + } + const item = find(app.goldenLayout.rootItem); + if (item) item.focus(); +} + +app.focusComponent = focusComponent; + +// ─── Menu Bar ──────────────────────────────────────────────────────────────── + +createMenuBar(app); + +// ─── WebSocket Init ───────────────────────────────────────────────────────── + +const websocketUrl = `ws://${window.location.hostname || 'localhost'}:8080/ws`; +app.websocketManager = new WebSocketManager(websocketUrl, updateStatus); + +// Handle server-push notifications (e.g. search indices ready) +app.websocketManager.onPush = (msg) => { + if (msg.type === 'refresh') { + document.getElementById('loading-overlay').style.display = 'none'; + redrawAllLayers(); + } +}; + +app.websocketManager.readyPromise.then(async () => { + try { + const [techData, boundsData, heatMapData] = await Promise.all([ + app.websocketManager.request({ type: 'tech' }), + app.websocketManager.request({ type: 'bounds' }), + app.websocketManager.request({ type: 'heatmaps' }), + ]); + app.hasLiberty = techData.has_liberty; + app.techData = techData; + + // --- Set Bounds --- + const designBounds = boundsData.bounds; + + const minY = designBounds[0][0]; + const minX = designBounds[0][1]; + const maxY = designBounds[1][0]; + const maxX = designBounds[1][1]; + + const designWidth = maxX - minX; + const designHeight = maxY - minY; + + // No design loaded — skip map setup, let user open a DB via menu. + const hasDesign = designWidth > 0 && designHeight > 0; + if (hasDesign) { + const tileSize = 256; + const scale = tileSize / Math.max(designWidth, designHeight); + app.designScale = scale; + app.designMaxDXDY = Math.max(designWidth, designHeight); + + app.fitBounds = [ + [-minY * scale, minX * scale], + [-maxY * scale, maxX * scale] + ]; + app.map.fitBounds(app.fitBounds); + } + + // Click-to-select: convert click position to DBU and query server + app.map.on('click', (e) => { + if (!app.designScale) return; + const { dbuX: dbu_x, dbuY: dbu_y } = latLngToDbu( + e.latlng.lat, e.latlng.lng, app.designScale, app.designMaxDXDY); + + const vf = {}; + for (const [k, v] of Object.entries(visibility)) { + vf[k] = v ? 1 : 0; + } + app.websocketManager.request({ type: 'select', dbu_x, dbu_y, zoom: app.map.getZoom(), visible_layers: [...app.visibleLayers], ...vf }) + .then(data => { + console.log('Select response:', data, 'at dbu', dbu_x, dbu_y); + app.map.closePopup(); + if (data.selected && data.selected.length > 0) { + const inst = data.selected[0]; + updateInspector(data); + focusComponent('Inspector'); + // Highlight selected instance bbox + if (inst.bbox) { + highlightBBox(inst.bbox[0], inst.bbox[1], + inst.bbox[2], inst.bbox[3]); + } + } else { + updateInspector(null); + if (app.highlightRect) { + app.map.removeLayer(app.highlightRect); + app.highlightRect = null; + } + } + redrawAllLayers(); + }) + .catch(err => { + console.error('Select failed:', err); + }); + }); + + // ─── Right-click rubber-band zoom ────────────────────────────── + { + const container = app.map.getContainer(); + let rbStart = null; // {x, y} in client coords + let rbDiv = null; // overlay element + + container.addEventListener('contextmenu', (e) => { + e.preventDefault(); + }); + + container.addEventListener('mousedown', (e) => { + if (e.button !== 2) return; + rbStart = { x: e.clientX, y: e.clientY }; + app.map.dragging.disable(); + }); + + window.addEventListener('mousemove', (e) => { + if (!rbStart) return; + const dx = e.clientX - rbStart.x; + const dy = e.clientY - rbStart.y; + if (!rbDiv && Math.abs(dx) >= 4 && Math.abs(dy) >= 4) { + rbDiv = document.createElement('div'); + rbDiv.className = 'rubber-band'; + document.body.appendChild(rbDiv); + } + if (rbDiv) { + const left = Math.min(rbStart.x, e.clientX); + const top = Math.min(rbStart.y, e.clientY); + rbDiv.style.left = left + 'px'; + rbDiv.style.top = top + 'px'; + rbDiv.style.width = Math.abs(dx) + 'px'; + rbDiv.style.height = Math.abs(dy) + 'px'; + } + }); + + window.addEventListener('mouseup', (e) => { + if (!rbStart) return; + const wasShowing = !!rbDiv; + if (rbDiv) { + rbDiv.remove(); + rbDiv = null; + } + const start = rbStart; + rbStart = null; + app.map.dragging.enable(); + + if (!wasShowing) return; + + // Convert the two screen corners to lat/lng and zoom + const rect = container.getBoundingClientRect(); + const p1 = app.map.containerPointToLatLng([ + start.x - rect.left, start.y - rect.top]); + const p2 = app.map.containerPointToLatLng([ + e.clientX - rect.left, e.clientY - rect.top]); + app.map.fitBounds([ + [Math.min(p1.lat, p2.lat), Math.min(p1.lng, p2.lng)], + [Math.max(p1.lat, p2.lat), Math.max(p1.lng, p2.lng)], + ]); + }); + } + + populateDisplayControls(app, visibility, WebSocketTileLayer, + techData, redrawAllLayers, HeatMapTileLayer); + updateHeatMaps(heatMapData); + + // Only show the loading overlay if a design is loaded but shapes + // aren't ready yet. On browser reload (without server restart), + // shapes are already built so we skip the overlay. + if (hasDesign && !boundsData.shapes_ready) { + document.getElementById('loading-overlay').style.display = 'flex'; + } + } catch (err) { + console.error('Failed to load initial data from server:', err); + } +}); + +// ─── Keyboard Shortcuts ───────────────────────────────────────────────────── + +document.addEventListener('keydown', (e) => { + // Ignore shortcuts when typing in an input field + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + + if (e.key === 'f' && !e.ctrlKey && !e.metaKey && app.fitBounds) { + app.map.fitBounds(app.fitBounds); + } else if (e.key === 'z' && !e.shiftKey && !e.ctrlKey && app.map) { + app.map.zoomIn(); + } else if (e.key === 'Z' && e.shiftKey && !e.ctrlKey && app.map) { + app.map.zoomOut(); + } +}); diff --git a/src/web/src/menu-bar.js b/src/web/src/menu-bar.js new file mode 100644 index 00000000000..c850db88d29 --- /dev/null +++ b/src/web/src/menu-bar.js @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Creates a menu bar in #menu-bar and returns keyboard shortcut bindings. +export function createMenuBar(app) { + const menus = [ + { label: 'File', items: [ + { label: 'Open DB...', action: () => showPathDialog(app, 'Open DB', 'read_db'), + enabledWhen: () => !app.designScale }, + { label: 'Save DB...', action: () => showPathDialog(app, 'Save DB', 'write_db'), + enabledWhen: () => !!app.designScale }, + ]}, + { label: 'View', items: [ + { label: 'Fit', shortcut: 'F', action: () => { if (app.fitBounds) app.map.fitBounds(app.fitBounds); } }, + { label: 'Zoom In', shortcut: 'Z', action: () => app.map.zoomIn() }, + { label: 'Zoom Out', shortcut: 'Shift+Z', action: () => app.map.zoomOut() }, + { type: 'separator' }, + { label: 'Find...', shortcut: 'Ctrl+F', disabled: true }, + { label: 'Go to Position...', shortcut: 'Shift+G', disabled: true }, + ]}, + { label: 'Tools', items: [ + { label: 'Ruler', shortcut: 'K', disabled: true }, + ]}, + { label: 'Windows', items: [ + { label: 'Layout Viewer', action: () => app.focusComponent('LayoutViewer') }, + { label: 'Display Controls', action: () => app.focusComponent('DisplayControls') }, + { label: 'Inspector', action: () => app.focusComponent('Inspector') }, + { label: 'Tcl Console', action: () => app.focusComponent('TclConsole') }, + { label: 'Hierarchy Browser', action: () => app.focusComponent('Browser') }, + { label: 'Timing', action: () => app.focusComponent('TimingWidget') }, + { label: 'DRC Viewer', action: () => app.focusComponent('DRCWidget') }, + { label: 'Clock Tree', action: () => app.focusComponent('ClockWidget') }, + { label: 'Charts', action: () => app.focusComponent('ChartsWidget') }, + { label: 'Help', action: () => app.focusComponent('HelpWidget') }, + ]}, + { label: 'Help', items: [ + { label: 'Keyboard Shortcuts', action: () => app.focusComponent('HelpWidget') }, + ]}, + ]; + + const bar = document.getElementById('menu-bar'); + let openMenu = null; + + function closeAll() { + if (openMenu) { + openMenu.classList.remove('open'); + openMenu = null; + } + } + + for (const menu of menus) { + const label = document.createElement('div'); + label.className = 'menu-label'; + label.textContent = menu.label; + + const dropdown = document.createElement('div'); + dropdown.className = 'menu-dropdown'; + + for (const item of menu.items) { + if (item.type === 'separator') { + const sep = document.createElement('div'); + sep.className = 'menu-separator'; + dropdown.appendChild(sep); + continue; + } + + const row = document.createElement('div'); + row.className = 'menu-item'; + if (item.disabled) row.classList.add('disabled'); + // Track items with dynamic enabled state for refresh on open. + if (item.enabledWhen) row._enabledWhen = item.enabledWhen; + + const text = document.createElement('span'); + text.textContent = item.label; + row.appendChild(text); + + if (item.shortcut) { + const shortcut = document.createElement('span'); + shortcut.className = 'shortcut'; + shortcut.textContent = item.shortcut; + row.appendChild(shortcut); + } + + if (item.action) { + row.addEventListener('click', (e) => { + if (row.classList.contains('disabled')) return; + e.stopPropagation(); + closeAll(); + item.action(); + }); + } + + dropdown.appendChild(row); + } + + label.appendChild(dropdown); + + function refreshDynamicItems() { + for (const row of dropdown.querySelectorAll('.menu-item')) { + if (row._enabledWhen) { + row.classList.toggle('disabled', !row._enabledWhen()); + } + } + } + + label.addEventListener('click', (e) => { + e.stopPropagation(); + if (openMenu === label) { + closeAll(); + } else { + closeAll(); + refreshDynamicItems(); + label.classList.add('open'); + openMenu = label; + } + }); + + // Hover-open when another menu is already open + label.addEventListener('mouseenter', () => { + if (openMenu && openMenu !== label) { + closeAll(); + refreshDynamicItems(); + label.classList.add('open'); + openMenu = label; + } + }); + + bar.appendChild(label); + } + + document.addEventListener('click', closeAll); +} + +// ─── File Browser Dialog (Open/Save DB) ───────────────────────────────────── + +function showPathDialog(app, title, tclCmd) { + const isSave = tclCmd === 'write_db'; + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + + overlay.innerHTML = ` + `; + + document.body.appendChild(overlay); + + const breadcrumb = overlay.querySelector('.fb-breadcrumb'); + const fileList = overlay.querySelector('.fb-file-list'); + const pathInput = overlay.querySelector('.fb-path-input'); + const errorDiv = overlay.querySelector('.modal-error'); + const okBtn = overlay.querySelector('.ok'); + const cancelBtn = overlay.querySelector('.cancel'); + let currentPath = ''; + let selectedEntry = null; + + function close() { + overlay.remove(); + } + + function updateOkState() { + const val = pathInput.value.trim(); + okBtn.disabled = !val; + } + + function renderBreadcrumb(dirPath) { + breadcrumb.innerHTML = ''; + const parts = dirPath.split('/').filter(Boolean); + // Root + const rootSpan = document.createElement('span'); + rootSpan.className = 'fb-crumb'; + rootSpan.textContent = '/'; + rootSpan.addEventListener('click', () => navigate('/')); + breadcrumb.appendChild(rootSpan); + + let accumulated = ''; + for (const part of parts) { + accumulated += '/' + part; + const sep = document.createElement('span'); + sep.className = 'fb-crumb-sep'; + sep.textContent = ' / '; + breadcrumb.appendChild(sep); + + const crumb = document.createElement('span'); + crumb.className = 'fb-crumb'; + crumb.textContent = part; + const target = accumulated; + crumb.addEventListener('click', () => navigate(target)); + breadcrumb.appendChild(crumb); + } + } + + function renderEntries(entries) { + fileList.innerHTML = ''; + selectedEntry = null; + + if (!entries || entries.length === 0) { + const empty = document.createElement('div'); + empty.className = 'fb-empty'; + empty.textContent = '(empty directory)'; + fileList.appendChild(empty); + return; + } + + for (const entry of entries) { + const row = document.createElement('div'); + row.className = 'fb-entry'; + if (entry.is_dir) row.classList.add('fb-dir'); + + const icon = document.createElement('span'); + icon.className = 'fb-icon'; + icon.textContent = entry.is_dir ? '\u{1F4C1}' : '\u{1F4C4}'; + row.appendChild(icon); + + const name = document.createElement('span'); + name.className = 'fb-name'; + name.textContent = entry.name; + row.appendChild(name); + + if (!entry.is_dir && entry.size !== undefined) { + const size = document.createElement('span'); + size.className = 'fb-size'; + size.textContent = formatSize(entry.size); + row.appendChild(size); + } + + if (entry.is_dir) { + row.addEventListener('click', () => { + navigate(currentPath + '/' + entry.name); + }); + } else { + row.addEventListener('click', () => { + // Deselect previous + const prev = fileList.querySelector('.fb-selected'); + if (prev) prev.classList.remove('fb-selected'); + row.classList.add('fb-selected'); + selectedEntry = entry; + pathInput.value = currentPath + '/' + entry.name; + updateOkState(); + }); + row.addEventListener('dblclick', () => { + pathInput.value = currentPath + '/' + entry.name; + updateOkState(); + submit(); + }); + } + + fileList.appendChild(row); + } + } + + async function navigate(dirPath) { + errorDiv.style.display = 'none'; + fileList.innerHTML = '
Loading...
'; + + try { + const resp = await app.websocketManager.request({ + type: 'list_dir', + path: dirPath || '', + }); + currentPath = resp.path; + renderBreadcrumb(resp.path); + renderEntries(resp.entries); + // For save mode, keep the filename if user typed one + if (isSave) { + const existing = pathInput.value.trim(); + const basename = existing.split('/').pop(); + if (basename && !basename.includes('/')) { + pathInput.value = currentPath + '/' + basename; + } else { + pathInput.value = currentPath + '/'; + } + } else { + pathInput.value = ''; + selectedEntry = null; + } + updateOkState(); + } catch (err) { + fileList.innerHTML = ''; + errorDiv.textContent = err.message || String(err); + errorDiv.style.display = ''; + } + } + + async function submit() { + const path = pathInput.value.trim(); + if (!path) return; + + okBtn.disabled = true; + okBtn.textContent = 'Working...'; + errorDiv.style.display = 'none'; + + try { + const resp = await app.websocketManager.request({ + type: 'tcl_eval', + cmd: `${tclCmd} ${path}`, + }); + + if (resp.error) { + errorDiv.textContent = resp.output || resp.result || 'Command failed'; + errorDiv.style.display = ''; + okBtn.disabled = false; + okBtn.textContent = isSave ? 'Save' : 'Open'; + return; + } + + close(); + + if (tclCmd === 'read_db') { + // Reload the page so the full init flow runs against the + // newly loaded design (bounds, tech, display controls, etc.). + window.location.reload(); + return; + } + } catch (err) { + errorDiv.textContent = err.message || 'Request failed'; + errorDiv.style.display = ''; + okBtn.disabled = false; + okBtn.textContent = isSave ? 'Save' : 'Open'; + } + } + + pathInput.addEventListener('input', updateOkState); + pathInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const val = pathInput.value.trim(); + // If it looks like a directory path (ends with /), navigate there + if (val.endsWith('/')) { + navigate(val.replace(/\/+$/, '') || '/'); + } else { + submit(); + } + } + if (e.key === 'Escape') close(); + }); + okBtn.addEventListener('click', submit); + cancelBtn.addEventListener('click', close); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + + // Start browsing from server's cwd + navigate(''); +} + +function formatSize(bytes) { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'; +} diff --git a/src/web/src/request_handler.cpp b/src/web/src/request_handler.cpp new file mode 100644 index 00000000000..e35060a2e96 --- /dev/null +++ b/src/web/src/request_handler.cpp @@ -0,0 +1,1635 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include "request_handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "clock_tree_report.h" +#include "color.h" +#include "gui/descriptor_registry.h" +#include "gui/gui.h" +#include "gui/heatMap.h" +#include "hierarchy_report.h" +#include "json_builder.h" +#include "odb/db.h" +#include "odb/geom.h" +#include "tile_generator.h" +#include "timing_report.h" +#include "utl/Logger.h" + +namespace web { + +//------------------------------------------------------------------------------ +// ShapeCollector — a gui::Painter that collects rectangles from +// descriptor->highlight() calls for use in tile rendering. +//------------------------------------------------------------------------------ + +class ShapeCollector : public gui::Painter +{ + public: + ShapeCollector() : Painter(nullptr, odb::Rect(), 1.0) {} + + std::vector rects; + std::vector polys; + + void drawRect(const odb::Rect& rect, int, int) override + { + rects.push_back(rect); + } + void drawPolygon(const odb::Polygon& polygon) override + { + polys.push_back(polygon); + } + void drawOctagon(const odb::Oct& oct) override { polys.emplace_back(oct); } + + // No-ops + Color getPenColor() override { return {}; } + void setPen(odb::dbTechLayer*, bool) override {} + void setPen(const Color&, bool, int) override {} + void setPenWidth(int) override {} + void setBrush(odb::dbTechLayer*, int) override {} + void setBrush(const Color&, const Brush&) override {} + void setFont(const Font&) override {} + void saveState() override {} + void restoreState() override {} + void drawLine(const odb::Point&, const odb::Point&) override {} + void drawCircle(int, int, int) override {} + void drawX(int, int, int) override {} + void drawPolygon(const std::vector&) override {} + void drawString(int, int, Anchor, const std::string&, bool) override {} + odb::Rect stringBoundaries(int, int, Anchor, const std::string&) override + { + return {}; + } + void drawRuler(int, int, int, int, bool, const std::string&) override {} +}; + +//------------------------------------------------------------------------------ +// JSON utilities +//------------------------------------------------------------------------------ + +std::string extract_string(const std::string& json, const std::string& key) +{ + const std::string needle = "\"" + key + "\""; + auto pos = json.find(needle); + if (pos == std::string::npos) { + return {}; + } + pos = json.find(':', pos + needle.size()); + if (pos == std::string::npos) { + return {}; + } + auto quote_start = json.find('"', pos + 1); + if (quote_start == std::string::npos) { + return {}; + } + // Find closing quote, skipping escaped quotes + std::string result; + for (size_t i = quote_start + 1; i < json.size(); i++) { + if (json[i] == '\\' && i + 1 < json.size()) { + switch (json[i + 1]) { + case '"': + result += '"'; + break; + case '\\': + result += '\\'; + break; + case 'n': + result += '\n'; + break; + case 'r': + result += '\r'; + break; + case 't': + result += '\t'; + break; + default: + result += json[i + 1]; + break; + } + i++; // skip the escaped char + } else if (json[i] == '"') { + break; // closing quote + } else { + result += json[i]; + } + } + return result; +} + +std::set extract_string_array(const std::string& json, + const std::string& key) +{ + std::set result; + const std::string needle = "\"" + key + "\""; + auto pos = json.find(needle); + if (pos == std::string::npos) { + return result; + } + pos = json.find('[', pos + needle.size()); + if (pos == std::string::npos) { + return result; + } + auto end = json.find(']', pos); + if (end == std::string::npos) { + return result; + } + // Extract each quoted string between [ and ] + for (auto i = pos; i < end;) { + auto qs = json.find('"', i); + if (qs == std::string::npos || qs >= end) { + break; + } + auto qe = json.find('"', qs + 1); + if (qe == std::string::npos || qe >= end) { + break; + } + result.insert(json.substr(qs + 1, qe - qs - 1)); + i = qe + 1; + } + return result; +} + +int extract_int(const std::string& json, const std::string& key) +{ + const std::string needle = "\"" + key + "\""; + auto pos = json.find(needle); + if (pos == std::string::npos) { + return 0; + } + pos = json.find(':', pos + needle.size()); + if (pos == std::string::npos) { + return 0; + } + // Skip whitespace after colon + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) { + pos++; + } + try { + return std::stoi(json.substr(pos)); + } catch (...) { + return 0; + } +} + +int extract_int_or(const std::string& json, + const std::string& key, + const int default_val) +{ + const std::string needle = "\"" + key + "\""; + if (json.find(needle) == std::string::npos) { + return default_val; + } + return extract_int(json, key); +} + +float extract_float_or(const std::string& json, + const std::string& key, + float default_val) +{ + const std::string needle = "\"" + key + "\""; + auto pos = json.find(needle); + if (pos == std::string::npos) { + return default_val; + } + pos = json.find(':', pos + needle.size()); + if (pos == std::string::npos) { + return default_val; + } + pos++; + while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t')) { + pos++; + } + try { + return std::stof(json.substr(pos)); + } catch (...) { + return default_val; + } +} + +// Store a Selected in the clickables vector and return its index. +static int storeSelectable(std::vector& selectables, + const gui::Selected& sel) +{ + int id = static_cast(selectables.size()); + selectables.push_back(sel); + return id; +} + +// Emit a bbox as a JSON array field: "key": [xMin, yMin, xMax, yMax] +static void writeBBox(JsonBuilder& builder, const char* key, const odb::Rect& r) +{ + builder.beginArray(key); + builder.value(r.xMin()); + builder.value(r.yMin()); + builder.value(r.xMax()); + builder.value(r.yMax()); + builder.endArray(); +} + +static void serializeAnyValue(JsonBuilder& builder, + const char* field_name, + const std::any& value, + std::vector& selectables, + bool short_name = false) +{ + if (auto* sel = std::any_cast(&value)) { + if (*sel) { + std::string name = short_name ? sel->getShortName() : sel->getName(); + int id = storeSelectable(selectables, *sel); + builder.field(field_name, name); + std::string id_key = std::string(field_name) + "_select_id"; + builder.field(id_key, id); + return; + } + } + std::string str = gui::Descriptor::Property::toString(value); + builder.field(field_name, str); +} + +static void serializeProperty(JsonBuilder& builder, + const gui::Descriptor::Property& prop, + std::vector& selectables) +{ + builder.beginObject(); + builder.field("name", prop.name); + + if (auto* plist = std::any_cast(&prop.value)) { + builder.beginArray("children"); + for (const auto& [key, val] : *plist) { + builder.beginObject(); + serializeAnyValue(builder, "name", key, selectables, /*short_name=*/true); + serializeAnyValue(builder, "value", val, selectables); + builder.endObject(); + } + builder.endArray(); + } else if (auto* sel_set = std::any_cast(&prop.value)) { + builder.beginArray("children"); + for (const auto& sel : *sel_set) { + builder.beginObject(); + int id = storeSelectable(selectables, sel); + builder.field("name", sel.getName()); + builder.field("name_select_id", id); + builder.endObject(); + } + builder.endArray(); + } else if (auto* sel = std::any_cast(&prop.value)) { + if (*sel) { + int id = storeSelectable(selectables, *sel); + builder.field("value", sel->getName()); + builder.field("value_select_id", id); + } + } else { + std::string val_str = prop.toString(); + builder.field("value", val_str); + } + + builder.endObject(); +} + +static void collectHighlightShapes(const gui::Selected& sel, + std::vector& rects, + std::vector& polys) +{ + rects.clear(); + polys.clear(); + if (!sel) { + return; + } + ShapeCollector collector; + sel.highlight(collector); + rects = std::move(collector.rects); + polys = std::move(collector.polys); +} + +static void writeInspectPayload(JsonBuilder& builder, + const gui::Selected& sel, + std::vector& new_selectables, + bool can_navigate_back) +{ + builder.field("can_navigate_back", can_navigate_back ? 1 : 0); + if (!sel) { + builder.field("error", "invalid select_id"); + return; + } + + auto props = sel.getProperties(); + builder.field("name", sel.getName()); + builder.field("type", sel.getTypeName()); + builder.beginArray("properties"); + for (const auto& prop : props) { + serializeProperty(builder, prop, new_selectables); + } + builder.endArray(); + + odb::Rect bbox; + if (sel.getBBox(bbox)) { + writeBBox(builder, "bbox", bbox); + } + + if (sel.isNet()) { + auto* net = std::any_cast(sel.getObject()); + if (net && !net->getGuides().empty()) { + builder.field("has_guides", 1); + } + } +} + +// Serialize a TimingNode to JSON. +static void serializeTimingNode(JsonBuilder& builder, const TimingNode& n) +{ + builder.beginObject(); + builder.field("pin", n.pin_name); + builder.field("fanout", n.fanout); + builder.field("rise", n.is_rising); + builder.field("clk", n.is_clock); + builder.field("time", n.time); + builder.field("delay", n.delay); + builder.field("slew", n.slew); + builder.field("load", n.load); + builder.endObject(); +} + +static double extract_double_value(const std::string& json) +{ + return extract_float_or(json, "value", 0.0F); +} + +static bool extract_bool_value(const std::string& json) +{ + if (json.find("\"value\":true") != std::string::npos) { + return true; + } + if (json.find("\"value\":false") != std::string::npos) { + return false; + } + return extract_int_or(json, "value", 0) != 0; +} + +static void writeColorArray(JsonBuilder& builder, + const char* key, + const gui::Painter::Color& color) +{ + builder.beginArray(key); + builder.value(color.r); + builder.value(color.g); + builder.value(color.b); + builder.value(color.a); + builder.endArray(); +} + +static void serializeHeatMapOption( + JsonBuilder& builder, + const gui::HeatMapDataSource::MapSetting& option) +{ + builder.beginObject(); + if (std::holds_alternative( + option)) { + const auto& setting + = std::get(option); + builder.field("type", "bool"); + builder.field("name", setting.name); + builder.field("label", setting.label); + builder.field("value", setting.getter()); + } else { + const auto& setting + = std::get(option); + builder.field("type", "choice"); + builder.field("name", setting.name); + builder.field("label", setting.label); + builder.field("value", setting.getter()); + builder.beginArray("choices"); + for (const auto& choice : setting.choices()) { + builder.value(choice); + } + builder.endArray(); + } + builder.endObject(); +} + +static void serializeHeatMap(JsonBuilder& builder, + gui::HeatMapDataSource& source, + const bool active) +{ + if (active) { + source.ensureMap(); + } + const bool populated = source.isPopulated(); + const bool has_data = source.hasData(); + + builder.beginObject(); + builder.field("name", source.getShortName()); + builder.field("title", source.getName()); + builder.field("active", active); + builder.field("settings_group", source.getSettingsGroupName()); + builder.field("has_data", has_data); + builder.field("can_adjust_grid", source.canAdjustGrid()); + builder.field("show_numbers", source.getShowNumbers()); + builder.field("show_legend", source.getShowLegend()); + builder.field("supports_numbers", true); + builder.field("units", source.getValueUnits()); + builder.field("display_range_increment", source.getDisplayRangeIncrement()); + builder.field("display_min", + source.convertPercentToValue(source.getDisplayRangeMin())); + builder.field("display_max", + source.convertPercentToValue(source.getDisplayRangeMax())); + builder.field( + "display_min_limit", + source.convertPercentToValue(source.getDisplayRangeMinimumValue())); + builder.field( + "display_max_limit", + source.convertPercentToValue(source.getDisplayRangeMaximumValue())); + builder.field("draw_below_min", source.getDrawBelowRangeMin()); + builder.field("draw_above_max", source.getDrawAboveRangeMax()); + builder.field("log_scale", source.getLogScale()); + builder.field("reverse_log", source.getReverseLogScale()); + builder.field("grid_x", source.getGridXSize()); + builder.field("grid_y", source.getGridYSize()); + builder.field("grid_min", source.getGridSizeMinimumValue()); + builder.field("grid_max", source.getGridSizeMaximumValue()); + builder.field("alpha", source.getColorAlpha()); + builder.field("alpha_min", source.getColorAlphaMinimum()); + builder.field("alpha_max", source.getColorAlphaMaximum()); + writeBBox(builder, "bounds", source.getBounds()); + + builder.beginArray("options"); + for (const auto& option : source.getMapSettings()) { + serializeHeatMapOption(builder, option); + } + builder.endArray(); + + builder.beginArray("legend"); + if (populated) { + const auto& generator = source.getColorGenerator(); + const int color_count = generator.getColorCount(); + for (const auto& [color_index, color_value] : source.getLegendValues()) { + builder.beginObject(); + builder.field("value", source.formatValue(color_value, true)); + const gui::Painter::Color color + = generator.getColor(100.0 * color_index / std::max(1, color_count), + source.getColorAlpha()); + writeColorArray(builder, "color", color); + builder.endObject(); + } + } + builder.endArray(); + + builder.endObject(); +} + +static std::string buildHeatMapsPayloadLocked(SessionState& state) +{ + JsonBuilder builder; + builder.beginObject(); + builder.field("active", state.active_heatmap); + builder.beginArray("heatmaps"); + for (const auto& [name, source] : state.heatmaps) { + serializeHeatMap(builder, *source, name == state.active_heatmap); + } + builder.endArray(); + builder.endObject(); + return builder.str(); +} + +//------------------------------------------------------------------------------ +// dispatch_request — handles BOUNDS, TECH, TILE +//------------------------------------------------------------------------------ + +WebSocketResponse dispatch_request( + const WebSocketRequest& req, + const TileGenerator& gen, + const std::vector& highlight_rects, + const std::vector& highlight_polys, + const std::vector& colored_rects, + const std::vector& flight_lines, + const std::map* module_colors, + const std::set* focus_net_ids, + const std::set* route_guide_net_ids) +{ + WebSocketResponse resp; + resp.id = req.id; + + switch (req.type) { + case WebSocketRequest::BOUNDS: { + resp.type = 0; + const odb::Rect bounds = gen.getBounds(); + JsonBuilder builder; + builder.beginObject(); + builder.beginArray("bounds"); + builder.beginArray(); + builder.value(bounds.yMin()); + builder.value(bounds.xMin()); + builder.endArray(); + builder.beginArray(); + builder.value(bounds.yMax()); + builder.value(bounds.xMax()); + builder.endArray(); + builder.endArray(); + builder.field("shapes_ready", gen.shapesReady()); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + break; + } + case WebSocketRequest::TECH: { + resp.type = 0; + JsonBuilder builder; + builder.beginObject(); + builder.beginArray("layers"); + for (const auto& name : gen.getLayers()) { + builder.value(name); + } + builder.endArray(); + builder.beginArray("sites"); + for (const auto& name : gen.getSites()) { + builder.value(name); + } + builder.endArray(); + builder.field("has_liberty", gen.hasSta()); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + break; + } + case WebSocketRequest::TILE: { + resp.type = 1; + resp.payload = gen.generateTile(req.layer, + req.z, + req.x, + req.y, + req.vis, + highlight_rects, + highlight_polys, + colored_rects, + flight_lines, + module_colors, + focus_net_ids, + route_guide_net_ids); + break; + } + default: { + resp.type = 2; + const std::string err = "Unknown request type"; + resp.payload.assign(err.begin(), err.end()); + break; + } + } + + return resp; +} + +//------------------------------------------------------------------------------ +// SelectHandler +//------------------------------------------------------------------------------ + +SelectHandler::SelectHandler(std::shared_ptr gen, + std::shared_ptr tcl_eval) + : gen_(std::move(gen)), tcl_eval_(std::move(tcl_eval)) +{ +} + +WebSocketResponse SelectHandler::handleSelect(const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + try { + auto results = gen_->selectAt(req.select_x, + req.select_y, + req.select_zoom, + req.vis, + req.visible_layers); + + // STA's highlight() and getProperties() are not thread-safe; + // serialize with other STA callers (timing, clock tree, tcl eval). + std::lock_guard sta_lock(tcl_eval_->mutex); + + // Build JSON response with selection and properties + resp.type = 0; + JsonBuilder builder; + builder.beginObject(); + builder.beginArray("selected"); + for (const auto& r : results) { + builder.beginObject(); + builder.field("name", r.name); + builder.field("type", r.type_name); + writeBBox(builder, "bbox", r.bbox); + builder.endObject(); + } + builder.endArray(); + + // Pick which result to inspect, cycling through overlapping objects. + // If the currently inspected object is in the results, select the next one. + std::vector new_selectables; + auto* registry = gui::DescriptorRegistry::instance(); + gui::Selected inspected_sel; + if (!results.empty()) { + int pick = 0; + if (results.size() > 1) { + gui::Selected current; + { + std::lock_guard lock(state.selection_mutex); + current = state.current_inspected; + } + if (current) { + for (int i = 0; i < static_cast(results.size()); ++i) { + gui::Selected candidate = registry->makeSelected(results[i].object); + if (candidate == current) { + pick = (i + 1) % static_cast(results.size()); + break; + } + } + } + } + inspected_sel = registry->makeSelected(results[pick].object); + writeInspectPayload( + builder, inspected_sel, new_selectables, /*can_navigate_back=*/false); + } else { + builder.field("can_navigate_back", 0); + } + { + std::lock_guard lock(state.selectables_mutex); + state.selectables = std::move(new_selectables); + } + { + std::lock_guard lock(state.selection_mutex); + state.hover_rects.clear(); + state.timing_rects.clear(); + state.timing_lines.clear(); + collectHighlightShapes( + inspected_sel, state.highlight_rects, state.highlight_polys); + state.current_inspected = inspected_sel; + state.navigation_history.clear(); + } + + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse SelectHandler::handleInspect(const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + try { + gui::Selected sel; + { + std::lock_guard lock(state.selectables_mutex); + if (req.select_id >= 0 + && req.select_id < static_cast(state.selectables.size())) { + sel = state.selectables[req.select_id]; + } + } + + // STA's highlight() and getProperties() are not thread-safe; + // serialize with other STA callers (timing, clock tree, tcl eval). + std::lock_guard sta_lock(tcl_eval_->mutex); + + bool can_navigate_back = false; + { + std::lock_guard lock(state.selection_mutex); + state.hover_rects.clear(); + state.timing_rects.clear(); + state.timing_lines.clear(); + collectHighlightShapes(sel, state.highlight_rects, state.highlight_polys); + if (sel) { + if (state.current_inspected && state.current_inspected != sel) { + state.navigation_history.push_back(state.current_inspected); + } + state.current_inspected = sel; + } + can_navigate_back = !state.navigation_history.empty(); + } + + resp.type = 0; + JsonBuilder builder; + builder.beginObject(); + std::vector new_selectables; + writeInspectPayload(builder, sel, new_selectables, can_navigate_back); + { + std::lock_guard lock(state.selectables_mutex); + state.selectables = std::move(new_selectables); + } + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse SelectHandler::handleInspectBack(const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + try { + gui::Selected sel; + bool can_navigate_back = false; + + std::lock_guard sta_lock(tcl_eval_->mutex); + { + std::lock_guard lock(state.selection_mutex); + state.hover_rects.clear(); + state.timing_rects.clear(); + state.timing_lines.clear(); + + if (!state.navigation_history.empty()) { + sel = state.navigation_history.back(); + state.navigation_history.pop_back(); + state.current_inspected = sel; + } else { + sel = state.current_inspected; + } + + collectHighlightShapes(sel, state.highlight_rects, state.highlight_polys); + can_navigate_back = !state.navigation_history.empty(); + } + + resp.type = 0; + JsonBuilder builder; + builder.beginObject(); + std::vector new_selectables; + writeInspectPayload(builder, sel, new_selectables, can_navigate_back); + { + std::lock_guard lock(state.selectables_mutex); + state.selectables = std::move(new_selectables); + } + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse SelectHandler::handleHover(const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + try { + int count = 0; + std::vector hover_rects; + { + std::lock_guard lock(state.selection_mutex); + state.hover_rects.clear(); + + if (req.select_id >= 0) { + gui::Selected sel; + { + std::lock_guard slock(state.selectables_mutex); + if (req.select_id < static_cast(state.selectables.size())) { + sel = state.selectables[req.select_id]; + } + } + if (sel) { + ShapeCollector collector; + sel.highlight(collector); + if (!collector.rects.empty()) { + state.hover_rects = std::move(collector.rects); + } else { + odb::Rect bbox; + if (sel.getBBox(bbox)) { + state.hover_rects.push_back(bbox); + } + } + count = static_cast(state.hover_rects.size()); + hover_rects = state.hover_rects; + } + } + // select_id < 0 just clears hover_rects (mouseleave) + } + + resp.type = 0; + JsonBuilder builder; + builder.beginObject(); + builder.field("ok", 1); + builder.field("count", count); + builder.beginArray("rects"); + for (const auto& rect : hover_rects) { + builder.beginArray(); + builder.value(rect.xMin()); + builder.value(rect.yMin()); + builder.value(rect.xMax()); + builder.value(rect.yMax()); + builder.endArray(); + } + builder.endArray(); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse SelectHandler::handleSetFocusNets(const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(state.focus_nets_mutex); + if (req.focus_action == "clear") { + state.focus_net_ids.clear(); + } else { + odb::dbBlock* block = gen_->getBlock(); + odb::dbNet* net + = block ? block->findNet(req.focus_net_name.c_str()) : nullptr; + if (net) { + if (req.focus_action == "add") { + state.focus_net_ids.insert(net->getId()); + } else if (req.focus_action == "remove") { + state.focus_net_ids.erase(net->getId()); + } + } + } + const int count = static_cast(state.focus_net_ids.size()); + const std::string json + = R"({"ok":1,"count":)" + std::to_string(count) + "}"; + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse SelectHandler::handleSetRouteGuides( + const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(state.route_guides_mutex); + if (req.route_guide_action == "clear") { + state.route_guide_net_ids.clear(); + } else { + odb::dbBlock* block = gen_->getBlock(); + odb::dbNet* net + = block ? block->findNet(req.route_guide_net_name.c_str()) : nullptr; + if (net) { + if (req.route_guide_action == "add") { + state.route_guide_net_ids.insert(net->getId()); + } else if (req.route_guide_action == "remove") { + state.route_guide_net_ids.erase(net->getId()); + } + } + } + const int count = static_cast(state.route_guide_net_ids.size()); + const std::string json + = R"({"ok":1,"count":)" + std::to_string(count) + "}"; + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +//------------------------------------------------------------------------------ +// TclHandler +//------------------------------------------------------------------------------ + +TclHandler::TclHandler(std::shared_ptr tcl_eval) + : tcl_eval_(std::move(tcl_eval)) +{ +} + +WebSocketResponse TclHandler::handleTclEval(const WebSocketRequest& req) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + auto result = tcl_eval_->eval(req.tcl_cmd); + JsonBuilder builder; + builder.beginObject(); + builder.field("output", result.output); + builder.field("result", result.result); + builder.field("is_error", result.is_error); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +//------------------------------------------------------------------------------ +// TimingHandler +//------------------------------------------------------------------------------ + +TimingHandler::TimingHandler(std::shared_ptr gen, + std::shared_ptr timing_report, + std::shared_ptr tcl_eval) + : gen_(std::move(gen)), + timing_report_(std::move(timing_report)), + tcl_eval_(std::move(tcl_eval)) +{ +} + +WebSocketResponse TimingHandler::handleTimingReport(const WebSocketRequest& req) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(tcl_eval_->mutex); + auto paths = timing_report_->getReport(req.timing_is_setup, + req.timing_max_paths, + req.timing_slack_min, + req.timing_slack_max); + JsonBuilder builder; + builder.beginObject(); + builder.beginArray("paths"); + for (const auto& p : paths) { + builder.beginObject(); + builder.field("start_clk", p.start_clk); + builder.field("end_clk", p.end_clk); + builder.field("required", p.required); + builder.field("arrival", p.arrival); + builder.field("slack", p.slack); + builder.field("skew", p.skew); + builder.field("path_delay", p.path_delay); + builder.field("logic_depth", p.logic_depth); + builder.field("fanout", p.fanout); + builder.field("start_pin", p.start_pin); + builder.field("end_pin", p.end_pin); + builder.beginArray("data_nodes"); + for (const auto& n : p.data_nodes) { + serializeTimingNode(builder, n); + } + builder.endArray(); + builder.beginArray("capture_nodes"); + for (const auto& n : p.capture_nodes) { + serializeTimingNode(builder, n); + } + builder.endArray(); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse TimingHandler::handleTimingHighlight( + const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::vector new_rects; + std::vector new_lines; + + if (req.timing_path_index >= 0) { + std::lock_guard sta_lock(tcl_eval_->mutex); + auto paths = timing_report_->getReport(req.timing_highlight_setup); + if (req.timing_path_index < static_cast(paths.size())) { + odb::dbBlock* block = gen_->getBlock(); + collectTimingPathShapes( + block, paths[req.timing_path_index], new_rects, new_lines); + + if (!req.timing_pin_name.empty()) { + static const Color kStageColor{.r = 255, .g = 255, .b = 0, .a = 180}; + auto [iterm, bterm] = resolvePin(block, req.timing_pin_name); + + odb::dbNet* net = nullptr; + if (iterm) { + net = iterm->getNet(); + } else if (bterm) { + net = bterm->getNet(); + } + + if (net) { + collectNetShapes(net, + iterm, + bterm, + nullptr, + nullptr, + kStageColor, + new_rects, + new_lines); + } + } + } + } + + debugPrint(tcl_eval_->logger, + utl::WEB, + "timing", + 1, + "TIMING_HIGHLIGHT: {} rects, {} lines, pin_name='{}'", + new_rects.size(), + new_lines.size(), + req.timing_pin_name); + + { + std::lock_guard lock(state.selection_mutex); + state.timing_rects = std::move(new_rects); + state.timing_lines = std::move(new_lines); + state.highlight_rects.clear(); + state.highlight_polys.clear(); + } + + const std::string json = "{\"ok\": true}"; + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse TimingHandler::handleSlackHistogram( + const WebSocketRequest& req) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(tcl_eval_->mutex); + auto histogram = timing_report_->getSlackHistogram( + req.histogram_is_setup, req.histogram_path_group, req.histogram_clock); + JsonBuilder builder; + builder.beginObject(); + builder.beginArray("bins"); + for (const auto& bin : histogram.bins) { + builder.beginObject(); + builder.field("lower", bin.lower); + builder.field("upper", bin.upper); + builder.field("count", bin.count); + builder.field("negative", bin.is_negative); + builder.endObject(); + } + builder.endArray(); + builder.field("unconstrained_count", histogram.unconstrained_count); + builder.field("total_endpoints", histogram.total_endpoints); + builder.field("time_unit", histogram.time_unit); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse TimingHandler::handleChartFilters(const WebSocketRequest& req) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(tcl_eval_->mutex); + auto filters = timing_report_->getChartFilters(); + JsonBuilder builder; + builder.beginObject(); + builder.beginArray("path_groups"); + for (const auto& name : filters.path_groups) { + builder.value(name); + } + builder.endArray(); + builder.beginArray("clocks"); + for (const auto& name : filters.clocks) { + builder.value(name); + } + builder.endArray(); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +//------------------------------------------------------------------------------ +// ClockTreeHandler +//------------------------------------------------------------------------------ + +ClockTreeHandler::ClockTreeHandler( + std::shared_ptr gen, + std::shared_ptr clock_report, + std::shared_ptr tcl_eval) + : gen_(std::move(gen)), + clock_report_(std::move(clock_report)), + tcl_eval_(std::move(tcl_eval)) +{ +} + +WebSocketResponse ClockTreeHandler::handleClockTree(const WebSocketRequest& req) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(tcl_eval_->mutex); + auto clocks = clock_report_->getReport(); + JsonBuilder builder; + builder.beginObject(); + builder.beginArray("clocks"); + for (const auto& clk : clocks) { + builder.beginObject(); + builder.field("name", clk.clock_name); + builder.field("min_arrival", clk.min_arrival); + builder.field("max_arrival", clk.max_arrival); + builder.field("time_unit", clk.time_unit); + builder.beginArray("nodes"); + for (const auto& n : clk.nodes) { + builder.beginObject(); + builder.field("id", n.id); + builder.field("parent_id", n.parent_id); + builder.field("name", n.name); + builder.field("pin_name", n.pin_name); + builder.field("type", ClockTreeNode::typeToString(n.type)); + builder.field("arrival", n.arrival); + builder.field("delay", n.delay); + builder.field("fanout", n.fanout); + builder.field("level", n.level); + builder.field("dbu_x", n.dbu_x); + builder.field("dbu_y", n.dbu_y); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse ClockTreeHandler::handleClockTreeHighlight( + const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(state.selection_mutex); + state.highlight_rects.clear(); + state.highlight_polys.clear(); + state.timing_rects.clear(); + state.timing_lines.clear(); + + if (!req.clock_tree_inst_name.empty()) { + odb::dbBlock* block = gen_->getBlock(); + if (block) { + odb::dbInst* inst = block->findInst(req.clock_tree_inst_name.c_str()); + if (inst) { + state.highlight_rects.push_back(inst->getBBox()->getBox()); + } + } + } + + const std::string json = "{\"ok\": true}"; + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +//------------------------------------------------------------------------------ +// TileHandler +//------------------------------------------------------------------------------ + +TileHandler::TileHandler(std::shared_ptr gen) + : gen_(std::move(gen)) +{ +} + +void TileHandler::initializeHeatMaps(SessionState& state) +{ + std::lock_guard lock(state.heatmap_mutex); + state.heatmaps.clear(); + for (const auto& source_handle : gui::getRegisteredHeatMapSources()) { + auto source = source_handle->createInstance(); + source->setBlock(gen_->getBlock()); + state.heatmaps[source_handle->getShortName()] = std::move(source); + } +} + +WebSocketResponse TileHandler::handleTile(const WebSocketRequest& req, + SessionState& state) +{ + // Snapshot current highlight state + std::vector rects; + std::vector polys; + std::vector colored; + std::vector lines; + { + std::lock_guard lock(state.selection_mutex); + rects = state.highlight_rects; + rects.insert( + rects.end(), state.hover_rects.begin(), state.hover_rects.end()); + polys = state.highlight_polys; + colored = state.timing_rects; + lines = state.timing_lines; + } + + // Snapshot module colors for _modules layer + std::map mod_colors; + { + std::lock_guard lock(state.module_colors_mutex); + mod_colors = state.module_colors; + } + const std::map* mod_ptr + = mod_colors.empty() ? nullptr : &mod_colors; + + // Snapshot focus nets + std::set focus_nets; + { + std::lock_guard lock(state.focus_nets_mutex); + focus_nets = state.focus_net_ids; + } + const std::set* focus_ptr + = focus_nets.empty() ? nullptr : &focus_nets; + + // Snapshot route guide nets + std::set route_guides; + { + std::lock_guard lock(state.route_guides_mutex); + route_guides = state.route_guide_net_ids; + } + const std::set* route_guide_ptr + = route_guides.empty() ? nullptr : &route_guides; + + return dispatch_request(req, + *gen_, + rects, + polys, + colored, + lines, + mod_ptr, + focus_ptr, + route_guide_ptr); +} + +WebSocketResponse TileHandler::handleModuleHierarchy( + const WebSocketRequest& req) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + odb::dbBlock* block = gen_->getBlock(); + HierarchyReport report(block, gen_->getSta()); + auto result = report.getReport(); + + JsonBuilder builder; + builder.beginObject(); + builder.beginArray("nodes"); + for (const auto& n : result.nodes) { + builder.beginObject(); + builder.field("id", n.id); + builder.field("parent_id", n.parent_id); + builder.field("inst_name", n.inst_name); + builder.field("module_name", n.module_name); + builder.field("insts", n.insts); + builder.field("macros", n.macros); + builder.field("modules", n.modules); + builder.field("area", n.area); + builder.field("local_insts", n.local_insts); + builder.field("local_macros", n.local_macros); + builder.field("local_modules", n.local_modules); + if (n.node_kind != HierarchyNodeKind::MODULE) { + builder.field("node_kind", static_cast(n.node_kind)); + } + if (n.node_kind == HierarchyNodeKind::MODULE) { + builder.field("odb_id", static_cast(n.odb_id)); + } + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse TileHandler::handleSetModuleColors( + const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + + // Parse compact format: "id:r,g,b,a;id:r,g,b,a;..." + std::map colors; + const std::string data = extract_string(req.vis.raw_json_, "colors"); + if (!data.empty()) { + size_t pos = 0; + while (pos < data.size()) { + size_t colon = data.find(':', pos); + if (colon == std::string::npos) { + break; + } + const uint32_t mod_id + = static_cast(std::stoul(data.substr(pos, colon - pos))); + pos = colon + 1; + + auto next_num = [&]() -> int { + size_t end = data.find_first_of(",;", pos); + if (end == std::string::npos) { + end = data.size(); + } + const int val = std::stoi(data.substr(pos, end - pos)); + pos = end + 1; + return val; + }; + + const uint8_t r = static_cast(next_num()); + const uint8_t g = static_cast(next_num()); + const uint8_t b = static_cast(next_num()); + const uint8_t a = static_cast(next_num()); + colors[mod_id] = Color{.r = r, .g = g, .b = b, .a = a}; + } + } + + const int count = static_cast(colors.size()); + { + std::lock_guard lock(state.module_colors_mutex); + state.module_colors = std::move(colors); + } + + const std::string ok = R"({"ok":1,"count":)" + std::to_string(count) + "}"; + resp.payload.assign(ok.begin(), ok.end()); + return resp; +} + +WebSocketResponse TileHandler::handleHeatMaps(const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + const std::string json = buildHeatMapsPayloadLocked(state); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse TileHandler::handleSetActiveHeatMap( + const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(state.heatmap_mutex); + if (!state.active_heatmap.empty()) { + auto current = state.heatmaps.find(state.active_heatmap); + if (current != state.heatmaps.end()) { + current->second->onHide(); + } + } + + state.active_heatmap.clear(); + if (!req.heatmap_name.empty()) { + auto next = state.heatmaps.find(req.heatmap_name); + if (next == state.heatmaps.end()) { + throw std::runtime_error("invalid heat map"); + } + state.active_heatmap = req.heatmap_name; + next->second->onShow(); + } + + const std::string json = buildHeatMapsPayloadLocked(state); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse TileHandler::handleSetHeatMap(const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + try { + std::lock_guard lock(state.heatmap_mutex); + auto source_itr = state.heatmaps.find(req.heatmap_name); + if (source_itr == state.heatmaps.end()) { + throw std::runtime_error("invalid heat map"); + } + + auto& source = *source_itr->second; + if (req.heatmap_option == "rebuild") { + source.destroyMap(); + source.ensureMap(); + } else { + auto settings = source.getSettings(); + auto setting_itr = settings.find(req.heatmap_option); + if (setting_itr == settings.end()) { + throw std::runtime_error("invalid heat map option"); + } + + const auto& current_value = setting_itr->second; + if (std::holds_alternative(current_value)) { + settings[req.heatmap_option] = extract_bool_value(req.raw_json); + } else if (std::holds_alternative(current_value)) { + settings[req.heatmap_option] = extract_int(req.raw_json, "value"); + } else if (std::holds_alternative(current_value)) { + settings[req.heatmap_option] = extract_double_value(req.raw_json); + } else { + settings[req.heatmap_option] = req.heatmap_string_value; + } + source.setSettings(settings); + } + + const std::string json = buildHeatMapsPayloadLocked(state); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse TileHandler::handleHeatMapTile(const WebSocketRequest& req, + SessionState& state) +{ + WebSocketResponse resp; + resp.id = req.id; + resp.type = 1; + try { + std::shared_ptr source; + { + std::lock_guard lock(state.heatmap_mutex); + const std::string name + = req.heatmap_name.empty() ? state.active_heatmap : req.heatmap_name; + auto source_itr = state.heatmaps.find(name); + if (source_itr == state.heatmaps.end()) { + throw std::runtime_error("invalid heat map"); + } + source = source_itr->second; + } + resp.payload = gen_->generateHeatMapTile(*source, req.z, req.x, req.y); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("server error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +WebSocketResponse handleListDir(const WebSocketRequest& req) +{ + namespace fs = std::filesystem; + + WebSocketResponse resp; + resp.id = req.id; + resp.type = 0; + + try { + fs::path dir_path + = req.dir_path.empty() ? fs::current_path() : fs::path(req.dir_path); + dir_path = fs::canonical(dir_path); + + struct Entry + { + std::string name; + bool is_dir; + std::uintmax_t size; + }; + std::vector entries; + + for (const auto& entry : fs::directory_iterator( + dir_path, fs::directory_options::skip_permission_denied)) { + const auto& name = entry.path().filename().string(); + // Skip hidden files/directories. + if (!name.empty() && name[0] == '.') { + continue; + } + bool is_dir = entry.is_directory(); + std::uintmax_t size = 0; + if (!is_dir) { + std::error_code ec; + size = entry.file_size(ec); + if (ec) { + size = 0; + } + } + entries.push_back({name, is_dir, size}); + } + + // Sort: directories first, then alphabetical within each group. + std::ranges::sort(entries, [](const Entry& a, const Entry& b) { + if (a.is_dir != b.is_dir) { + return a.is_dir > b.is_dir; + } + return a.name < b.name; + }); + + JsonBuilder builder; + builder.beginObject(); + builder.field("path", dir_path.string()); + builder.field("parent", dir_path.parent_path().string()); + builder.beginArray("entries"); + for (const auto& entry : entries) { + builder.beginObject(); + builder.field("name", entry.name); + builder.field("is_dir", entry.is_dir); + if (!entry.is_dir) { + builder.field("size", static_cast(entry.size)); + } + builder.endObject(); + } + builder.endArray(); + builder.endObject(); + + const std::string& json = builder.str(); + resp.payload.assign(json.begin(), json.end()); + } catch (const std::exception& e) { + resp.type = 2; + std::string err = std::string("list_dir error: ") + e.what(); + resp.payload.assign(err.begin(), err.end()); + } + return resp; +} + +} // namespace web diff --git a/src/web/src/request_handler.h b/src/web/src/request_handler.h new file mode 100644 index 00000000000..e6f2445cde1 --- /dev/null +++ b/src/web/src/request_handler.h @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "color.h" +#include "gui/gui.h" +#include "odb/db.h" +#include "odb/geom.h" +#include "tcl.h" +#include "tile_generator.h" +#include "utl/Logger.h" + +namespace web { + +class TimingReport; +class ClockTreeReport; + +// Thread-safe Tcl command evaluation with output capture. +struct TclEvaluator +{ + Tcl_Interp* interp; + utl::Logger* logger; + std::mutex mutex; + + struct Result + { + std::string output; + std::string result; + bool is_error; + }; + + TclEvaluator(Tcl_Interp* interp, utl::Logger* logger) + : interp(interp), logger(logger) + { + } + + Result eval(const std::string& cmd) + { + std::lock_guard lock(mutex); + logger->redirectStringBegin(); + const int rc = Tcl_Eval(interp, cmd.c_str()); + Result r; + r.output = logger->redirectStringEnd(); + r.result = Tcl_GetStringResult(interp); + r.is_error = (rc != TCL_OK); + return r; + } +}; + +struct WebSocketRequest +{ + enum Type + { + TILE, + BOUNDS, + TECH, + SELECT, + INSPECT, + INSPECT_BACK, + HOVER, + TCL_EVAL, + TIMING_REPORT, + TIMING_HIGHLIGHT, + CLOCK_TREE, + CLOCK_TREE_HIGHLIGHT, + SLACK_HISTOGRAM, + CHART_FILTERS, + MODULE_HIERARCHY, + SET_MODULE_COLORS, + SET_FOCUS_NETS, + SET_ROUTE_GUIDES, + HEATMAPS, + SET_ACTIVE_HEATMAP, + SET_HEATMAP, + HEATMAP_TILE, + LIST_DIR, + UNKNOWN + }; + + uint32_t id = 0; + Type type = UNKNOWN; + std::string layer; + int z = 0; + int x = 0; + int y = 0; + + // SELECT fields + int select_x = 0; + int select_y = 0; + int select_zoom = 0; + std::set visible_layers; + + // INSPECT / HOVER fields + int select_id = -1; + + // TCL_EVAL fields + std::string tcl_cmd; + + // TIMING_REPORT fields + bool timing_is_setup = true; + int timing_max_paths = 100; + float timing_slack_min = -std::numeric_limits::max(); + float timing_slack_max = std::numeric_limits::max(); + + // TIMING_HIGHLIGHT fields + int timing_path_index = -1; // -1 = clear + bool timing_highlight_setup = true; + std::string timing_pin_name; // optional: highlight this pin's net in yellow + + // CLOCK_TREE_HIGHLIGHT fields + std::string clock_tree_inst_name; + + // SLACK_HISTOGRAM fields + bool histogram_is_setup = true; + std::string histogram_path_group; + std::string histogram_clock; + + // SET_FOCUS_NETS fields + std::string focus_action; // "add", "remove", "clear" + std::string focus_net_name; + + // SET_ROUTE_GUIDES fields + std::string route_guide_action; // "add", "remove", "clear" + std::string route_guide_net_name; + + // LIST_DIR fields + std::string dir_path; + + // Heat map fields + std::string heatmap_name; + std::string heatmap_option; + std::string heatmap_string_value; + std::string raw_json; + + // Visibility flags (default: all visible) + TileVisibility vis; +}; + +struct WebSocketResponse +{ + uint32_t id = 0; + // 0 = JSON payload, 1 = PNG payload, 2 = error + uint8_t type = 0; + std::vector payload; +}; + +// Shared mutable state for a WebSocket session. +// Handlers receive a reference; WebSocketSession owns the instance. +struct SessionState +{ + std::mutex selection_mutex; + std::vector highlight_rects; + std::vector highlight_polys; + std::vector hover_rects; + std::vector timing_rects; + std::vector timing_lines; + + std::mutex selectables_mutex; + std::vector selectables; + + gui::Selected current_inspected; + std::vector navigation_history; + + std::mutex module_colors_mutex; + std::map module_colors; // odb module id → RGBA color + + std::mutex focus_nets_mutex; + std::set focus_net_ids; // dbNet ODB IDs + + std::mutex route_guides_mutex; + std::set route_guide_net_ids; // dbNet ODB IDs + + std::mutex heatmap_mutex; + std::map> heatmaps; + std::string active_heatmap; +}; + +// Minimal JSON field extraction (no JSON library dependency). +std::string extract_string(const std::string& json, const std::string& key); +int extract_int(const std::string& json, const std::string& key); +int extract_int_or(const std::string& json, + const std::string& key, + int default_val); +float extract_float_or(const std::string& json, + const std::string& key, + float default_val); +std::set extract_string_array(const std::string& json, + const std::string& key); + +// Dispatch BOUNDS/LAYERS/TILE/INFO requests (used by HTTP and WebSocket). +WebSocketResponse dispatch_request( + const WebSocketRequest& req, + const TileGenerator& gen, + const std::vector& highlight_rects = {}, + const std::vector& highlight_polys = {}, + const std::vector& colored_rects = {}, + const std::vector& flight_lines = {}, + const std::map* module_colors = nullptr, + const std::set* focus_net_ids = nullptr, + const std::set* route_guide_net_ids = nullptr); + +// Handles SELECT, INSPECT, and HOVER requests. +class SelectHandler +{ + public: + SelectHandler(std::shared_ptr gen, + std::shared_ptr tcl_eval); + + WebSocketResponse handleSelect(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleInspect(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleInspectBack(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleHover(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleSetFocusNets(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleSetRouteGuides(const WebSocketRequest& req, + SessionState& state); + + private: + std::shared_ptr gen_; + std::shared_ptr tcl_eval_; +}; + +// Handles TCL_EVAL requests. +class TclHandler +{ + public: + explicit TclHandler(std::shared_ptr tcl_eval); + + WebSocketResponse handleTclEval(const WebSocketRequest& req); + + private: + std::shared_ptr tcl_eval_; +}; + +// Handles TIMING_REPORT and TIMING_HIGHLIGHT requests. +class TimingHandler +{ + public: + TimingHandler(std::shared_ptr gen, + std::shared_ptr timing_report, + std::shared_ptr tcl_eval); + + WebSocketResponse handleTimingReport(const WebSocketRequest& req); + WebSocketResponse handleTimingHighlight(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleSlackHistogram(const WebSocketRequest& req); + WebSocketResponse handleChartFilters(const WebSocketRequest& req); + + private: + std::shared_ptr gen_; + std::shared_ptr timing_report_; + std::shared_ptr tcl_eval_; +}; + +// Handles CLOCK_TREE and CLOCK_TREE_HIGHLIGHT requests. +class ClockTreeHandler +{ + public: + ClockTreeHandler(std::shared_ptr gen, + std::shared_ptr clock_report, + std::shared_ptr tcl_eval); + + WebSocketResponse handleClockTree(const WebSocketRequest& req); + WebSocketResponse handleClockTreeHighlight(const WebSocketRequest& req, + SessionState& state); + + private: + std::shared_ptr gen_; + std::shared_ptr clock_report_; + std::shared_ptr tcl_eval_; +}; + +// Handles TILE/BOUNDS/TECH requests. +class TileHandler +{ + public: + explicit TileHandler(std::shared_ptr gen); + + void initializeHeatMaps(SessionState& state); + WebSocketResponse handleTile(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleModuleHierarchy(const WebSocketRequest& req); + WebSocketResponse handleSetModuleColors(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleHeatMaps(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleSetActiveHeatMap(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleSetHeatMap(const WebSocketRequest& req, + SessionState& state); + WebSocketResponse handleHeatMapTile(const WebSocketRequest& req, + SessionState& state); + + private: + std::shared_ptr gen_; +}; + +// Handles LIST_DIR requests (server-side file browsing). +WebSocketResponse handleListDir(const WebSocketRequest& req); + +} // namespace web diff --git a/src/web/src/search.cpp b/src/web/src/search.cpp new file mode 100644 index 00000000000..277d3036b38 --- /dev/null +++ b/src/web/src/search.cpp @@ -0,0 +1,1001 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2026, The OpenROAD Authors + +#include "search.h" + +#include +#include +#include +#include +#include +#include + +#include "boost/geometry/geometry.hpp" +#include "odb/db.h" +#include "odb/dbShape.h" +#include "odb/dbTypes.h" +#include "odb/geom.h" +#include "utl/Logger.h" + +namespace web { + +// CountdownLatch replacement for compilers that lack (e.g. GCC 10). +class CountdownLatch +{ + public: + explicit CountdownLatch(int count) : count_(count) {} + + void count_down() + { + std::lock_guard lock(mu_); + if (--count_ <= 0) { + cv_.notify_all(); + } + } + + void wait() + { + std::unique_lock lock(mu_); + cv_.wait(lock, [this] { return count_ <= 0; }); + } + + private: + std::mutex mu_; + std::condition_variable cv_; + int count_; +}; + +Search::Search(utl::Logger* logger) : logger_(logger) +{ +} + +Search::~Search() +{ + if (top_chip_ != nullptr) { + removeOwner(); // unregister as a callback object + } +} + +void Search::inDbNetDestroy(odb::dbNet* net) +{ + clearShapes(); +} + +void Search::inDbInstDestroy(odb::dbInst* inst) +{ + if (inst->isPlaced()) { + clearInsts(); + } +} + +void Search::inDbInstSwapMasterAfter(odb::dbInst* inst) +{ + if (inst->isPlaced()) { + clearInsts(); + } +} + +void Search::inDbInstPlacementStatusBefore(odb::dbInst* inst, + const odb::dbPlacementStatus& status) +{ + if (inst->getPlacementStatus().isPlaced() != status.isPlaced()) { + clearInsts(); + } +} + +void Search::inDbPostMoveInst(odb::dbInst* inst) +{ + if (inst->isPlaced()) { + clearInsts(); + } +} + +void Search::inDbBPinCreate(odb::dbBPin* pin) +{ + clearShapes(); +} + +void Search::inDbBPinDestroy(odb::dbBPin* pin) +{ + clearShapes(); +} + +void Search::inDbFillCreate(odb::dbFill* fill) +{ + clearFills(); +} + +void Search::inDbWireCreate(odb::dbWire* wire) +{ + clearShapes(); +} + +void Search::inDbWireDestroy(odb::dbWire* wire) +{ + clearShapes(); +} + +void Search::inDbSWireCreate(odb::dbSWire* wire) +{ + clearShapes(); +} + +void Search::inDbSWireDestroy(odb::dbSWire* wire) +{ + clearShapes(); +} + +void Search::inDbSWireAddSBox(odb::dbSBox* box) +{ + clearShapes(); +} + +void Search::inDbSWireRemoveSBox(odb::dbSBox* box) +{ + clearShapes(); +} + +void Search::inDbBlockageCreate(odb::dbBlockage* blockage) +{ + clearBlockages(); +} + +void Search::inDbBlockageDestroy(odb::dbBlockage* blockage) +{ + clearBlockages(); +} + +void Search::inDbObstructionCreate(odb::dbObstruction* obs) +{ + clearObstructions(); +} + +void Search::inDbObstructionDestroy(odb::dbObstruction* obs) +{ + clearObstructions(); +} + +void Search::inDbBlockSetDieArea(odb::dbBlock* block) +{ + setTopChip(block->getChip()); +} + +void Search::inDbBlockSetCoreArea(odb::dbBlock* block) +{ + // emit modified(); +} + +void Search::inDbRegionAddBox(odb::dbRegion*, odb::dbBox*) +{ + // emit modified(); +} + +void Search::inDbRegionDestroy(odb::dbRegion* region) +{ + // emit modified(); +} + +void Search::inDbRowCreate(odb::dbRow* row) +{ + clearRows(); +} + +void Search::inDbRowDestroy(odb::dbRow* row) +{ + clearRows(); +} + +void Search::inDbWirePostModify(odb::dbWire* wire) +{ + clearShapes(); +} + +void Search::setTopChip(odb::dbChip* chip) +{ + if (!chip) { + return; + } + odb::dbBlock* block = chip->getBlock(); + if (top_chip_ != chip) { + clear(); + + if (top_chip_ != nullptr) { + removeOwner(); + } + + addOwner(block); // register as a callback object + + // Pre-populate children so we don't have to lock access to + // child_block_data_ later + if (block) { + for (auto child : block->getChildren()) { + child_block_data_[child]; + } + } + } + + top_chip_ = chip; + + // emit newChip(chip); +} + +void Search::announceModified(std::atomic_bool& flag) +{ + const bool prev_flag = flag.exchange(false); + + if (prev_flag) { + // emit modified(); + } +} + +void Search::clear() +{ + child_block_data_.clear(); + clearShapes(); + clearFills(); + clearInsts(); + clearBlockages(); + clearObstructions(); + clearRows(); +} + +void Search::clearShapes() +{ + announceModified(top_block_data_.shapes_init); +} + +void Search::clearFills() +{ + announceModified(top_block_data_.fills_init); +} + +void Search::clearInsts() +{ + announceModified(top_block_data_.insts_init); +} + +void Search::clearBlockages() +{ + announceModified(top_block_data_.blockages_init); +} + +void Search::clearObstructions() +{ + announceModified(top_block_data_.obstructions_init); +} + +void Search::clearRows() +{ + announceModified(top_block_data_.rows_init); +} + +void Search::eagerInit(odb::dbBlock* block) +{ + const auto t0 = std::chrono::steady_clock::now(); + + CountdownLatch done(6); + auto run = [&](auto fn) { + boost::asio::post(pool_, [&done, fn] { + fn(); + done.count_down(); + }); + }; + run([&] { updateShapes(block); }); + run([&] { updateInsts(block); }); + run([&] { updateFills(block); }); + run([&] { updateBlockages(block); }); + run([&] { updateObstructions(block); }); + run([&] { updateRows(block); }); + done.wait(); + + const auto t1 = std::chrono::steady_clock::now(); + const auto ms + = std::chrono::duration_cast(t1 - t0).count(); + debugPrint( + logger_, utl::WEB, "timing", 1, "Search init took {}ms (parallel)", ms); +} + +Search::BlockData& Search::getData(odb::dbBlock* block) +{ + return block->getChip() == top_chip_ ? top_block_data_ + : child_block_data_[block]; +} + +void Search::updateShapes(odb::dbBlock* block) +{ + BlockData& data = getData(block); + std::lock_guard lock(data.shapes_init_mutex); + if (data.shapes_init) { + return; // already done by another thread + } + + const auto t0 = std::chrono::steady_clock::now(); + + data.box_shapes.clear(); + data.snet_via_shapes.clear(); + data.snet_shapes.clear(); + + // Single pass over all nets to collect both special and routing shapes. + LayerMap>> snet_shapes; + LayerMap>> snet_net_via_shapes; + LayerMap>> net_shapes; + + for (odb::dbNet* net : block->getNets()) { + addSNet(net, snet_shapes, snet_net_via_shapes); + addNet(net, net_shapes); + } + + for (odb::dbBTerm* term : block->getBTerms()) { + for (odb::dbBPin* pin : term->getBPins()) { + odb::dbPlacementStatus status = pin->getPlacementStatus(); + if (status == odb::dbPlacementStatus::NONE + || status == odb::dbPlacementStatus::UNPLACED) { + continue; + } + for (odb::dbBox* box : pin->getBoxes()) { + if (!box) { + continue; + } + odb::dbTechLayer* layer = box->getTechLayer(); + net_shapes[layer].emplace_back(box->getBox(), BTERM, term->getNet()); + } + } + } + + const auto t_collect = std::chrono::steady_clock::now(); + debugPrint( + logger_, + utl::WEB, + "timing", + 1, + "updateShapes collect took {}ms", + std::chrono::duration_cast(t_collect - t0) + .count()); + + // Pre-populate map keys so pool tasks only write to existing entries. + for (const auto& [layer, _] : snet_shapes) { + data.snet_shapes[layer]; + } + for (const auto& [layer, _] : snet_net_via_shapes) { + data.snet_via_shapes[layer]; + } + for (const auto& [layer, _] : net_shapes) { + data.box_shapes[layer]; + } + + // Build R-trees in parallel — one pool task per layer per map. + const auto num_tasks + = snet_shapes.size() + snet_net_via_shapes.size() + net_shapes.size(); + CountdownLatch rtree_done(num_tasks); + + for (auto& [layer, layer_shapes] : snet_shapes) { + boost::asio::post(pool_, [&data, layer, &layer_shapes, &rtree_done] { + data.snet_shapes[layer] = RtreeSNetShapes( + layer_shapes.begin(), layer_shapes.end()); + rtree_done.count_down(); + }); + } + for (auto& [layer, layer_shapes] : snet_net_via_shapes) { + boost::asio::post(pool_, [&data, layer, &layer_shapes, &rtree_done] { + data.snet_via_shapes[layer] = RtreeSNetDBoxShapes( + layer_shapes.begin(), layer_shapes.end()); + rtree_done.count_down(); + }); + } + for (auto& [layer, layer_shapes] : net_shapes) { + boost::asio::post(pool_, [&data, layer, &layer_shapes, &rtree_done] { + data.box_shapes[layer] = RtreeRoutingShapes( + layer_shapes.begin(), layer_shapes.end()); + rtree_done.count_down(); + }); + } + rtree_done.wait(); + + const auto t_rtree = std::chrono::steady_clock::now(); + + data.shapes_init = true; + + const auto rtree_ms = std::chrono::duration_cast( + t_rtree - t_collect) + .count(); + debugPrint(logger_, + utl::WEB, + "timing", + 1, + "updateShapes rtree took {}ms ({} tasks)", + rtree_ms, + num_tasks); + + const auto total_ms + = std::chrono::duration_cast(t_rtree - t0) + .count(); + debugPrint( + logger_, utl::WEB, "timing", 1, "updateShapes took {}ms", total_ms); +} + +void Search::updateFills(odb::dbBlock* block) +{ + BlockData& data = getData(block); + std::lock_guard lock(data.fills_init_mutex); + if (data.fills_init) { + return; // already done by another thread + } + + const auto t0 = std::chrono::steady_clock::now(); + + data.fills.clear(); + + LayerMap> fills; + for (odb::dbFill* fill : block->getFills()) { + fills[fill->getTechLayer()].push_back(fill); + } + + // Pre-populate map keys, then build R-trees in parallel. + for (const auto& [layer, _] : fills) { + data.fills[layer]; + } + CountdownLatch done(fills.size()); + for (auto& [layer, layer_fill] : fills) { + boost::asio::post(pool_, [&data, layer, &layer_fill, &done] { + data.fills[layer] = RtreeFill(layer_fill.begin(), layer_fill.end()); + done.count_down(); + }); + } + done.wait(); + + data.fills_init = true; + + const auto t1 = std::chrono::steady_clock::now(); + const auto ms + = std::chrono::duration_cast(t1 - t0).count(); + if (ms > 10) { + logger_->info(utl::WEB, 12, "Search::updateFills took {}ms", ms); + } +} + +void Search::updateInsts(odb::dbBlock* block) +{ + BlockData& data = getData(block); + std::lock_guard lock(data.insts_init_mutex); + if (data.insts_init) { + return; // already done by another thread + } + + const auto t0 = std::chrono::steady_clock::now(); + + data.insts.clear(); + + std::vector insts; + for (odb::dbInst* inst : block->getInsts()) { + if (inst->isPlaced()) { + insts.push_back(inst); + } + } + data.insts = RtreeDBox(insts.begin(), insts.end()); + + data.insts_init = true; + + const auto t1 = std::chrono::steady_clock::now(); + const auto ms + = std::chrono::duration_cast(t1 - t0).count(); + logger_->info(utl::WEB, 13, "Search::updateInsts took {}ms", ms); +} + +void Search::updateBlockages(odb::dbBlock* block) +{ + BlockData& data = getData(block); + std::lock_guard lock(data.blockages_init_mutex); + if (data.blockages_init) { + return; // already done by another thread + } + + const auto t0 = std::chrono::steady_clock::now(); + + data.blockages.clear(); + + std::vector blockages; + for (odb::dbBlockage* blockage : block->getBlockages()) { + if (blockage->isSystemReserved()) { + continue; + } + blockages.push_back(blockage); + } + data.blockages + = RtreeDBox(blockages.begin(), blockages.end()); + + data.blockages_init = true; + + const auto t1 = std::chrono::steady_clock::now(); + const auto ms + = std::chrono::duration_cast(t1 - t0).count(); + if (ms > 10) { + logger_->info(utl::WEB, 14, "Search::updateBlockages took {}ms", ms); + } +} + +void Search::updateObstructions(odb::dbBlock* block) +{ + BlockData& data = getData(block); + std::lock_guard lock(data.obstructions_init_mutex); + if (data.obstructions_init) { + return; // already done by another thread + } + + const auto t0 = std::chrono::steady_clock::now(); + + data.obstructions.clear(); + + LayerMap> obstructions; + for (odb::dbObstruction* obs : block->getObstructions()) { + if (obs->isSystemReserved()) { + continue; + } + odb::dbBox* bbox = obs->getBBox(); + obstructions[bbox->getTechLayer()].push_back(obs); + } + // Pre-populate map keys, then build R-trees in parallel. + for (const auto& [layer, _] : obstructions) { + data.obstructions[layer]; + } + CountdownLatch done(obstructions.size()); + for (auto& [layer, layer_obs] : obstructions) { + boost::asio::post(pool_, [&data, layer, &layer_obs, &done] { + data.obstructions[layer] + = RtreeDBox(layer_obs.begin(), layer_obs.end()); + done.count_down(); + }); + } + done.wait(); + + data.obstructions_init = true; + + const auto t1 = std::chrono::steady_clock::now(); + const auto ms + = std::chrono::duration_cast(t1 - t0).count(); + if (ms > 10) { + logger_->info(utl::WEB, 15, "Search::updateObstructions took {}ms", ms); + } +} + +void Search::updateRows(odb::dbBlock* block) +{ + BlockData& data = getData(block); + std::lock_guard lock(data.rows_init_mutex); + if (data.rows_init) { + return; // already done by another thread + } + + const auto t0 = std::chrono::steady_clock::now(); + + data.rows.clear(); + + std::vector> rows; + for (odb::dbRow* row : block->getRows()) { + rows.emplace_back(row->getBBox(), row); + } + data.rows = RtreeRect(rows.begin(), rows.end()); + + data.rows_init = true; + + const auto t1 = std::chrono::steady_clock::now(); + const auto ms + = std::chrono::duration_cast(t1 - t0).count(); + if (ms > 10) { + logger_->info(utl::WEB, 16, "Search::updateRows took {}ms", ms); + } +} + +void Search::addVia( + odb::dbNet* net, + odb::dbShape* shape, + int x, + int y, + LayerMap>>& tree_shapes) +{ + if (shape->getType() == odb::dbShape::TECH_VIA) { + odb::dbTechVia* via = shape->getTechVia(); + for (odb::dbBox* box : via->getBoxes()) { + odb::Rect bbox = box->getBox(); + bbox.moveDelta(x, y); + tree_shapes[box->getTechLayer()].emplace_back(bbox, VIA, net); + } + } else { + odb::dbVia* via = shape->getVia(); + for (odb::dbBox* box : via->getBoxes()) { + odb::Rect bbox = box->getBox(); + bbox.moveDelta(x, y); + tree_shapes[box->getTechLayer()].emplace_back(bbox, VIA, net); + } + } +} + +void Search::addSNet( + odb::dbNet* net, + LayerMap>>& net_shapes, + LayerMap>>& via_shapes) +{ + for (odb::dbSWire* swire : net->getSWires()) { + for (odb::dbSBox* box : swire->getWires()) { + if (box->isVia()) { + odb::dbTechLayer* layer; + if (auto via = box->getTechVia()) { + layer = via->getBottomLayer()->getUpperLayer(); + } else { + auto block_via = box->getBlockVia(); + layer = block_via->getBottomLayer()->getUpperLayer(); + } + via_shapes[layer].emplace_back(box, net); + } else { + if (box->getDirection() == odb::dbSBox::OCTILINEAR) { + net_shapes[box->getTechLayer()].emplace_back(box, box->getOct(), net); + } else { + net_shapes[box->getTechLayer()].emplace_back(box, box->getBox(), net); + } + } + } + } +} + +void Search::addNet( + odb::dbNet* net, + LayerMap>>& tree_shapes) +{ + odb::dbWire* wire = net->getWire(); + + if (wire == nullptr) { + return; + } + + odb::dbWireShapeItr itr; + odb::dbShape s; + + for (itr.begin(wire); itr.next(s);) { + if (s.isVia()) { + addVia(net, &s, itr.prev_x_, itr.prev_y_, tree_shapes); + } else { + tree_shapes[s.getTechLayer()].emplace_back(s.getBox(), WIRE, net); + } + } +} + +template +class Search::MinSizePredicate +{ + public: + MinSizePredicate(int min_size) : min_size_(min_size) {} + bool operator()(const SNetValue& o) const + { + return checkBox(std::get<0>(o)->getBox()); + } + + bool operator()(const RectValue& o) const { return checkBox(o.first); } + + bool operator()(const RouteBoxValue& o) const + { + return checkBox(std::get<0>(o)); + } + + bool operator()(const SNetDBoxValue& o) const + { + return checkBox(o.first->getBox()); + } + + bool operator()(odb::dbObstruction* o) const + { + return checkBox(o->getBBox()->getBox()); + } + + bool operator()(odb::dbFill* o) const + { + odb::Rect fill; + o->getRect(fill); + return checkBox(fill); + } + + bool checkBox(const odb::Rect& box) const + { + return box.maxDXDY() >= min_size_; + } + + private: + int min_size_; +}; + +template +class Search::PolygonIntersectPredicate +{ + public: + PolygonIntersectPredicate(const odb::Rect& region) : region_(region) {} + bool operator()(const SNetValue& o) const + { + return checkPolygon(std::get<1>(o)); + } + + bool operator()(const RectValue& o) const { return checkPolygon(o.first); } + + bool operator()(const RouteBoxValue& o) const + { + return checkPolygon(std::get<0>(o)); + } + + bool checkPolygon(const odb::Polygon& poly) const + { + return boost::geometry::intersects(region_, poly); + } + + private: + odb::Rect region_; +}; + +template +class Search::MinHeightPredicate +{ + public: + MinHeightPredicate(int min_height) : min_height_(min_height) {} + bool operator()(const SNetValue& o) const + { + return checkBox(std::get<0>(o)); + } + + bool operator()(const RouteBoxValue& o) const + { + return checkBox(std::get<0>(o)); + } + + bool operator()(const RectValue& o) const { return checkBox(o.first); } + + bool operator()(odb::dbInst* o) const + { + return checkBox(o->getBBox()->getBox()); + } + + bool operator()(odb::dbBlockage* o) const + { + return checkBox(o->getBBox()->getBox()); + } + + bool checkBox(const odb::Rect& box) const { return box.dy() >= min_height_; } + + private: + int min_height_; +}; + +Search::RoutingRange Search::searchBoxShapes(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size) +{ + BlockData& data = getData(block); + if (!data.shapes_init) { + updateShapes(block); + } + + auto it = data.box_shapes.find(layer); + if (it == data.box_shapes.end()) { + return RoutingRange(); + } + + auto& rtree = it->second; + + const odb::Rect query(x_lo, y_lo, x_hi, y_hi); + if (min_size > 0) { + return RoutingRange( + rtree.qbegin( + bgi::intersects(query) + && bgi::satisfies(MinSizePredicate(min_size))), + rtree.qend()); + } + + return RoutingRange(rtree.qbegin(bgi::intersects(query)), rtree.qend()); +} + +Search::SNetSBoxRange Search::searchSNetViaShapes(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size) +{ + BlockData& data = getData(block); + if (!data.shapes_init) { + updateShapes(block); + } + + auto it = data.snet_via_shapes.find(layer); + if (it == data.snet_via_shapes.end()) { + return SNetSBoxRange(); + } + + auto& rtree = it->second; + + const odb::Rect query(x_lo, y_lo, x_hi, y_hi); + if (min_size > 0) { + return SNetSBoxRange( + rtree.qbegin( + bgi::intersects(query) + && bgi::satisfies(MinSizePredicate(min_size))), + rtree.qend()); + } + + return SNetSBoxRange(rtree.qbegin(bgi::intersects(query)), rtree.qend()); +} + +Search::SNetShapeRange Search::searchSNetShapes(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size) +{ + BlockData& data = getData(block); + if (!data.shapes_init) { + updateShapes(block); + } + + auto it = data.snet_shapes.find(layer); + if (it == data.snet_shapes.end()) { + return SNetShapeRange(); + } + + auto& rtree = it->second; + + const odb::Rect query(x_lo, y_lo, x_hi, y_hi); + if (min_size > 0) { + return SNetShapeRange( + rtree.qbegin( + bgi::intersects(query) + && bgi::satisfies(MinSizePredicate(min_size)) + && bgi::satisfies(PolygonIntersectPredicate(query))), + rtree.qend()); + } + + return SNetShapeRange( + rtree.qbegin( + bgi::intersects(query) + && bgi::satisfies(PolygonIntersectPredicate(query))), + rtree.qend()); +} + +Search::FillRange Search::searchFills(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size) +{ + BlockData& data = getData(block); + if (!data.fills_init) { + updateFills(block); + } + + auto it = data.fills.find(layer); + if (it == data.fills.end()) { + return FillRange(); + } + + auto& rtree = it->second; + const odb::Rect query(x_lo, y_lo, x_hi, y_hi); + if (min_size > 0) { + return FillRange( + rtree.qbegin( + bgi::intersects(query) + && bgi::satisfies(MinSizePredicate(min_size))), + rtree.qend()); + } + + return FillRange(rtree.qbegin(bgi::intersects(query)), rtree.qend()); +} + +Search::InstRange Search::searchInsts(odb::dbBlock* block, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_height) +{ + BlockData& data = getData(block); + if (!data.insts_init) { + updateInsts(block); + } + + const odb::Rect query(x_lo, y_lo, x_hi, y_hi); + if (min_height > 0) { + return InstRange( + data.insts.qbegin( + bgi::intersects(query) + && bgi::satisfies(MinHeightPredicate(min_height))), + data.insts.qend()); + } + + return InstRange(data.insts.qbegin(bgi::intersects(query)), + data.insts.qend()); +} + +Search::BlockageRange Search::searchBlockages(odb::dbBlock* block, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_height) +{ + BlockData& data = getData(block); + if (!data.blockages_init) { + updateBlockages(block); + } + + const odb::Rect query(x_lo, y_lo, x_hi, y_hi); + if (min_height > 0) { + return BlockageRange( + data.blockages.qbegin( + bgi::intersects(query) + && bgi::satisfies( + MinHeightPredicate(min_height))), + data.blockages.qend()); + } + + return BlockageRange(data.blockages.qbegin(bgi::intersects(query)), + data.blockages.qend()); +} + +Search::ObstructionRange Search::searchObstructions(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size) +{ + BlockData& data = getData(block); + if (!data.obstructions_init) { + updateObstructions(block); + } + + auto it = data.obstructions.find(layer); + if (it == data.obstructions.end()) { + return ObstructionRange(); + } + + auto& rtree = it->second; + const odb::Rect query(x_lo, y_lo, x_hi, y_hi); + if (min_size > 0) { + return ObstructionRange( + rtree.qbegin( + bgi::intersects(query) + && bgi::satisfies(MinSizePredicate(min_size))), + rtree.qend()); + } + + return ObstructionRange(rtree.qbegin(bgi::intersects(query)), rtree.qend()); +} + +Search::RowRange Search::searchRows(odb::dbBlock* block, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_height) +{ + BlockData& data = getData(block); + if (!data.rows_init) { + updateRows(block); + } + + const odb::Rect query(x_lo, y_lo, x_hi, y_hi); + if (min_height > 0) { + return RowRange( + data.rows.qbegin( + bgi::intersects(query) + && bgi::satisfies(MinHeightPredicate(min_height))), + data.rows.qend()); + } + + return RowRange(data.rows.qbegin(bgi::intersects(query)), data.rows.qend()); +} + +} // namespace web diff --git a/src/web/src/search.h b/src/web/src/search.h new file mode 100644 index 00000000000..74e25ceeded --- /dev/null +++ b/src/web/src/search.h @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "boost/asio/post.hpp" +#include "boost/asio/thread_pool.hpp" + +namespace utl { +class Logger; +} + +#include "boost/geometry/geometry.hpp" +#include "boost/geometry/index/rtree.hpp" +#include "odb/db.h" +#include "odb/dbBlockCallBackObj.h" +#include "odb/geom.h" +#include "odb/geom_boost.h" + +namespace web { + +namespace bgi = boost::geometry::index; + +// This is a geometric search structure. It wraps up Boost's +// rtree. OpenDB also has some code for this purpose but I +// find it confusing so just made a simpler solution for now. +// +// Currently this class is static once built and doesn't follow +// db changes. TODO: this should be into an observer of OpenDB. +class Search : public odb::dbBlockCallBackObj +{ + template + class MinSizePredicate; + + template + class MinHeightPredicate; + + template + class PolygonIntersectPredicate; + + public: + enum RouteBoxType + { + WIRE, + VIA, + BTERM + }; + + template + using LayerMap = std::map; + + template + using RectValue = std::pair; + template + using RouteBoxValue = std::tuple; + template + using SNetValue = std::tuple; + template + using SNetDBoxValue = std::pair; + ; + + template + struct BBoxIndexableGetter + { + using result_type = odb::Rect; + odb::Rect operator()(T t) const { return t->getBBox()->getBox(); } + odb::Rect operator()(const SNetValue& t) const + { + return std::get<0>(t)->getBox(); + } + odb::Rect operator()(const SNetDBoxValue& t) const + { + return std::get<0>(t)->getBox(); + } + }; + + struct FillIndexableGetter + { + using result_type = odb::Rect; + odb::Rect operator()(odb::dbFill* t) const + { + odb::Rect fill; + t->getRect(fill); + return fill; + } + }; + + template + using RtreeRect = bgi::rtree, bgi::quadratic<16>>; + template + using RtreeDBox = bgi::rtree, BBoxIndexableGetter>; + template + using RtreeRoutingShapes = bgi::rtree, bgi::quadratic<16>>; + template + using RtreeSNetShapes + = bgi::rtree, bgi::quadratic<16>, BBoxIndexableGetter>; + template + using RtreeSNetDBoxShapes = bgi:: + rtree, bgi::quadratic<16>, BBoxIndexableGetter>; + using RtreeFill + = bgi::rtree, FillIndexableGetter>; + + // This is an iterator range for return values + template + class Range + { + public: + using Iterator = typename Tree::const_query_iterator; + + Range() = default; + Range(const Iterator& begin, const Iterator& end) : begin_(begin), end_(end) + { + } + + Iterator begin() { return begin_; } + Iterator end() { return end_; } + + private: + Iterator begin_; + Iterator end_; + }; + using InstRange = Range>; + using RoutingRange = Range>; + using SNetSBoxRange = Range>; + using SNetShapeRange = Range>; + using FillRange = Range; + using ObstructionRange = Range>; + using BlockageRange = Range>; + using RowRange = Range>; + + explicit Search(utl::Logger* logger); + ~Search() override; + + // Pre-build all R-tree indices in parallel. + void eagerInit(odb::dbBlock* block); + + // Returns true once shape R-trees are built. + bool shapesReady() const { return top_block_data_.shapes_init.load(); } + + // Build the structure for the given chip. + void setTopChip(odb::dbChip* chip); + + // Find all box shapes in the given bounds on the given layer which + // are at least min_size in either dimension. + RoutingRange searchBoxShapes(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size = 0); + + // Find all via sbox shapes in the given bounds on the given layer which + // are at least min_size in either dimension. + SNetSBoxRange searchSNetViaShapes(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size = 0); + + // Find all polgyon shapes in the given bounds on the given layer which + // are at least min_size in either dimension. + SNetShapeRange searchSNetShapes(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size = 0); + + // Find all fills in the given bounds on the given layer which + // are at least min_size in either dimension. + FillRange searchFills(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size = 0); + + // Find all instances in the given bounds with height of at least min_height + InstRange searchInsts(odb::dbBlock* block, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_height = 0); + + // Find all blockages in the given bounds with height of at least min_height + BlockageRange searchBlockages(odb::dbBlock* block, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_height = 0); + + // Find all obstructions in the given bounds on the given layer which + // are at least min_size in either dimension. + ObstructionRange searchObstructions(odb::dbBlock* block, + odb::dbTechLayer* layer, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_size = 0); + + // Find all rows in the given bounds with height of at least min_height. + RowRange searchRows(odb::dbBlock* block, + int x_lo, + int y_lo, + int x_hi, + int y_hi, + int min_height = 0); + + void clearShapes(); + void clearFills(); + void clearInsts(); + void clearBlockages(); + void clearObstructions(); + void clearRows(); + + // From dbBlockCallBackObj + void inDbNetDestroy(odb::dbNet* net) override; + void inDbInstDestroy(odb::dbInst* inst) override; + void inDbInstSwapMasterAfter(odb::dbInst* inst) override; + void inDbInstPlacementStatusBefore( + odb::dbInst* inst, + const odb::dbPlacementStatus& status) override; + void inDbPostMoveInst(odb::dbInst* inst) override; + void inDbBPinCreate(odb::dbBPin* pin) override; + void inDbBPinDestroy(odb::dbBPin* pin) override; + void inDbFillCreate(odb::dbFill* fill) override; + void inDbWireCreate(odb::dbWire* wire) override; + void inDbWireDestroy(odb::dbWire* wire) override; + void inDbSWireCreate(odb::dbSWire* wire) override; + void inDbSWireDestroy(odb::dbSWire* wire) override; + void inDbSWireAddSBox(odb::dbSBox* box) override; + void inDbSWireRemoveSBox(odb::dbSBox* box) override; + void inDbBlockSetDieArea(odb::dbBlock* block) override; + void inDbBlockSetCoreArea(odb::dbBlock* block) override; + void inDbBlockageCreate(odb::dbBlockage* blockage) override; + void inDbBlockageDestroy(odb::dbBlockage* blockage) override; + void inDbObstructionCreate(odb::dbObstruction* obs) override; + void inDbObstructionDestroy(odb::dbObstruction* obs) override; + void inDbRegionAddBox(odb::dbRegion*, odb::dbBox*) override; + void inDbRegionDestroy(odb::dbRegion* region) override; + void inDbRowCreate(odb::dbRow* row) override; + void inDbRowDestroy(odb::dbRow* row) override; + void inDbWirePostModify(odb::dbWire* wire) override; + + // signals: + // void modified(); + // void newChip(odb::dbChip* chip); + + private: + struct BlockData; + + void addSNet(odb::dbNet* net, + LayerMap>>& net_shapes, + LayerMap>>& via_shapes); + void addNet(odb::dbNet* net, + LayerMap>>& tree_shapes); + void addVia(odb::dbNet* net, + odb::dbShape* shape, + int x, + int y, + LayerMap>>& tree_shapes); + + void updateShapes(odb::dbBlock* block); + void updateFills(odb::dbBlock* block); + void updateInsts(odb::dbBlock* block); + void updateBlockages(odb::dbBlock* block); + void updateObstructions(odb::dbBlock* block); + void updateRows(odb::dbBlock* block); + + void clear(); + + void announceModified(std::atomic_bool& flag); + BlockData& getData(odb::dbBlock* block); + + utl::Logger* logger_; + odb::dbChip* top_chip_{nullptr}; + boost::asio::thread_pool pool_{std::thread::hardware_concurrency()}; + + struct BlockData + { + RtreeDBox insts; + RtreeDBox blockages; + RtreeRect rows; + + std::mutex shapes_init_mutex; + std::mutex fills_init_mutex; + std::mutex insts_init_mutex; + std::mutex blockages_init_mutex; + std::mutex obstructions_init_mutex; + std::mutex rows_init_mutex; + + // The net is used for filter shapes by net type + LayerMap> box_shapes; + // Special net vias may be large multi-cut vias. It is more efficient + // to store the dbSBox (ie the via) than all the cuts. This is + // particularly true when you have parallel straps like m1 & m2 in asap7. + LayerMap> snet_via_shapes; + LayerMap> snet_shapes; + LayerMap fills; + LayerMap> obstructions; + + std::atomic_bool shapes_init{false}; + std::atomic_bool fills_init{false}; + std::atomic_bool insts_init{false}; + std::atomic_bool blockages_init{false}; + std::atomic_bool obstructions_init{false}; + std::atomic_bool rows_init{false}; + }; + std::map child_block_data_; + BlockData top_block_data_; +}; + +} // namespace web diff --git a/src/web/src/style.css b/src/web/src/style.css new file mode 100644 index 00000000000..8a48dc2e394 --- /dev/null +++ b/src/web/src/style.css @@ -0,0 +1,1025 @@ +/* Rubber-band zoom overlay (right-click drag) */ +.rubber-band { + position: fixed; + border: 1px solid white; + background: rgba(255, 255, 255, 0.1); + pointer-events: none; + z-index: 10000; +} + +/* Full viewport GL container */ +html, body { + height: 100%; + margin: 0; + overflow: hidden; + background: #1e1e1e; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + color: #ccc; +} + +#gl-container { + width: 100vw; + height: calc(100vh - 24px); +} + +/* Menu bar */ +#menu-bar { + display: flex; + align-items: center; + height: 24px; + background: #2d2d2d; + border-bottom: 1px solid #1a1a1a; + font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + color: #ccc; + user-select: none; + z-index: 10001; +} +#menu-bar .menu-label { + padding: 0 10px; + height: 24px; + line-height: 24px; + cursor: default; + position: relative; +} +#menu-bar .menu-label:hover, +#menu-bar .menu-label.open { + background: #3c3c3c; +} +#menu-bar .menu-dropdown { + display: none; + position: absolute; + top: 24px; + left: 0; + min-width: 200px; + background: #2d2d2d; + border: 1px solid #1a1a1a; + box-shadow: 0 4px 12px rgba(0,0,0,0.5); + z-index: 10002; + padding: 4px 0; +} +#menu-bar .menu-label.open .menu-dropdown { + display: block; +} +#menu-bar .menu-item { + display: flex; + justify-content: space-between; + padding: 4px 20px; + cursor: default; + white-space: nowrap; +} +#menu-bar .menu-item:hover { + background: #094771; + color: #fff; +} +#menu-bar .menu-item.disabled { + color: #666; + pointer-events: none; +} +#menu-bar .menu-item .shortcut { + color: #888; + margin-left: 24px; +} +#menu-bar .menu-item:hover .shortcut { + color: #aaa; +} +#menu-bar .menu-separator { + height: 1px; + background: #444; + margin: 4px 8px; +} + +/* Modal dialog (Open/Save DB) */ +.modal-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + z-index: 10003; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; +} +.modal-dialog { + background: #2d2d2d; + border: 1px solid #555; + border-radius: 6px; + padding: 16px 20px; + min-width: 400px; + color: #ccc; + font: 13px monospace; +} +.modal-dialog h3 { + margin: 0 0 12px 0; + font-size: 14px; + font-weight: 600; +} +.modal-dialog input[type="text"] { + width: 100%; + padding: 6px 8px; + background: #1e1e1e; + border: 1px solid #555; + border-radius: 3px; + color: #ccc; + font: 13px monospace; + box-sizing: border-box; +} +.modal-dialog input[type="text"]:focus { + outline: none; + border-color: #094771; +} +.modal-dialog .modal-buttons { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} +.modal-dialog button { + padding: 5px 14px; + border: 1px solid #555; + border-radius: 3px; + background: #3c3c3c; + color: #ccc; + font: 13px sans-serif; + cursor: pointer; +} +.modal-dialog button:hover { + background: #4c4c4c; +} +.modal-dialog button.primary { + background: #094771; + border-color: #094771; + color: #fff; +} +.modal-dialog button.primary:hover { + background: #0b5a8f; +} +.modal-dialog .modal-error { + color: #f88; + margin-top: 8px; + font-size: 12px; +} +.modal-dialog button:disabled { + opacity: 0.5; + cursor: default; +} + +/* File browser dialog */ +.file-browser-dialog { + min-width: 520px; + max-width: 640px; +} +.fb-breadcrumb { + background: #1e1e1e; + padding: 4px 8px; + border: 1px solid #555; + border-radius: 3px; + margin-bottom: 8px; + font-size: 12px; + white-space: nowrap; + overflow-x: auto; +} +.fb-crumb { + cursor: pointer; + color: #6cb0f6; +} +.fb-crumb:hover { + text-decoration: underline; +} +.fb-crumb-sep { + color: #777; +} +.fb-file-list { + background: #1e1e1e; + border: 1px solid #555; + border-radius: 3px; + max-height: 320px; + min-height: 160px; + overflow-y: auto; + margin-bottom: 8px; +} +.fb-entry { + display: flex; + align-items: center; + padding: 3px 8px; + cursor: pointer; + gap: 6px; +} +.fb-entry:hover { + background: #2a2d2e; +} +.fb-entry.fb-selected { + background: #094771; +} +.fb-icon { + flex-shrink: 0; + width: 16px; + text-align: center; + font-size: 13px; +} +.fb-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.fb-size { + flex-shrink: 0; + color: #888; + font-size: 11px; + margin-left: 8px; +} +.fb-empty, .fb-loading { + padding: 16px; + text-align: center; + color: #888; + font-style: italic; +} +.fb-path-input { + margin-top: 0; +} + +/* GL dark theme overrides */ +.lm_goldenlayout { + background: #1e1e1e; +} +.lm_content { + background: #252525; + overflow: hidden; +} +.lm_header .lm_tab { + background: #2d2d2d; + color: #aaa; +} +.lm_header .lm_tab.lm_active { + background: #252525; + color: #eee; +} +.lm_splitter { + background: #333; +} + +/* Leaflet tiles */ +.leaflet-tile { + image-rendering: auto; +} + +.layout-viewer { + position: relative; +} + +/* Leaflet popups */ +.leaflet-popup-content-wrapper { + background: #2b2b2b; + color: #eee; + border-radius: 4px; +} +.leaflet-popup-content { + font-family: monospace; +} +.leaflet-popup-content strong { + color: #87cfff; +} + +/* Status overlay */ +#websocket-status { + position: fixed; + top: 8px; + right: 8px; + z-index: 10000; + background: rgba(0,0,0,0.7); + color: #aaa; + padding: 4px 10px; + font: 12px monospace; + border-radius: 4px; + pointer-events: none; + display: none; +} + +/* Loading overlay */ +#loading-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + z-index: 9999; + background: rgba(0, 0, 0, 0.5); + display: none; + align-items: center; + justify-content: center; +} +#loading-overlay .loading-overlay-content { + display: flex; + align-items: center; + gap: 12px; + color: #ccc; + font: 16px monospace; +} +#loading-overlay .spinner { + width: 20px; + height: 20px; + border: 3px solid #555; + border-top-color: #ccc; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* Stub panel placeholder */ +.stub-panel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: #666; + font-size: 14px; + text-align: center; + padding: 20px; +} +.stub-title { + font-size: 15px; + font-weight: 600; + color: #888; + margin-bottom: 8px; +} +.stub-desc { + color: #555; + max-width: 300px; + line-height: 1.5; +} + +/* Display Controls panel */ +.display-controls { + padding: 8px; + padding-bottom: 20px; + box-sizing: border-box; + overflow-y: auto; + height: 100%; + font-size: 13px; +} +.display-controls .loading { + color: #666; + padding: 12px 0; +} +.display-controls label { + display: flex; + align-items: center; + padding: 3px 0; + color: #ccc; + cursor: pointer; +} +.display-controls input[type="checkbox"] { + margin-right: 8px; +} +.display-controls .layer-color { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + margin-right: 6px; + border: 1px solid #555; +} +.display-controls .vis-group { + margin: 1px 0; +} +.display-controls .vis-group-header { + display: flex; + align-items: center; + padding: 3px 0; + color: #ccc; + cursor: pointer; +} +.display-controls .vis-arrow { + font-size: 9px; + width: 14px; + text-align: center; + color: #888; + cursor: pointer; + user-select: none; + flex-shrink: 0; +} +.display-controls .vis-group-children { + padding-left: 22px; +} +.display-controls .vis-group-children > label { + margin-left: 14px; +} +.display-controls .vis-group-children.collapsed { + display: none; +} +.display-controls .vis-group-children.disabled { + opacity: 0.4; + pointer-events: none; +} + +/* Layer context menu */ +.context-menu { + position: fixed; + background: #2a2a2a; + border: 1px solid #555; + border-radius: 4px; + min-width: 180px; + z-index: 10000; + padding: 4px 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.5); + font-size: 13px; + color: #ccc; +} +.context-menu-item { + padding: 5px 12px; + cursor: pointer; + white-space: nowrap; +} +.context-menu-item:hover { + background: #3a3a3a; + color: #fff; +} + +/* Tcl Console */ +.tcl-console { + display: flex; + flex-direction: column; + height: 100%; + font-family: monospace; + font-size: 13px; +} +.tcl-output { + flex: 1; + width: 100%; + box-sizing: border-box; + background: #1a1a1a; + color: #aaa; + padding: 8px; + overflow-y: auto; + white-space: pre-wrap; + font-family: inherit; + font-size: inherit; +} +.tcl-cmd { + color: #87cfff; +} +.tcl-error { + color: #f55; +} +.tcl-input-row { + display: flex; + align-items: center; + background: #111; + border-top: 1px solid #444; +} +.tcl-prompt { + color: #87cfff; + padding: 6px 4px 6px 8px; + user-select: none; +} +.tcl-input { + flex: 1; + background: #111; + color: #eee; + border: none; + outline: none; + padding: 6px 8px 6px 0; + font-family: monospace; + font-size: 13px; +} + +/* Help panel */ +.help-panel { + padding: 16px; + font-size: 13px; + line-height: 1.6; + overflow-y: auto; + height: 100%; +} +.help-panel h3 { + color: #aaa; + margin: 0 0 12px 0; + font-size: 14px; +} +.help-panel table { + border-collapse: collapse; +} +.help-panel td { + padding: 2px 16px 2px 0; +} +.help-panel kbd { + background: #333; + border: 1px solid #555; + border-radius: 3px; + padding: 1px 6px; + font-family: monospace; + font-size: 12px; + color: #ddd; +} + +/* Inspector panel */ +.inspector { + padding: 8px; + overflow-y: auto; + height: 100%; + font-size: 13px; + font-family: monospace; +} +.inspector-prop { + display: flex; + padding: 2px 0; + border-bottom: 1px solid #2a2a2a; +} +.inspector-prop-name { + color: #87cfff; + min-width: 120px; + padding-right: 8px; + word-break: break-all; +} +.inspector-prop-value { + color: #ccc; + word-break: break-all; +} +.inspector-group { + margin: 1px 0; +} +.inspector-group-header { + display: flex; + align-items: center; + padding: 2px 0; + cursor: pointer; + color: #ccc; + border-bottom: 1px solid #2a2a2a; +} +.inspector-group-header:hover { + background: #2a2a2a; +} +.inspector-group-header .vis-arrow { + font-size: 9px; + width: 14px; + text-align: center; + color: #888; + flex-shrink: 0; +} +.inspector-count { + color: #666; + margin-left: 6px; + font-size: 11px; +} +.inspector-group-children { + padding-left: 16px; +} +.inspector-group-children.collapsed { + display: none; +} +.inspector-link { + color: #4fc3f7; + cursor: pointer; + text-decoration: none; +} +.inspector-link:hover { + text-decoration: underline; +} +.inspector-toolbar { + display: flex; + gap: 4px; + padding: 2px 0 6px; + border-bottom: 1px solid #333; + margin-bottom: 4px; +} +.inspector-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + background: #333; + border: 1px solid #555; + border-radius: 3px; + color: #ccc; + cursor: pointer; +} +.inspector-btn:hover { + background: #444; + color: #fff; +} +.inspector-btn:disabled { + opacity: 0.45; + cursor: default; + background: #2a2a2a; + color: #777; +} + +/* Hover highlight pulse animation */ +.hover-highlight { + stroke: #ffe85c; + stroke-width: 3px; + stroke-opacity: 1; + fill: #fff27a; + fill-opacity: 0.14; + vector-effect: non-scaling-stroke; + animation: hover-pulse 0.9s ease-out 3; +} +@keyframes hover-pulse { + 0% { + stroke: #fff6b0; + stroke-width: 7px; + stroke-opacity: 1; + fill-opacity: 0.28; + } + 55% { + stroke: #ffffff; + stroke-width: 5px; + stroke-opacity: 1; + fill-opacity: 0.06; + } + 100% { + stroke: #ffe85c; + stroke-width: 3px; + stroke-opacity: 0.95; + fill-opacity: 0.14; + } +} + +/* ─── Timing Widget ────────────────────────────────────────────────────── */ + +.timing-widget { + display: flex; + flex-direction: column; + height: 100%; + font-size: 13px; + font-family: monospace; + overflow: hidden; +} + +.timing-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-bottom: 1px solid #333; + flex-shrink: 0; +} + +.timing-btn { + background: #333; + border: 1px solid #555; + border-radius: 3px; + color: #ccc; + padding: 4px 12px; + cursor: pointer; + font-size: 12px; +} +.timing-btn:hover { background: #444; color: #fff; } +.timing-btn:disabled { opacity: 0.5; cursor: default; } + +.timing-path-count { + color: #888; + font-size: 12px; +} + +.timing-tab-bar { + display: flex; + border-bottom: 1px solid #333; + flex-shrink: 0; +} + +.timing-tab { + background: #2d2d2d; + border: none; + border-bottom: 2px solid transparent; + color: #888; + padding: 6px 16px; + cursor: pointer; + font-size: 12px; + font-family: inherit; +} +.timing-tab.active { + color: #eee; + border-bottom-color: #4fc3f7; + background: #252525; +} +.timing-tab:hover { color: #ccc; } + +.timing-path-table-container { + flex: 1; + overflow-y: auto; + min-height: 80px; +} + +.timing-detail-table-container { + flex: 1; + overflow-y: auto; + min-height: 80px; + border-top: 1px solid #444; +} + +.timing-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.timing-table thead th { + position: sticky; + top: 0; + z-index: 1; + background: #2a2a2a; + color: #aaa; + padding: 4px 8px; + text-align: left; + border-bottom: 1px solid #444; + font-weight: 600; + white-space: nowrap; +} + +.timing-table tbody td { + padding: 3px 8px; + border-bottom: 1px solid #2a2a2a; + white-space: nowrap; + color: #ccc; +} + +.timing-table tbody tr:hover { + background: #2d2d2d; +} + +.timing-table tbody tr.selected { + background: #1a3a5c; +} + +.timing-table tbody tr.timing-selected-row { + background: #3a3a1c; +} + + + +.timing-table tbody tr.timing-clock-row td { + color: #888; + font-style: italic; +} + +.timing-table tbody td.slack-negative { + color: #f55; +} + +.col-resize-grip { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 5px; + cursor: col-resize; +} + +.col-resize-grip:hover { + background: rgba(255, 255, 255, 0.15); +} + +.timing-table td:first-child { + white-space: normal; + word-break: break-all; + overflow-wrap: break-word; +} + +/* ─── Hierarchy Browser ──────────────────────────────────────────────── */ + +.hierarchy-widget { + display: flex; + flex-direction: column; + height: 100%; + font-size: 13px; + font-family: monospace; + overflow: hidden; +} + +.hierarchy-table-container { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +.hierarchy-arrow { + display: inline-block; + width: 14px; + text-align: center; + cursor: pointer; + color: #888; + user-select: none; + font-size: 9px; + margin-right: 4px; +} + +.hierarchy-color-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + border: 1px solid #555; + margin-right: 4px; + vertical-align: middle; +} + +.hierarchy-module-cb { + margin-right: 4px; +} + +/* ─── Clock Tree Widget ───────────────────────────────────────────────── */ + +.clock-tree-widget { + display: flex; + flex-direction: column; + height: 100%; + font-size: 13px; + font-family: monospace; + overflow: hidden; + position: relative; +} + +.clock-tree-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-bottom: 1px solid #333; + flex-shrink: 0; +} + +.clock-tree-canvas { + flex: 1; + min-height: 0; + width: 100%; + display: block; +} + +.clock-tree-tooltip { + position: absolute; + background: rgba(30, 30, 30, 0.95); + border: 1px solid #555; + border-radius: 4px; + padding: 6px 10px; + font-size: 12px; + color: #ccc; + pointer-events: none; + white-space: nowrap; + z-index: 100; + display: none; +} + +/* ─── Charts Widget ──────────────────────────────────────────────────── */ + +.charts-widget { + display: flex; + flex-direction: column; + height: 100%; + font-size: 13px; + font-family: monospace; + overflow: hidden; + position: relative; +} + +.charts-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-bottom: 1px solid #333; + flex-shrink: 0; +} + +.charts-select { + background: #2a2a2a; + color: #ccc; + border: 1px solid #555; + border-radius: 3px; + padding: 3px 6px; + font-family: monospace; + font-size: 12px; + cursor: pointer; +} +.charts-select:hover { + border-color: #777; +} + +.charts-canvas { + flex: 1; + min-height: 0; + width: 100%; + display: block; +} + +.charts-tooltip { + position: absolute; + background: rgba(30, 30, 30, 0.95); + border: 1px solid #555; + border-radius: 4px; + padding: 6px 10px; + font-size: 12px; + color: #ccc; + pointer-events: none; + white-space: pre; + z-index: 100; + display: none; +} + +.heatmap-controls { + margin-top: 12px; + padding-top: 8px; + padding-bottom: 12px; + border-top: 1px solid #333; + display: flex; + flex-direction: column; + gap: 6px; + font-family: monospace; + font-size: 12px; +} + +.heatmap-header { + color: #ddd; + font-weight: 700; +} + +.heatmap-list, +.heatmap-settings { + display: flex; + flex-direction: column; + gap: 4px; +} + +.heatmap-setting { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + color: #ccc; +} + +.heatmap-setting input[type="number"], +.heatmap-setting select { + width: 96px; + background: #1f1f1f; + color: #ddd; + border: 1px solid #555; + border-radius: 3px; + padding: 2px 4px; + font-family: monospace; + font-size: 12px; +} + +.heatmap-rebuild { + margin-top: 6px; + background: #2a2a2a; + color: #ddd; + border: 1px solid #666; + border-radius: 3px; + padding: 4px 8px; + cursor: pointer; +} + +.heatmap-legend-row { + display: flex; + align-items: center; + gap: 8px; + color: #bbb; +} + +.heatmap-legend-swatch { + width: 14px; + height: 14px; + border: 1px solid #666; + border-radius: 2px; + flex: 0 0 auto; +} + +.heatmap-map-legend { + position: absolute; + right: 14px; + bottom: 14px; + z-index: 900; + min-width: 140px; + max-width: 220px; + padding: 10px 12px; + background: rgba(18, 20, 24, 0.88); + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 6px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35); + color: #ddd; + font-family: monospace; + font-size: 12px; + pointer-events: none; + backdrop-filter: blur(2px); +} + +.heatmap-map-legend.hidden { + display: none; +} + +.heatmap-map-legend-title { + color: #f2f2f2; + font-weight: 700; + margin-bottom: 2px; +} + +.heatmap-map-legend-units { + color: #9ca3af; + margin-bottom: 8px; +} + +.heatmap-map-legend-list { + display: flex; + flex-direction: column; + gap: 4px; +} diff --git a/src/web/src/tile_generator.cpp b/src/web/src/tile_generator.cpp new file mode 100644 index 00000000000..cd9e68d5c01 --- /dev/null +++ b/src/web/src/tile_generator.cpp @@ -0,0 +1,1752 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include "tile_generator.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "color.h" +#include "db_sta/dbSta.hh" +#include "gui/heatMap.h" +#include "lodepng.h" +#include "odb/db.h" +#include "odb/dbShape.h" +#include "odb/dbTransform.h" +#include "odb/dbTypes.h" +#include "odb/geom.h" +#include "request_handler.h" +#include "search.h" +#include "timing_report.h" +#include "utl/Logger.h" + +namespace web { + +namespace { + +constexpr int kBitmapGlyphWidth = 5; +constexpr int kBitmapGlyphHeight = 7; +constexpr int kBitmapGlyphSpacing = 1; + +const unsigned char* getBitmapGlyph(const char ch) +{ + // Minimal 5x7 bitmap font. Each byte is one row, only the low 5 bits are + // used. + switch (ch) { + case '0': { + static constexpr unsigned char glyph[] + = {0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E}; + return glyph; + } + case '1': { + static constexpr unsigned char glyph[] + = {0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E}; + return glyph; + } + case '2': { + static constexpr unsigned char glyph[] + = {0x0E, 0x11, 0x01, 0x06, 0x08, 0x10, 0x1F}; + return glyph; + } + case '3': { + static constexpr unsigned char glyph[] + = {0x0E, 0x11, 0x01, 0x06, 0x01, 0x11, 0x0E}; + return glyph; + } + case '4': { + static constexpr unsigned char glyph[] + = {0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02}; + return glyph; + } + case '5': { + static constexpr unsigned char glyph[] + = {0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E}; + return glyph; + } + case '6': { + static constexpr unsigned char glyph[] + = {0x06, 0x08, 0x10, 0x1E, 0x11, 0x11, 0x0E}; + return glyph; + } + case '7': { + static constexpr unsigned char glyph[] + = {0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}; + return glyph; + } + case '8': { + static constexpr unsigned char glyph[] + = {0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E}; + return glyph; + } + case '9': { + static constexpr unsigned char glyph[] + = {0x0E, 0x11, 0x11, 0x0F, 0x01, 0x02, 0x0C}; + return glyph; + } + case '.': { + static constexpr unsigned char glyph[] + = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C}; + return glyph; + } + case '-': { + static constexpr unsigned char glyph[] + = {0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00}; + return glyph; + } + case '/': { + static constexpr unsigned char glyph[] + = {0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10}; + return glyph; + } + case '=': { + static constexpr unsigned char glyph[] + = {0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00}; + return glyph; + } + case 'x': { + static constexpr unsigned char glyph[] + = {0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11}; + return glyph; + } + case 'y': { + static constexpr unsigned char glyph[] + = {0x00, 0x00, 0x11, 0x0A, 0x04, 0x04, 0x04}; + return glyph; + } + case 'z': { + static constexpr unsigned char glyph[] + = {0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F}; + return glyph; + } + default: + return nullptr; + } +} + +int getBitmapGlyphAdvance(const char ch) +{ + if (ch == ' ') { + return kBitmapGlyphWidth; + } + return kBitmapGlyphWidth + kBitmapGlyphSpacing; +} + +} // namespace + +void TileVisibility::parseFromJson(const std::string& json) +{ + struct BoolField + { + const char* key; + bool TileVisibility::*field; + bool default_val; + }; + + // clang-format off + // NOLINTBEGIN(modernize-use-designated-initializers) + static const BoolField fields[] = { + {"stdcells", &TileVisibility::stdcells, true}, + {"macros", &TileVisibility::macros, true}, + {"pad_input", &TileVisibility::pad_input, true}, + {"pad_output", &TileVisibility::pad_output, true}, + {"pad_inout", &TileVisibility::pad_inout, true}, + {"pad_power", &TileVisibility::pad_power, true}, + {"pad_spacer", &TileVisibility::pad_spacer, true}, + {"pad_areaio", &TileVisibility::pad_areaio, true}, + {"pad_other", &TileVisibility::pad_other, true}, + {"phys_fill", &TileVisibility::phys_fill, true}, + {"phys_endcap", &TileVisibility::phys_endcap, true}, + {"phys_welltap", &TileVisibility::phys_welltap, true}, + {"phys_tie", &TileVisibility::phys_tie, true}, + {"phys_antenna", &TileVisibility::phys_antenna, true}, + {"phys_cover", &TileVisibility::phys_cover, true}, + {"phys_bump", &TileVisibility::phys_bump, true}, + {"phys_other", &TileVisibility::phys_other, true}, + {"std_bufinv", &TileVisibility::std_bufinv, true}, + {"std_bufinv_timing", &TileVisibility::std_bufinv_timing, true}, + {"std_clock_bufinv", &TileVisibility::std_clock_bufinv, true}, + {"std_clock_gate", &TileVisibility::std_clock_gate, true}, + {"std_level_shift", &TileVisibility::std_level_shift, true}, + {"std_sequential", &TileVisibility::std_sequential, true}, + {"std_combinational", &TileVisibility::std_combinational, true}, + {"net_signal", &TileVisibility::net_signal, true}, + {"net_power", &TileVisibility::net_power, true}, + {"net_ground", &TileVisibility::net_ground, true}, + {"net_clock", &TileVisibility::net_clock, true}, + {"net_reset", &TileVisibility::net_reset, true}, + {"net_tieoff", &TileVisibility::net_tieoff, true}, + {"net_scan", &TileVisibility::net_scan, true}, + {"net_analog", &TileVisibility::net_analog, true}, + {"routing", &TileVisibility::routing, true}, + {"special_nets", &TileVisibility::special_nets, true}, + {"pins", &TileVisibility::pins, true}, + {"blockages", &TileVisibility::blockages, true}, + {"placement_blockages", &TileVisibility::placement_blockages, true}, + {"routing_obstructions", &TileVisibility::routing_obstructions, true}, + {"rows", &TileVisibility::rows, false}, + {"tracks_pref", &TileVisibility::tracks_pref, false}, + {"tracks_non_pref", &TileVisibility::tracks_non_pref, false}, + {"debug", &TileVisibility::debug, false}, + }; + // NOLINTEND(modernize-use-designated-initializers) + // clang-format on + + for (const auto& f : fields) { + this->*(f.field) = extract_int_or(json, f.key, f.default_val ? 1 : 0); + } + raw_json_ = json; +} + +bool TileVisibility::isSiteVisible(const std::string& site_name) const +{ + if (!rows) { + return false; + } + const std::string key = "site_" + site_name; + return extract_int_or(raw_json_, key, 0); +} + +bool TileVisibility::isNetVisible(odb::dbNet* net) const +{ + switch (net->getSigType().getValue()) { + case odb::dbSigType::SIGNAL: + return net_signal; + case odb::dbSigType::POWER: + return net_power; + case odb::dbSigType::GROUND: + return net_ground; + case odb::dbSigType::CLOCK: + return net_clock; + case odb::dbSigType::RESET: + return net_reset; + case odb::dbSigType::TIEOFF: + return net_tieoff; + case odb::dbSigType::SCAN: + return net_scan; + case odb::dbSigType::ANALOG: + return net_analog; + } + return true; +} + +bool TileVisibility::isInstVisible(odb::dbInst* inst, sta::dbSta* sta) const +{ + odb::dbMaster* master = inst->getMaster(); + odb::dbMasterType mtype = master->getType(); + + if (sta) { + using IT = sta::dbSta::InstType; + switch (sta->getInstanceType(inst)) { + case IT::BLOCK: + return macros; + case IT::PAD_INPUT: + return pad_input; + case IT::PAD_OUTPUT: + return pad_output; + case IT::PAD_INOUT: + return pad_inout; + case IT::PAD_POWER: + return pad_power; + case IT::PAD_SPACER: + return pad_spacer; + case IT::PAD_AREAIO: + return pad_areaio; + case IT::PAD: + return pad_other; + case IT::ENDCAP: + return phys_endcap; + case IT::FILL: + return phys_fill; + case IT::TAPCELL: + return phys_welltap; + case IT::TIE: + return phys_tie; + case IT::ANTENNA: + return phys_antenna; + case IT::COVER: + return phys_cover; + case IT::BUMP: + return phys_bump; + case IT::LEF_OTHER: + return phys_other; + case IT::STD_BUF: + case IT::STD_INV: + return std_bufinv; + case IT::STD_BUF_TIMING_REPAIR: + case IT::STD_INV_TIMING_REPAIR: + return std_bufinv_timing; + case IT::STD_BUF_CLK_TREE: + case IT::STD_INV_CLK_TREE: + return std_clock_bufinv; + case IT::STD_CLOCK_GATE: + return std_clock_gate; + case IT::STD_LEVEL_SHIFT: + return std_level_shift; + case IT::STD_SEQUENTIAL: + return std_sequential; + case IT::STD_COMBINATIONAL: + return std_combinational; + case IT::STD_CELL: + case IT::STD_PHYSICAL: + case IT::STD_OTHER: + default: + return stdcells; + } + } + + // Fallback: dbMasterType-only classification (no Liberty) + if (mtype.isBlock()) { + return macros; + } + if (mtype.isPad()) { + if (mtype == odb::dbMasterType::PAD_INPUT) { + return pad_input; + } + if (mtype == odb::dbMasterType::PAD_OUTPUT) { + return pad_output; + } + if (mtype == odb::dbMasterType::PAD_INOUT) { + return pad_inout; + } + if (mtype == odb::dbMasterType::PAD_POWER) { + return pad_power; + } + if (mtype == odb::dbMasterType::PAD_SPACER) { + return pad_spacer; + } + if (mtype == odb::dbMasterType::PAD_AREAIO) { + return pad_areaio; + } + return pad_other; + } + if (mtype.isEndCap()) { + return phys_endcap; + } + if (master->isFiller()) { + return phys_fill; + } + if (mtype == odb::dbMasterType::CORE_WELLTAP) { + return phys_welltap; + } + if (mtype == odb::dbMasterType::CORE_TIEHIGH + || mtype == odb::dbMasterType::CORE_TIELOW) { + return phys_tie; + } + if (mtype == odb::dbMasterType::CORE_ANTENNACELL) { + return phys_antenna; + } + if (mtype.isCover()) { + if (mtype == odb::dbMasterType::COVER_BUMP) { + return phys_bump; + } + return phys_cover; + } + if (mtype == odb::dbMasterType::CORE_SPACER + || inst->getSourceType() == odb::dbSourceType::DIST) { + return phys_other; + } + return stdcells; +} + +////////////////////////////////////////////////// + +TileGenerator::TileGenerator(odb::dbDatabase* db, + sta::dbSta* sta, + utl::Logger* logger) + : db_(db), + sta_(sta), + logger_(logger), + search_(std::make_unique(logger)) +{ + odb::dbChip* chip = db_->getChip(); + if (chip) { + search_->setTopChip(chip); + } +} + +TileGenerator::~TileGenerator() = default; + +void TileGenerator::eagerInit() +{ + odb::dbChip* chip = db_->getChip(); + if (chip) { + search_->setTopChip(chip); + } + odb::dbBlock* block = getBlock(); + if (block) { + search_->eagerInit(block); + } +} + +bool TileGenerator::shapesReady() const +{ + return search_->shapesReady(); +} + +/* static */ +odb::Rect TileGenerator::toPixels(const double scale, + const odb::Rect& rect, + const odb::Rect& dbu_tile) +{ + return odb::Rect((rect.xMin() - dbu_tile.xMin()) * scale, + (rect.yMin() - dbu_tile.yMin()) * scale, + std::ceil((rect.xMax() - dbu_tile.xMin()) * scale), + std::ceil((rect.yMax() - dbu_tile.yMin()) * scale)); +} + +void TileGenerator::setPixel(std::vector& image, + const int x, + const int y, + const Color& c) const +{ + if (x < 0 || x >= kTileSizeInPixel || y < 0 || y >= kTileSizeInPixel) { + return; + } + const int index = (y * kTileSizeInPixel + x) * 4; + image[index + 0] = c.r; + image[index + 1] = c.g; + image[index + 2] = c.b; + image[index + 3] = c.a; +} + +void TileGenerator::fillPolygon(std::vector& image, + const odb::Polygon& poly, + const odb::Rect& dbu_tile, + const double scale, + const Color& color, + const bool blend) const +{ + const auto& points = poly.getPoints(); + const int n = static_cast(points.size()); + if (n < 3) { + return; + } + + // Convert polygon points to pixel coordinates (floating point for precision). + std::vector px(n), py(n); + for (int i = 0; i < n; ++i) { + px[i] = (points[i].x() - dbu_tile.xMin()) * scale; + py[i] = (points[i].y() - dbu_tile.yMin()) * scale; + } + + // Compute pixel bounding box, clamped to tile. + const double min_py = std::ranges::min(py); + const double max_py = std::ranges::max(py); + const int iy_min = std::max(0, static_cast(min_py)); + const int iy_max + = std::min(kTileSizeInPixel, static_cast(std::ceil(max_py))); + + // Scanline fill: for each row, find edge intersections and fill between + // pairs. + std::vector x_intercepts; + for (int iy = iy_min; iy < iy_max; ++iy) { + const double scanline = iy + 0.5; // test at pixel center + x_intercepts.clear(); + + for (int i = 0, j = n - 1; i < n; j = i++) { + // Skip degenerate (horizontal) edges and only process edges that + // straddle the scanline. + if ((py[i] <= scanline) == (py[j] <= scanline)) { + continue; + } + const double x + = px[i] + (scanline - py[i]) / (py[j] - py[i]) * (px[j] - px[i]); + x_intercepts.push_back(x); + } + + std::ranges::sort(x_intercepts); + + for (size_t k = 0; k + 1 < x_intercepts.size(); k += 2) { + const int ix_min = std::max(0, static_cast(x_intercepts[k])); + const int ix_max = std::min( + kTileSizeInPixel, static_cast(std::ceil(x_intercepts[k + 1]))); + const int draw_y = 255 - iy; + for (int ix = ix_min; ix < ix_max; ++ix) { + if (blend) { + blendPixel(image, ix, draw_y, color); + } else { + setPixel(image, ix, draw_y, color); + } + } + } + } +} + +odb::Rect TileGenerator::getBounds() const +{ + odb::Rect bounds; + if (odb::dbBlock* block = getBlock()) { + bounds = block->getBBox()->getBox(); + } + return bounds; +} + +std::vector TileGenerator::getLayers() const +{ + std::vector layers; + odb::dbTech* tech = db_->getTech(); + if (!tech) { + return layers; + } + for (odb::dbTechLayer* layer : tech->getLayers()) { + if (layer->getRoutingLevel() > 0 + || layer->getType() == odb::dbTechLayerType::CUT) { + layers.push_back(layer->getName()); + } + } + return layers; +} + +std::vector TileGenerator::getSites() const +{ + std::set seen; + std::vector sites; + odb::dbBlock* block = getBlock(); + if (!block) { + return sites; + } + for (odb::dbRow* row : block->getRows()) { + odb::dbSite* site = row->getSite(); + if (site && seen.insert(site->getName()).second) { + sites.push_back(site->getName()); + } + } + return sites; +} + +std::vector TileGenerator::selectAt( + const int dbu_x, + const int dbu_y, + const int zoom, + const TileVisibility& vis, + const std::set& visible_layers) +{ + std::vector results; + odb::dbBlock* block = getBlock(); + if (!block) { + return results; + } + // Compute a search margin of 2 pixels at the current zoom level. + // This accounts for coordinate conversion rounding between the client's + // Leaflet CRS.Simple coordinates and the server's DBU space. + const int num_tiles = 1 << std::max(0, zoom); + const int margin + = std::max(1, getBounds().maxDXDY() / (kTileSizeInPixel * num_tiles) * 2); + debugPrint(logger_, + utl::WEB, + "select", + 1, + "selectAt dbu=({},{}) zoom={} margin={}", + dbu_x, + dbu_y, + zoom, + margin); + + const int x_lo = dbu_x - margin; + const int y_lo = dbu_y - margin; + const int x_hi = dbu_x + margin; + const int y_hi = dbu_y + margin; + const odb::Point click_pt(dbu_x, dbu_y); + + // Search instances + for (odb::dbInst* inst : + search_->searchInsts(block, x_lo, y_lo, x_hi, y_hi)) { + const odb::Rect bbox = inst->getBBox()->getBox(); + if (bbox.intersects(click_pt) && vis.isInstVisible(inst, sta_)) { + results.push_back({inst, inst->getName(), "Inst", bbox}); + } + } + // Sort instances by area descending so larger instances (macros) come first + std::ranges::sort(results, [](const auto& a, const auto& b) { + return a.bbox.area() > b.bbox.area(); + }); + + // Search nets via routing shapes on each layer + std::set seen_nets; + odb::dbTech* tech = db_->getTech(); + for (odb::dbTechLayer* layer : tech->getLayers()) { + if (layer->getRoutingLevel() <= 0 + && layer->getType() != odb::dbTechLayerType::CUT) { + continue; + } + if (!visible_layers.empty() && !visible_layers.contains(layer->getName())) { + continue; + } + + // Regular routing shapes (wires, vias, bterms) + if (vis.routing) { + for (const auto& shape : + search_->searchBoxShapes(block, layer, x_lo, y_lo, x_hi, y_hi)) { + odb::dbNet* net = std::get<2>(shape); + if (seen_nets.contains(net)) { + continue; + } + const odb::Rect& box = std::get<0>(shape); + if (box.intersects(click_pt) && vis.isNetVisible(net)) { + seen_nets.insert(net); + results.push_back({net, net->getName(), "Net", net->getTermBBox()}); + } + } + } + + // Special net shapes (power/ground straps) + if (vis.special_nets) { + for (const auto& shape : + search_->searchSNetViaShapes(block, layer, x_lo, y_lo, x_hi, y_hi)) { + odb::dbNet* net = std::get<1>(shape); + if (seen_nets.contains(net)) { + continue; + } + const odb::Rect box = std::get<0>(shape)->getBox(); + if (box.intersects(click_pt) && vis.isNetVisible(net)) { + seen_nets.insert(net); + results.push_back({net, net->getName(), "Net", net->getTermBBox()}); + } + } + + for (const auto& shape : + search_->searchSNetShapes(block, layer, x_lo, y_lo, x_hi, y_hi)) { + odb::dbNet* net = std::get<2>(shape); + if (seen_nets.contains(net)) { + continue; + } + const odb::Rect box = std::get<0>(shape)->getBox(); + if (box.intersects(click_pt) && vis.isNetVisible(net)) { + seen_nets.insert(net); + results.push_back({net, net->getName(), "Net", net->getTermBBox()}); + } + } + } + } + + debugPrint(logger_, + utl::WEB, + "select", + 1, + " selected={} (insts={}, nets={})", + results.size(), + results.size() - seen_nets.size(), + seen_nets.size()); + return results; +} + +odb::dbBlock* TileGenerator::getBlock() const +{ + odb::dbChip* chip = db_->getChip(); + return chip ? chip->getBlock() : nullptr; +} + +std::vector TileGenerator::generateTile( + const std::string& layer, + const int z, + const int x, + int y, + const TileVisibility& vis, + const std::vector& highlight_rects, + const std::vector& highlight_polys, + const std::vector& colored_rects, + const std::vector& flight_lines, + const std::map* module_colors, + const std::set* focus_net_ids, + const std::set* route_guide_net_ids) const +{ + static_assert(sizeof(Color) == 4); + constexpr int buffer_size = kTileSizeInPixel * kTileSizeInPixel * 4; + std::vector image_buffer(buffer_size, 0); + + // No design loaded — return blank tile. + if (!getBlock()) { + std::vector png_data; + lodepng::encode(png_data, image_buffer, kTileSizeInPixel, kTileSizeInPixel); + return png_data; + } + + // Per-layer colors: routing level 1=blue, 2=red, then distinct hues + static const Color palette[] = { + // clang-format off + // NOLINTBEGIN(modernize-use-designated-initializers) + { 70, 130, 210, 180}, // moderate blue + {200, 50, 50, 180}, // red + { 50, 180, 80, 180}, // green + {200, 160, 40, 180}, // amber + {160, 60, 200, 180}, // purple + { 40, 190, 190, 180}, // teal + {220, 120, 50, 180}, // orange + {180, 70, 150, 180}, // magenta + // NOLINTEND(modernize-use-designated-initializers) + // clang-format on + }; + static constexpr int palette_size = sizeof(palette) / sizeof(palette[0]); + + odb::dbTech* tech = db_->getTech(); + odb::dbTechLayer* tech_layer = tech->findLayer(layer.c_str()); + + int layer_index = 0; + if (tech_layer) { + const auto all_layers = getLayers(); + auto it = std::ranges::find(all_layers, layer); + if (it != all_layers.end()) { + layer_index = std::distance(all_layers.begin(), it); + } + } + const Color color = palette[layer_index % palette_size]; + Color obs_color = color.lighter(); + + // Determine our tile's bounding box in dbu coordinates. + const double num_tiles_at_zoom = pow(2, z); + if (x >= 0 && y >= 0 && x < num_tiles_at_zoom && y < num_tiles_at_zoom) { + y = num_tiles_at_zoom - 1 - y; // flip + const double tile_dbu_size = getBounds().maxDXDY() / num_tiles_at_zoom; + const int dbu_x_min = x * tile_dbu_size; + const int dbu_y_min = y * tile_dbu_size; + const int dbu_x_max = std::ceil((x + 1) * tile_dbu_size); + const int dbu_y_max = std::ceil((y + 1) * tile_dbu_size); + const odb::Rect dbu_tile(dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max); + const double scale = kTileSizeInPixel / tile_dbu_size; + + odb::dbBlock* block = getBlock(); + + // Special "_modules" layer: draw filled module-colored rectangles + const bool modules_layer + = (layer == "_modules" && module_colors && !module_colors->empty()); + if (modules_layer) { + for (odb::dbInst* inst : search_->searchInsts( + block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { + odb::Rect inst_bbox = inst->getBBox()->getBox(); + if (!dbu_tile.overlaps(inst_bbox)) { + continue; + } + odb::dbModule* mod = inst->getModule(); + if (!mod) { + continue; + } + auto it = module_colors->find(mod->getId()); + if (it == module_colors->end()) { + continue; + } + const Color& c = it->second; + const int pxl + = std::max(0, (int) ((inst_bbox.xMin() - dbu_x_min) * scale)); + const int pyl + = std::max(0, (int) ((inst_bbox.yMin() - dbu_y_min) * scale)); + const int pxh = std::min( + 255, (int) std::ceil((inst_bbox.xMax() - dbu_x_min) * scale)); + const int pyh = std::min( + 255, (int) std::ceil((inst_bbox.yMax() - dbu_y_min) * scale)); + for (int iy = pyl; iy < pyh; ++iy) { + for (int ix = pxl; ix < pxh; ++ix) { + blendPixel(image_buffer, ix, 255 - iy, c); + } + } + } + } + + // Special "_instances" layer: only draw instance borders, no routing + const bool instances_only = (layer == "_instances"); + + // "_modules" layer only draws filled module-color rects (already done + // above); skip all other drawing (instances, routing, etc.) + if (!modules_layer) { + // Draw instances + for (odb::dbInst* inst : search_->searchInsts( + block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { + odb::Rect inst_bbox = inst->getBBox()->getBox(); + if (!dbu_tile.overlaps(inst_bbox)) { + continue; + } + odb::dbMaster* master = inst->getMaster(); + + if (!vis.isInstVisible(inst, sta_)) { + continue; + } + const int xl = inst_bbox.xMin(); + const int yl = inst_bbox.yMin(); + const int xh = inst_bbox.xMax(); + const int yh = inst_bbox.yMax(); + + const int pixel_xl = (int) ((xl - dbu_x_min) * scale); + const int pixel_yl = (int) ((yl - dbu_y_min) * scale); + const int pixel_xh = (int) std::ceil((xh - dbu_x_min) * scale); + const int pixel_yh = (int) std::ceil((yh - dbu_y_min) * scale); + + if (instances_only) { + // Draw the rectangle border (instances-only layer) + Color gray{.r = 128, .g = 128, .b = 128, .a = 255}; + if (dbu_x_min <= xl && xl <= dbu_x_max) { + for (int iy = pixel_yl; iy < pixel_yh; ++iy) { + const int draw_y = (255 - iy); + setPixel(image_buffer, pixel_xl, draw_y, gray); + } + } + if (dbu_x_min <= xh && xh <= dbu_x_max) { + for (int iy = pixel_yl; iy < pixel_yh; ++iy) { + const int draw_y = (255 - iy); + setPixel(image_buffer, pixel_xh, draw_y, gray); + } + } + if (dbu_y_min <= yl && yl <= dbu_y_max) { + for (int ix = pixel_xl; ix < pixel_xh; ++ix) { + const int draw_y = (255 - pixel_yl); + setPixel(image_buffer, ix, draw_y, gray); + } + } + if (dbu_y_min <= yh && yh <= dbu_y_max) { + for (int ix = pixel_xl; ix < pixel_xh; ++ix) { + const int draw_y = (255 - pixel_yh); + setPixel(image_buffer, ix, draw_y, gray); + } + } + } else { + // Layer-specific: obstructions and pins + if (vis.blockages) { + for (odb::dbPolygon* poly_obs : master->getPolygonObstructions()) { + if (tech_layer && poly_obs->getTechLayer() != tech_layer) { + continue; + } + odb::Polygon poly = poly_obs->getPolygon(); + inst->getTransform().apply(poly); + fillPolygon(image_buffer, poly, dbu_tile, scale, obs_color); + } + for (odb::dbBox* obs : master->getObstructions(false)) { + if (tech_layer && obs->getTechLayer() != tech_layer) { + continue; + } + odb::Rect box = obs->getBox(); + inst->getTransform().apply(box); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + const int draw_y = (255 - iy); + setPixel(image_buffer, ix, draw_y, obs_color); + } + } + } + } + + if (vis.pins) { + for (odb::dbMTerm* mterm : master->getMTerms()) { + for (odb::dbMPin* mpin : mterm->getMPins()) { + for (odb::dbPolygon* poly_geom : mpin->getPolygonGeometry()) { + if (tech_layer && poly_geom->getTechLayer() != tech_layer) { + continue; + } + odb::Polygon poly = poly_geom->getPolygon(); + inst->getTransform().apply(poly); + fillPolygon(image_buffer, poly, dbu_tile, scale, color); + } + for (odb::dbBox* geom : mpin->getGeometry(false)) { + if (tech_layer && geom->getTechLayer() != tech_layer) { + continue; + } + odb::Rect box = geom->getBox(); + inst->getTransform().apply(box); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + const int draw_y = (255 - iy); + setPixel(image_buffer, ix, draw_y, color); + } + } + } + } + } + } + } + } + + // Draw routing shapes (wires, vias, bterms) on top of instances + if (!instances_only && tech_layer && vis.routing && shapesReady()) { + for (const auto& shape : search_->searchBoxShapes(block, + tech_layer, + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max)) { + odb::dbNet* net = std::get<2>(shape); + if (!vis.isNetVisible(net)) { + continue; + } + if (focus_net_ids && !focus_net_ids->empty() + && focus_net_ids->find(net->getId()) == focus_net_ids->end()) { + continue; + } + const odb::Rect& box = std::get<0>(shape); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + const int draw_y = (255 - iy); + setPixel(image_buffer, ix, draw_y, color); + } + } + } + } + + // Draw special net shapes (power/ground straps) on top of instances + if (!instances_only && tech_layer && vis.special_nets && shapesReady()) { + for (const auto& shape : search_->searchSNetShapes(block, + tech_layer, + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max)) { + odb::dbNet* snet = std::get<2>(shape); + if (!vis.isNetVisible(snet)) { + continue; + } + if (focus_net_ids && !focus_net_ids->empty() + && focus_net_ids->find(snet->getId()) == focus_net_ids->end()) { + continue; + } + const odb::Rect box = std::get<0>(shape)->getBox(); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Polygon& poly = std::get<1>(shape); + fillPolygon(image_buffer, poly, dbu_tile, scale, color); + } + } + + // Draw special net vias — decompose into individual cut boxes + if (!instances_only && tech_layer && vis.special_nets && shapesReady()) { + for (const auto& shape : search_->searchSNetViaShapes(block, + tech_layer, + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max)) { + odb::dbNet* via_net = std::get<1>(shape); + if (!vis.isNetVisible(via_net)) { + continue; + } + if (focus_net_ids && !focus_net_ids->empty() + && focus_net_ids->find(via_net->getId()) + == focus_net_ids->end()) { + continue; + } + odb::dbSBox* sbox = std::get<0>(shape); + std::vector via_boxes; + if (auto tech_via = sbox->getTechVia()) { + via_boxes.assign(tech_via->getBoxes().begin(), + tech_via->getBoxes().end()); + } else if (auto block_via = sbox->getBlockVia()) { + via_boxes.assign(block_via->getBoxes().begin(), + block_via->getBoxes().end()); + } + const odb::Point origin((sbox->xMin() + sbox->xMax()) / 2, + (sbox->yMin() + sbox->yMax()) / 2); + for (odb::dbBox* vbox : via_boxes) { + if (vbox->getTechLayer() != tech_layer) { + continue; + } + odb::Rect box = vbox->getBox(); + box.moveDelta(origin.x(), origin.y()); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + const int draw_y = (255 - iy); + setPixel(image_buffer, ix, draw_y, color); + } + } + } + } + } + + // Draw placement blockages (dbBlockage) on the _instances layer. + // Diagonal white hash lines in pixel space, with period anchored in dbu + // coordinates so the pattern is seamless across tile boundaries. + if (instances_only && vis.placement_blockages) { + const Color hash_color{.r = 255, .g = 255, .b = 255, .a = 180}; + constexpr int kPixelPeriod = 20; // pixels between line centers + constexpr int kLineWidth = 2; // pixels wide + for (odb::dbBlockage* blk : search_->searchBlockages( + block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { + odb::Rect box = blk->getBBox()->getBox(); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + // Offset in absolute pixel coordinates for seamless tiling + const int ox = (int) (dbu_x_min * scale); + const int oy = (int) (dbu_y_min * scale); + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + if (((ix + ox) + (iy + oy)) % kPixelPeriod < kLineWidth) { + blendPixel(image_buffer, ix, 255 - iy, hash_color); + } + } + } + } + } + + // Draw routing obstructions (dbObstruction) on per-layer tiles. + // Same diagonal white hash lines. + if (!instances_only && tech_layer && vis.routing_obstructions) { + const Color hash_color{.r = 255, .g = 255, .b = 255, .a = 180}; + constexpr int kPixelPeriod = 20; + constexpr int kLineWidth = 2; + for (odb::dbObstruction* obs : search_->searchObstructions(block, + tech_layer, + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max)) { + odb::Rect box = obs->getBBox()->getBox(); + if (!box.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + const int ox = (int) (dbu_x_min * scale); + const int oy = (int) (dbu_y_min * scale); + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + if (((ix + ox) + (iy + oy)) % kPixelPeriod < kLineWidth) { + blendPixel(image_buffer, ix, 255 - iy, hash_color); + } + } + } + } + } + + // Draw rows as outlines on the _instances layer + if (instances_only && vis.rows) { + const Color row_color{ + .r = 60, .g = 180, .b = 60, .a = 180}; // green outlines + for (const auto& [row_rect, row] : search_->searchRows( + block, dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max)) { + if (!row_rect.overlaps(dbu_tile)) { + continue; + } + odb::dbSite* site = row->getSite(); + if (site && !vis.isSiteVisible(site->getName())) { + continue; + } + const odb::Rect draw = toPixels(scale, row_rect, dbu_tile); + // Draw outline only (top, bottom, left, right edges) + for (int ix = draw.xMin(); ix <= draw.xMax(); ++ix) { + blendPixel(image_buffer, ix, 255 - draw.yMin(), row_color); + blendPixel(image_buffer, ix, 255 - draw.yMax(), row_color); + } + for (int iy = draw.yMin(); iy <= draw.yMax(); ++iy) { + blendPixel(image_buffer, draw.xMin(), 255 - iy, row_color); + blendPixel(image_buffer, draw.xMax(), 255 - iy, row_color); + } + } + } + + // Draw tracks on per-layer tiles + if (!instances_only && tech_layer + && (vis.tracks_pref || vis.tracks_non_pref)) { + odb::dbTrackGrid* grid = block->findTrackGrid(tech_layer); + debugPrint(logger_, + utl::WEB, + "tile", + 1, + "tracks: layer={} grid={} pref={} non_pref={}", + layer, + grid != nullptr, + vis.tracks_pref, + vis.tracks_non_pref); + if (grid) { + Color track_color = color; + track_color.a = 150; + const bool is_horizontal + = tech_layer->getDirection() == odb::dbTechLayerDir::HORIZONTAL; + + // X-direction tracks (vertical lines on screen) + // Preferred for vertical layers, non-preferred for horizontal layers + if ((!is_horizontal && vis.tracks_pref) + || (is_horizontal && vis.tracks_non_pref)) { + std::vector x_grid; + grid->getGridX(x_grid); + debugPrint(logger_, + utl::WEB, + "tile", + 1, + " x_tracks: count={} tile=[{},{},{},{}]", + x_grid.size(), + dbu_x_min, + dbu_y_min, + dbu_x_max, + dbu_y_max); + for (int tx : x_grid) { + if (tx < dbu_x_min || tx > dbu_x_max) { + continue; + } + const int px = static_cast((tx - dbu_x_min) * scale); + if (px >= 0 && px < kTileSizeInPixel) { + for (int py = 0; py < kTileSizeInPixel; ++py) { + blendPixel(image_buffer, px, py, track_color); + } + } + } + } + + // Y-direction tracks (horizontal lines on screen) + // Preferred for horizontal layers, non-preferred for vertical layers + if ((is_horizontal && vis.tracks_pref) + || (!is_horizontal && vis.tracks_non_pref)) { + std::vector y_grid; + grid->getGridY(y_grid); + debugPrint(logger_, + utl::WEB, + "tile", + 1, + " y_tracks: count={}", + y_grid.size()); + for (int ty : y_grid) { + if (ty < dbu_y_min || ty > dbu_y_max) { + continue; + } + const int py = 255 - static_cast((ty - dbu_y_min) * scale); + if (py >= 0 && py < kTileSizeInPixel) { + for (int px = 0; px < kTileSizeInPixel; ++px) { + blendPixel(image_buffer, px, py, track_color); + } + } + } + } + } + } + + } // end if (!modules_layer) + + if (!highlight_rects.empty() || !highlight_polys.empty()) { + drawHighlight( + image_buffer, highlight_rects, highlight_polys, dbu_tile, scale); + } + if (!colored_rects.empty()) { + drawColoredHighlight(image_buffer, colored_rects, layer, dbu_tile, scale); + } + if (!flight_lines.empty()) { + drawFlightLines(image_buffer, flight_lines, dbu_tile, scale); + } + if (route_guide_net_ids && !route_guide_net_ids->empty() && tech_layer) { + drawRouteGuides( + image_buffer, *route_guide_net_ids, layer, color, dbu_tile, scale); + } + } + + if (vis.debug) { + drawDebugOverlay(image_buffer, z, x, y); + } + + std::vector png_data; + unsigned error = lodepng::encode( + png_data, image_buffer, kTileSizeInPixel, kTileSizeInPixel); + if (error) { + logger_->report("PNG encoder error: {}", lodepng_error_text(error)); + } + + if (logger_->debugCheck(utl::WEB, "tile_generator", 1)) { + std::string filename = "/tmp/tile_" + layer + "_" + std::to_string(z) + "_" + + std::to_string(x) + "_" + std::to_string(y) + + ".png"; + lodepng::save_file(png_data, filename); + } + + return png_data; +} + +std::vector TileGenerator::generateHeatMapTile( + gui::HeatMapDataSource& source, + const int z, + const int x, + int y) const +{ + constexpr int buffer_size = kTileSizeInPixel * kTileSizeInPixel * 4; + std::vector image_buffer(buffer_size, 0); + + const double num_tiles_at_zoom = pow(2, z); + if (x < 0 || y < 0 || x >= num_tiles_at_zoom || y >= num_tiles_at_zoom) { + return {}; + } + + y = num_tiles_at_zoom - 1 - y; + const double tile_dbu_size = getBounds().maxDXDY() / num_tiles_at_zoom; + const int dbu_x_min = x * tile_dbu_size; + const int dbu_y_min = y * tile_dbu_size; + const int dbu_x_max = std::ceil((x + 1) * tile_dbu_size); + const int dbu_y_max = std::ceil((y + 1) * tile_dbu_size); + const odb::Rect dbu_tile(dbu_x_min, dbu_y_min, dbu_x_max, dbu_y_max); + const double scale = kTileSizeInPixel / tile_dbu_size; + constexpr double text_rect_margin = 0.8; + constexpr int text_scale = 2; + const Color text_color{.r = 255, .g = 255, .b = 255, .a = 255}; + + for (const auto& map_point : source.getVisibleMap(dbu_tile, scale)) { + if (!map_point.rect.overlaps(dbu_tile)) { + continue; + } + const odb::Rect overlap = map_point.rect.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + const Color color{.r = static_cast(map_point.color.r), + .g = static_cast(map_point.color.g), + .b = static_cast(map_point.color.b), + .a = static_cast(map_point.color.a)}; + + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + blendPixel(image_buffer, ix, 255 - iy, color); + } + } + + if (!source.getShowNumbers() || !map_point.has_value) { + continue; + } + + const std::string text = source.formatValue(map_point.value, false); + const int text_width = getBitmapTextWidth(text, text_scale); + const int text_height = getBitmapTextHeight(text_scale); + const double rect_width = map_point.rect.dx() * scale; + const double rect_height = map_point.rect.dy() * scale; + if (text_width >= text_rect_margin * rect_width + || text_height >= text_rect_margin * rect_height) { + continue; + } + + const double center_x + = 0.5 * (map_point.rect.xMin() + map_point.rect.xMax()); + const double center_y + = 0.5 * (map_point.rect.yMin() + map_point.rect.yMax()); + if (center_x < dbu_tile.xMin() || center_x >= dbu_tile.xMax() + || center_y < dbu_tile.yMin() || center_y >= dbu_tile.yMax()) { + continue; + } + + const int pixel_x = std::lround((center_x - dbu_tile.xMin()) * scale); + const int pixel_y = 255 - std::lround((center_y - dbu_tile.yMin()) * scale); + drawBitmapText(image_buffer, + pixel_x - text_width / 2, + pixel_y - text_height / 2, + text, + text_scale, + text_color); + } + + std::vector png_data; + unsigned error = lodepng::encode( + png_data, image_buffer, kTileSizeInPixel, kTileSizeInPixel); + if (error) { + logger_->report("PNG encoder error: {}", lodepng_error_text(error)); + } + return png_data; +} + +void TileGenerator::drawDebugOverlay(std::vector& image, + const int z, + const int x, + const int y) const +{ + const Color yellow{.r = 255, .g = 255, .b = 0, .a = 255}; + const int last = kTileSizeInPixel - 1; + + // Draw 1-pixel yellow border + for (int i = 0; i < kTileSizeInPixel; ++i) { + setPixel(image, i, 0, yellow); + setPixel(image, i, last, yellow); + setPixel(image, 0, i, yellow); + setPixel(image, last, i, yellow); + } + + // Build the label string "z= /" + std::string label = "z=" + std::to_string(z) + " " + std::to_string(x) + "/" + + std::to_string(y); + + drawBitmapText(image, 4, 4, label, 3, yellow); +} + +/* static */ +int TileGenerator::getBitmapTextWidth(const std::string_view text, + const int scale) +{ + if (text.empty()) { + return 0; + } + + int width = 0; + for (const char ch : text) { + width += getBitmapGlyphAdvance(ch); + } + + return (width - kBitmapGlyphSpacing) * scale; +} + +/* static */ +int TileGenerator::getBitmapTextHeight(const int scale) +{ + return kBitmapGlyphHeight * scale; +} + +/* static */ +void TileGenerator::drawBitmapText(std::vector& image, + const int x, + const int y, + const std::string_view text, + const int scale, + const Color& color) +{ + int cursor_x = x; + for (const char ch : text) { + if (ch == ' ') { + cursor_x += getBitmapGlyphAdvance(ch) * scale; + continue; + } + + const unsigned char* glyph = getBitmapGlyph(ch); + if (glyph == nullptr) { + cursor_x += getBitmapGlyphAdvance(ch) * scale; + continue; + } + + for (int row = 0; row < kBitmapGlyphHeight; ++row) { + const unsigned char bits = glyph[row]; + for (int col = 0; col < kBitmapGlyphWidth; ++col) { + if ((bits & (0x10 >> col)) == 0) { + continue; + } + for (int sy = 0; sy < scale; ++sy) { + for (int sx = 0; sx < scale; ++sx) { + blendPixel(image, + cursor_x + col * scale + sx, + y + row * scale + sy, + color); + } + } + } + } + + cursor_x += getBitmapGlyphAdvance(ch) * scale; + } +} + +/* static */ +void TileGenerator::blendPixel(std::vector& image, + const int x, + const int y, + const Color& c) +{ + if (x < 0 || x >= kTileSizeInPixel || y < 0 || y >= kTileSizeInPixel) { + return; + } + const int i = (y * kTileSizeInPixel + x) * 4; + const float src_a = c.a / 255.0f; + const float dst_a = image[i + 3] / 255.0f; + const float out_a = src_a + dst_a * (1.0f - src_a); + + if (out_a <= 0.0f) { + image[i + 0] = 0; + image[i + 1] = 0; + image[i + 2] = 0; + image[i + 3] = 0; + return; + } + + const auto blend_channel = [&](const int src, const int dst) { + const float out = (src * src_a + dst * dst_a * (1.0f - src_a)) / out_a; + return static_cast(std::lround(out)); + }; + + image[i + 0] = blend_channel(c.r, image[i + 0]); + image[i + 1] = blend_channel(c.g, image[i + 1]); + image[i + 2] = blend_channel(c.b, image[i + 2]); + image[i + 3] = static_cast(std::lround(out_a * 255.0f)); +} + +void TileGenerator::drawHighlight(std::vector& image, + const std::vector& rects, + const std::vector& polys, + const odb::Rect& dbu_tile, + const double scale) const +{ + const Color fill{.r = 255, .g = 255, .b = 0, .a = 30}; + const Color border{.r = 255, .g = 255, .b = 0, .a = 255}; + + for (const odb::Rect& rect : rects) { + if (!dbu_tile.overlaps(rect)) { + continue; + } + const odb::Rect overlap = rect.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + + // Semi-transparent yellow fill + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + blendPixel(image, ix, 255 - iy, fill); + } + } + + // Solid yellow border (only where edge is within the tile) + const odb::Rect full_draw = toPixels(scale, rect, dbu_tile); + if (full_draw.xMin() >= 0 && full_draw.xMin() < kTileSizeInPixel) { + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + setPixel(image, full_draw.xMin(), 255 - iy, border); + } + } + if (full_draw.xMax() > 0 && full_draw.xMax() <= kTileSizeInPixel) { + const int rx = full_draw.xMax() - 1; + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + setPixel(image, rx, 255 - iy, border); + } + } + if (full_draw.yMin() >= 0 && full_draw.yMin() < kTileSizeInPixel) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + setPixel(image, ix, 255 - full_draw.yMin(), border); + } + } + if (full_draw.yMax() > 0 && full_draw.yMax() <= kTileSizeInPixel) { + const int ty = full_draw.yMax() - 1; + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + setPixel(image, ix, 255 - ty, border); + } + } + } + + // Polygon highlights (octilinear shapes) + for (const odb::Polygon& poly : polys) { + const odb::Rect bbox = poly.getEnclosingRect(); + if (!dbu_tile.overlaps(bbox)) { + continue; + } + + // Semi-transparent yellow fill + fillPolygon(image, poly, dbu_tile, scale, fill, /*blend=*/true); + + // Solid yellow border — draw each edge + const auto& points = poly.getPoints(); + const int n = static_cast(points.size()); + for (int i = 0; i < n - 1; ++i) { + const int px0 + = static_cast((points[i].x() - dbu_tile.xMin()) * scale); + const int py0 + = 255 - static_cast((points[i].y() - dbu_tile.yMin()) * scale); + const int px1 + = static_cast((points[i + 1].x() - dbu_tile.xMin()) * scale); + const int py1 + = 255 + - static_cast((points[i + 1].y() - dbu_tile.yMin()) * scale); + drawLine(image, px0, py0, px1, py1, border); + } + } +} + +void TileGenerator::drawColoredHighlight(std::vector& image, + const std::vector& rects, + const std::string& current_layer, + const odb::Rect& dbu_tile, + const double scale) const +{ + const bool is_instances_layer = (current_layer == "_instances"); + for (const auto& cr : rects) { + // Layer filtering: draw on _instances (overview) or matching layer + if (!is_instances_layer && !cr.layer.empty() && cr.layer != current_layer) { + continue; + } + if (!dbu_tile.overlaps(cr.rect)) { + continue; + } + const odb::Rect overlap = cr.rect.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + + // Draw a fixed-width centerline through the shape (cosmetic pen style, + // matching the GUI's 2px cosmetic pen approach from dbDescriptors.cpp). + // This ensures consistent visibility regardless of zoom level. + const int cx = (draw.xMin() + draw.xMax()) / 2; + const int cy = (draw.yMin() + draw.yMax()) / 2; + + Color line_color = cr.color; + line_color.a = 255; + + if (draw.dx() >= draw.dy()) { + // Horizontal shape: draw horizontal centerline + drawLine(image, draw.xMin(), 255 - cy, draw.xMax(), 255 - cy, line_color); + } else { + // Vertical shape: draw vertical centerline + drawLine(image, cx, 255 - draw.yMin(), cx, 255 - draw.yMax(), line_color); + } + } +} + +/* static */ +void TileGenerator::drawLine(std::vector& image, + int x0, + int y0, + int x1, + int y1, + const Color& c) +{ + // Bresenham's line algorithm + int dx = std::abs(x1 - x0); + int dy = std::abs(y1 - y0); + int sx = x0 < x1 ? 1 : -1; + int sy = y0 < y1 ? 1 : -1; + int err = dx - dy; + + while (true) { + // Draw 3px wide + for (int dy2 = -1; dy2 <= 1; dy2++) { + for (int dx2 = -1; dx2 <= 1; dx2++) { + blendPixel(image, x0 + dx2, y0 + dy2, c); + } + } + + if (x0 == x1 && y0 == y1) { + break; + } + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } +} + +void TileGenerator::drawFlightLines(std::vector& image, + const std::vector& lines, + const odb::Rect& dbu_tile, + const double scale) const +{ + for (const auto& fl : lines) { + // Convert DBU to pixel coordinates + int px0 = static_cast((fl.p1.x() - dbu_tile.xMin()) * scale); + int py0 = 255 - static_cast((fl.p1.y() - dbu_tile.yMin()) * scale); + int px1 = static_cast((fl.p2.x() - dbu_tile.xMin()) * scale); + int py1 = 255 - static_cast((fl.p2.y() - dbu_tile.yMin()) * scale); + + // Rough bounding-box check: skip if line can't cross this tile + int lx0 = std::min(px0, px1), lx1 = std::max(px0, px1); + int ly0 = std::min(py0, py1), ly1 = std::max(py0, py1); + if (lx1 < 0 || lx0 >= kTileSizeInPixel || ly1 < 0 + || ly0 >= kTileSizeInPixel) { + continue; + } + + Color c = fl.color; + c.a = 220; + drawLine(image, px0, py0, px1, py1, c); + } +} + +void TileGenerator::drawRouteGuides(std::vector& image, + const std::set& net_ids, + const std::string& layer, + const Color& layer_color, + const odb::Rect& dbu_tile, + const double scale) const +{ + odb::dbBlock* block = getBlock(); + if (!block) { + return; + } + + const Color fill{ + .r = layer_color.r, .g = layer_color.g, .b = layer_color.b, .a = 50}; + const Color border{ + .r = layer_color.r, .g = layer_color.g, .b = layer_color.b, .a = 180}; + + for (const uint32_t net_id : net_ids) { + odb::dbNet* net = odb::dbNet::getNet(block, net_id); + if (!net) { + continue; + } + for (odb::dbGuide* guide : net->getGuides()) { + if (guide->getLayer()->getName() != layer) { + continue; + } + const odb::Rect box = guide->getBox(); + if (!dbu_tile.overlaps(box)) { + continue; + } + const odb::Rect overlap = box.intersect(dbu_tile); + const odb::Rect draw = toPixels(scale, overlap, dbu_tile); + + // Semi-transparent fill + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + blendPixel(image, ix, 255 - iy, fill); + } + } + + // Border (only where guide edge is within this tile) + const odb::Rect full_draw = toPixels(scale, box, dbu_tile); + if (full_draw.xMin() >= 0 && full_draw.xMin() < kTileSizeInPixel) { + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + blendPixel(image, full_draw.xMin(), 255 - iy, border); + } + } + if (full_draw.xMax() > 0 && full_draw.xMax() <= kTileSizeInPixel) { + const int rx = full_draw.xMax() - 1; + for (int iy = draw.yMin(); iy < draw.yMax(); ++iy) { + blendPixel(image, rx, 255 - iy, border); + } + } + if (full_draw.yMin() >= 0 && full_draw.yMin() < kTileSizeInPixel) { + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + blendPixel(image, ix, 255 - full_draw.yMin(), border); + } + } + if (full_draw.yMax() > 0 && full_draw.yMax() <= kTileSizeInPixel) { + const int ty = full_draw.yMax() - 1; + for (int ix = draw.xMin(); ix < draw.xMax(); ++ix) { + blendPixel(image, ix, 255 - ty, border); + } + } + } + } +} + +//------------------------------------------------------------------------------ +// Timing path highlight shape collection +//------------------------------------------------------------------------------ + +std::pair resolvePin(odb::dbBlock* block, + const std::string& pin_name) +{ + odb::dbITerm* iterm = block->findITerm(pin_name.c_str()); + if (iterm) { + return {iterm, nullptr}; + } + return {nullptr, block->findBTerm(pin_name.c_str())}; +} + +static odb::Point getPinLocation(odb::dbITerm* iterm, odb::dbBTerm* bterm) +{ + if (iterm) { + int x, y; + if (iterm->getAvgXY(&x, &y)) { + return {x, y}; + } + // Fallback to instance center + odb::Rect bbox = iterm->getInst()->getBBox()->getBox(); + return {(bbox.xMin() + bbox.xMax()) / 2, (bbox.yMin() + bbox.yMax()) / 2}; + } + if (bterm) { + for (odb::dbBPin* bpin : bterm->getBPins()) { + odb::Rect r = bpin->getBBox(); + return {(r.xMin() + r.xMax()) / 2, (r.yMin() + r.yMax()) / 2}; + } + } + return {0, 0}; +} + +void collectNetShapes(odb::dbNet* net, + odb::dbITerm* drv_iterm, + odb::dbBTerm* drv_bterm, + odb::dbITerm* snk_iterm, + odb::dbBTerm* snk_bterm, + const Color& color, + std::vector& rects, + std::vector& lines) +{ + odb::dbWire* wire = net->getWire(); + if (wire) { + odb::dbWireShapeItr itr; + odb::dbShape shape; + for (itr.begin(wire); itr.next(shape);) { + if (shape.isVia()) { + std::vector via_boxes; + odb::dbShape::getViaBoxes(shape, via_boxes); + for (const auto& vbox : via_boxes) { + odb::dbTechLayer* layer = vbox.getTechLayer(); + rects.push_back( + {vbox.getBox(), color, layer ? layer->getName() : ""}); + } + } else { + odb::dbTechLayer* layer = shape.getTechLayer(); + rects.push_back({shape.getBox(), color, layer ? layer->getName() : ""}); + } + } + } else { + // Unrouted: draw flight line between driver and sink + odb::Point p1 = getPinLocation(drv_iterm, drv_bterm); + odb::Point p2 = getPinLocation(snk_iterm, snk_bterm); + lines.push_back({p1, p2, color}); + } +} + +void collectTimingPathShapes(odb::dbBlock* block, + const TimingPathSummary& path, + std::vector& rects, + std::vector& lines) +{ + static const Color kLaunchClkColor{ + .r = 0, .g = 255, .b = 255, .a = 180}; // Cyan + static const Color kSignalColor{.r = 255, .g = 0, .b = 0, .a = 180}; // Red + static const Color kCaptureClkColor{ + .r = 0, .g = 255, .b = 0, .a = 180}; // Green + + // Track nets already collected to avoid duplicates + std::set seen_nets; + + auto processNodes = [&](const std::vector& nodes, + const Color& clk_color, + const Color& data_color) { + for (size_t i = 0; i + 1 < nodes.size(); i++) { + auto [a_iterm, a_bterm] = resolvePin(block, nodes[i].pin_name); + auto [b_iterm, b_bterm] = resolvePin(block, nodes[i + 1].pin_name); + + odb::dbNet* net_a = nullptr; + if (a_iterm) { + net_a = a_iterm->getNet(); + } else if (a_bterm) { + net_a = a_bterm->getNet(); + } + + odb::dbNet* net_b = nullptr; + if (b_iterm) { + net_b = b_iterm->getNet(); + } else if (b_bterm) { + net_b = b_bterm->getNet(); + } + + // Only draw when consecutive pins are on the same net (wire segment) + if (net_a && net_a == net_b && seen_nets.insert(net_a).second) { + const Color& c = nodes[i].is_clock ? clk_color : data_color; + collectNetShapes( + net_a, a_iterm, a_bterm, b_iterm, b_bterm, c, rects, lines); + } + } + }; + + // data_nodes: launch clock (is_clock=true) then signal portion + processNodes(path.data_nodes, kLaunchClkColor, kSignalColor); + + // capture_nodes: capture clock path + processNodes(path.capture_nodes, kCaptureClkColor, kCaptureClkColor); +} + +} // namespace web diff --git a/src/web/src/tile_generator.h b/src/web/src/tile_generator.h new file mode 100644 index 00000000000..26f93733638 --- /dev/null +++ b/src/web/src/tile_generator.h @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "color.h" +#include "odb/db.h" +#include "odb/geom.h" + +namespace sta { +class dbSta; +} + +namespace gui { +class HeatMapDataSource; +} + +namespace utl { +class Logger; +} + +namespace web { + +class Search; + +struct ColoredRect +{ + odb::Rect rect; + Color color; + std::string layer; // empty = draw on all layers +}; + +struct FlightLine +{ + odb::Point p1; + odb::Point p2; + Color color; +}; + +struct SelectionResult +{ + std::any object; // dbInst*, dbNet*, etc. + std::string name; + std::string type_name; // "Inst", "Net", etc. + odb::Rect bbox; +}; + +struct TileVisibility +{ + bool stdcells = true; + bool macros = true; + + // Pad sub-types + bool pad_input = true; + bool pad_output = true; + bool pad_inout = true; + bool pad_power = true; + bool pad_spacer = true; + bool pad_areaio = true; + bool pad_other = true; + + // Physical sub-types + bool phys_fill = true; + bool phys_endcap = true; + bool phys_welltap = true; + bool phys_tie = true; + bool phys_antenna = true; + bool phys_cover = true; + bool phys_bump = true; + bool phys_other = true; + + // Std cell sub-types (used when Liberty/STA is available) + bool std_bufinv = true; + bool std_bufinv_timing = true; + bool std_clock_bufinv = true; + bool std_clock_gate = true; + bool std_level_shift = true; + bool std_sequential = true; + bool std_combinational = true; + + // Net sub-types (by dbSigType) + bool net_signal = true; + bool net_power = true; + bool net_ground = true; + bool net_clock = true; + bool net_reset = true; + bool net_tieoff = true; + bool net_scan = true; + bool net_analog = true; + + // Shapes + bool routing = true; + bool special_nets = true; + bool pins = true; + bool blockages = true; + + // Blockages (dbBlockage / dbObstruction) + bool placement_blockages = true; + bool routing_obstructions = true; + + // Rows (off by default, matching GUI) + bool rows = false; + std::string raw_json_; // stored for dynamic per-site lookups + bool isSiteVisible(const std::string& site_name) const; + + // Tracks (off by default, matching GUI) + bool tracks_pref = false; + bool tracks_non_pref = false; + + // Debug + bool debug = false; + + void parseFromJson(const std::string& json); + + bool isNetVisible(odb::dbNet* net) const; + bool isInstVisible(odb::dbInst* inst, sta::dbSta* sta) const; +}; + +class TileGenerator +{ + public: + TileGenerator(odb::dbDatabase* db, sta::dbSta* sta, utl::Logger* logger); + ~TileGenerator(); + + void eagerInit(); + bool shapesReady() const; + + bool hasSta() const { return sta_ != nullptr; } + sta::dbSta* getSta() const { return sta_; } + + odb::Rect getBounds() const; + + std::vector getLayers() const; + std::vector getSites() const; + + std::vector selectAt( + int dbu_x, + int dbu_y, + int zoom = 0, + const TileVisibility& vis = {}, + const std::set& visible_layers = {}); + + odb::dbBlock* getBlock() const; + + std::vector generateTile( + const std::string& layer, + int z, + int x, + int y, + const TileVisibility& vis = {}, + const std::vector& highlight_rects = {}, + const std::vector& highlight_polys = {}, + const std::vector& colored_rects = {}, + const std::vector& flight_lines = {}, + const std::map* module_colors = nullptr, + const std::set* focus_net_ids = nullptr, + const std::set* route_guide_net_ids = nullptr) const; + std::vector generateHeatMapTile(gui::HeatMapDataSource& source, + int z, + int x, + int y) const; + + private: + void setPixel(std::vector& image, + int x, + int y, + const Color& c) const; + + void drawDebugOverlay(std::vector& image, + int z, + int x, + int y) const; + + static int getBitmapTextWidth(std::string_view text, int scale); + static int getBitmapTextHeight(int scale); + static void drawBitmapText(std::vector& image, + int x, + int y, + std::string_view text, + int scale, + const Color& color); + + void drawHighlight(std::vector& image, + const std::vector& rects, + const std::vector& polys, + const odb::Rect& dbu_tile, + double scale) const; + + void drawColoredHighlight(std::vector& image, + const std::vector& rects, + const std::string& current_layer, + const odb::Rect& dbu_tile, + double scale) const; + + void drawFlightLines(std::vector& image, + const std::vector& lines, + const odb::Rect& dbu_tile, + double scale) const; + + void drawRouteGuides(std::vector& image, + const std::set& net_ids, + const std::string& layer, + const Color& color, + const odb::Rect& dbu_tile, + double scale) const; + + static odb::Rect toPixels(double scale, + const odb::Rect& rect, + const odb::Rect& dbu_tile); + + void fillPolygon(std::vector& image, + const odb::Polygon& poly, + const odb::Rect& dbu_tile, + double scale, + const Color& color, + bool blend = false) const; + + static void blendPixel(std::vector& image, + int x, + int y, + const Color& c); + + static void drawLine(std::vector& image, + int x0, + int y0, + int x1, + int y1, + const Color& c); + + odb::dbDatabase* db_; + sta::dbSta* sta_; + utl::Logger* logger_; + std::unique_ptr search_; + static constexpr int kTileSizeInPixel = 256; +}; + +struct TimingPathSummary; + +std::pair resolvePin(odb::dbBlock* block, + const std::string& pin_name); + +void collectNetShapes(odb::dbNet* net, + odb::dbITerm* drv_iterm, + odb::dbBTerm* drv_bterm, + odb::dbITerm* snk_iterm, + odb::dbBTerm* snk_bterm, + const Color& color, + std::vector& rects, + std::vector& lines); + +void collectTimingPathShapes(odb::dbBlock* block, + const TimingPathSummary& path, + std::vector& rects, + std::vector& lines); + +} // namespace web diff --git a/src/web/src/timing-widget.js b/src/web/src/timing-widget.js new file mode 100644 index 00000000000..30205c44eba --- /dev/null +++ b/src/web/src/timing-widget.js @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Timing report widget — path summary + detail tables. + +import { makeResizableHeaders } from './ui-utils.js'; + +export class TimingWidget { + constructor(container, app, redrawAllLayers) { + this._app = app; + this._redrawAllLayers = redrawAllLayers; + + this._currentTab = 'setup'; + this._setupPaths = []; + this._holdPaths = []; + this._selectedPathIndex = -1; + this._detailTab = 'data'; + this._selectedDetailIndex = -1; + + this._build(container); + } + + _build(container) { + const el = document.createElement('div'); + el.className = 'timing-widget'; + + // --- Toolbar --- + const toolbar = document.createElement('div'); + toolbar.className = 'timing-toolbar'; + + this._updateBtn = document.createElement('button'); + this._updateBtn.className = 'timing-btn'; + this._updateBtn.textContent = 'Update'; + + this._pathCountLabel = document.createElement('span'); + this._pathCountLabel.className = 'timing-path-count'; + + toolbar.appendChild(this._updateBtn); + toolbar.appendChild(this._pathCountLabel); + el.appendChild(toolbar); + + // --- Setup/Hold Tab Bar --- + const tabBar = document.createElement('div'); + tabBar.className = 'timing-tab-bar'; + + this._setupTab = this._makeTab('Setup', true); + this._holdTab = this._makeTab('Hold', false); + tabBar.appendChild(this._setupTab); + tabBar.appendChild(this._holdTab); + el.appendChild(tabBar); + + // --- Path listing table --- + this._pathTableContainer = document.createElement('div'); + this._pathTableContainer.className = 'timing-path-table-container'; + this._pathTable = document.createElement('table'); + this._pathTable.className = 'timing-table'; + this._pathTableContainer.appendChild(this._pathTable); + el.appendChild(this._pathTableContainer); + + // --- Detail Tab Bar --- + const detailTabBar = document.createElement('div'); + detailTabBar.className = 'timing-tab-bar'; + this._dataTab = this._makeTab('Data Path', true); + this._captureTab = this._makeTab('Capture Path', false); + detailTabBar.appendChild(this._dataTab); + detailTabBar.appendChild(this._captureTab); + el.appendChild(detailTabBar); + + // --- Detail table --- + this._detailTableContainer = document.createElement('div'); + this._detailTableContainer.className = 'timing-detail-table-container'; + this._detailTable = document.createElement('table'); + this._detailTable.className = 'timing-table'; + this._detailTableContainer.appendChild(this._detailTable); + el.appendChild(this._detailTableContainer); + + container.element.appendChild(el); + + this._bindEvents(); + } + + _makeTab(label, active) { + const btn = document.createElement('button'); + btn.className = 'timing-tab' + (active ? ' active' : ''); + btn.textContent = label; + return btn; + } + + _bindEvents() { + // Tab switching + this._setupTab.addEventListener('click', () => { + this._currentTab = 'setup'; + this._setupTab.classList.add('active'); + this._holdTab.classList.remove('active'); + this._selectedPathIndex = -1; + this._renderPathTable(); + this._renderDetailTable(); + this._clearTimingHighlight(); + }); + this._holdTab.addEventListener('click', () => { + this._currentTab = 'hold'; + this._holdTab.classList.add('active'); + this._setupTab.classList.remove('active'); + this._selectedPathIndex = -1; + this._renderPathTable(); + this._renderDetailTable(); + this._clearTimingHighlight(); + }); + this._dataTab.addEventListener('click', () => { + this._detailTab = 'data'; + this._dataTab.classList.add('active'); + this._captureTab.classList.remove('active'); + this._renderDetailTable(); + }); + this._captureTab.addEventListener('click', () => { + this._detailTab = 'capture'; + this._captureTab.classList.add('active'); + this._dataTab.classList.remove('active'); + this._renderDetailTable(); + }); + + // Fetch paths + this._updateBtn.addEventListener('click', () => this.update()); + + // Keyboard navigation — path table + this._pathTableContainer.setAttribute('tabindex', '0'); + this._pathTableContainer.style.outline = 'none'; + this._pathTableContainer.addEventListener('keydown', (e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + const rows = this._pathTable.querySelectorAll('tbody tr'); + if (this._selectedPathIndex < rows.length - 1) { + this._selectPathRow(this._selectedPathIndex + 1); + } + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (this._selectedPathIndex > 0) { + this._selectPathRow(this._selectedPathIndex - 1); + } + } + }); + + // Keyboard navigation — detail table + this._detailTableContainer.setAttribute('tabindex', '0'); + this._detailTableContainer.style.outline = 'none'; + this._detailTableContainer.addEventListener('keydown', (e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + const rows = this._detailTable.querySelectorAll('tbody tr'); + if (this._selectedDetailIndex < rows.length - 1) { + this._selectDetailRow(this._selectedDetailIndex + 1); + } + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (this._selectedDetailIndex > 0) { + this._selectDetailRow(this._selectedDetailIndex - 1); + } + } + }); + } + + showPaths(tab, paths) { + this._currentTab = tab; + if (tab === 'setup') { + this._setupPaths = paths; + this._setupTab.classList.add('active'); + this._holdTab.classList.remove('active'); + } else { + this._holdPaths = paths; + this._holdTab.classList.add('active'); + this._setupTab.classList.remove('active'); + } + this._selectedPathIndex = -1; + this._pathCountLabel.textContent = paths.length + ' paths'; + this._renderPathTable(); + this._renderDetailTable(); + this._clearTimingHighlight(); + } + + async update() { + this._updateBtn.disabled = true; + this._updateBtn.textContent = 'Loading...'; + try { + const [setupData, holdData] = await Promise.all([ + this._app.websocketManager.request({ type: 'timing_report', is_setup: 1, max_paths: 100 }), + this._app.websocketManager.request({ type: 'timing_report', is_setup: 0, max_paths: 100 }), + ]); + this._setupPaths = setupData.paths || []; + this._holdPaths = holdData.paths || []; + this._selectedPathIndex = -1; + this._renderPathTable(); + this._renderDetailTable(); + this._clearTimingHighlight(); + } catch (e) { + console.error('Timing fetch failed:', e); + } + this._updateBtn.disabled = false; + this._updateBtn.textContent = 'Update'; + } + + _clearTimingHighlight() { + this._app.websocketManager.request({ type: 'timing_highlight', path_index: -1 }) + .then(() => this._redrawAllLayers()); + } + + _selectPathRow(idx) { + const rows = this._pathTable.querySelectorAll('tbody tr'); + if (idx < 0 || idx >= rows.length) return; + this._selectedPathIndex = idx; + for (const row of rows) row.classList.remove('selected'); + rows[idx].classList.add('selected'); + rows[idx].scrollIntoView({ block: 'nearest' }); + this._pathTableContainer.focus(); + this._renderDetailTable(); + this._app.websocketManager.request({ + type: 'timing_highlight', + path_index: idx, + is_setup: this._currentTab === 'setup' ? 1 : 0, + }).then(() => this._redrawAllLayers()) + .catch(err => console.error('timing_highlight error:', err)); + } + + _renderPathTable() { + const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; + this._pathCountLabel.textContent = paths.length + ' paths'; + this._pathTable.innerHTML = ''; + + const thead = document.createElement('thead'); + const hr = document.createElement('tr'); + for (const col of TimingWidget.PATH_COLS) { + const th = document.createElement('th'); + th.textContent = col; + hr.appendChild(th); + } + thead.appendChild(hr); + this._pathTable.appendChild(thead); + + const tbody = document.createElement('tbody'); + paths.forEach((p, idx) => { + const tr = document.createElement('tr'); + if (idx === this._selectedPathIndex) tr.classList.add('selected'); + const vals = [ + p.end_clk, + fmtTime(p.required), + fmtTime(p.arrival), + fmtTime(p.slack), + fmtTime(p.skew), + fmtTime(p.path_delay), + p.logic_depth, + p.fanout, + p.start_pin, + p.end_pin, + ]; + vals.forEach((v, ci) => { + const td = document.createElement('td'); + td.textContent = v; + if (ci === 3 && p.slack < 0) td.classList.add('slack-negative'); + tr.appendChild(td); + }); + tr.style.cursor = 'pointer'; + tr.addEventListener('click', () => this._selectPathRow(idx)); + tbody.appendChild(tr); + }); + this._pathTable.appendChild(tbody); + makeResizableHeaders(this._pathTable); + } + + _selectDetailRow(idx) { + const rows = this._detailTable.querySelectorAll('tbody tr'); + if (idx < 0 || idx >= rows.length) return; + this._selectedDetailIndex = idx; + for (const row of rows) { + row.classList.remove('timing-selected-row'); + } + rows[idx].classList.add('timing-selected-row'); + rows[idx].scrollIntoView({ block: 'nearest' }); + this._detailTableContainer.focus(); + + const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; + const path = paths[this._selectedPathIndex]; + const nodes = this._detailTab === 'data' ? path.data_nodes : path.capture_nodes; + this._app.websocketManager.request({ + type: 'timing_highlight', + path_index: this._selectedPathIndex, + is_setup: this._currentTab === 'setup' ? 1 : 0, + pin_name: nodes[idx].pin, + }).then(() => this._redrawAllLayers()); + } + + _renderDetailTable() { + this._detailTable.innerHTML = ''; + this._selectedDetailIndex = -1; + const paths = this._currentTab === 'setup' ? this._setupPaths : this._holdPaths; + if (this._selectedPathIndex < 0 || this._selectedPathIndex >= paths.length) return; + + const path = paths[this._selectedPathIndex]; + const nodes = this._detailTab === 'data' ? path.data_nodes : path.capture_nodes; + + const thead = document.createElement('thead'); + const hr = document.createElement('tr'); + for (const col of TimingWidget.DETAIL_COLS) { + const th = document.createElement('th'); + th.textContent = col; + hr.appendChild(th); + } + thead.appendChild(hr); + this._detailTable.appendChild(thead); + + const tbody = document.createElement('tbody'); + nodes.forEach((n, idx) => { + const tr = document.createElement('tr'); + if (n.clk) tr.classList.add('timing-clock-row'); + const vals = [ + n.pin, + n.fanout, + n.rise ? '↑' : '↓', + fmtTime(n.time), + fmtTime(n.delay), + fmtTime(n.slew), + fmtTime(n.load), + ]; + for (const v of vals) { + const td = document.createElement('td'); + td.textContent = v; + tr.appendChild(td); + } + tr.style.cursor = 'pointer'; + tr.addEventListener('click', () => this._selectDetailRow(idx)); + tbody.appendChild(tr); + }); + this._detailTable.appendChild(tbody); + makeResizableHeaders(this._detailTable); + // Pin column: set initial width to 30 characters + const pinTh = this._detailTable.querySelector('thead th'); + if (pinTh) { + pinTh.style.width = '30ch'; + } + } + +} + +export function fmtTime(v) { + if (v === undefined || v === null) return ''; + return typeof v === 'number' ? v.toFixed(4) : String(v); +} + +TimingWidget.PATH_COLS = ['Clock', 'Required', 'Arrival', 'Slack', 'Skew', + 'Logic Delay', 'Logic Depth', 'Fanout', 'Start', 'End']; +TimingWidget.DETAIL_COLS = ['Pin', 'Fanout', 'R/F', 'Time', 'Delay', 'Slew', 'Load']; diff --git a/src/web/src/timing_report.cpp b/src/web/src/timing_report.cpp new file mode 100644 index 00000000000..7d34797ef54 --- /dev/null +++ b/src/web/src/timing_report.cpp @@ -0,0 +1,544 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include "timing_report.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "db_sta/dbNetwork.hh" +#include "db_sta/dbSta.hh" +#include "odb/db.h" +#include "sta/Clock.hh" +#include "sta/ExceptionPath.hh" +#include "sta/Graph.hh" +#include "sta/GraphDelayCalc.hh" +#include "sta/Liberty.hh" +#include "sta/MinMax.hh" +#include "sta/Mode.hh" +#include "sta/Path.hh" +#include "sta/PathEnd.hh" +#include "sta/PathExpanded.hh" +#include "sta/PathGroup.hh" +#include "sta/Scene.hh" +#include "sta/Sdc.hh" +#include "sta/Search.hh" +#include "sta/SearchClass.hh" +#include "sta/Sta.hh" +#include "sta/Units.hh" +#include "sta/VisitPathEnds.hh" + +namespace web { + +TimingReport::TimingReport(sta::dbSta* sta) : sta_(sta) +{ +} + +void TimingReport::expandPath(sta::Path* path, + float offset, + bool clock_expanded, + std::vector& nodes, + int& logic_depth, + int& total_fanout) const +{ + if (!path) { + return; + } + + float arrival_prev = 0.0f; + float arrival_cur = 0.0f; + + const float time_scale = sta_->units()->timeUnit()->scale(); + const float cap_scale = sta_->units()->capacitanceUnit()->scale(); + + sta::PathExpanded expand(path, sta_); + auto* graph = sta_->graph(); + auto* network = sta_->network(); + auto* mode = sta_->cmdMode(); + auto* sdc = mode->sdc(); + + // Track logic instances for depth computation + std::set logic_insts; + + for (size_t i = 0; i < expand.size(); i++) { + const auto* ref = expand.path(i); + sta::Vertex* vertex = ref->vertex(sta_); + const auto pin = vertex->pin(); + const bool pin_is_clock = sta_->isClock(pin, mode); + const bool is_driver = network->isDriver(pin); + const bool is_rising = ref->transition(sta_) == sta::RiseFall::rise(); + + // Compute fanout (wire edges from this vertex) + int node_fanout = 0; + sta::VertexOutEdgeIterator iter(vertex, graph); + while (iter.hasNext()) { + sta::Edge* edge = iter.next(); + if (edge->isWire()) { + const sta::Pin* to_pin = edge->to(graph)->pin(); + if (network->isTopLevelPort(to_pin)) { + sta::Port* port = network->port(to_pin); + node_fanout += sdc->portExtFanout(port, sta::MinMax::max()) + 1; + } else { + node_fanout++; + } + } + } + + // Load capacitance + float cap = 0.0f; + if (is_driver && (clock_expanded || (!network->isCheckClk(pin) && i))) { + sta::GraphDelayCalc* gdc = sta_->graphDelayCalc(); + cap = gdc->loadCap( + pin, ref->transition(sta_), ref->scene(sta_), ref->minMax(sta_)); + } + + // Pin name via dbNetwork + odb::dbITerm* term; + odb::dbBTerm* port; + odb::dbModITerm* moditerm; + sta_->getDbNetwork()->staToDb(pin, term, port, moditerm); + + std::string pin_name; + if (term) { + pin_name = term->getName(); + } else if (port) { + pin_name = port->getName(); + } + + // Arrival and slew + float slew = 0.0f; + if (!sta_->isIdealClock(pin, mode)) { + arrival_cur = ref->arrival(); + slew = ref->slew(sta_); + } + const float pin_delay = arrival_cur - arrival_prev; + + // Track logic depth for data path nodes + if (!pin_is_clock) { + sta::Instance* inst = network->instance(pin); + if (inst) { + sta::LibertyCell* lib_cell = network->libertyCell(inst); + if (lib_cell && !lib_cell->isBuffer() && !lib_cell->isInverter()) { + logic_insts.insert(inst); + } + } + total_fanout += node_fanout; + } + + nodes.push_back(TimingNode{.pin_name = pin_name, + .fanout = node_fanout, + .is_rising = is_rising, + .is_clock = pin_is_clock, + .time = (arrival_cur + offset) / time_scale, + .delay = pin_delay / time_scale, + .slew = slew / time_scale, + .load = cap / cap_scale}); + + arrival_prev = arrival_cur; + } + + logic_depth = static_cast(logic_insts.size()); +} + +std::vector TimingReport::getReport(bool is_setup, + int max_paths, + float slack_min, + float slack_max) const +{ + std::vector result; + if (!sta_) { + return result; + } + + sta_->ensureGraph(); + sta_->searchPreamble(); + + // Convert user-unit slack bounds to internal units. + const float time_scale = sta_->units()->timeUnit()->scale(); + const float sta_slack_min + = (slack_min <= -1e30f) ? -sta::INF : slack_min * time_scale; + const float sta_slack_max + = (slack_max >= 1e30f) ? sta::INF : slack_max * time_scale; + + sta::Search* search = sta_->search(); + sta::SceneSeq scenes = sta_->scenes(); + sta::StringSeq group_names; + sta::PathEndSeq path_ends = search->findPathEnds( + /*from*/ nullptr, + /*thrus*/ nullptr, + /*to*/ nullptr, + /*unconstrained*/ false, + scenes, + is_setup ? sta::MinMaxAll::max() : sta::MinMaxAll::min(), + /*group_count*/ max_paths, + /*endpoint_count*/ 1, + /*unique_pins*/ true, + /*unique_edges*/ true, + sta_slack_min, + sta_slack_max, + /*sort_by_slack*/ true, + group_names, + /*setup*/ is_setup, + /*hold*/ !is_setup, + /*recovery*/ false, + /*removal*/ false, + /*clk_gating_setup*/ false, + /*clk_gating_hold*/ false); + + for (auto& path_end : path_ends) { + TimingPathSummary summary; + + sta::Path* path = path_end->path(); + + // Clocks + auto* src_edge = path_end->sourceClkEdge(sta_); + summary.start_clk = src_edge ? src_edge->clock()->name() : ""; + auto* end_clk = path_end->targetClk(sta_); + summary.end_clk = end_clk ? end_clk->name() : ""; + + // Path-level metrics (convert from internal units to user units) + const float time_scale = sta_->units()->timeUnit()->scale(); + summary.slack = path_end->slack(sta_) / time_scale; + summary.arrival = path_end->dataArrivalTime(sta_) / time_scale; + summary.required = path_end->requiredTime(sta_) / time_scale; + summary.skew = path_end->clkSkew(sta_) / time_scale; + auto* pd = path_end->pathDelay(); + summary.path_delay = pd ? pd->delay() / time_scale : 0.0f; + + bool clock_propagated = src_edge && src_edge->clock()->isPropagated(); + + // Expand data path + expandPath(path, + 0.0f, + clock_propagated, + summary.data_nodes, + summary.logic_depth, + summary.fanout); + + // Expand capture path + int capture_depth = 0; + int capture_fanout = 0; + expandPath(path_end->targetClkPath(), + path_end->targetClkOffset(sta_), + clock_propagated, + summary.capture_nodes, + capture_depth, + capture_fanout); + + // Start/end pin names + // Find first non-clock node in data path + int clk_end = -1; + for (int i = 0; i < static_cast(summary.data_nodes.size()); i++) { + if (!summary.data_nodes[i].is_clock) { + clk_end = i - 1; + break; + } + } + if (clk_end < 0) { + clk_end = static_cast(summary.data_nodes.size()) - 1; + } + + if (!summary.data_nodes.empty()) { + int start_idx = clk_end + 1; + if (start_idx >= static_cast(summary.data_nodes.size())) { + start_idx = 0; + } + summary.start_pin = summary.data_nodes[start_idx].pin_name; + if (!summary.data_nodes.empty()) { + summary.end_pin = summary.data_nodes.back().pin_name; + } + } + result.push_back(std::move(summary)); + } + + return result; +} + +// Snap an exact bin interval to a "nice" value (1, 2, 5, or 10 × 10^n). +// Matches the Qt GUI algorithm in chartsWidget.cpp:computeSnapBucketInterval(). +static float snapBinInterval(float exact_interval) +{ + const int exp = static_cast(std::floor(std::log10(exact_interval))); + const double mag = std::pow(10.0, exp); + const double residual = exact_interval / mag; + + double nice_coeff; + if (residual < 1.5) { + nice_coeff = 1.0; + } else if (residual < 3.0) { + nice_coeff = 2.0; + } else if (residual < 7.0) { + nice_coeff = 5.0; + } else { + nice_coeff = 10.0; + } + + return static_cast(nice_coeff * mag); +} + +// Visitor that finds worst slack per endpoint within a specific path group, +// optionally filtered by clock. Mirrors PathGroupSlackEndVisitor from +// staGuiInterface.cpp. +class PathGroupSlackVisitor : public sta::PathEndVisitor +{ + public: + PathGroupSlackVisitor(const sta::PathGroup* group, + const sta::Clock* clk, + sta::Sta* sta) + : group_(group), sta_(sta), clk_(clk) + { + } + + sta::PathEndVisitor* copy() const override + { + return new PathGroupSlackVisitor(*this); + } + + void visit(sta::PathEnd* path_end) override + { + sta::PathGroupSeq groups = sta_->cmdScene()->mode()->pathGroups(path_end); + if (std::ranges::find(groups, group_) == groups.end()) { + return; + } + if (clk_) { + const sta::Clock* end_clk = path_end->targetClk(sta_); + if (end_clk != clk_) { + return; + } + } + float slack = path_end->slack(sta_); + if (slack < worst_slack_) { + worst_slack_ = slack; + has_slack_ = true; + } + } + + float worstSlack() const { return worst_slack_; } + bool hasSlack() const { return has_slack_; } + void reset() + { + worst_slack_ = std::numeric_limits::max(); + has_slack_ = false; + } + + private: + const sta::PathGroup* group_; + sta::Sta* sta_; + const sta::Clock* clk_; + bool has_slack_ = false; + float worst_slack_ = std::numeric_limits::max(); +}; + +// Ensure path groups exist so findPathGroup works. +static void ensurePathGroups(sta::dbSta* sta, bool is_setup) +{ + sta::StringSeq empty_names; + for (sta::Mode* mode : sta->modes()) { + mode->makePathGroups(1, + 1, + false, + false, + -sta::INF, + sta::INF, + empty_names, + is_setup, + !is_setup, + false, + false, + false, + false, + false); + } +} + +// Collect per-endpoint slacks using the path group visitor pattern. +// Returns slacks in user units. +static void collectFilteredSlacks(sta::dbSta* sta, + bool is_setup, + const std::string& path_group, + const std::string& clock_name, + std::vector& slacks, + int& total_endpoints, + int& unconstrained_count) +{ + ensurePathGroups(sta, is_setup); + + const sta::MinMax* min_max + = is_setup ? sta::MinMax::max() : sta::MinMax::min(); + const float time_scale = sta->units()->timeUnit()->scale(); + + // Find the clock pointer if a clock name was given. + const sta::Clock* clk = nullptr; + if (!clock_name.empty()) { + for (sta::Clock* c : sta->cmdScene()->sdc()->clocks()) { + if (clock_name == c->name()) { + clk = c; + break; + } + } + } + + // Find the path group. + sta::PathGroup* pg = nullptr; + if (!path_group.empty()) { + pg = sta->cmdMode()->pathGroups()->findPathGroup(path_group.c_str(), + min_max); + } else if (clk) { + pg = sta->cmdMode()->pathGroups()->findPathGroup(clk, min_max); + } + + if (!pg) { + return; + } + + sta::VisitPathEnds visit_ends(sta); + PathGroupSlackVisitor visitor(pg, clk, sta); + + for (sta::Vertex* vertex : sta->endpoints()) { + total_endpoints++; + visit_ends.visitPathEnds(vertex, &visitor); + if (visitor.hasSlack()) { + float slack = visitor.worstSlack(); + if (slack >= sta::INF || slack <= -sta::INF) { + unconstrained_count++; + } else { + slacks.push_back(slack / time_scale); + } + } else { + unconstrained_count++; + } + visitor.reset(); + } +} + +// Helper: given a vector of slack values, bin them and populate result. +static void binSlacks(const std::vector& slacks, + SlackHistogramResult& result) +{ + float min_slack = std::numeric_limits::max(); + float max_slack = std::numeric_limits::lowest(); + for (float s : slacks) { + min_slack = std::min(min_slack, s); + max_slack = std::max(max_slack, s); + } + + // Extend range to include zero so negative/positive split is meaningful. + min_slack = std::min(0.0f, min_slack); + max_slack = std::max(0.0f, max_slack); + + int num_bins; + float bin_min; + float bin_width; + + if (min_slack == max_slack) { + num_bins = 1; + bin_min = min_slack - 0.1f; + bin_width = 0.3f; + } else { + constexpr int kDefaultBuckets = 10; + bin_width = snapBinInterval((max_slack - min_slack) / kDefaultBuckets); + bin_min = std::floor(min_slack / bin_width) * bin_width; + const float bin_max = std::ceil(max_slack / bin_width) * bin_width; + num_bins = static_cast(std::round((bin_max - bin_min) / bin_width)); + num_bins = std::max(num_bins, 1); + } + + std::vector counts(num_bins, 0); + for (float s : slacks) { + int idx = static_cast((s - bin_min) / bin_width); + idx = std::clamp(idx, 0, num_bins - 1); + counts[idx]++; + } + + result.bins.reserve(num_bins); + for (int i = 0; i < num_bins; i++) { + float lower = bin_min + i * bin_width; + float upper = lower + bin_width; + float center = (lower + upper) / 2.0f; + result.bins.push_back({lower, upper, counts[i], center < 0}); + } +} + +SlackHistogramResult TimingReport::getSlackHistogram( + bool is_setup, + const std::string& path_group, + const std::string& clock_name) const +{ + SlackHistogramResult result; + if (!sta_) { + return result; + } + + sta_->ensureGraph(); + sta_->searchPreamble(); + + result.time_unit = sta_->units()->timeUnit()->scaleAbbrevSuffix(); + + std::vector slacks; + + if (!path_group.empty() || !clock_name.empty()) { + // Filtered mode: use path group visitor pattern. + collectFilteredSlacks(sta_, + is_setup, + path_group, + clock_name, + slacks, + result.total_endpoints, + result.unconstrained_count); + } else { + // Unfiltered mode: simple slack query per endpoint. + const sta::MinMax* min_max + = is_setup ? sta::MinMax::max() : sta::MinMax::min(); + sta::SceneSeq scenes = sta_->scenes(); + const float time_scale = sta_->units()->timeUnit()->scale(); + + for (sta::Vertex* vertex : sta_->endpoints()) { + result.total_endpoints++; + const sta::Pin* pin = vertex->pin(); + float slack + = sta_->slack(pin, sta::RiseFallBoth::riseFall(), scenes, min_max); + + if (slack >= sta::INF || slack <= -sta::INF) { + result.unconstrained_count++; + continue; + } + + slacks.push_back(slack / time_scale); + } + } + + if (!slacks.empty()) { + binSlacks(slacks, result); + } + + return result; +} + +ChartFilters TimingReport::getChartFilters() const +{ + ChartFilters filters; + if (!sta_) { + return filters; + } + + // Path groups. + sta::Sdc* sdc = sta_->cmdScene()->sdc(); + for (const auto& [name, group_paths] : sdc->groupPaths()) { + filters.path_groups.emplace_back(name); + } + + // Clocks. + for (sta::Clock* clk : sdc->clocks()) { + filters.clocks.emplace_back(clk->name()); + } + + return filters; +} + +} // namespace web diff --git a/src/web/src/timing_report.h b/src/web/src/timing_report.h new file mode 100644 index 00000000000..745b764ec7f --- /dev/null +++ b/src/web/src/timing_report.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include + +namespace sta { +class dbSta; +class Path; +} // namespace sta + +namespace web { + +struct TimingNode +{ + std::string pin_name; + int fanout = 0; + bool is_rising = false; + bool is_clock = false; + float time = 0.0f; // arrival + float delay = 0.0f; // incremental + float slew = 0.0f; + float load = 0.0f; +}; + +struct TimingPathSummary +{ + std::string start_clk; + std::string end_clk; + float required = 0.0f; + float arrival = 0.0f; + float slack = 0.0f; + float skew = 0.0f; + float path_delay = 0.0f; + int logic_depth = 0; + int fanout = 0; + std::string start_pin; + std::string end_pin; + std::vector data_nodes; + std::vector capture_nodes; +}; + +struct SlackHistogramBin +{ + float lower; // bin lower edge (user units) + float upper; // bin upper edge (user units) + int count; // number of endpoints in this bin + bool is_negative; // true if bin center < 0 +}; + +struct SlackHistogramResult +{ + std::vector bins; + int unconstrained_count = 0; + int total_endpoints = 0; + std::string time_unit; +}; + +struct ChartFilters +{ + std::vector path_groups; + std::vector clocks; +}; + +class TimingReport +{ + public: + explicit TimingReport(sta::dbSta* sta); + + std::vector getReport( + bool is_setup, + int max_paths = 100, + float slack_min = -std::numeric_limits::max(), + float slack_max = std::numeric_limits::max()) const; + + SlackHistogramResult getSlackHistogram(bool is_setup, + const std::string& path_group = "", + const std::string& clock_name + = "") const; + + ChartFilters getChartFilters() const; + + private: + void expandPath(sta::Path* path, + float offset, + bool clock_expanded, + std::vector& nodes, + int& logic_depth, + int& total_fanout) const; + + sta::dbSta* sta_; +}; + +} // namespace web diff --git a/src/web/src/ui-utils.js b/src/web/src/ui-utils.js new file mode 100644 index 00000000000..99f9cffb5fe --- /dev/null +++ b/src/web/src/ui-utils.js @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Shared UI utilities. + +// Make table column headers resizable by dragging. +export function makeResizableHeaders(table) { + // Reset to auto layout so browser computes natural column widths + table.style.tableLayout = 'auto'; + const headers = table.querySelectorAll('thead th'); + headers.forEach((th) => th.style.width = ''); + // Force reflow to get natural widths + const widths = Array.from(headers, (th) => th.offsetWidth); + // Now lock in widths and switch to fixed layout + headers.forEach((th, i) => th.style.width = widths[i] + 'px'); + table.style.tableLayout = 'fixed'; + + headers.forEach((th, idx) => { + if (idx === headers.length - 1) return; // skip last column + const grip = document.createElement('div'); + grip.className = 'col-resize-grip'; + th.style.position = 'relative'; + th.appendChild(grip); + + let startX, startW; + const onMouseMove = (e) => { + th.style.width = Math.max(30, startW + e.clientX - startX) + 'px'; + }; + const onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + grip.addEventListener('mousedown', (e) => { + e.preventDefault(); + startX = e.clientX; + startW = th.offsetWidth; + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }); + }); +} diff --git a/src/web/src/vis-tree.js b/src/web/src/vis-tree.js new file mode 100644 index 00000000000..924eb9fd13d --- /dev/null +++ b/src/web/src/vis-tree.js @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Data-model-driven checkbox tree (mirrors Qt's QStandardItemModel pattern). +// State lives in CheckboxTreeModel; DOM is synced from model after every change. + +import { CheckboxTreeModel } from './checkbox-tree-model.js'; + +export class VisTree { + constructor(visibility, onChange) { + this.visibility = visibility; + this.onChange = onChange; + this.model = new CheckboxTreeModel(() => { + this._syncAll(); + this.onChange(); + }); + } + + // Add a tree from a declarative spec. + // Leaf: { key, label } + // Group: { label, children: [...], visKey?, disabled? } + add(spec) { + const enriched = this._enrichSpec(spec); + this.model.addFromSpec(enriched); + return this; + } + + render(container) { + for (const r of this.model.roots) container.appendChild(this._dom(r)); + this._syncAll(); + } + + // -- model helpers -- + + // Convert user spec to model spec, initializing checked from visibility. + _enrichSpec(spec) { + const id = spec.key || spec.label; + const result = { id, data: spec }; + if (spec.children) { + result.children = spec.children.map(c => this._enrichSpec(c)); + } else if (spec.key) { + result.checked = !!this.visibility[spec.key]; + } + if (spec.disabled != null) result.disabled = spec.disabled; + return result; + } + + _syncAll() { + this.model.forEach(node => { + if (node.cb) { + node.cb.checked = node.checked; + node.cb.indeterminate = node.indeterminate; + } + const spec = node.data; + if (spec.key) this.visibility[spec.key] = node.checked; + if (spec.visKey) { + this.visibility[spec.visKey] = node.checked || node.indeterminate; + } + }); + } + + // -- DOM -- + + _dom(node) { + const spec = node.data; + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + node.cb = cb; + cb.addEventListener('change', () => this.model.check(node.id, cb.checked)); + + if (!node.children.length) { + const label = document.createElement('label'); + const spacer = document.createElement('span'); + spacer.className = 'vis-arrow'; + spacer.style.visibility = 'hidden'; + spacer.textContent = '\u25B6'; + label.appendChild(spacer); + label.appendChild(cb); + label.appendChild(document.createTextNode(spec.label)); + return label; + } + + const div = document.createElement('div'); + div.className = 'vis-group'; + + const header = document.createElement('label'); + header.className = 'vis-group-header'; + const arrow = document.createElement('span'); + arrow.className = 'vis-arrow'; + arrow.textContent = '▶'; + header.appendChild(arrow); + header.appendChild(cb); + header.appendChild(document.createTextNode(spec.label)); + div.appendChild(header); + + const kids = document.createElement('div'); + kids.className = 'vis-group-children collapsed'; + if (spec.disabled) kids.classList.add('disabled'); + for (const c of node.children) kids.appendChild(this._dom(c)); + div.appendChild(kids); + + arrow.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + kids.classList.toggle('collapsed'); + arrow.textContent = kids.classList.contains('collapsed') + ? '▶' : '▼'; + }); + + return div; + } +} diff --git a/src/web/src/web-py.i b/src/web/src/web-py.i new file mode 100644 index 00000000000..6e8cc0408ef --- /dev/null +++ b/src/web/src/web-py.i @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +%module web_py + +%{ +#include "web/web.h" +#include "ord/OpenRoad.hh" +%} + +%include "../../Exception-py.i" + +%include "web/web.h" diff --git a/src/web/src/web.cpp b/src/web/src/web.cpp new file mode 100644 index 00000000000..0f49647f805 --- /dev/null +++ b/src/web/src/web.cpp @@ -0,0 +1,970 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include "web/web.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "clock_tree_report.h" +#include "gui/heatMap.h" +#include "odb/db.h" +#include "request_handler.h" +#include "tcl.h" +#include "timing_report.h" +#include "utl/Logger.h" + +namespace web { + +namespace beast = boost::beast; +namespace http = beast::http; +namespace websocket = beast::websocket; +namespace net = boost::asio; +using tcp = net::ip::tcp; + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ + +static WebSocketRequest parse_web_socket_request(const std::string& msg) +{ + WebSocketRequest req; + req.id = static_cast(extract_int(msg, "id")); + req.raw_json = msg; + + std::string type_str = extract_string(msg, "type"); + if (type_str == "tile") { + req.type = WebSocketRequest::TILE; + req.layer = extract_string(msg, "layer"); + req.z = extract_int(msg, "z"); + req.x = extract_int(msg, "x"); + req.y = extract_int(msg, "y"); + req.vis.parseFromJson(msg); + } else if (type_str == "bounds") { + req.type = WebSocketRequest::BOUNDS; + } else if (type_str == "tech") { + req.type = WebSocketRequest::TECH; + } else if (type_str == "inspect") { + req.type = WebSocketRequest::INSPECT; + req.select_id = extract_int(msg, "select_id"); + } else if (type_str == "inspect_back") { + req.type = WebSocketRequest::INSPECT_BACK; + } else if (type_str == "hover") { + req.type = WebSocketRequest::HOVER; + req.select_id = extract_int(msg, "select_id"); + } else if (type_str == "tcl_eval") { + req.type = WebSocketRequest::TCL_EVAL; + req.tcl_cmd = extract_string(msg, "cmd"); + } else if (type_str == "timing_report") { + req.type = WebSocketRequest::TIMING_REPORT; + req.timing_is_setup = extract_int_or(msg, "is_setup", 1); + req.timing_max_paths = extract_int_or(msg, "max_paths", 100); + req.timing_slack_min = extract_float_or( + msg, "slack_min", -std::numeric_limits::max()); + req.timing_slack_max + = extract_float_or(msg, "slack_max", std::numeric_limits::max()); + } else if (type_str == "timing_highlight") { + req.type = WebSocketRequest::TIMING_HIGHLIGHT; + req.timing_path_index = extract_int_or(msg, "path_index", -1); + req.timing_highlight_setup = extract_int_or(msg, "is_setup", 1); + req.timing_pin_name = extract_string(msg, "pin_name"); + } else if (type_str == "clock_tree") { + req.type = WebSocketRequest::CLOCK_TREE; + } else if (type_str == "clock_tree_highlight") { + req.type = WebSocketRequest::CLOCK_TREE_HIGHLIGHT; + req.clock_tree_inst_name = extract_string(msg, "inst_name"); + } else if (type_str == "slack_histogram") { + req.type = WebSocketRequest::SLACK_HISTOGRAM; + req.histogram_is_setup = extract_int_or(msg, "is_setup", 1); + req.histogram_path_group = extract_string(msg, "path_group"); + req.histogram_clock = extract_string(msg, "clock_name"); + } else if (type_str == "chart_filters") { + req.type = WebSocketRequest::CHART_FILTERS; + } else if (type_str == "module_hierarchy") { + req.type = WebSocketRequest::MODULE_HIERARCHY; + } else if (type_str == "set_module_colors") { + req.type = WebSocketRequest::SET_MODULE_COLORS; + req.vis.parseFromJson(msg); + } else if (type_str == "set_focus_nets") { + req.type = WebSocketRequest::SET_FOCUS_NETS; + req.focus_action = extract_string(msg, "action"); + req.focus_net_name = extract_string(msg, "net_name"); + } else if (type_str == "set_route_guides") { + req.type = WebSocketRequest::SET_ROUTE_GUIDES; + req.route_guide_action = extract_string(msg, "action"); + req.route_guide_net_name = extract_string(msg, "net_name"); + } else if (type_str == "select") { + req.type = WebSocketRequest::SELECT; + req.select_x = extract_int(msg, "dbu_x"); + req.select_y = extract_int(msg, "dbu_y"); + req.select_zoom = extract_int_or(msg, "zoom", 0); + req.visible_layers = extract_string_array(msg, "visible_layers"); + req.vis.parseFromJson(msg); + } else if (type_str == "heatmaps") { + req.type = WebSocketRequest::HEATMAPS; + } else if (type_str == "set_active_heatmap") { + req.type = WebSocketRequest::SET_ACTIVE_HEATMAP; + req.heatmap_name = extract_string(msg, "name"); + } else if (type_str == "set_heatmap") { + req.type = WebSocketRequest::SET_HEATMAP; + req.heatmap_name = extract_string(msg, "name"); + req.heatmap_option = extract_string(msg, "option"); + req.heatmap_string_value = extract_string(msg, "value"); + } else if (type_str == "heatmap_tile") { + req.type = WebSocketRequest::HEATMAP_TILE; + req.heatmap_name = extract_string(msg, "name"); + req.z = extract_int(msg, "z"); + req.x = extract_int(msg, "x"); + req.y = extract_int(msg, "y"); + } else if (type_str == "list_dir") { + req.type = WebSocketRequest::LIST_DIR; + req.dir_path = extract_string(msg, "path"); + } else { + req.type = WebSocketRequest::UNKNOWN; + } + return req; +} + +// Serialize a WebSocketResponse into the binary wire format: +// [0..3] uint32_t id (big-endian) +// [4] uint8_t type +// [5..7] reserved +// [8..] payload +static std::vector serialize_response( + const WebSocketResponse& resp) +{ + std::vector frame(8 + resp.payload.size()); + const uint32_t id_be = htonl(resp.id); + std::memcpy(frame.data(), &id_be, 4); + frame[4] = resp.type; + frame[5] = frame[6] = frame[7] = 0; + if (!resp.payload.empty()) { + std::memcpy(frame.data() + 8, resp.payload.data(), resp.payload.size()); + } + return frame; +} + +//------------------------------------------------------------------------------ +// HTTP request handler (wraps dispatch_request for HTTP transport) +//------------------------------------------------------------------------------ + +static std::string content_type_for(const std::string& path) +{ + auto ext = std::filesystem::path(path).extension().string(); + if (ext == ".html") { + return "text/html"; + } + if (ext == ".js") { + return "application/javascript"; + } + if (ext == ".css") { + return "text/css"; + } + if (ext == ".png") { + return "image/png"; + } + if (ext == ".json") { + return "application/json"; + } + return "application/octet-stream"; +} + +static http::response handle_request( + http::request&& req, + const TileGenerator& generator, + const std::string& doc_root) +{ + http::response res{http::status::ok, req.version()}; + res.set(http::field::server, "Boost.Beast Server (C++17)"); + res.set(http::field::content_type, "text/plain"); + res.keep_alive(req.keep_alive()); + res.set(http::field::access_control_allow_origin, "*"); + + std::regex tile_regex(R"(/tile/(\w+)/(\d+)/(-?\d+)/(-?\d+)\.png)"); + std::smatch match_pieces; + std::string target_path(req.target()); + + if (req.method() == http::verb::get && req.target() == "/bounds") { + WebSocketRequest websocket_req; + websocket_req.type = WebSocketRequest::BOUNDS; + WebSocketResponse websocket_resp + = dispatch_request(websocket_req, generator); + res.set(http::field::content_type, "application/json"); + res.body() = std::string(websocket_resp.payload.begin(), + websocket_resp.payload.end()); + } else if (req.method() == http::verb::get && req.target() == "/tech") { + WebSocketRequest websocket_req; + websocket_req.type = WebSocketRequest::TECH; + WebSocketResponse websocket_resp + = dispatch_request(websocket_req, generator); + res.set(http::field::content_type, "application/json"); + res.body() = std::string(websocket_resp.payload.begin(), + websocket_resp.payload.end()); + } else if (req.method() == http::verb::get + && std::regex_match(target_path, match_pieces, tile_regex)) { + WebSocketRequest websocket_req; + websocket_req.type = WebSocketRequest::TILE; + websocket_req.layer = match_pieces[1].str(); + websocket_req.z = std::stoi(match_pieces[2].str()); + websocket_req.x = std::stoi(match_pieces[3].str()); + websocket_req.y = std::stoi(match_pieces[4].str()); + WebSocketResponse websocket_resp + = dispatch_request(websocket_req, generator); + + res.set(http::field::content_type, "image/png"); + res.body() = std::string(websocket_resp.payload.begin(), + websocket_resp.payload.end()); + res.set(http::field::cache_control, "public, max-age=604800"); + } else if (req.method() == http::verb::get && !doc_root.empty()) { + // Serve static files from doc_root + std::string file_path = target_path; + if (file_path == "/") { + file_path = "/index.html"; + } + // Reject paths with ".." to preventd irectory traversal + if (file_path.find("..") == std::string::npos) { + auto full_path = std::filesystem::path(doc_root) / file_path.substr(1); + std::ifstream file(full_path, std::ios::binary); + if (file) { + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + res.set(http::field::content_type, content_type_for(file_path)); + res.body() = std::move(content); + } else { + res.result(http::status::not_found); + res.body() = "File not found."; + } + } else { + res.result(http::status::bad_request); + res.body() = "Invalid path."; + } + } else { + res.result(http::status::not_found); + res.body() = "Resource not found."; + } + + res.prepare_payload(); + return res; +} + +//------------------------------------------------------------------------------ +// WebSocket session - multiplexes many requests over a single connection +//------------------------------------------------------------------------------ + +class WebSocketSession : public std::enable_shared_from_this +{ + websocket::stream websocket_; + beast::flat_buffer buffer_; + utl::Logger* logger_; + + // Handler objects (transport-independent, testable) + SessionState state_; + SelectHandler select_handler_; + TclHandler tcl_handler_; + TimingHandler timing_handler_; + ClockTreeHandler clock_tree_handler_; + TileHandler tile_handler_; + + // Write serialization: strand + queue ensures one async_write at a time + net::strand strand_; + std::deque> write_queue_; + bool writing_ = false; + + // Background search index initialization + std::shared_ptr generator_; + std::thread init_thread_; + + public: + WebSocketSession(tcp::socket&& socket, + std::shared_ptr generator, + std::shared_ptr tcl_eval, + std::shared_ptr timing_report, + std::shared_ptr clock_report, + utl::Logger* logger); + ~WebSocketSession(); + + void run(http::request&& req); + + private: + void on_accept(beast::error_code ec); + void do_read(); + void on_read(beast::error_code ec); + void queue_response(const WebSocketResponse& resp); + void do_write(); +}; + +WebSocketSession::WebSocketSession( + tcp::socket&& socket, + // NOLINTBEGIN(performance-unnecessary-value-param) + std::shared_ptr generator, + std::shared_ptr tcl_eval, + // NOLINTEND(performance-unnecessary-value-param) + std::shared_ptr timing_report, + std::shared_ptr clock_report, + utl::Logger* logger) + : websocket_(std::move(socket)), + logger_(logger), + select_handler_(generator, tcl_eval), + tcl_handler_(tcl_eval), + timing_handler_(generator, std::move(timing_report), tcl_eval), + clock_tree_handler_(generator, std::move(clock_report), tcl_eval), + tile_handler_(generator), + strand_(net::make_strand(websocket_.get_executor())), + generator_(generator) +{ + if (generator_->getBlock()) { + tile_handler_.initializeHeatMaps(state_); + } +} + +WebSocketSession::~WebSocketSession() +{ + if (init_thread_.joinable()) { + init_thread_.join(); + } + std::lock_guard lock(state_.heatmap_mutex); + if (!state_.active_heatmap.empty()) { + auto active = state_.heatmaps.find(state_.active_heatmap); + if (active != state_.heatmaps.end()) { + active->second->onHide(); + } + } +} + +void WebSocketSession::run(http::request&& req) +{ + websocket_.set_option( + websocket::stream_base::timeout::suggested(beast::role_type::server)); + websocket_.set_option( + websocket::stream_base::decorator([](websocket::response_type& res) { + res.set(http::field::server, "OpenROAD WebSocket Server"); + })); + + // Build search indices in the background; tiles render without shapes + // until ready, then a "refresh" push notification triggers a redraw. + init_thread_ = std::thread([self = shared_from_this()]() { + self->generator_->eagerInit(); + // Only send refresh if there's actually a design to render. + // Without this guard, eagerInit returns instantly when no block is + // loaded and the push races with async_accept (Beast soft_mutex crash). + if (!self->generator_->getBlock()) { + return; + } + // Send server-push refresh notification (id=0) + WebSocketResponse resp; + resp.id = 0; + resp.type = 0; // JSON + const std::string json = R"({"type":"refresh"})"; + resp.payload.assign(json.begin(), json.end()); + self->queue_response(resp); + }); + + websocket_.async_accept(req, + [self = shared_from_this()](beast::error_code ec) { + self->on_accept(ec); + }); +} + +void WebSocketSession::on_accept(beast::error_code ec) +{ + if (ec) { + debugPrint(logger_, + utl::WEB, + "websocket", + 1, + "websocket accept error: {}", + ec.message()); + return; + } + + do_read(); +} + +void WebSocketSession::do_read() +{ + websocket_.async_read( + buffer_, [self = shared_from_this()](beast::error_code ec, std::size_t) { + self->on_read(ec); + }); +} + +void WebSocketSession::on_read(beast::error_code ec) +{ + if (ec) { + if (ec != websocket::error::closed) { + debugPrint(logger_, + utl::WEB, + "websocket", + 1, + "websocket read error: {}", + ec.message()); + } + return; + } + + const std::string msg = beast::buffers_to_string(buffer_.data()); + buffer_.consume(buffer_.size()); + + const WebSocketRequest req = parse_web_socket_request(msg); + auto self = shared_from_this(); + + switch (req.type) { + case WebSocketRequest::SELECT: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->select_handler_.handleSelect(req, self->state_)); + }); + break; + case WebSocketRequest::INSPECT: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->select_handler_.handleInspect(req, self->state_)); + }); + break; + case WebSocketRequest::INSPECT_BACK: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->select_handler_.handleInspectBack(req, self->state_)); + }); + break; + case WebSocketRequest::HOVER: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->select_handler_.handleHover(req, self->state_)); + }); + break; + case WebSocketRequest::TCL_EVAL: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response(self->tcl_handler_.handleTclEval(req)); + }); + break; + case WebSocketRequest::TIMING_REPORT: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response(self->timing_handler_.handleTimingReport(req)); + }); + break; + case WebSocketRequest::TIMING_HIGHLIGHT: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->timing_handler_.handleTimingHighlight(req, self->state_)); + }); + break; + case WebSocketRequest::CLOCK_TREE: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response(self->clock_tree_handler_.handleClockTree(req)); + }); + break; + case WebSocketRequest::CLOCK_TREE_HIGHLIGHT: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response(self->clock_tree_handler_.handleClockTreeHighlight( + req, self->state_)); + }); + break; + case WebSocketRequest::SLACK_HISTOGRAM: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response(self->timing_handler_.handleSlackHistogram(req)); + }); + break; + case WebSocketRequest::CHART_FILTERS: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response(self->timing_handler_.handleChartFilters(req)); + }); + break; + case WebSocketRequest::MODULE_HIERARCHY: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response(self->tile_handler_.handleModuleHierarchy(req)); + }); + break; + case WebSocketRequest::SET_MODULE_COLORS: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->tile_handler_.handleSetModuleColors(req, self->state_)); + }); + break; + case WebSocketRequest::SET_FOCUS_NETS: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->select_handler_.handleSetFocusNets(req, self->state_)); + }); + break; + case WebSocketRequest::SET_ROUTE_GUIDES: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->select_handler_.handleSetRouteGuides(req, self->state_)); + }); + break; + case WebSocketRequest::HEATMAPS: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->tile_handler_.handleHeatMaps(req, self->state_)); + }); + break; + case WebSocketRequest::SET_ACTIVE_HEATMAP: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->tile_handler_.handleSetActiveHeatMap(req, self->state_)); + }); + break; + case WebSocketRequest::SET_HEATMAP: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->tile_handler_.handleSetHeatMap(req, self->state_)); + }); + break; + case WebSocketRequest::HEATMAP_TILE: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response( + self->tile_handler_.handleHeatMapTile(req, self->state_)); + }); + break; + case WebSocketRequest::LIST_DIR: + net::post(websocket_.get_executor(), + [self, req]() { self->queue_response(handleListDir(req)); }); + break; + default: + net::post(websocket_.get_executor(), [self, req]() { + self->queue_response(self->tile_handler_.handleTile(req, self->state_)); + }); + break; + } + + do_read(); +} + +void WebSocketSession::queue_response(const WebSocketResponse& resp) +{ + std::vector frame = serialize_response(resp); + + // Post to the strand to serialize write queue access + net::post(strand_, + [self = shared_from_this(), frame = std::move(frame)]() mutable { + self->write_queue_.push_back(std::move(frame)); + if (!self->writing_) { + self->do_write(); + } + }); +} + +void WebSocketSession::do_write() +{ + if (write_queue_.empty()) { + writing_ = false; + return; + } + writing_ = true; + websocket_.binary(true); + websocket_.async_write( + net::buffer(write_queue_.front()), + [self = shared_from_this()](beast::error_code ec, std::size_t) { + net::post(self->strand_, [self, ec]() { + if (ec) { + debugPrint(self->logger_, + utl::WEB, + "websocket", + 1, + "websocket write error: {}", + ec.message()); + return; + } + self->write_queue_.pop_front(); + self->do_write(); + }); + }); +} + +//------------------------------------------------------------------------------ +// HTTP session - handles traditional HTTP connections +//------------------------------------------------------------------------------ + +class HttpSession : public std::enable_shared_from_this +{ + beast::tcp_stream stream_; + beast::flat_buffer buffer_; + std::shared_ptr generator_; + std::shared_ptr> res_; + http::request req_; + std::string doc_root_; + utl::Logger* logger_; + + public: + HttpSession(tcp::socket&& socket, + std::shared_ptr generator, + std::string doc_root, + utl::Logger* logger); + + void run() { do_read(); } + + void run_with_request(http::request req, + beast::flat_buffer buffer); + + private: + void do_read(); + void on_read(beast::error_code ec); + void do_write(); + void on_write(beast::error_code ec); + void do_close(); +}; + +HttpSession::HttpSession(tcp::socket&& socket, + std::shared_ptr generator, + std::string doc_root, + utl::Logger* logger) + : stream_(std::move(socket)), + generator_(std::move(generator)), + doc_root_(std::move(doc_root)), + logger_(logger) +{ +} + +void HttpSession::run_with_request(http::request req, + beast::flat_buffer buffer) +{ + req_ = std::move(req); + buffer_ = std::move(buffer); + on_read({}); +} + +void HttpSession::do_read() +{ + req_ = {}; + http::async_read( + stream_, + buffer_, + req_, + [self = shared_from_this()](beast::error_code ec, std::size_t) { + self->on_read(ec); + }); +} + +void HttpSession::on_read(beast::error_code ec) +{ + if (ec == http::error::end_of_stream) { + do_close(); + return; + } + if (ec) { + debugPrint( + logger_, utl::WEB, "http", 1, "http read error: {}", ec.message()); + return; + } + + res_ = std::make_shared>( + handle_request(std::move(req_), *generator_, doc_root_)); + do_write(); +} + +void HttpSession::do_write() +{ + http::async_write( + stream_, + *res_, + [self = shared_from_this()](beast::error_code ec, std::size_t) { + self->on_write(ec); + }); +} + +void HttpSession::on_write(beast::error_code ec) +{ + if (ec) { + debugPrint( + logger_, utl::WEB, "http", 1, "http write error: {}", ec.message()); + return; + } + + bool keep_alive = res_->keep_alive(); + res_ = nullptr; + + if (keep_alive) { + do_read(); + } else { + do_close(); + } +} + +void HttpSession::do_close() +{ + beast::error_code ec; + stream_.socket().shutdown(tcp::socket::shutdown_send, ec); +} + +//------------------------------------------------------------------------------ +// Detect session - reads first HTTP request, routes to WS or HTTP session +//------------------------------------------------------------------------------ + +class DetectSession : public std::enable_shared_from_this +{ + beast::tcp_stream stream_; + beast::flat_buffer buffer_; + std::shared_ptr generator_; + std::shared_ptr tcl_eval_; + std::shared_ptr timing_report_; + std::shared_ptr clock_report_; + http::request req_; + std::string doc_root_; + utl::Logger* logger_; + + public: + DetectSession(tcp::socket&& socket, + std::shared_ptr generator, + std::shared_ptr tcl_eval, + std::shared_ptr timing_report, + std::shared_ptr clock_report, + std::string doc_root, + utl::Logger* logger); + + void run(); + + private: + void on_read(beast::error_code ec); +}; + +DetectSession::DetectSession(tcp::socket&& socket, + std::shared_ptr generator, + std::shared_ptr tcl_eval, + std::shared_ptr timing_report, + std::shared_ptr clock_report, + std::string doc_root, + utl::Logger* logger) + : stream_(std::move(socket)), + generator_(std::move(generator)), + tcl_eval_(std::move(tcl_eval)), + timing_report_(std::move(timing_report)), + clock_report_(std::move(clock_report)), + doc_root_(std::move(doc_root)), + logger_(logger) +{ +} + +void DetectSession::run() +{ + http::async_read( + stream_, + buffer_, + req_, + [self = shared_from_this()](beast::error_code ec, std::size_t) { + self->on_read(ec); + }); +} + +void DetectSession::on_read(beast::error_code ec) +{ + if (ec) { + debugPrint( + logger_, utl::WEB, "http", 1, "detect read error: {}", ec.message()); + return; + } + + if (websocket::is_upgrade(req_)) { + // WebSocket upgrade - hand off to WebSocketSession + auto websocket_session + = std::make_shared(stream_.release_socket(), + generator_, + tcl_eval_, + timing_report_, + clock_report_, + logger_); + websocket_session->run(std::move(req_)); + } else { + // Regular HTTP - hand off to session with already-read request + auto s = std::make_shared( + stream_.release_socket(), generator_, doc_root_, logger_); + s->run_with_request(std::move(req_), std::move(buffer_)); + } +} + +//------------------------------------------------------------------------------ +// Listener - accepts incoming connections +//------------------------------------------------------------------------------ + +class Listener : public std::enable_shared_from_this +{ + net::io_context& ioc_; + tcp::acceptor acceptor_; + std::shared_ptr generator_; + std::shared_ptr tcl_eval_; + std::shared_ptr timing_report_; + std::shared_ptr clock_report_; + std::string doc_root_; + utl::Logger* logger_; + + public: + Listener(net::io_context& ioc, + const tcp::endpoint& endpoint, + std::shared_ptr generator, + std::shared_ptr tcl_eval, + std::shared_ptr timing_report, + std::shared_ptr clock_report, + std::string doc_root, + utl::Logger* logger); + + void run() { do_accept(); } + + private: + void do_accept(); + void on_accept(beast::error_code ec, tcp::socket socket); +}; + +Listener::Listener(net::io_context& ioc, + const tcp::endpoint& endpoint, + std::shared_ptr generator, + std::shared_ptr tcl_eval, + std::shared_ptr timing_report, + std::shared_ptr clock_report, + std::string doc_root, + utl::Logger* logger) + : ioc_(ioc), + acceptor_(ioc), + generator_(std::move(generator)), + tcl_eval_(std::move(tcl_eval)), + timing_report_(std::move(timing_report)), + clock_report_(std::move(clock_report)), + doc_root_(std::move(doc_root)), + logger_(logger) +{ + beast::error_code ec; + + acceptor_.open(endpoint.protocol(), ec); + if (ec) { + return; + } + + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec) { + return; + } + + acceptor_.bind(endpoint, ec); + if (ec) { + return; + } + + acceptor_.listen(net::socket_base::max_listen_connections, ec); + if (ec) { + return; + } +} + +void Listener::do_accept() +{ + acceptor_.async_accept( + ioc_, + [self = shared_from_this()](beast::error_code ec, tcp::socket socket) { + self->on_accept(ec, std::move(socket)); + }); +} + +void Listener::on_accept(beast::error_code ec, tcp::socket socket) +{ + if (ec) { + debugPrint(logger_, utl::WEB, "http", 1, "accept error: {}", ec.message()); + } else { + // Route through DetectSession to handle both HTTP and WebSocket + std::make_shared(std::move(socket), + generator_, + tcl_eval_, + timing_report_, + clock_report_, + doc_root_, + logger_) + ->run(); + } + do_accept(); +} + +//------------------------------------------------------------------------------ +// WebServer +//------------------------------------------------------------------------------ + +WebServer::WebServer(odb::dbDatabase* db, + sta::dbSta* sta, + utl::Logger* logger, + Tcl_Interp* interp) + : db_(db), sta_(sta), logger_(logger), interp_(interp) +{ +} + +WebServer::~WebServer() = default; + +void WebServer::serve(const std::string& doc_root) +{ + try { + generator_ = std::make_shared(db_, sta_, logger_); + auto timing_report = std::make_shared(sta_); + auto clock_report = std::make_shared(sta_); + + // Create Tcl evaluator with logger sink for output capture + auto tcl_eval = std::make_shared(interp_, logger_); + + auto const address = net::ip::make_address("127.0.0.1"); + uint16_t const port = 8080; + int const num_threads = 32; + + if (!doc_root.empty()) { + logger_->info(utl::WEB, 4, "Serving static files from {}", doc_root); + } + logger_->info(utl::WEB, + 1, + "Server starting on http://:{} with {} threads...", + port, + num_threads); + + net::io_context ioc{num_threads}; + + std::make_shared(ioc, + tcp::endpoint{address, port}, + generator_, + tcl_eval, + timing_report, + clock_report, + doc_root, + logger_) + ->run(); + + net::signal_set signals(ioc, SIGINT, SIGTERM); + signals.async_wait([&](auto, auto) { + logger_->info(utl::WEB, 3, "Shutting down..."); + ioc.stop(); + }); + + std::vector threads; + threads.reserve(num_threads - 1); + for (int i = 0; i < num_threads - 1; ++i) { + threads.emplace_back([&ioc] { ioc.run(); }); + } + + ioc.run(); + + for (auto& t : threads) { + t.join(); + } + + logger_->info(utl::WEB, 5, "Server stopped."); + } catch (std::exception const& e) { + logger_->error(utl::WEB, 2, "Server error : {}", e.what()); + } +} + +} // namespace web diff --git a/src/web/src/web.i b/src/web/src/web.i new file mode 100644 index 00000000000..5d21be1a727 --- /dev/null +++ b/src/web/src/web.i @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +%{ +#include "ord/OpenRoad.hh" +#include "web/web.h" +%} + +%include "../../Exception.i" + +%inline %{ + +namespace web { + +void +web_server_cmd(const char* doc_root) +{ + web::WebServer *server = ord::OpenRoad::openRoad()->getWebServer(); + server->serve(doc_root); +} + +} // namespace web + +%} // inline diff --git a/src/web/src/web.tcl b/src/web/src/web.tcl new file mode 100644 index 00000000000..7a20fbc3e0c --- /dev/null +++ b/src/web/src/web.tcl @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2019-2026, The OpenROAD Authors + +sta::define_cmd_args "web_server" { [-dir dir] } + +proc web_server { args } { + sta::parse_key_args "web_server" args \ + keys {-dir} flags {} + + sta::check_argc_eq0 "web_server" $args + + set doc_root "" + if { [info exists keys(-dir)] } { + set doc_root $keys(-dir) + } + + web::web_server_cmd $doc_root +} diff --git a/src/web/src/websocket-manager.js b/src/web/src/websocket-manager.js new file mode 100644 index 00000000000..2eec00424ba --- /dev/null +++ b/src/web/src/websocket-manager.js @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// WebSocket manager with request/response tracking and auto-reconnect. + +export class WebSocketManager { + constructor(url, onStatusChange) { + this.url = url; + this.socket = null; + this.nextId = 1; + this.pending = new Map(); // id -> {resolve, reject} + this.reconnectDelay = 1000; + this.readyPromise = null; + this.readyResolve = null; + this.onStatusChange = onStatusChange || (() => {}); + this.onPush = null; // callback for server-push notifications + this.connect(); + } + + connect() { + this.readyPromise = new Promise(resolve => { + this.readyResolve = resolve; + }); + + this.socket = new WebSocket(this.url); + this.socket.binaryType = 'arraybuffer'; + + this.socket.onopen = () => { + console.log('WebSocket connected'); + this.reconnectDelay = 1000; + this.readyResolve(); + }; + + this.socket.onmessage = (event) => { + this.handleMessage(event.data); + }; + + this.socket.onclose = () => { + console.log('WebSocket closed, reconnecting...'); + for (const [id, handler] of this.pending) { + handler.reject(new Error('WebSocket closed')); + } + this.pending.clear(); + setTimeout(() => this.connect(), this.reconnectDelay); + this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000); + }; + + this.socket.onerror = (err) => { + console.error('WebSocket error:', err); + }; + } + + handleMessage(data) { + // Binary frame: [4B id][1B type][3B reserved][payload...] + const view = new DataView(data); + const id = view.getUint32(0); // big-endian + const type = view.getUint8(4); // 0=JSON, 1=PNG, 2=error + + // id=0 is a server-push notification (not a response to a request) + if (id === 0 && type === 0) { + const payload = data.slice(8); + const msg = JSON.parse(new TextDecoder().decode(payload)); + if (this.onPush) { + this.onPush(msg); + } + return; + } + + const handler = this.pending.get(id); + if (!handler) { + return; // stale response (e.g. tile scrolled away) + } + this.pending.delete(id); + this.onStatusChange(); + + const payload = data.slice(8); + + if (type === 2) { + handler.reject(new Error(new TextDecoder().decode(payload))); + } else if (type === 0) { + handler.resolve(JSON.parse(new TextDecoder().decode(payload))); + } else if (type === 1) { + handler.resolve(new Blob([payload], { type: 'image/png' })); + } + } + + request(msg) { + const id = this.nextId++; + msg.id = id; + const promise = new Promise((resolve, reject) => { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + reject(new Error('WebSocket not connected')); + return; + } + this.pending.set(id, { resolve, reject }); + this.socket.send(JSON.stringify(msg)); + this.onStatusChange(); + }); + promise.requestId = id; + return promise; + } + + cancel(id) { + this.pending.delete(id); + this.onStatusChange(); + } +} diff --git a/src/web/src/websocket-tile-layer.js b/src/web/src/websocket-tile-layer.js new file mode 100644 index 00000000000..daee657a623 --- /dev/null +++ b/src/web/src/websocket-tile-layer.js @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Leaflet tile layer that fetches tiles via WebSocket. + +export function createWebSocketTileLayer(visibility) { + return L.GridLayer.extend({ + initialize: function(websocketManager, layerName, options) { + this._websocketManager = websocketManager; + this._layerName = layerName; + L.GridLayer.prototype.initialize.call(this, options); + }, + + createTile: function(coords, done) { + const tile = document.createElement('img'); + tile.alt = ''; + tile.setAttribute('role', 'presentation'); + + // Set up onload/onerror BEFORE any src assignment so that + // refreshTiles() can set tile.src and still trigger done(). + tile._tileDone = false; + tile.onload = () => { + if (tile.src && tile.src.startsWith('blob:')) { + URL.revokeObjectURL(tile.src); + } + if (!tile._tileDone) { + tile._tileDone = true; + done(null, tile); + } + }; + tile.onerror = () => { + if (!tile._tileDone) { + tile._tileDone = true; + done(new Error('tile image load error'), tile); + } + }; + + const vf = {}; + for (const [k, v] of Object.entries(visibility)) { + vf[k] = v ? 1 : 0; + } + this._websocketManager.request({ + type: 'tile', + layer: this._layerName, + z: coords.z, + x: coords.x, + y: coords.y, + ...vf, + }).then(blob => { + tile.src = URL.createObjectURL(blob); + }).catch(() => { + // Request was cancelled (e.g. by refreshTiles); ignore + }); + + return tile; + }, + + // Re-request all existing tiles in place (no removal/flash). + // Use this instead of redraw() for visibility changes. + refreshTiles: function() { + if (!this._map) return; + + const vf = {}; + for (const [k, v] of Object.entries(visibility)) { + vf[k] = v ? 1 : 0; + } + + for (const key in this._tiles) { + const tileInfo = this._tiles[key]; + if (!tileInfo || !tileInfo.el) continue; + + const tile = tileInfo.el; + const coords = tileInfo.coords; + + // Cancel any pending request for this tile + if (tile._websocketRequestId !== undefined) { + this._websocketManager.cancel(tile._websocketRequestId); + } + + const requestId = this._websocketManager.nextId; + tile._websocketRequestId = requestId; + + this._websocketManager.request({ + type: 'tile', + layer: this._layerName, + z: coords.z, + x: coords.x, + y: coords.y, + ...vf, + }).then(blob => { + if (tile.src && tile.src.startsWith('blob:')) { + URL.revokeObjectURL(tile.src); + } + tile.src = URL.createObjectURL(blob); + }).catch(() => { + // Tile refresh failed; keep existing image + }); + } + }, + + _removeTile: function(key) { + const tile = this._tiles[key]; + if (tile && tile.el) { + if (tile.el._websocketRequestId !== undefined) { + this._websocketManager.cancel(tile.el._websocketRequestId); + } + if (tile.el.src && tile.el.src.startsWith('blob:')) { + URL.revokeObjectURL(tile.el.src); + } + } + L.GridLayer.prototype._removeTile.call(this, key); + } + }); +} diff --git a/src/web/test/BUILD b/src/web/test/BUILD new file mode 100644 index 00000000000..f920b728cd2 --- /dev/null +++ b/src/web/test/BUILD @@ -0,0 +1,161 @@ +load("@aspect_rules_js//js:defs.bzl", "js_test") + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2022-2026, The OpenROAD Authors +load("@rules_cc//cc:cc_test.bzl", "cc_test") +load("//test:regression.bzl", "regression_test") + +package(features = ["layering_check"]) + +TESTS = [ +] + +filegroup( + name = "test_resources", + # overly broad glob, could be refined later, but + # symlinks are cheap and OpenROAD binary changes, the common + # use case is that all tests have to be re-run. + srcs = glob( + ["**/*"], + exclude = [ + test + "." + ext + for test in TESTS + for ext in [ + "tcl", + "py", + ] + ], + ), +) + +[regression_test( + name = test_name, + data = [":test_resources"], +) for test_name in TESTS] + +js_test( + name = "coordinates_test", + data = ["//src/web:js_sources"], + entry_point = "js/test-coordinates.js", + no_copy_to_bin = ["//src/web:js_sources"], +) + +js_test( + name = "vis_tree_test", + data = [ + "js/setup-dom.js", + "//src/web:js_sources", + "//src/web:node_modules/jsdom", + ], + entry_point = "js/test-vis-tree.js", + no_copy_to_bin = ["//src/web:js_sources"], +) + +js_test( + name = "websocket_manager_test", + data = ["//src/web:js_sources"], + entry_point = "js/test-websocket-manager.js", + no_copy_to_bin = ["//src/web:js_sources"], +) + +js_test( + name = "checkbox_tree_model_test", + data = ["//src/web:js_sources"], + entry_point = "js/test-checkbox-tree-model.js", + no_copy_to_bin = ["//src/web:js_sources"], +) + +js_test( + name = "display_controls_test", + data = ["//src/web:js_sources"], + entry_point = "js/test-display-controls.js", + no_copy_to_bin = ["//src/web:js_sources"], +) + +js_test( + name = "clock_tree_widget_test", + data = ["//src/web:js_sources"], + entry_point = "js/test-clock-tree-widget.js", + no_copy_to_bin = ["//src/web:js_sources"], +) + +js_test( + name = "charts_widget_test", + data = ["//src/web:js_sources"], + entry_point = "js/test-charts-widget.js", + no_copy_to_bin = ["//src/web:js_sources"], +) + +js_test( + name = "inspector_test", + data = [ + "js/setup-dom.js", + "//src/web:js_sources", + "//src/web:node_modules/jsdom", + ], + entry_point = "js/test-inspector.js", + no_copy_to_bin = ["//src/web:js_sources"], +) + +cc_test( + name = "clock_tree_report_test", + srcs = ["cpp/TestClockTreeReport.cpp"], + copts = ["-Isrc/web/src"], + data = [ + "//test:nangate45_data", + ], + features = ["-layering_check"], + deps = [ + "//src/gui", + "//src/odb", + "//src/tst", + "//src/tst:nangate45_fixture", + "//src/utl", + "//src/web", + "//third-party/lodepng", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "request_handler_test", + srcs = ["cpp/TestRequestHandler.cpp"], + copts = ["-Isrc/web/src"], + data = [ + "//test:nangate45_data", + ], + features = ["-layering_check"], + deps = [ + "//src/gui", + "//src/odb", + "//src/tst", + "//src/tst:nangate45_fixture", + "//src/utl", + "//src/web", + "//third-party/lodepng", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "tile_generator_test", + srcs = ["cpp/TestTileGenerator.cpp"], + copts = ["-Isrc/web/src"], + data = [ + "//test:nangate45_data", + ], + features = ["-layering_check"], + deps = [ + "//src/gui", + "//src/odb", + "//src/tst", + "//src/tst:nangate45_fixture", + "//src/utl", + "//src/web", + "//third-party/lodepng", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) diff --git a/src/web/test/CMakeLists.txt b/src/web/test/CMakeLists.txt new file mode 100644 index 00000000000..f2a3b850a9a --- /dev/null +++ b/src/web/test/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors + +or_integration_tests( + "web" + PASSFAIL_TESTS + cpp_tests +) + +add_subdirectory(cpp) diff --git a/src/web/test/Nangate45 b/src/web/test/Nangate45 new file mode 120000 index 00000000000..7f1df955c57 --- /dev/null +++ b/src/web/test/Nangate45 @@ -0,0 +1 @@ +../../../test/Nangate45 \ No newline at end of file diff --git a/src/web/test/cpp/CMakeLists.txt b/src/web/test/cpp/CMakeLists.txt new file mode 100644 index 00000000000..87f5b0bee1e --- /dev/null +++ b/src/web/test/cpp/CMakeLists.txt @@ -0,0 +1,42 @@ +include("openroad") + +set(TEST_LIBS + web + odb + utl_lib + lodepng + tst + GTest::gtest + GTest::gtest_main +) + +add_executable(TestTileGenerator TestTileGenerator.cpp) + +target_link_libraries(TestTileGenerator ${TEST_LIBS}) + +target_include_directories(TestTileGenerator + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../src +) + +gtest_discover_tests(TestTileGenerator + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +add_executable(TestRequestHandler TestRequestHandler.cpp) + +target_link_libraries(TestRequestHandler ${TEST_LIBS} gui_descriptors) + +target_include_directories(TestRequestHandler + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../src +) + +gtest_discover_tests(TestRequestHandler + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +add_dependencies(build_and_test + TestTileGenerator + TestRequestHandler +) diff --git a/src/web/test/cpp/TestClockTreeReport.cpp b/src/web/test/cpp/TestClockTreeReport.cpp new file mode 100644 index 00000000000..307d311ec4f --- /dev/null +++ b/src/web/test/cpp/TestClockTreeReport.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include +#include +#include + +#include "clock_tree_report.h" +#include "gtest/gtest.h" +#include "odb/db.h" +#include "request_handler.h" +#include "tile_generator.h" +#include "tst/nangate45_fixture.h" + +namespace web { +namespace { + +// Helper to extract payload as string. +std::string payloadStr(const WebSocketResponse& resp) +{ + return std::string(resp.payload.begin(), resp.payload.end()); +} + +//------------------------------------------------------------------------------ +// ClockTreeNode::typeToString tests (no fixture needed) +//------------------------------------------------------------------------------ + +TEST(ClockTreeNodeTest, TypeToStringRoot) +{ + EXPECT_STREQ(ClockTreeNode::typeToString(ClockTreeNode::ROOT), "root"); +} + +TEST(ClockTreeNodeTest, TypeToStringBuffer) +{ + EXPECT_STREQ(ClockTreeNode::typeToString(ClockTreeNode::BUFFER), "buffer"); +} + +TEST(ClockTreeNodeTest, TypeToStringInverter) +{ + EXPECT_STREQ(ClockTreeNode::typeToString(ClockTreeNode::INVERTER), + "inverter"); +} + +TEST(ClockTreeNodeTest, TypeToStringClockGate) +{ + EXPECT_STREQ(ClockTreeNode::typeToString(ClockTreeNode::CLOCK_GATE), + "clock_gate"); +} + +TEST(ClockTreeNodeTest, TypeToStringRegister) +{ + EXPECT_STREQ(ClockTreeNode::typeToString(ClockTreeNode::REGISTER), + "register"); +} + +TEST(ClockTreeNodeTest, TypeToStringMacro) +{ + EXPECT_STREQ(ClockTreeNode::typeToString(ClockTreeNode::MACRO), "macro"); +} + +TEST(ClockTreeNodeTest, TypeToStringUnknown) +{ + EXPECT_STREQ(ClockTreeNode::typeToString(ClockTreeNode::UNKNOWN), "unknown"); +} + +//------------------------------------------------------------------------------ +// ClockTreeData default values +//------------------------------------------------------------------------------ + +TEST(ClockTreeDataTest, DefaultValues) +{ + ClockTreeData data; + EXPECT_TRUE(data.clock_name.empty()); + EXPECT_EQ(data.min_arrival, 0.0f); + EXPECT_EQ(data.max_arrival, 0.0f); + EXPECT_TRUE(data.time_unit.empty()); + EXPECT_TRUE(data.nodes.empty()); +} + +//------------------------------------------------------------------------------ +// ClockTreeNode default values +//------------------------------------------------------------------------------ + +TEST(ClockTreeNodeTest, DefaultValues) +{ + ClockTreeNode node; + EXPECT_EQ(node.id, 0); + EXPECT_EQ(node.parent_id, -1); + EXPECT_TRUE(node.name.empty()); + EXPECT_TRUE(node.pin_name.empty()); + EXPECT_EQ(node.type, ClockTreeNode::UNKNOWN); + EXPECT_EQ(node.arrival, 0.0f); + EXPECT_EQ(node.delay, 0.0f); + EXPECT_EQ(node.fanout, 0); + EXPECT_EQ(node.level, 0); + EXPECT_EQ(node.dbu_x, 0); + EXPECT_EQ(node.dbu_y, 0); +} + +//------------------------------------------------------------------------------ +// ClockTreeHandler::handleClockTreeHighlight tests +//------------------------------------------------------------------------------ + +class ClockTreeHighlightTest : public tst::Nangate45Fixture +{ + protected: + void SetUp() override + { + block_->setDieArea(odb::Rect(0, 0, 100000, 100000)); + placeInst("BUF_X16", "clkbuf1", 5000, 5000); + gen_ = std::make_shared( + getDb(), /*sta=*/nullptr, getLogger()); + } + + odb::dbInst* placeInst(const char* master_name, + const char* inst_name, + int x, + int y) + { + odb::dbMaster* master = lib_->findMaster(master_name); + EXPECT_NE(master, nullptr); + odb::dbInst* inst = odb::dbInst::create(block_, master, inst_name); + inst->setLocation(x, y); + inst->setPlacementStatus(odb::dbPlacementStatus::PLACED); + return inst; + } + + std::shared_ptr gen_; + SessionState state_; +}; + +TEST_F(ClockTreeHighlightTest, HighlightExistingInstance) +{ + auto handler = std::make_unique(gen_, nullptr, nullptr); + + WebSocketRequest req; + req.id = 1; + req.type = WebSocketRequest::CLOCK_TREE_HIGHLIGHT; + req.clock_tree_inst_name = "clkbuf1"; + + auto resp = handler->handleClockTreeHighlight(req, state_); + EXPECT_EQ(resp.id, 1u); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"ok\""), std::string::npos); + + // Should have populated highlight_rects with the instance bbox + std::lock_guard lock(state_.selection_mutex); + EXPECT_EQ(state_.highlight_rects.size(), 1u); +} + +TEST_F(ClockTreeHighlightTest, HighlightNonExistentInstance) +{ + auto handler = std::make_unique(gen_, nullptr, nullptr); + + WebSocketRequest req; + req.id = 2; + req.type = WebSocketRequest::CLOCK_TREE_HIGHLIGHT; + req.clock_tree_inst_name = "does_not_exist"; + + auto resp = handler->handleClockTreeHighlight(req, state_); + EXPECT_EQ(resp.type, 0); + + // No instance found → no highlight rects + std::lock_guard lock(state_.selection_mutex); + EXPECT_TRUE(state_.highlight_rects.empty()); +} + +TEST_F(ClockTreeHighlightTest, EmptyNameClearsState) +{ + // Pre-populate some state + { + std::lock_guard lock(state_.selection_mutex); + state_.highlight_rects.emplace_back(0, 0, 100, 100); + state_.timing_rects.push_back( + {odb::Rect(0, 0, 1, 1), {.r = 255, .g = 0, .b = 0, .a = 255}, ""}); + } + + auto handler = std::make_unique(gen_, nullptr, nullptr); + + WebSocketRequest req; + req.id = 3; + req.type = WebSocketRequest::CLOCK_TREE_HIGHLIGHT; + req.clock_tree_inst_name = ""; + + auto resp = handler->handleClockTreeHighlight(req, state_); + EXPECT_EQ(resp.type, 0); + + // All state should be cleared + std::lock_guard lock(state_.selection_mutex); + EXPECT_TRUE(state_.highlight_rects.empty()); + EXPECT_TRUE(state_.timing_rects.empty()); + EXPECT_TRUE(state_.timing_lines.empty()); +} + +} // namespace +} // namespace web diff --git a/src/web/test/cpp/TestRequestHandler.cpp b/src/web/test/cpp/TestRequestHandler.cpp new file mode 100644 index 00000000000..c021d0afff6 --- /dev/null +++ b/src/web/test/cpp/TestRequestHandler.cpp @@ -0,0 +1,779 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include +#include +#include + +#include "gtest/gtest.h" +#include "gui/heatMap.h" +#include "json_builder.h" +#include "odb/db.h" +#include "request_handler.h" +#include "tile_generator.h" +#include "tst/nangate45_fixture.h" + +namespace web { +namespace { + +struct FakeInspectable +{ + std::string name; + std::string type; + odb::Rect bbox; +}; + +class FakeDescriptor : public gui::Descriptor +{ + public: + std::string getName(const std::any& object) const override + { + return std::any_cast(object)->name; + } + + std::string getTypeName() const override { return "Fake"; } + + std::string getTypeName(const std::any& object) const override + { + return std::any_cast(object)->type; + } + + bool getBBox(const std::any& object, odb::Rect& bbox) const override + { + bbox = std::any_cast(object)->bbox; + return true; + } + + void visitAllObjects( + const std::function&) const override + { + } + + Properties getProperties(const std::any&) const override { return {}; } + + gui::Selected makeSelected(const std::any& object) const override + { + return gui::Selected(object, this); + } + + bool lessThan(const std::any& l, const std::any& r) const override + { + return std::any_cast(l) + < std::any_cast(r); + } + + void highlight(const std::any& object, gui::Painter& painter) const override + { + painter.drawRect(std::any_cast(object)->bbox); + } +}; + +class LazyMetadataHeatMap : public gui::HeatMapDataSource +{ + public: + explicit LazyMetadataHeatMap(utl::Logger* logger, int* populate_calls) + : gui::HeatMapDataSource(logger, + "Lazy Metadata Heat Map", + "LazyMeta", + "LazyMeta"), + populate_calls_(populate_calls) + { + } + + protected: + bool populateMap() override + { + ++(*populate_calls_); + return false; + } + + void combineMapData(bool, double&, double, double, double, double) override {} + + private: + int* populate_calls_; +}; + +// Helper to extract payload as string. +std::string payloadStr(const WebSocketResponse& resp) +{ + return std::string(resp.payload.begin(), resp.payload.end()); +} + +//------------------------------------------------------------------------------ +// json_escape tests (no fixture needed) +//------------------------------------------------------------------------------ + +TEST(JsonEscapeTest, PlainString) +{ + EXPECT_EQ(json_escape("hello"), "hello"); +} + +TEST(JsonEscapeTest, EscapesSpecialChars) +{ + EXPECT_EQ(json_escape("a\"b"), "a\\\"b"); + EXPECT_EQ(json_escape("a\\b"), "a\\\\b"); + EXPECT_EQ(json_escape("a\nb"), "a\\nb"); + EXPECT_EQ(json_escape("a\tb"), "a\\tb"); + EXPECT_EQ(json_escape("a\rb"), "a\\rb"); +} + +TEST(JsonEscapeTest, ControlChars) +{ + std::string input(1, '\x01'); + EXPECT_EQ(json_escape(input), "\\u0001"); +} + +//------------------------------------------------------------------------------ +// dispatch_request tests (BOUNDS, LAYERS, INFO) +//------------------------------------------------------------------------------ + +class DispatchRequestTest : public tst::Nangate45Fixture +{ + protected: + void SetUp() override + { + block_->setDieArea(odb::Rect(0, 0, 100000, 100000)); + gen_ = std::make_shared( + getDb(), /*sta=*/nullptr, getLogger()); + } + + std::shared_ptr gen_; +}; + +TEST_F(DispatchRequestTest, BoundsReturnsJson) +{ + WebSocketRequest req; + req.id = 42; + req.type = WebSocketRequest::BOUNDS; + + auto resp = dispatch_request(req, *gen_); + EXPECT_EQ(resp.id, 42u); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"bounds\""), std::string::npos); +} + +TEST_F(DispatchRequestTest, TechReturnsJson) +{ + WebSocketRequest req; + req.id = 7; + req.type = WebSocketRequest::TECH; + + auto resp = dispatch_request(req, *gen_); + EXPECT_EQ(resp.id, 7u); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"layers\""), std::string::npos); + EXPECT_NE(json.find("\"metal1\""), std::string::npos); + EXPECT_NE(json.find("\"sites\""), std::string::npos); + EXPECT_NE(json.find("\"has_liberty\""), std::string::npos); +} + +TEST_F(DispatchRequestTest, TileReturnsPng) +{ + WebSocketRequest req; + req.id = 99; + req.type = WebSocketRequest::TILE; + req.layer = "metal1"; + req.z = 0; + req.x = 0; + req.y = 0; + + auto resp = dispatch_request(req, *gen_); + EXPECT_EQ(resp.id, 99u); + EXPECT_EQ(resp.type, 1); // PNG + EXPECT_FALSE(resp.payload.empty()); + // PNG magic bytes + EXPECT_GE(resp.payload.size(), 8u); + EXPECT_EQ(resp.payload[0], 0x89); + EXPECT_EQ(resp.payload[1], 'P'); + EXPECT_EQ(resp.payload[2], 'N'); + EXPECT_EQ(resp.payload[3], 'G'); +} + +TEST_F(DispatchRequestTest, UnknownTypeReturnsError) +{ + WebSocketRequest req; + req.id = 5; + req.type = WebSocketRequest::UNKNOWN; + + auto resp = dispatch_request(req, *gen_); + EXPECT_EQ(resp.type, 2); // error +} + +//------------------------------------------------------------------------------ +// TileHandler tests +//------------------------------------------------------------------------------ + +class TileHandlerTest : public tst::Nangate45Fixture +{ + protected: + void SetUp() override + { + block_->setDieArea(odb::Rect(0, 0, 100000, 100000)); + block_->setCoreArea(odb::Rect(0, 0, 100000, 100000)); + gen_ = std::make_shared( + getDb(), /*sta=*/nullptr, getLogger()); + handler_ = std::make_unique(gen_); + } + + std::shared_ptr gen_; + std::unique_ptr handler_; + SessionState state_; +}; + +TEST_F(TileHandlerTest, EmptyTile) +{ + WebSocketRequest req; + req.id = 1; + req.type = WebSocketRequest::TILE; + req.layer = "metal1"; + req.z = 0; + req.x = 0; + req.y = 0; + + auto resp = handler_->handleTile(req, state_); + EXPECT_EQ(resp.type, 1); // PNG + EXPECT_FALSE(resp.payload.empty()); +} + +TEST_F(TileHandlerTest, UsesHighlightState) +{ + // Put a highlight rect in the state + { + std::lock_guard lock(state_.selection_mutex); + state_.highlight_rects.emplace_back(0, 0, 50000, 50000); + } + + WebSocketRequest req; + req.id = 2; + req.type = WebSocketRequest::TILE; + req.layer = "_instances"; + req.z = 0; + req.x = 0; + req.y = 0; + + // Should not crash and should return valid PNG + auto resp = handler_->handleTile(req, state_); + EXPECT_EQ(resp.type, 1); + EXPECT_FALSE(resp.payload.empty()); +} + +TEST_F(TileHandlerTest, HeatMapsReturnsMetadata) +{ + gui::registerBuiltinHeatMapSources(/*sta=*/nullptr, getLogger()); + handler_->initializeHeatMaps(state_); + + WebSocketRequest req; + req.id = 3; + req.type = WebSocketRequest::HEATMAPS; + + auto resp = handler_->handleHeatMaps(req, state_); + EXPECT_EQ(resp.type, 0); + const std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"heatmaps\""), std::string::npos); + EXPECT_NE(json.find("\"Pin\""), std::string::npos); + EXPECT_NE(json.find("\"Placement\""), std::string::npos); +} + +TEST_F(TileHandlerTest, HeatMapSettingsAreSessionLocal) +{ + gui::registerBuiltinHeatMapSources(/*sta=*/nullptr, getLogger()); + SessionState state1; + SessionState state2; + handler_->initializeHeatMaps(state1); + handler_->initializeHeatMaps(state2); + + WebSocketRequest active_req; + active_req.type = WebSocketRequest::SET_ACTIVE_HEATMAP; + active_req.heatmap_name = "Pin"; + + EXPECT_EQ(handler_->handleSetActiveHeatMap(active_req, state1).type, 0); + EXPECT_EQ(handler_->handleSetActiveHeatMap(active_req, state2).type, 0); + + WebSocketRequest set_req; + set_req.id = 4; + set_req.type = WebSocketRequest::SET_HEATMAP; + set_req.heatmap_name = "Pin"; + set_req.heatmap_option = "DisplayMin"; + set_req.raw_json = R"({"value":12.5})"; + + auto set_resp = handler_->handleSetHeatMap(set_req, state1); + EXPECT_EQ(set_resp.type, 0); + + WebSocketRequest meta_req; + meta_req.id = 5; + meta_req.type = WebSocketRequest::HEATMAPS; + + const std::string json1 + = payloadStr(handler_->handleHeatMaps(meta_req, state1)); + const std::string json2 + = payloadStr(handler_->handleHeatMaps(meta_req, state2)); + + EXPECT_NE(json1, json2); +} + +TEST_F(TileHandlerTest, HeatMapShowNumbersCanBeUpdated) +{ + gui::registerBuiltinHeatMapSources(/*sta=*/nullptr, getLogger()); + handler_->initializeHeatMaps(state_); + + WebSocketRequest set_req; + set_req.id = 8; + set_req.type = WebSocketRequest::SET_HEATMAP; + set_req.heatmap_name = "Pin"; + set_req.heatmap_option = "ShowNumbers"; + set_req.raw_json = R"({"value":true})"; + + auto set_resp = handler_->handleSetHeatMap(set_req, state_); + EXPECT_EQ(set_resp.type, 0); + { + std::lock_guard lock(state_.heatmap_mutex); + ASSERT_TRUE(state_.heatmaps.count("Pin")); + EXPECT_TRUE(state_.heatmaps.at("Pin")->getShowNumbers()); + } +} + +TEST_F(TileHandlerTest, HeatMapsMetadataIsLazyForInactiveSources) +{ + static int populate_calls = 0; + populate_calls = 0; + + gui::registerHeatMapSource( + "Lazy Metadata Heat Map", "LazyMeta", "LazyMeta", [this]() { + return std::make_shared(getLogger(), + &populate_calls); + }); + + handler_->initializeHeatMaps(state_); + + WebSocketRequest meta_req; + meta_req.id = 6; + meta_req.type = WebSocketRequest::HEATMAPS; + + auto meta_resp = handler_->handleHeatMaps(meta_req, state_); + EXPECT_EQ(meta_resp.type, 0); + EXPECT_EQ(populate_calls, 0); + + WebSocketRequest active_req; + active_req.id = 7; + active_req.type = WebSocketRequest::SET_ACTIVE_HEATMAP; + active_req.heatmap_name = "LazyMeta"; + + auto active_resp = handler_->handleSetActiveHeatMap(active_req, state_); + EXPECT_EQ(active_resp.type, 0); + EXPECT_EQ(populate_calls, 1); +} + +//------------------------------------------------------------------------------ +// SelectHandler tests +//------------------------------------------------------------------------------ + +class SelectHandlerTest : public tst::Nangate45Fixture +{ + protected: + void SetUp() override + { + block_->setDieArea(odb::Rect(0, 0, 100000, 100000)); + block_->setCoreArea(odb::Rect(0, 0, 100000, 100000)); + placeInst("BUF_X16", "buf1", 0, 0); + fake_current_ = {"current", "FakeCurrent", odb::Rect(0, 0, 100, 100)}; + fake_previous_ + = {"previous", "FakePrevious", odb::Rect(100, 100, 200, 200)}; + gen_ = std::make_shared( + getDb(), /*sta=*/nullptr, getLogger()); + tcl_eval_ = std::make_shared(/*interp=*/nullptr, getLogger()); + handler_ = std::make_unique(gen_, tcl_eval_); + } + + gui::Selected makeFakeSelected(FakeInspectable* object) + { + return gui::Selected(object, &fake_descriptor_); + } + + odb::dbInst* placeInst(const char* master_name, + const char* inst_name, + int x, + int y) + { + odb::dbMaster* master = lib_->findMaster(master_name); + EXPECT_NE(master, nullptr); + odb::dbInst* inst = odb::dbInst::create(block_, master, inst_name); + inst->setLocation(x, y); + inst->setPlacementStatus(odb::dbPlacementStatus::PLACED); + return inst; + } + + std::shared_ptr gen_; + std::shared_ptr tcl_eval_; + std::unique_ptr handler_; + SessionState state_; + FakeDescriptor fake_descriptor_; + FakeInspectable fake_current_; + FakeInspectable fake_previous_; +}; + +TEST_F(SelectHandlerTest, SelectAtOriginFindsInstance) +{ + WebSocketRequest req; + req.id = 10; + req.type = WebSocketRequest::SELECT; + req.select_x = 1000; + req.select_y = 1000; + req.select_zoom = 0; + + auto resp = handler_->handleSelect(req, state_); + EXPECT_EQ(resp.id, 10u); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"selected\""), std::string::npos); +} + +TEST_F(SelectHandlerTest, SelectAtEmptyAreaReturnsEmptyList) +{ + WebSocketRequest req; + req.id = 11; + req.type = WebSocketRequest::SELECT; + req.select_x = 99000; + req.select_y = 99000; + req.select_zoom = 10; // high zoom = small area + + auto resp = handler_->handleSelect(req, state_); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"selected\": []"), std::string::npos); +} + +TEST_F(SelectHandlerTest, InspectInvalidIdReturnsError) +{ + WebSocketRequest req; + req.id = 12; + req.type = WebSocketRequest::INSPECT; + req.select_id = 999; // no selectables stored + + auto resp = handler_->handleInspect(req, state_); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"error\""), std::string::npos); +} + +TEST_F(SelectHandlerTest, HoverInvalidIdReturnsOkZeroCount) +{ + WebSocketRequest req; + req.id = 13; + req.type = WebSocketRequest::HOVER; + req.select_id = 999; + + auto resp = handler_->handleHover(req, state_); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"ok\": 1"), std::string::npos); + EXPECT_NE(json.find("\"count\": 0"), std::string::npos); +} + +TEST_F(SelectHandlerTest, SelectClearsTimingState) +{ + // Populate timing state + { + std::lock_guard lock(state_.selection_mutex); + state_.timing_rects.push_back( + {odb::Rect(0, 0, 1, 1), {.r = 255, .g = 0, .b = 0, .a = 255}, ""}); + state_.timing_lines.push_back({odb::Point(0, 0), + odb::Point(1, 1), + {.r = 0, .g = 255, .b = 0, .a = 255}}); + } + + WebSocketRequest req; + req.id = 14; + req.type = WebSocketRequest::SELECT; + req.select_x = 1000; + req.select_y = 1000; + req.select_zoom = 0; + + handler_->handleSelect(req, state_); + + // Timing state should be cleared after select + std::lock_guard lock(state_.selection_mutex); + EXPECT_TRUE(state_.timing_rects.empty()); + EXPECT_TRUE(state_.timing_lines.empty()); +} + +TEST_F(SelectHandlerTest, SelectClearsInspectorHistoryWhenNothingIsPicked) +{ + { + std::lock_guard lock(state_.selection_mutex); + state_.current_inspected = makeFakeSelected(&fake_current_); + state_.navigation_history.push_back(makeFakeSelected(&fake_previous_)); + } + + WebSocketRequest req; + req.id = 15; + req.type = WebSocketRequest::SELECT; + req.select_x = 99000; + req.select_y = 99000; + req.select_zoom = 10; + + auto resp = handler_->handleSelect(req, state_); + EXPECT_EQ(resp.type, 0); + + const std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"can_navigate_back\": 0"), std::string::npos); + EXPECT_NE(json.find("\"selected\": []"), std::string::npos); + + std::lock_guard lock(state_.selection_mutex); + EXPECT_FALSE(state_.current_inspected); + EXPECT_TRUE(state_.navigation_history.empty()); +} + +TEST_F(SelectHandlerTest, InspectBackRestoresPreviousObject) +{ + const gui::Selected initial_selected = makeFakeSelected(&fake_current_); + const gui::Selected block_selected = makeFakeSelected(&fake_previous_); + ASSERT_TRUE(initial_selected); + ASSERT_TRUE(block_selected); + + { + std::lock_guard lock(state_.selection_mutex); + state_.current_inspected = initial_selected; + } + { + std::lock_guard lock(state_.selectables_mutex); + state_.selectables = {block_selected}; + } + + WebSocketRequest inspect_req; + inspect_req.id = 17; + inspect_req.type = WebSocketRequest::INSPECT; + inspect_req.select_id = 0; + + auto inspect_resp = handler_->handleInspect(inspect_req, state_); + EXPECT_EQ(inspect_resp.type, 0); + EXPECT_NE(payloadStr(inspect_resp).find("\"can_navigate_back\": 1"), + std::string::npos); + + { + std::lock_guard lock(state_.selection_mutex); + EXPECT_TRUE(state_.current_inspected); + EXPECT_NE(state_.current_inspected, initial_selected); + ASSERT_EQ(state_.navigation_history.size(), 1u); + EXPECT_EQ(state_.navigation_history.back(), initial_selected); + } + + WebSocketRequest back_req; + back_req.id = 18; + back_req.type = WebSocketRequest::INSPECT_BACK; + + auto back_resp = handler_->handleInspectBack(back_req, state_); + EXPECT_EQ(back_resp.type, 0); + EXPECT_NE(payloadStr(back_resp).find("\"can_navigate_back\": 0"), + std::string::npos); + EXPECT_NE(payloadStr(back_resp).find(initial_selected.getName()), + std::string::npos); + + { + std::lock_guard lock(state_.selection_mutex); + EXPECT_EQ(state_.current_inspected, initial_selected); + EXPECT_TRUE(state_.navigation_history.empty()); + } +} + +TEST_F(SelectHandlerTest, InspectBackWithoutHistoryKeepsCurrentObject) +{ + const gui::Selected initial_selected = makeFakeSelected(&fake_current_); + ASSERT_TRUE(initial_selected); + { + std::lock_guard lock(state_.selection_mutex); + state_.current_inspected = initial_selected; + } + + WebSocketRequest back_req; + back_req.id = 20; + back_req.type = WebSocketRequest::INSPECT_BACK; + + auto back_resp = handler_->handleInspectBack(back_req, state_); + EXPECT_EQ(back_resp.type, 0); + EXPECT_NE(payloadStr(back_resp).find("\"can_navigate_back\": 0"), + std::string::npos); + EXPECT_NE(payloadStr(back_resp).find(initial_selected.getName()), + std::string::npos); + + { + std::lock_guard lock(state_.selection_mutex); + EXPECT_EQ(state_.current_inspected, initial_selected); + EXPECT_TRUE(state_.navigation_history.empty()); + } +} + +//------------------------------------------------------------------------------ +// Focus nets tests +//------------------------------------------------------------------------------ + +TEST_F(SelectHandlerTest, FocusNetAddValid) +{ + // Create a net in the block + odb::dbNet::create(block_, "clk"); + + WebSocketRequest req; + req.id = 20; + req.type = WebSocketRequest::SET_FOCUS_NETS; + req.focus_action = "add"; + req.focus_net_name = "clk"; + + auto resp = handler_->handleSetFocusNets(req, state_); + EXPECT_EQ(resp.id, 20u); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"ok\":1"), std::string::npos); + EXPECT_NE(json.find("\"count\":1"), std::string::npos); + + std::lock_guard lock(state_.focus_nets_mutex); + EXPECT_EQ(state_.focus_net_ids.size(), 1u); +} + +TEST_F(SelectHandlerTest, FocusNetAddInvalidNetReturnsZeroCount) +{ + WebSocketRequest req; + req.id = 21; + req.type = WebSocketRequest::SET_FOCUS_NETS; + req.focus_action = "add"; + req.focus_net_name = "nonexistent_net"; + + auto resp = handler_->handleSetFocusNets(req, state_); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"count\":0"), std::string::npos); + + std::lock_guard lock(state_.focus_nets_mutex); + EXPECT_TRUE(state_.focus_net_ids.empty()); +} + +TEST_F(SelectHandlerTest, FocusNetRemove) +{ + odb::dbNet* net = odb::dbNet::create(block_, "data"); + + // Add first + { + std::lock_guard lock(state_.focus_nets_mutex); + state_.focus_net_ids.insert(net->getId()); + } + + WebSocketRequest req; + req.id = 22; + req.type = WebSocketRequest::SET_FOCUS_NETS; + req.focus_action = "remove"; + req.focus_net_name = "data"; + + auto resp = handler_->handleSetFocusNets(req, state_); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"count\":0"), std::string::npos); + + std::lock_guard lock(state_.focus_nets_mutex); + EXPECT_TRUE(state_.focus_net_ids.empty()); +} + +TEST_F(SelectHandlerTest, FocusNetClear) +{ + odb::dbNet* n1 = odb::dbNet::create(block_, "net1"); + odb::dbNet* n2 = odb::dbNet::create(block_, "net2"); + + { + std::lock_guard lock(state_.focus_nets_mutex); + state_.focus_net_ids.insert(n1->getId()); + state_.focus_net_ids.insert(n2->getId()); + } + + WebSocketRequest req; + req.id = 23; + req.type = WebSocketRequest::SET_FOCUS_NETS; + req.focus_action = "clear"; + req.focus_net_name = ""; + + auto resp = handler_->handleSetFocusNets(req, state_); + EXPECT_EQ(resp.type, 0); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"count\":0"), std::string::npos); + + std::lock_guard lock(state_.focus_nets_mutex); + EXPECT_TRUE(state_.focus_net_ids.empty()); +} + +TEST_F(SelectHandlerTest, FocusNetAddMultiple) +{ + odb::dbNet::create(block_, "clk"); + odb::dbNet::create(block_, "reset"); + + WebSocketRequest req1; + req1.id = 24; + req1.type = WebSocketRequest::SET_FOCUS_NETS; + req1.focus_action = "add"; + req1.focus_net_name = "clk"; + handler_->handleSetFocusNets(req1, state_); + + WebSocketRequest req2; + req2.id = 25; + req2.type = WebSocketRequest::SET_FOCUS_NETS; + req2.focus_action = "add"; + req2.focus_net_name = "reset"; + auto resp = handler_->handleSetFocusNets(req2, state_); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"count\":2"), std::string::npos); + + std::lock_guard lock(state_.focus_nets_mutex); + EXPECT_EQ(state_.focus_net_ids.size(), 2u); +} + +TEST_F(SelectHandlerTest, FocusNetAddDuplicateNoop) +{ + odb::dbNet::create(block_, "clk"); + + WebSocketRequest req; + req.id = 26; + req.type = WebSocketRequest::SET_FOCUS_NETS; + req.focus_action = "add"; + req.focus_net_name = "clk"; + + handler_->handleSetFocusNets(req, state_); + auto resp = handler_->handleSetFocusNets(req, state_); + + std::string json = payloadStr(resp); + EXPECT_NE(json.find("\"count\":1"), std::string::npos); +} + +TEST_F(SelectHandlerTest, TileHandlerSnapshotsFocusNets) +{ + // Verify that TileHandler passes focus net state through to tiles. + odb::dbNet* net = odb::dbNet::create(block_, "focused_net"); + { + std::lock_guard lock(state_.focus_nets_mutex); + state_.focus_net_ids.insert(net->getId()); + } + + auto tile_handler = std::make_unique(gen_); + WebSocketRequest req; + req.id = 27; + req.type = WebSocketRequest::TILE; + req.layer = "metal1"; + req.z = 0; + req.x = 0; + req.y = 0; + + // Should not crash and should return valid PNG + auto resp = tile_handler->handleTile(req, state_); + EXPECT_EQ(resp.type, 1); // PNG + EXPECT_FALSE(resp.payload.empty()); +} + +} // namespace +} // namespace web diff --git a/src/web/test/cpp/TestTileGenerator.cpp b/src/web/test/cpp/TestTileGenerator.cpp new file mode 100644 index 00000000000..2d913d9ca2b --- /dev/null +++ b/src/web/test/cpp/TestTileGenerator.cpp @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "lodepng.h" +#include "odb/db.h" +#include "tile_generator.h" +#include "tst/nangate45_fixture.h" + +namespace web { +namespace { + +class TileGeneratorTest : public tst::Nangate45Fixture +{ + protected: + void SetUp() override + { + // Nangate45Fixture gives us a chip + block with die area (0,0)-(1000,1000). + // Enlarge to fit standard cells (Nangate45 LEF units = 2000, so + // 100000 dbu = 50 um). + block_->setDieArea(odb::Rect(0, 0, 100000, 100000)); + } + + // Create TileGenerator. Call this after placing any instances so + // that the block BBox (used by getBounds) is up to date. + void makeTileGen() + { + tile_gen_ = std::make_unique( + getDb(), /*sta=*/nullptr, getLogger()); + } + + // Decode a PNG byte vector into raw RGBA pixels. + std::vector decodePng( + const std::vector& png_data, + unsigned& width, + unsigned& height) + { + std::vector pixels; + unsigned err = lodepng::decode(pixels, width, height, png_data); + EXPECT_EQ(err, 0u) << lodepng_error_text(err); + return pixels; + } + + // Return true if any pixel in the RGBA buffer has alpha > 0. + static bool hasNonTransparentPixel(const std::vector& rgba) + { + for (size_t i = 3; i < rgba.size(); i += 4) { + if (rgba[i] > 0) { + return true; + } + } + return false; + } + + odb::dbInst* placeInst(const char* master_name, + const char* inst_name, + int x, + int y) + { + odb::dbMaster* master = lib_->findMaster(master_name); + EXPECT_NE(master, nullptr) << "Master not found: " << master_name; + odb::dbInst* inst = odb::dbInst::create(block_, master, inst_name); + inst->setLocation(x, y); + inst->setPlacementStatus(odb::dbPlacementStatus::PLACED); + return inst; + } + + std::unique_ptr tile_gen_; +}; + +TEST_F(TileGeneratorTest, HasStaFalseWhenNull) +{ + makeTileGen(); + EXPECT_FALSE(tile_gen_->hasSta()); +} + +TEST_F(TileGeneratorTest, GetBoundsReflectsInstances) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + odb::Rect bounds = tile_gen_->getBounds(); + // Bounds should encompass the placed instance. + EXPECT_GT(bounds.dx(), 0); + EXPECT_GT(bounds.dy(), 0); + EXPECT_LE(bounds.xMin(), 10000); + EXPECT_LE(bounds.yMin(), 10000); +} + +TEST_F(TileGeneratorTest, GetLayers) +{ + makeTileGen(); + std::vector layers = tile_gen_->getLayers(); + // 10 routing + 9 cut layers + EXPECT_EQ(layers.size(), 19); + EXPECT_EQ(layers.front(), "metal1"); + EXPECT_EQ(layers.back(), "metal10"); +} + +TEST_F(TileGeneratorTest, GenerateTileReturnsValidPng) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + auto png = tile_gen_->generateTile("metal1", 0, 0, 0); + ASSERT_FALSE(png.empty()); + + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_EQ(w, 256u); + EXPECT_EQ(h, 256u); +} + +TEST_F(TileGeneratorTest, EmptyDesignProducesTransparentTile) +{ + makeTileGen(); + + // No instances or routing, so the tile should be transparent. + auto png = tile_gen_->generateTile("metal1", 0, 0, 0); + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_FALSE(hasNonTransparentPixel(pixels)); +} + +TEST_F(TileGeneratorTest, PlacedInstanceDrawsPixels) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + // Use the special "_instances" layer to draw instance borders. + auto png = tile_gen_->generateTile("_instances", 0, 0, 0); + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_TRUE(hasNonTransparentPixel(pixels)); +} + +TEST_F(TileGeneratorTest, StdcellVisibilityFilter) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + TileVisibility vis; + vis.stdcells = false; + + auto png = tile_gen_->generateTile("_instances", 0, 0, 0, vis); + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_FALSE(hasNonTransparentPixel(pixels)); +} + +TEST_F(TileGeneratorTest, IsNetVisibleRespectsSignalType) +{ + odb::dbNet* sig_net = odb::dbNet::create(block_, "sig"); + sig_net->setSigType(odb::dbSigType::SIGNAL); + + odb::dbNet* pwr_net = odb::dbNet::create(block_, "vdd"); + pwr_net->setSigType(odb::dbSigType::POWER); + + odb::dbNet* clk_net = odb::dbNet::create(block_, "clk"); + clk_net->setSigType(odb::dbSigType::CLOCK); + + // Default visibility: all visible + TileVisibility vis; + EXPECT_TRUE(vis.isNetVisible(sig_net)); + EXPECT_TRUE(vis.isNetVisible(pwr_net)); + EXPECT_TRUE(vis.isNetVisible(clk_net)); + + // Disable signal nets + vis.net_signal = false; + EXPECT_FALSE(vis.isNetVisible(sig_net)); + EXPECT_TRUE(vis.isNetVisible(pwr_net)); + + // Disable power nets + vis.net_power = false; + EXPECT_FALSE(vis.isNetVisible(pwr_net)); + + // Disable clock nets + vis.net_clock = false; + EXPECT_FALSE(vis.isNetVisible(clk_net)); +} + +TEST_F(TileGeneratorTest, TileVisibilityDefaultAllTrue) +{ + TileVisibility vis; + EXPECT_TRUE(vis.stdcells); + EXPECT_TRUE(vis.macros); + EXPECT_TRUE(vis.routing); + EXPECT_TRUE(vis.special_nets); + EXPECT_TRUE(vis.pins); + EXPECT_TRUE(vis.blockages); + EXPECT_TRUE(vis.net_signal); + EXPECT_TRUE(vis.net_power); + EXPECT_TRUE(vis.net_ground); + EXPECT_TRUE(vis.net_clock); + EXPECT_TRUE(vis.phys_fill); + EXPECT_TRUE(vis.phys_endcap); +} + +TEST_F(TileGeneratorTest, InvalidLayerProducesValidPng) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + auto png = tile_gen_->generateTile("nonexistent_layer", 0, 0, 0); + ASSERT_FALSE(png.empty()); + + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_EQ(w, 256u); + EXPECT_EQ(h, 256u); +} + +TEST_F(TileGeneratorTest, OutOfBoundsTileIsTransparent) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + // At zoom=1 valid tiles are (0,0),(0,1),(1,0),(1,1). Tile (5,5) is out. + auto png = tile_gen_->generateTile("_instances", 1, 5, 5); + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_FALSE(hasNonTransparentPixel(pixels)); +} + +TEST_F(TileGeneratorTest, DebugModeDrawsBorder) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + TileVisibility vis; + vis.debug = true; + + auto png = tile_gen_->generateTile("_instances", 0, 0, 0, vis); + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + ASSERT_EQ(w, 256u); + ASSERT_EQ(h, 256u); + + // Check corners for yellow border pixels (R=255, G=255, B=0, A=255). + // Pixel at (0,0): + EXPECT_EQ(pixels[0], 255); // R + EXPECT_EQ(pixels[1], 255); // G + EXPECT_EQ(pixels[2], 0); // B + EXPECT_EQ(pixels[3], 255); // A + + // Pixel at (255,255): + const int last = (255 * 256 + 255) * 4; + EXPECT_EQ(pixels[last + 0], 255); // R + EXPECT_EQ(pixels[last + 1], 255); // G + EXPECT_EQ(pixels[last + 2], 0); // B + EXPECT_EQ(pixels[last + 3], 255); // A +} + +TEST_F(TileGeneratorTest, DebugDefaultOff) +{ + TileVisibility vis; + EXPECT_FALSE(vis.debug); +} + +//------------------------------------------------------------------------------ +// Focus net filtering tests +//------------------------------------------------------------------------------ + +TEST_F(TileGeneratorTest, FocusNetEmptySetSameAsNull) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + // Empty focus_net_ids should behave the same as nullptr (all nets visible). + std::set empty_set; + auto png = tile_gen_->generateTile( + "metal1", 0, 0, 0, {}, {}, {}, {}, {}, nullptr, &empty_set); + ASSERT_FALSE(png.empty()); + + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_EQ(w, 256u); + EXPECT_EQ(h, 256u); +} + +TEST_F(TileGeneratorTest, FocusNetNonMatchingIdProducesValidTile) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + // Focus on a net ID that doesn't correspond to any routing. + // Should produce a valid tile (instances still drawn, just net shapes + // filtered). + std::set focus_ids{99999}; + auto png = tile_gen_->generateTile( + "metal1", 0, 0, 0, {}, {}, {}, {}, {}, nullptr, &focus_ids); + ASSERT_FALSE(png.empty()); + + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_EQ(w, 256u); + EXPECT_EQ(h, 256u); +} + +TEST_F(TileGeneratorTest, FocusNetWithRealNetId) +{ + placeInst("BUF_X16", "buf1", 0, 0); + odb::dbNet* net = odb::dbNet::create(block_, "focus_test_net"); + makeTileGen(); + + // Focus on the created net's ID. Even without routing shapes, + // the tile should be generated without errors. + std::set focus_ids{net->getId()}; + auto png = tile_gen_->generateTile( + "metal1", 0, 0, 0, {}, {}, {}, {}, {}, nullptr, &focus_ids); + ASSERT_FALSE(png.empty()); + + unsigned w = 0, h = 0; + auto pixels = decodePng(png, w, h); + EXPECT_EQ(w, 256u); + EXPECT_EQ(h, 256u); +} + +TEST_F(TileGeneratorTest, FocusNetNullPtrAllowsAllNets) +{ + placeInst("BUF_X16", "buf1", 0, 0); + makeTileGen(); + + // nullptr means no focus filtering — should match default behavior. + auto png_default = tile_gen_->generateTile("metal1", 0, 0, 0); + auto png_null = tile_gen_->generateTile( + "metal1", 0, 0, 0, {}, {}, {}, {}, {}, nullptr, nullptr); + EXPECT_EQ(png_default, png_null); +} + +TEST_F(TileGeneratorTest, SemiTransparentOverlayUsesStraightAlpha) +{ + placeInst("BUF_X16", "buf0", 0, 0); + placeInst("BUF_X16", "buf1", 90000, 90000); + makeTileGen(); + + const odb::Rect rect(0, 0, 100000, 100000); + auto png + = tile_gen_->generateTile("nonexistent_layer", 0, 0, 0, {}, {rect}, {}); + + unsigned w = 0; + unsigned h = 0; + const auto pixels = decodePng(png, w, h); + ASSERT_EQ(w, 256u); + ASSERT_EQ(h, 256u); + + const int center = (128 * 256 + 128) * 4; + EXPECT_EQ(pixels[center + 0], 255); + EXPECT_EQ(pixels[center + 1], 255); + EXPECT_EQ(pixels[center + 2], 0); + EXPECT_EQ(pixels[center + 3], 30); +} + +} // namespace +} // namespace web diff --git a/src/web/test/cpp_tests.tcl b/src/web/test/cpp_tests.tcl new file mode 100644 index 00000000000..00740d9f9b8 --- /dev/null +++ b/src/web/test/cpp_tests.tcl @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026, The OpenROAD Authors + +source "helpers.tcl" + +set test_dir [pwd] +set openroad_dir [file dirname [file dirname [file dirname $test_dir]]] +set tests_path [file join $openroad_dir "build" "src" "web" "test" "cpp"] + +set tests_list [split [exec sh -c "find $tests_path -maxdepth 2 \ + -name 'Test*' ! -name '*.cmake' -type f"] \n] + +foreach test $tests_list { + set test_name [file tail $test] + puts "Running test: $test_name" + puts "**********" + + set test_status [catch { exec sh -c "BASE_DIR=$test_dir $test" } \ + output option] + + puts $test_status + puts $output + if { $test_status == 0 } { + puts "$test_name passed" + } else { + set test_err_info [lassign [dict get $option -errorcode] err_type] + switch -exact -- $err_type { + NONE { + puts "$test_name passed" + } + CHILDSTATUS { + # non-zero exit status + set exit_status [lindex $test_err_info 1] + set process_id [lindex $test_err_info 0] + + puts "ERROR: test returned exit code $exit_status" + exit 1 + } + default { + puts "ERROR: $option" + exit 1 + } + } + } + + puts "******************" +} + +puts "pass" +exit 0 diff --git a/src/web/test/helpers.py b/src/web/test/helpers.py new file mode 120000 index 00000000000..e10a2da7588 --- /dev/null +++ b/src/web/test/helpers.py @@ -0,0 +1 @@ +../../../test/helpers.py \ No newline at end of file diff --git a/src/web/test/helpers.tcl b/src/web/test/helpers.tcl new file mode 120000 index 00000000000..509ca07b27e --- /dev/null +++ b/src/web/test/helpers.tcl @@ -0,0 +1 @@ +../../../test/helpers.tcl \ No newline at end of file diff --git a/src/web/test/js/setup-dom.js b/src/web/test/js/setup-dom.js new file mode 100644 index 00000000000..346aa04d814 --- /dev/null +++ b/src/web/test/js/setup-dom.js @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +// Minimal DOM environment for tests that need document/window. +import { JSDOM } from 'jsdom'; +const dom = new JSDOM(''); +globalThis.document = dom.window.document; +globalThis.window = dom.window; +globalThis.Event = dom.window.Event; +export { dom }; diff --git a/src/web/test/js/test-charts-widget.js b/src/web/test/js/test-charts-widget.js new file mode 100644 index 00000000000..117e18c6ce6 --- /dev/null +++ b/src/web/test/js/test-charts-widget.js @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + computeHistogramLayout, + kLeftMargin, kRightMargin, kTopMargin, kBottomMargin, +} from '../../src/charts-widget.js'; + +describe('computeHistogramLayout', () => { + it('returns empty bars for null data', () => { + const result = computeHistogramLayout(null, 500, 400); + assert.equal(result.bars.length, 0); + }); + + it('returns empty bars for empty bins', () => { + const result = computeHistogramLayout({ bins: [] }, 500, 400); + assert.equal(result.bars.length, 0); + }); + + it('returns empty bars for undefined bins', () => { + const result = computeHistogramLayout({}, 500, 400); + assert.equal(result.bars.length, 0); + }); + + it('computes correct bar count for single bin', () => { + const result = computeHistogramLayout({ + bins: [{ lower: 0, upper: 0.1, count: 10, negative: false }], + time_unit: 'ns', + }, 500, 400); + assert.equal(result.bars.length, 1); + assert.equal(result.bars[0].count, 10); + assert.ok(result.bars[0].height > 0); + assert.equal(result.bars[0].negative, false); + }); + + it('preserves negative flag from bins', () => { + const result = computeHistogramLayout({ + bins: [ + { lower: -0.2, upper: -0.1, count: 5, negative: true }, + { lower: -0.1, upper: 0.0, count: 8, negative: true }, + { lower: 0.0, upper: 0.1, count: 20, negative: false }, + ], + time_unit: 'ns', + }, 500, 400); + assert.equal(result.bars.length, 3); + assert.ok(result.bars[0].negative); + assert.ok(result.bars[1].negative); + assert.ok(!result.bars[2].negative); + }); + + it('scales bar heights proportional to count', () => { + const result = computeHistogramLayout({ + bins: [ + { lower: 0, upper: 0.1, count: 100, negative: false }, + { lower: 0.1, upper: 0.2, count: 50, negative: false }, + ], + time_unit: 'ns', + }, 500, 400); + assert.ok(result.bars[0].height > result.bars[1].height); + }); + + it('positions bars within chart area', () => { + const w = 500, h = 400; + const result = computeHistogramLayout({ + bins: [ + { lower: 0, upper: 0.1, count: 10, negative: false }, + { lower: 0.1, upper: 0.2, count: 20, negative: false }, + ], + time_unit: 'ns', + }, w, h); + + for (const bar of result.bars) { + assert.ok(bar.x >= kLeftMargin, 'bar starts after left margin'); + assert.ok(bar.x + bar.width <= w - kRightMargin + 1, + 'bar ends before right margin'); + assert.ok(bar.y >= kTopMargin, 'bar top after top margin'); + assert.ok(bar.y + bar.height <= h - kBottomMargin + 1, + 'bar bottom before bottom margin'); + } + }); + + it('provides chart area bounds', () => { + const result = computeHistogramLayout({ + bins: [{ lower: 0, upper: 0.1, count: 5, negative: false }], + time_unit: 'ns', + }, 500, 400); + assert.ok(result.chartArea); + assert.equal(result.chartArea.left, kLeftMargin); + assert.equal(result.chartArea.right, 500 - kRightMargin); + assert.equal(result.chartArea.top, kTopMargin); + assert.equal(result.chartArea.bottom, 400 - kBottomMargin); + }); + + it('handles all-zero counts', () => { + const result = computeHistogramLayout({ + bins: [ + { lower: 0, upper: 0.1, count: 0, negative: false }, + { lower: 0.1, upper: 0.2, count: 0, negative: false }, + ], + time_unit: 'ns', + }, 500, 400); + assert.equal(result.bars.length, 2); + for (const bar of result.bars) { + assert.equal(bar.height, 0); + } + }); + + it('returns empty for tiny canvas', () => { + const result = computeHistogramLayout({ + bins: [{ lower: 0, upper: 0.1, count: 5, negative: false }], + }, 50, 50); + assert.equal(result.bars.length, 0); + }); + + it('provides yTicks for small counts', () => { + const result = computeHistogramLayout({ + bins: [{ lower: 0, upper: 0.1, count: 7, negative: false }], + time_unit: 'ns', + }, 500, 400); + assert.ok(result.yTicks.length > 0); + assert.equal(result.yTicks[0], 0); + assert.ok(result.yMax >= 7); + }); + + it('provides yTicks for large counts', () => { + const result = computeHistogramLayout({ + bins: [{ lower: 0, upper: 0.1, count: 350, negative: false }], + time_unit: 'ns', + }, 500, 400); + assert.ok(result.yMax >= 350); + assert.ok(result.yTicks.length >= 2); + }); +}); diff --git a/src/web/test/js/test-checkbox-tree-model.js b/src/web/test/js/test-checkbox-tree-model.js new file mode 100644 index 00000000000..a044f96ba42 --- /dev/null +++ b/src/web/test/js/test-checkbox-tree-model.js @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { describe, it, beforeEach } from 'node:test'; +import assert from 'node:assert/strict'; + +const { CheckboxTreeModel } = await import('../../src/checkbox-tree-model.js'); + +describe('CheckboxTreeModel', () => { + let changes, model; + + beforeEach(() => { + changes = 0; + model = new CheckboxTreeModel(() => { changes++; }); + }); + + // -- addFromSpec -- + + describe('addFromSpec', () => { + it('single leaf defaults to checked', () => { + model.addFromSpec({ id: 'a' }); + assert.equal(model.get('a').checked, true); + }); + + it('single leaf respects checked:false', () => { + model.addFromSpec({ id: 'a', checked: false }); + assert.equal(model.get('a').checked, false); + }); + + it('parent checked when all children checked', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: true }, + ]}); + assert.equal(model.get('p').checked, true); + assert.equal(model.get('p').indeterminate, false); + }); + + it('parent unchecked when all children unchecked', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: false }, + { id: 'b', checked: false }, + ]}); + assert.equal(model.get('p').checked, false); + assert.equal(model.get('p').indeterminate, false); + }); + + it('parent indeterminate when children mixed', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: false }, + ]}); + assert.equal(model.get('p').checked, false); + assert.equal(model.get('p').indeterminate, true); + }); + + it('three levels deep', () => { + model.addFromSpec({ id: 'root', children: [ + { id: 'g1', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: false }, + ]}, + { id: 'g2', children: [ + { id: 'c', checked: true }, + { id: 'd', checked: true }, + ]}, + ]}); + assert.equal(model.get('g1').indeterminate, true); + assert.equal(model.get('g2').checked, true); + assert.equal(model.get('root').indeterminate, true); + }); + }); + + // -- check -- + + describe('check', () => { + it('unchecking leaf updates parent', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: true }, + ]}); + model.check('a', false); + assert.equal(model.get('a').checked, false); + assert.equal(model.get('p').indeterminate, true); + assert.equal(changes, 1); + }); + + it('unchecking all children unchecks parent', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: true }, + ]}); + model.check('a', false); + model.check('b', false); + assert.equal(model.get('p').checked, false); + assert.equal(model.get('p').indeterminate, false); + }); + + it('checking parent cascades to children', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: false }, + { id: 'b', checked: false }, + ]}); + model.check('p', true); + assert.equal(model.get('a').checked, true); + assert.equal(model.get('b').checked, true); + }); + + it('unchecking parent cascades to children', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: true }, + ]}); + model.check('p', false); + assert.equal(model.get('a').checked, false); + assert.equal(model.get('b').checked, false); + }); + + it('deep propagation: unchecking grandchild updates grandparent', () => { + model.addFromSpec({ id: 'root', children: [ + { id: 'g', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: true }, + ]}, + ]}); + model.check('a', false); + assert.equal(model.get('g').indeterminate, true); + assert.equal(model.get('root').indeterminate, true); + }); + }); + + // -- hasCheckbox:false -- + + describe('hasCheckbox:false', () => { + it('non-checkbox children skipped in parent computation', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'structural', hasCheckbox: false }, + { id: 'a', checked: true }, + ]}); + // Parent state based only on 'a', not 'structural' + assert.equal(model.get('p').checked, true); + assert.equal(model.get('p').indeterminate, false); + }); + + it('setSubtree traverses through non-checkbox nodes', () => { + // p -> structural (no cb) -> a (cb) + model.addFromSpec({ id: 'p', children: [ + { id: 'structural', hasCheckbox: false, children: [ + { id: 'a', checked: true }, + ]}, + ]}); + model.check('p', false); + assert.equal(model.get('a').checked, false); + }); + + it('unchecking deep child through non-checkbox updates parent', () => { + // p -> structural (no cb) -> a (cb) + model.addFromSpec({ id: 'p', children: [ + { id: 'structural', hasCheckbox: false, children: [ + { id: 'a', checked: true }, + ]}, + { id: 'b', checked: true }, + ]}); + model.check('a', false); + assert.equal(model.get('p').indeterminate, true); + }); + }); + + // -- buildFromNodes -- + + describe('buildFromNodes', () => { + it('builds tree from flat list', () => { + model.buildFromNodes([ + { id: 0, parentId: -1, checked: true }, + { id: 1, parentId: 0, checked: true }, + { id: 2, parentId: 0, checked: false }, + ]); + assert.equal(model.roots.length, 1); + assert.equal(model.get(0).children.length, 2); + assert.equal(model.get(0).indeterminate, true); + }); + + it('check works after buildFromNodes', () => { + model.buildFromNodes([ + { id: 0, parentId: -1, checked: true }, + { id: 1, parentId: 0, checked: true }, + { id: 2, parentId: 0, checked: true }, + ]); + model.check(1, false); + assert.equal(model.get(0).indeterminate, true); + }); + + it('hasCheckbox:false in flat list', () => { + model.buildFromNodes([ + { id: 0, parentId: -1, checked: true }, // module + { id: 1, parentId: 0, hasCheckbox: false }, // leaf group + { id: 2, parentId: 0, checked: true }, // module + { id: 3, parentId: 0, checked: false }, // module + ]); + // Parent computed from children 2 and 3 only (1 has no checkbox) + assert.equal(model.get(0).indeterminate, true); + }); + }); + + // -- checkSet -- + + describe('checkSet', () => { + it('bulk update with single onChange', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: true }, + { id: 'c', checked: true }, + ]}); + changes = 0; + model.checkSet({ a: false, b: false }); + assert.equal(changes, 1); + assert.equal(model.get('a').checked, false); + assert.equal(model.get('b').checked, false); + assert.equal(model.get('c').checked, true); + assert.equal(model.get('p').indeterminate, true); + }); + + it('uncheck all via checkSet', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a', checked: true }, + { id: 'b', checked: true }, + ]}); + model.checkSet({ a: false, b: false }); + assert.equal(model.get('p').checked, false); + assert.equal(model.get('p').indeterminate, false); + }); + }); + + // -- forEach -- + + describe('forEach', () => { + it('visits all nodes in DFS order', () => { + model.addFromSpec({ id: 'p', children: [ + { id: 'a' }, + { id: 'b' }, + ]}); + const ids = []; + model.forEach(n => ids.push(n.id)); + assert.deepEqual(ids, ['p', 'a', 'b']); + }); + }); +}); diff --git a/src/web/test/js/test-clock-tree-widget.js b/src/web/test/js/test-clock-tree-widget.js new file mode 100644 index 00000000000..4c1a6011c66 --- /dev/null +++ b/src/web/test/js/test-clock-tree-widget.js @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + computeClockTreeLayout, + kNodeSpacing, kTopMargin, kBottomMargin, kLeftMargin, kRightMargin, +} from '../../src/clock-tree-widget.js'; + +describe('computeClockTreeLayout', () => { + it('returns empty layout for empty nodes', () => { + const result = computeClockTreeLayout({ + nodes: [], min_arrival: 0, max_arrival: 0, + }); + assert.equal(result.layout.length, 0); + assert.equal(result.layoutWidth, 0); + assert.equal(result.layoutHeight, 0); + assert.equal(result.sceneHeight, 0); + }); + + it('returns empty layout for null nodes', () => { + const result = computeClockTreeLayout({ + nodes: null, min_arrival: 0, max_arrival: 0, + }); + assert.equal(result.layout.length, 0); + }); + + it('returns empty layout for undefined nodes', () => { + const result = computeClockTreeLayout({ + min_arrival: 0, max_arrival: 0, + }); + assert.equal(result.layout.length, 0); + }); + + it('positions a single root node at top', () => { + const result = computeClockTreeLayout({ + nodes: [{ + id: 0, parent_id: -1, name: 'clk', type: 'root', + arrival: 0, delay: 0, fanout: 0, level: 0, + }], + min_arrival: 0, + max_arrival: 1, + time_unit: 'ns', + }); + assert.equal(result.layout.length, 1); + const root = result.layout[0]; + assert.equal(root.id, 0); + // arrival == min_arrival → y at top margin + assert.equal(root.y, kTopMargin); + // single node, width=1, bin [0,1), center at 0.5 + assert.equal(root.x, kLeftMargin + 0.5 * kNodeSpacing); + assert.equal(result.timeUnit, 'ns'); + }); + + it('positions children by subtree width', () => { + // Root (width=2) → two leaf children (width=1 each) + const result = computeClockTreeLayout({ + nodes: [ + { id: 0, parent_id: -1, name: 'root', type: 'root', + arrival: 0, delay: 0, fanout: 2, level: 0 }, + { id: 1, parent_id: 0, name: 'reg1', type: 'register', + arrival: 0.5, delay: 0, fanout: 0, level: 1 }, + { id: 2, parent_id: 0, name: 'reg2', type: 'register', + arrival: 1.0, delay: 0, fanout: 0, level: 1 }, + ], + min_arrival: 0, + max_arrival: 1, + }); + assert.equal(result.layout.length, 3); + + // Root has width 2, centered at bin 1.0 + const root = result.layout.find(n => n.id === 0); + assert.equal(root.x, kLeftMargin + 1 * kNodeSpacing); + + // reg1 has width 1, bin [0,1), center 0.5 + const reg1 = result.layout.find(n => n.id === 1); + assert.equal(reg1.x, kLeftMargin + 0.5 * kNodeSpacing); + + // reg2 has width 1, bin [1,2), center 1.5 + const reg2 = result.layout.find(n => n.id === 2); + assert.equal(reg2.x, kLeftMargin + 1.5 * kNodeSpacing); + + // reg2 (later arrival) should be below reg1 (earlier arrival) + assert.ok(reg2.y > reg1.y); + }); + + it('computes correct layout dimensions', () => { + const result = computeClockTreeLayout({ + nodes: [ + { id: 0, parent_id: -1, name: 'root', type: 'root', + arrival: 0, delay: 0, fanout: 3, level: 0 }, + { id: 1, parent_id: 0, name: 'a', type: 'register', + arrival: 1, delay: 0, fanout: 0, level: 1 }, + { id: 2, parent_id: 0, name: 'b', type: 'register', + arrival: 1, delay: 0, fanout: 0, level: 1 }, + { id: 3, parent_id: 0, name: 'c', type: 'register', + arrival: 1, delay: 0, fanout: 0, level: 1 }, + ], + min_arrival: 0, + max_arrival: 1, + }); + // Total width = 3 leaves + assert.equal(result.layoutWidth, + kLeftMargin + 3 * kNodeSpacing + kRightMargin); + // Height = top + sceneHeight + bottom + const expectedScene = Math.max(200, 3 * kNodeSpacing * 0.6); + assert.equal(result.layoutHeight, + kTopMargin + expectedScene + kBottomMargin); + }); + + it('builds layoutById map', () => { + const result = computeClockTreeLayout({ + nodes: [ + { id: 0, parent_id: -1, name: 'clk', type: 'root', + arrival: 0, delay: 0, fanout: 1, level: 0 }, + { id: 1, parent_id: 0, name: 'buf', type: 'buffer', + arrival: 0.1, delay: 0.05, fanout: 0, level: 1 }, + ], + min_arrival: 0, + max_arrival: 0.2, + }); + assert.ok(result.layoutById instanceof Map); + assert.ok(result.layoutById.has(0)); + assert.ok(result.layoutById.has(1)); + assert.equal(result.layoutById.get(1).name, 'buf'); + }); + + it('preserves node metadata in layout', () => { + const result = computeClockTreeLayout({ + nodes: [{ + id: 5, parent_id: -1, name: 'mybuf', pin_name: 'Z', + type: 'buffer', arrival: 0.3, delay: 0.1, + fanout: 4, level: 2, + }], + min_arrival: 0, + max_arrival: 1, + }); + const item = result.layout[0]; + assert.equal(item.name, 'mybuf'); + assert.equal(item.pin_name, 'Z'); + assert.equal(item.type, 'buffer'); + assert.equal(item.arrival, 0.3); + assert.equal(item.delay, 0.1); + assert.equal(item.fanout, 4); + assert.equal(item.level, 2); + assert.equal(item.parent_id, -1); + }); + + it('handles deep tree hierarchy', () => { + // root → buf1 → buf2 → reg (single chain) + const result = computeClockTreeLayout({ + nodes: [ + { id: 0, parent_id: -1, name: 'clk', type: 'root', + arrival: 0, delay: 0, fanout: 1, level: 0 }, + { id: 1, parent_id: 0, name: 'buf1', type: 'buffer', + arrival: 0.1, delay: 0.05, fanout: 1, level: 1 }, + { id: 2, parent_id: 1, name: 'buf2', type: 'buffer', + arrival: 0.2, delay: 0.05, fanout: 1, level: 2 }, + { id: 3, parent_id: 2, name: 'reg', type: 'register', + arrival: 0.3, delay: 0, fanout: 0, level: 3 }, + ], + min_arrival: 0, + max_arrival: 0.3, + }); + assert.equal(result.layout.length, 4); + + // Single chain: all have width 1, so all same x + const xs = result.layout.map(n => n.x); + assert.ok(xs.every(x => x === xs[0]), + 'all nodes in single chain should have same x'); + + // Y values should increase with arrival + for (let i = 0; i < result.layout.length - 1; i++) { + assert.ok(result.layout[i].y < result.layout[i + 1].y, + `node ${i} should be above node ${i + 1}`); + } + }); + + it('handles asymmetric tree', () => { + // root → buf1 (→ reg1, reg2), reg3 + const result = computeClockTreeLayout({ + nodes: [ + { id: 0, parent_id: -1, name: 'root', type: 'root', + arrival: 0, delay: 0, fanout: 2, level: 0 }, + { id: 1, parent_id: 0, name: 'buf1', type: 'buffer', + arrival: 0.1, delay: 0.05, fanout: 2, level: 1 }, + { id: 2, parent_id: 1, name: 'reg1', type: 'register', + arrival: 0.3, delay: 0, fanout: 0, level: 2 }, + { id: 3, parent_id: 1, name: 'reg2', type: 'register', + arrival: 0.3, delay: 0, fanout: 0, level: 2 }, + { id: 4, parent_id: 0, name: 'reg3', type: 'register', + arrival: 0.2, delay: 0, fanout: 0, level: 1 }, + ], + min_arrival: 0, + max_arrival: 0.3, + }); + assert.equal(result.layout.length, 5); + + // buf1 subtree has width 2, reg3 has width 1 → total width 3 + assert.equal(result.layoutWidth, + kLeftMargin + 3 * kNodeSpacing + kRightMargin); + + // buf1 centered over its 2 children, reg3 in its own bin + const buf1 = result.layout.find(n => n.id === 1); + const reg1 = result.layout.find(n => n.id === 2); + const reg2 = result.layout.find(n => n.id === 3); + const reg3 = result.layout.find(n => n.id === 4); + + // buf1 centered between reg1 and reg2 + assert.equal(buf1.x, (reg1.x + reg2.x) / 2); + // reg3 should be to the right of reg2 + assert.ok(reg3.x > reg2.x); + }); + + it('defaults timeUnit to empty string', () => { + const result = computeClockTreeLayout({ + nodes: [{ id: 0, parent_id: -1, name: 'clk', type: 'root', + arrival: 0, delay: 0, fanout: 0, level: 0 }], + min_arrival: 0, + max_arrival: 1, + }); + assert.equal(result.timeUnit, ''); + }); + + it('handles equal min and max arrival', () => { + // When min == max, timeRange defaults to 1 to avoid division by zero + const result = computeClockTreeLayout({ + nodes: [ + { id: 0, parent_id: -1, name: 'clk', type: 'root', + arrival: 0.5, delay: 0, fanout: 1, level: 0 }, + { id: 1, parent_id: 0, name: 'reg', type: 'register', + arrival: 0.5, delay: 0, fanout: 0, level: 1 }, + ], + min_arrival: 0.5, + max_arrival: 0.5, + }); + assert.equal(result.layout.length, 2); + // Both nodes have same arrival → same y + assert.equal(result.layout[0].y, result.layout[1].y); + }); +}); diff --git a/src/web/test/js/test-coordinates.js b/src/web/test/js/test-coordinates.js new file mode 100644 index 00000000000..e20996bfe2e --- /dev/null +++ b/src/web/test/js/test-coordinates.js @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { dbuToLatLng, dbuRectToBounds, latLngToDbu } from '../../src/coordinates.js'; + +describe('dbuToLatLng', () => { + const scale = 1e-6; + const maxDXDY = 1000000; + + it('converts origin', () => { + const [lat, lng] = dbuToLatLng(0, 0, scale, maxDXDY); + assert.equal(lng, 0); + assert.equal(lat, -maxDXDY * scale); + }); + + it('converts max corner', () => { + const [lat, lng] = dbuToLatLng(maxDXDY, maxDXDY, scale, maxDXDY); + assert.equal(lat, 0); + assert.equal(lng, maxDXDY * scale); + }); + + it('converts mid-point', () => { + const mid = maxDXDY / 2; + const [lat, lng] = dbuToLatLng(mid, mid, scale, maxDXDY); + assert.equal(lat, -mid * scale); + assert.equal(lng, mid * scale); + }); +}); + +describe('dbuRectToBounds', () => { + it('converts a rectangle', () => { + const scale = 1e-6; + const maxDXDY = 1000000; + const [[lat1, lng1], [lat2, lng2]] = dbuRectToBounds( + 100, 200, 300, 400, scale, maxDXDY); + assert.equal(lng1, 100 * scale); + assert.equal(lng2, 300 * scale); + assert.equal(lat1, (200 - maxDXDY) * scale); + assert.equal(lat2, (400 - maxDXDY) * scale); + }); +}); + +describe('latLngToDbu', () => { + it('converts back to dbu', () => { + const scale = 1e-6; + const maxDXDY = 1000000; + const { dbuX, dbuY } = latLngToDbu(-0.5, 0.25, scale, maxDXDY); + assert.equal(dbuX, Math.round(0.25 / scale)); + assert.equal(dbuY, Math.round(maxDXDY + (-0.5) / scale)); + }); +}); + +describe('round-trip dbu ↔ latLng', () => { + it('preserves coordinates', () => { + const scale = 1e-6, maxDXDY = 500000; + const [lat, lng] = dbuToLatLng(12345, 67890, scale, maxDXDY); + const { dbuX, dbuY } = latLngToDbu(lat, lng, scale, maxDXDY); + assert.equal(dbuX, 12345); + assert.equal(dbuY, 67890); + }); + + it('preserves zero', () => { + const scale = 1e-6, maxDXDY = 1000000; + const [lat, lng] = dbuToLatLng(0, 0, scale, maxDXDY); + const { dbuX, dbuY } = latLngToDbu(lat, lng, scale, maxDXDY); + assert.equal(dbuX, 0); + assert.equal(dbuY, 0); + }); + + it('preserves large values', () => { + const scale = 1e-6, maxDXDY = 10000000; + const [lat, lng] = dbuToLatLng(9999999, 9999999, scale, maxDXDY); + const { dbuX, dbuY } = latLngToDbu(lat, lng, scale, maxDXDY); + assert.equal(dbuX, 9999999); + assert.equal(dbuY, 9999999); + }); +}); diff --git a/src/web/test/js/test-display-controls.js b/src/web/test/js/test-display-controls.js new file mode 100644 index 00000000000..c872cacb705 --- /dev/null +++ b/src/web/test/js/test-display-controls.js @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { layerRangeSet } from '../../src/display-controls.js'; + +// 10 layers: Metal1, Via1, Metal2, Via2, ... Metal5, Via5 +const COUNT = 10; + +describe('layerRangeSet', () => { + it('show only selected (center only)', () => { + const s = layerRangeSet(3, 0, 0, COUNT); + assert.deepEqual(s, new Set([3])); + }); + + it('range ±1 in the middle', () => { + const s = layerRangeSet(4, 1, 1, COUNT); + assert.deepEqual(s, new Set([3, 4, 5])); + }); + + it('range ±2 in the middle', () => { + const s = layerRangeSet(5, 2, 2, COUNT); + assert.deepEqual(s, new Set([3, 4, 5, 6, 7])); + }); + + it('range down only (lower=1, upper=0)', () => { + const s = layerRangeSet(4, 1, 0, COUNT); + assert.deepEqual(s, new Set([3, 4])); + }); + + it('range up only (lower=0, upper=1)', () => { + const s = layerRangeSet(4, 0, 1, COUNT); + assert.deepEqual(s, new Set([4, 5])); + }); + + it('clamps at lower bound', () => { + const s = layerRangeSet(0, 2, 2, COUNT); + assert.deepEqual(s, new Set([0, 1, 2])); + }); + + it('clamps at upper bound', () => { + const s = layerRangeSet(9, 2, 2, COUNT); + assert.deepEqual(s, new Set([7, 8, 9])); + }); + + it('single layer total', () => { + const s = layerRangeSet(0, 1, 1, 1); + assert.deepEqual(s, new Set([0])); + }); + + it('range down at first layer returns only first', () => { + const s = layerRangeSet(0, 1, 0, COUNT); + assert.deepEqual(s, new Set([0])); + }); + + it('range up at last layer returns only last', () => { + const s = layerRangeSet(9, 0, 1, COUNT); + assert.deepEqual(s, new Set([9])); + }); +}); diff --git a/src/web/test/js/test-inspector.js b/src/web/test/js/test-inspector.js new file mode 100644 index 00000000000..1e732555544 --- /dev/null +++ b/src/web/test/js/test-inspector.js @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { describe, it, beforeEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; + +// Set up minimal DOM before importing inspector. +const dom = new JSDOM(''); +globalThis.document = dom.window.document; +globalThis.window = dom.window; +globalThis.Event = dom.window.Event; +globalThis.L = undefined; // Leaflet not needed for these tests + +const { createInspectorPanel } = await import('../../src/inspector.js'); + +// Helper to build a minimal app object with mocked websocket. +function makeApp() { + const requests = []; + return { + focusNets: new Set(), + routeGuideNets: new Set(), + inspectorEl: document.createElement('div'), + hoverRects: [], + highlightRect: null, + map: null, + designScale: null, + designMaxDXDY: null, + websocketManager: { + _nextId: 1, + request(msg) { + requests.push(msg); + const p = Promise.resolve({ ok: 1, count: 0 }); + p.requestId = this._nextId++; + return p; + }, + cancel() {}, + }, + _requests: requests, + }; +} + +// Minimal inspect data for a Net object. +function netData(name) { + return { + type: 'Net', + name, + bbox: [0, 0, 1000, 1000], + properties: [{ name: 'Name', value: name }], + }; +} + +// Minimal inspect data for an Inst object. +function instData(name) { + return { + type: 'Inst', + name, + bbox: [0, 0, 500, 500], + properties: [{ name: 'Name', value: name }], + }; +} + +describe('Inspector focus nets', () => { + let app, panel, redraws; + + beforeEach(() => { + app = makeApp(); + redraws = 0; + panel = createInspectorPanel(app, () => { redraws++; }); + }); + + describe('updateInspector toolbar buttons', () => { + it('shows focus button for Net type', () => { + panel.updateInspector(netData('clk')); + const btns = app.inspectorEl.querySelectorAll('.inspector-btn'); + // Should have back + zoom + focus buttons + assert.ok(btns.length >= 3, `expected >=3 buttons, got ${btns.length}`); + const focusBtn = Array.from(btns).find(b => b.title === 'Focus net'); + assert.ok(focusBtn, 'focus button should be present'); + }); + + it('does not show focus button for non-Net type', () => { + panel.updateInspector(instData('buf1')); + const btns = app.inspectorEl.querySelectorAll('.inspector-btn'); + // Should have back + zoom buttons, no focus button + assert.equal(btns.length, 2); + const focusBtn = Array.from(btns).find(b => b.title === 'Focus net'); + assert.equal(focusBtn, undefined, 'focus button should not be present'); + }); + + it('shows de-focus button when net is already focused', () => { + app.focusNets.add('clk'); + panel.updateInspector(netData('clk')); + const btns = app.inspectorEl.querySelectorAll('.inspector-btn'); + const defocusBtn = Array.from(btns).find(b => b.title === 'De-focus net'); + assert.ok(defocusBtn, 'de-focus button should be present'); + }); + + it('shows clear button when any nets are focused', () => { + app.focusNets.add('data'); + panel.updateInspector(netData('clk')); + const btns = app.inspectorEl.querySelectorAll('.inspector-btn'); + const clearBtn = Array.from(btns).find(b => b.title === 'Clear focus nets'); + assert.ok(clearBtn, 'clear button should be present'); + }); + + it('does not show clear button when no nets are focused', () => { + panel.updateInspector(netData('clk')); + const btns = app.inspectorEl.querySelectorAll('.inspector-btn'); + const clearBtn = Array.from(btns).find(b => b.title === 'Clear focus nets'); + assert.equal(clearBtn, undefined, 'clear button should not be present'); + }); + }); + + describe('toggleFocusNet via button click', () => { + it('adds net to focusNets on focus click', async () => { + panel.updateInspector(netData('clk')); + const focusBtn = Array.from(app.inspectorEl.querySelectorAll('.inspector-btn')) + .find(b => b.title === 'Focus net'); + focusBtn.click(); + // Let promises settle + await new Promise(r => setTimeout(r, 10)); + + assert.ok(app.focusNets.has('clk')); + assert.equal(app._requests.length, 1); + assert.equal(app._requests[0].type, 'set_focus_nets'); + assert.equal(app._requests[0].action, 'add'); + assert.equal(app._requests[0].net_name, 'clk'); + assert.equal(redraws, 1); + }); + + it('removes net from focusNets on de-focus click', async () => { + app.focusNets.add('clk'); + panel.updateInspector(netData('clk')); + const defocusBtn = Array.from(app.inspectorEl.querySelectorAll('.inspector-btn')) + .find(b => b.title === 'De-focus net'); + assert.ok(defocusBtn, 'de-focus button should be present'); + defocusBtn.click(); + await new Promise(r => setTimeout(r, 10)); + + assert.ok(!app.focusNets.has('clk')); + assert.equal(app._requests[0].action, 'remove'); + assert.equal(redraws, 1); + }); + }); + + describe('clearFocusNets via button click', () => { + it('clears all focus nets', async () => { + app.focusNets.add('clk'); + app.focusNets.add('data'); + panel.updateInspector(netData('clk')); + const clearBtn = Array.from( + app.inspectorEl.querySelectorAll('.inspector-btn') + ).find(b => b.title === 'Clear focus nets'); + assert.ok(clearBtn); + clearBtn.click(); + await new Promise(r => setTimeout(r, 10)); + + assert.equal(app.focusNets.size, 0); + assert.equal(app._requests[0].action, 'clear'); + assert.equal(redraws, 1); + }); + }); + + describe('placeholder', () => { + it('shows placeholder when no data', () => { + panel.updateInspector(null); + const stub = app.inspectorEl.querySelector('.stub-panel'); + assert.ok(stub, 'placeholder should be shown'); + }); + + it('shows placeholder when empty properties', () => { + panel.updateInspector({ properties: [] }); + const stub = app.inspectorEl.querySelector('.stub-panel'); + assert.ok(stub); + }); + }); + + describe('properties rendering', () => { + it('renders leaf properties', () => { + panel.updateInspector({ + type: 'Net', + name: 'sig', + bbox: [0, 0, 100, 100], + properties: [ + { name: 'Name', value: 'sig' }, + { name: 'Type', value: 'Signal' }, + ], + }); + const props = app.inspectorEl.querySelectorAll('.inspector-prop'); + assert.equal(props.length, 2); + assert.equal(props[0].querySelector('.inspector-prop-name').textContent, 'Name'); + assert.equal(props[0].querySelector('.inspector-prop-value').textContent, 'sig'); + }); + + it('renders group with children', () => { + panel.updateInspector({ + type: 'Inst', + name: 'buf1', + bbox: [0, 0, 100, 100], + properties: [{ + name: 'Pins', + children: [ + { name: 'A', value: 'connected' }, + { name: 'Z', value: 'connected' }, + ], + }], + }); + const groups = app.inspectorEl.querySelectorAll('.inspector-group'); + assert.equal(groups.length, 1); + const kids = groups[0].querySelector('.inspector-group-children'); + assert.equal(kids.children.length, 2); + }); + }); +}); diff --git a/src/web/test/js/test-vis-tree.js b/src/web/test/js/test-vis-tree.js new file mode 100644 index 00000000000..c60faba04ac --- /dev/null +++ b/src/web/test/js/test-vis-tree.js @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { describe, it, beforeEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { JSDOM } from 'jsdom'; + +// Set up minimal DOM before importing vis-tree. +const dom = new JSDOM(''); +globalThis.document = dom.window.document; + +const { VisTree } = await import('../../src/vis-tree.js'); + +describe('VisTree', () => { + let visibility, changes, tree; + + beforeEach(() => { + visibility = {}; + changes = 0; + tree = new VisTree(visibility, () => { changes++; }); + }); + + describe('leaf nodes', () => { + it('unchecked when key absent from visibility', () => { + tree.add({ key: 'a', label: 'A' }); + tree.render(document.createElement('div')); + assert.equal(visibility.a, false); + }); + + it('checked when visibility has key as true', () => { + visibility.a = true; + tree.add({ key: 'a', label: 'A' }); + tree.render(document.createElement('div')); + assert.equal(visibility.a, true); + }); + + it('unchecked when visibility has key as false', () => { + visibility.x = false; + tree.add({ key: 'x', label: 'X' }); + tree.render(document.createElement('div')); + assert.equal(visibility.x, false); + }); + }); + + describe('parent-child relationships', () => { + it('parent checked when all children checked', () => { + visibility.a = true; + visibility.b = true; + tree.add({ label: 'Group', children: [ + { key: 'a', label: 'A' }, + { key: 'b', label: 'B' }, + ]}); + const container = document.createElement('div'); + tree.render(container); + const parentCb = container.querySelector('.vis-group-header input'); + assert.equal(parentCb.checked, true); + assert.equal(parentCb.indeterminate, false); + }); + + it('parent unchecked when all children unchecked', () => { + tree.add({ label: 'Group', children: [ + { key: 'a', label: 'A' }, + { key: 'b', label: 'B' }, + ]}); + const container = document.createElement('div'); + tree.render(container); + const parentCb = container.querySelector('.vis-group-header input'); + assert.equal(parentCb.checked, false); + assert.equal(parentCb.indeterminate, false); + }); + + it('unchecking parent unchecks all children', () => { + visibility.a = true; + visibility.b = true; + tree.add({ label: 'Group', children: [ + { key: 'a', label: 'A' }, + { key: 'b', label: 'B' }, + ]}); + const container = document.createElement('div'); + tree.render(container); + const parentCb = container.querySelector('.vis-group-header input'); + parentCb.checked = false; + parentCb.dispatchEvent(new dom.window.Event('change')); + assert.equal(visibility.a, false); + assert.equal(visibility.b, false); + }); + + it('checking parent checks all children', () => { + tree.add({ label: 'Group', children: [ + { key: 'a', label: 'A' }, + { key: 'b', label: 'B' }, + ]}); + const container = document.createElement('div'); + tree.render(container); + const parentCb = container.querySelector('.vis-group-header input'); + parentCb.checked = true; + parentCb.dispatchEvent(new dom.window.Event('change')); + assert.equal(visibility.a, true); + assert.equal(visibility.b, true); + }); + + it('parent becomes indeterminate when children mixed', () => { + visibility.a = true; + visibility.b = true; + tree.add({ label: 'Group', children: [ + { key: 'a', label: 'A' }, + { key: 'b', label: 'B' }, + ]}); + const container = document.createElement('div'); + tree.render(container); + // Uncheck just one child + const childCbs = container.querySelectorAll('.vis-group-children input'); + childCbs[0].checked = false; + childCbs[0].dispatchEvent(new dom.window.Event('change')); + const parentCb = container.querySelector('.vis-group-header input'); + assert.equal(parentCb.indeterminate, true); + }); + }); + + describe('visKey', () => { + it('sets visKey true when all children checked', () => { + visibility.a = true; + visibility.b = true; + tree.add({ label: 'Group', visKey: 'group_vis', children: [ + { key: 'a', label: 'A' }, + { key: 'b', label: 'B' }, + ]}); + tree.render(document.createElement('div')); + assert.equal(visibility.group_vis, true); + }); + + it('sets visKey true when some children checked (indeterminate)', () => { + visibility.a = false; + visibility.b = true; + tree.add({ label: 'Group', visKey: 'group_vis', children: [ + { key: 'a', label: 'A' }, + { key: 'b', label: 'B' }, + ]}); + tree.render(document.createElement('div')); + // indeterminate: visKey should still be true + assert.equal(visibility.group_vis, true); + }); + + it('sets visKey false when no children checked', () => { + tree.add({ label: 'Group', visKey: 'group_vis', children: [ + { key: 'a', label: 'A' }, + { key: 'b', label: 'B' }, + ]}); + tree.render(document.createElement('div')); + assert.equal(visibility.group_vis, false); + }); + }); + + describe('onChange callback', () => { + it('fires on checkbox change', () => { + visibility.a = true; + tree.add({ key: 'a', label: 'A' }); + const container = document.createElement('div'); + tree.render(container); + const cb = container.querySelector('input'); + cb.checked = false; + cb.dispatchEvent(new dom.window.Event('change')); + assert.equal(changes, 1); + }); + }); + + describe('disabled groups', () => { + it('marks children container as disabled', () => { + tree.add({ label: 'Group', disabled: true, children: [ + { key: 'a', label: 'A' }, + ]}); + const container = document.createElement('div'); + tree.render(container); + const kids = container.querySelector('.vis-group-children'); + assert.ok(kids.classList.contains('disabled')); + }); + }); +}); diff --git a/src/web/test/js/test-websocket-manager.js b/src/web/test/js/test-websocket-manager.js new file mode 100644 index 00000000000..3f4d303e739 --- /dev/null +++ b/src/web/test/js/test-websocket-manager.js @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026, The OpenROAD Authors + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +// Mock WebSocket before importing the module. +class MockWebSocket { + constructor() { + this.readyState = 1; + this.sent = []; + this.binaryType = null; + // Auto-fire onopen so the manager considers itself connected. + queueMicrotask(() => { if (this.onopen) this.onopen(); }); + } + send(data) { this.sent.push(data); } + close() {} + static get OPEN() { return 1; } +} +globalThis.WebSocket = MockWebSocket; + +const { WebSocketManager } = await import('../../src/websocket-manager.js'); + +// Helper: build a binary response frame. +// Format: [4B id (big-endian)][1B type][3B reserved][payload] +function buildFrame(id, type, payload) { + const buf = new ArrayBuffer(8 + payload.length); + const view = new DataView(buf); + view.setUint32(0, id); + view.setUint8(4, type); + new Uint8Array(buf, 8).set(payload); + return buf; +} + +describe('WebSocketManager', () => { + describe('handleMessage', () => { + it('resolves JSON response', async () => { + const mgr = new WebSocketManager('ws://fake'); + const promise = new Promise((resolve, reject) => { + mgr.pending.set(42, { resolve, reject }); + }); + const json = JSON.stringify({ status: 'ok', value: 123 }); + const payload = new TextEncoder().encode(json); + mgr.handleMessage(buildFrame(42, 0, payload)); + const result = await promise; + assert.deepEqual(result, { status: 'ok', value: 123 }); + }); + + it('resolves PNG response as Blob', async () => { + // Blob is not available in Node 18 test runner without setup, + // but handleMessage uses `new Blob(...)` which exists in Node 18. + const mgr = new WebSocketManager('ws://fake'); + const promise = new Promise((resolve, reject) => { + mgr.pending.set(7, { resolve, reject }); + }); + const pngData = new Uint8Array([0x89, 0x50, 0x4E, 0x47]); + mgr.handleMessage(buildFrame(7, 1, pngData)); + const blob = await promise; + assert.equal(blob.type, 'image/png'); + assert.equal(blob.size, 4); + }); + + it('rejects error response', async () => { + const mgr = new WebSocketManager('ws://fake'); + const promise = new Promise((resolve, reject) => { + mgr.pending.set(99, { resolve, reject }); + }); + const errMsg = new TextEncoder().encode('something failed'); + mgr.handleMessage(buildFrame(99, 2, errMsg)); + await assert.rejects(promise, { message: 'something failed' }); + }); + + it('ignores unknown request IDs', () => { + const mgr = new WebSocketManager('ws://fake'); + // Should not throw. + const payload = new TextEncoder().encode('{}'); + mgr.handleMessage(buildFrame(999, 0, payload)); + assert.equal(mgr.pending.size, 0); + }); + }); + + describe('request', () => { + it('assigns incrementing IDs', async () => { + const mgr = new WebSocketManager('ws://fake'); + await mgr.readyPromise; + const startId = mgr.nextId; + mgr.request({ type: 'test1' }); + mgr.request({ type: 'test2' }); + assert.equal(mgr.nextId, startId + 2); + }); + + it('rejects when socket not connected', async () => { + const mgr = new WebSocketManager('ws://fake'); + mgr.socket.readyState = 3; // CLOSED + await assert.rejects( + mgr.request({ type: 'test' }), + { message: 'WebSocket not connected' } + ); + }); + }); + + describe('cancel', () => { + it('removes pending request', async () => { + const mgr = new WebSocketManager('ws://fake'); + await mgr.readyPromise; + mgr.pending.set(5, { resolve: () => {}, reject: () => {} }); + assert.equal(mgr.pending.has(5), true); + mgr.cancel(5); + assert.equal(mgr.pending.has(5), false); + }); + }); +}); diff --git a/src/web/test/regression b/src/web/test/regression new file mode 120000 index 00000000000..683895a42de --- /dev/null +++ b/src/web/test/regression @@ -0,0 +1 @@ +../../../test/shared/regression.sh \ No newline at end of file diff --git a/src/web/test/save_defok b/src/web/test/save_defok new file mode 120000 index 00000000000..7ee49ae334f --- /dev/null +++ b/src/web/test/save_defok @@ -0,0 +1 @@ +../../../test/shared/save_defok.sh \ No newline at end of file diff --git a/src/web/test/save_ok b/src/web/test/save_ok new file mode 120000 index 00000000000..c3cdfd3c124 --- /dev/null +++ b/src/web/test/save_ok @@ -0,0 +1 @@ +../../../test/shared/save_ok.sh \ No newline at end of file diff --git a/test/commands_without_load.tcl b/test/commands_without_load.tcl index 7b31f27a036..c8659b0260e 100644 --- a/test/commands_without_load.tcl +++ b/test/commands_without_load.tcl @@ -5,6 +5,7 @@ set skip { exit_summary run_unit_test_and_exit vwait + web_server exit } diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index a308f2136cb..39bd2d48497 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -56,3 +56,5 @@ add_compile_options( add_subdirectory(abc) endif() + +add_subdirectory(lodepng) diff --git a/third-party/lodepng/BUILD b/third-party/lodepng/BUILD new file mode 100644 index 00000000000..0c94fd6c9f9 --- /dev/null +++ b/third-party/lodepng/BUILD @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025, The OpenROAD Authors + +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +cc_library( + name = "lodepng", + srcs = ["lodepng.cpp"], + hdrs = ["lodepng.h"], + includes = ["."], + visibility = ["//visibility:public"], +) diff --git a/third-party/lodepng/CMakeLists.txt b/third-party/lodepng/CMakeLists.txt new file mode 100644 index 00000000000..cfec886b1ed --- /dev/null +++ b/third-party/lodepng/CMakeLists.txt @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2025-2025, The OpenROAD Authors + +add_library(lodepng + lodepng.cpp +) + +target_include_directories(lodepng + PUBLIC + . +) diff --git a/third-party/lodepng/lodepng.cpp b/third-party/lodepng/lodepng.cpp new file mode 100644 index 00000000000..442963720bc --- /dev/null +++ b/third-party/lodepng/lodepng.cpp @@ -0,0 +1,9953 @@ +/* +LodePNG version 20250506 + +Copyright (c) 2005-2025 Lode Vandevenne + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ + +/* +The manual and changelog are in the header file "lodepng.h" +Rename this file to lodepng.cpp to use it for C++, or to lodepng.c to use it for +C. +*/ + +#include "lodepng.h" + +#ifdef LODEPNG_COMPILE_DISK +#include /* LONG_MAX */ +#include /* file handling */ +#endif /* LODEPNG_COMPILE_DISK */ + +#ifdef LODEPNG_COMPILE_ALLOCATORS +#include /* allocations */ +#endif /* LODEPNG_COMPILE_ALLOCATORS */ + +#if defined(_MSC_VER) \ + && (_MSC_VER \ + >= 1310) /*Visual Studio: A few warning types are not desired here.*/ +#pragma warning(disable : 4244) /*implicit conversions: not warned by gcc \ + -Wall -Wextra and requires too much casts*/ +#pragma warning(disable : 4996) /*VS does not like fopen, but fopen_s is not \ + standard C so unusable here*/ +#endif /*_MSC_VER */ + +const char* LODEPNG_VERSION_STRING = "20250506"; + +/* +This source file is divided into the following large parts. The code sections +with the "LODEPNG_COMPILE_" #defines divide this up further in an intermixed +way. -Tools for C and common code for PNG and Zlib -C Code for Zlib (huffman, +deflate, ...) -C Code for PNG (file format chunks, adam7, PNG filters, color +conversions, ...) -The C++ wrapper around all of the above +*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* // Tools for C, and common code for PNG and Zlib. // */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/*The malloc, realloc and free functions defined here with "lodepng_" in front +of the name, so that you can easily change them to others related to your +platform if needed. Everything else in the code calls these. Pass +-DLODEPNG_NO_COMPILE_ALLOCATORS to the compiler, or comment out +#define LODEPNG_COMPILE_ALLOCATORS in the header, to disable the ones here and +define them in your own project's source files without needing to change +lodepng source code. Don't forget to remove "static" if you copypaste them +from here.*/ + +#ifdef LODEPNG_COMPILE_ALLOCATORS +static void* lodepng_malloc(size_t size) +{ +#ifdef LODEPNG_MAX_ALLOC + if (size > LODEPNG_MAX_ALLOC) { + return 0; + } +#endif + return malloc(size); +} + +/* NOTE: when realloc returns NULL, it leaves the original memory untouched */ +static void* lodepng_realloc(void* ptr, size_t new_size) +{ +#ifdef LODEPNG_MAX_ALLOC + if (new_size > LODEPNG_MAX_ALLOC) { + return 0; + } +#endif + return realloc(ptr, new_size); +} + +static void lodepng_free(void* ptr) +{ + free(ptr); +} +#else /*LODEPNG_COMPILE_ALLOCATORS*/ +/* TODO: support giving additional void* payload to the custom allocators */ +void* lodepng_malloc(size_t size); +void* lodepng_realloc(void* ptr, size_t new_size); +void lodepng_free(void* ptr); +#endif /*LODEPNG_COMPILE_ALLOCATORS*/ + +/* convince the compiler to inline a function, for use when this measurably + * improves performance */ +/* inline is not available in C90, but use it when supported by the compiler */ +#if (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) \ + || (defined(__cplusplus) && (__cplusplus >= 199711L)) +#define LODEPNG_INLINE inline +#else +#define LODEPNG_INLINE /* not available */ +#endif + +/* restrict is not available in C90, but use it when supported by the compiler + */ +#if (defined(__GNUC__) \ + && (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1))) \ + || (defined(_MSC_VER) && (_MSC_VER >= 1400)) \ + || (defined(__WATCOMC__) && (__WATCOMC__ >= 1250) \ + && !defined(__cplusplus)) +#define LODEPNG_RESTRICT __restrict +#else +#define LODEPNG_RESTRICT /* not available */ +#endif + +/* Replacements for C library functions such as memcpy and strlen, to support +platforms where a full C library is not available. The compiler can recognize +them and compile to something as fast. */ + +static void lodepng_memcpy(void* LODEPNG_RESTRICT dst, + const void* LODEPNG_RESTRICT src, + size_t size) +{ + size_t i; + for (i = 0; i < size; i++) { + ((char*) dst)[i] = ((const char*) src)[i]; + } +} + +static void lodepng_memset(void* LODEPNG_RESTRICT dst, int value, size_t num) +{ + size_t i; + for (i = 0; i < num; i++) { + ((char*) dst)[i] = (char) value; + } +} + +/* does not check memory out of bounds, do not use on untrusted data */ +static size_t lodepng_strlen(const char* a) +{ + const char* orig = a; + /* avoid warning about unused function in case of disabled COMPILE... macros + */ + (void) (&lodepng_strlen); + while (*a) { + a++; + } + return (size_t) (a - orig); +} + +#define LODEPNG_MAX(a, b) (((a) > (b)) ? (a) : (b)) +#define LODEPNG_MIN(a, b) (((a) < (b)) ? (a) : (b)) + +#if defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_DECODER) +/* Safely check if adding two integers will overflow (no undefined +behavior, compiler removing the code, etc...) and output result. */ +static int lodepng_addofl(size_t a, size_t b, size_t* result) +{ + *result = a + b; /* Unsigned addition is well defined and safe in C90 */ + return *result < a; +} +#endif /*defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_DECODER)*/ + +#ifdef LODEPNG_COMPILE_DECODER +/* Safely check if multiplying two integers will overflow (no undefined +behavior, compiler removing the code, etc...) and output result. */ +static int lodepng_mulofl(size_t a, size_t b, size_t* result) +{ + *result = a * b; /* Unsigned multiplication is well defined and safe in C90 */ + return (a != 0 && *result / a != b); +} + +#ifdef LODEPNG_COMPILE_ZLIB +/* Safely check if a + b > c, even if overflow could happen. */ +static int lodepng_gtofl(size_t a, size_t b, size_t c) +{ + size_t d; + if (lodepng_addofl(a, b, &d)) { + return 1; + } + return d > c; +} +#endif /*LODEPNG_COMPILE_ZLIB*/ +#endif /*LODEPNG_COMPILE_DECODER*/ + +/* +Often in case of an error a value is assigned to a variable and then it breaks +out of a loop (to go to the cleanup phase of a function). This macro does that. +It makes the error handling code shorter and more readable. + +Example: if(!uivector_resize(&lz77_encoded, datasize)) ERROR_BREAK(83); +*/ +#define CERROR_BREAK(errorvar, code) \ + { \ + errorvar = code; \ + break; \ + } + +/*version of CERROR_BREAK that assumes the common case where the error variable + * is named "error"*/ +#define ERROR_BREAK(code) CERROR_BREAK(error, code) + +/*Set error var to the error code, and return it.*/ +#define CERROR_RETURN_ERROR(errorvar, code) \ + { \ + errorvar = code; \ + return code; \ + } + +/*Try the code, if it returns error, also return the error.*/ +#define CERROR_TRY_RETURN(call) \ + { \ + unsigned error = call; \ + if (error) \ + return error; \ + } + +/*Set error var to the error code, and return from the void function.*/ +#define CERROR_RETURN(errorvar, code) \ + { \ + errorvar = code; \ + return; \ + } + +/* +About uivector, ucvector and string: +-All of them wrap dynamic arrays or text strings in a similar way. +-LodePNG was originally written in C++. The vectors replace the std::vectors +that were used in the C++ version. -The string tools are made to avoid problems +with compilers that declare things like strncat as deprecated. -They're not used +in the interface, only internally in this file as static functions. -As with +many other structs in this file, the init and cleanup functions serve as ctor +and dtor. +*/ + +#ifdef LODEPNG_COMPILE_ZLIB +#ifdef LODEPNG_COMPILE_ENCODER +/*dynamic vector of unsigned ints*/ +typedef struct uivector +{ + unsigned* data; + size_t size; /*size in number of unsigned longs*/ + size_t allocsize; /*allocated size in bytes*/ +} uivector; + +static void uivector_cleanup(void* p) +{ + ((uivector*) p)->size = ((uivector*) p)->allocsize = 0; + lodepng_free(((uivector*) p)->data); + ((uivector*) p)->data = NULL; +} + +/*returns 1 if success, 0 if failure ==> nothing done*/ +static unsigned uivector_resize(uivector* p, size_t size) +{ + size_t allocsize = size * sizeof(unsigned); + if (allocsize > p->allocsize) { + size_t newsize = allocsize + (p->allocsize >> 1u); + void* data = lodepng_realloc(p->data, newsize); + if (data) { + p->allocsize = newsize; + p->data = (unsigned*) data; + } else { + return 0; /*error: not enough memory*/ + } + } + p->size = size; + return 1; /*success*/ +} + +static void uivector_init(uivector* p) +{ + p->data = NULL; + p->size = p->allocsize = 0; +} + +/*returns 1 if success, 0 if failure ==> nothing done*/ +static unsigned uivector_push_back(uivector* p, unsigned c) +{ + if (!uivector_resize(p, p->size + 1)) { + return 0; + } + p->data[p->size - 1] = c; + return 1; +} +#endif /*LODEPNG_COMPILE_ENCODER*/ +#endif /*LODEPNG_COMPILE_ZLIB*/ + +/* /////////////////////////////////////////////////////////////////////////// + */ + +/*dynamic vector of unsigned chars*/ +typedef struct ucvector +{ + unsigned char* data; + size_t size; /*used size*/ + size_t allocsize; /*allocated size*/ +} ucvector; + +/*returns 1 if success, 0 if failure ==> nothing done*/ +static unsigned ucvector_reserve(ucvector* p, size_t size) +{ + if (size > p->allocsize) { + size_t newsize = size + (p->allocsize >> 1u); + void* data = lodepng_realloc(p->data, newsize); + if (data) { + p->allocsize = newsize; + p->data = (unsigned char*) data; + } else { + return 0; /*error: not enough memory*/ + } + } + return 1; /*success*/ +} + +/*returns 1 if success, 0 if failure ==> nothing done*/ +static unsigned ucvector_resize(ucvector* p, size_t size) +{ + p->size = size; + return ucvector_reserve(p, size); +} + +static ucvector ucvector_init(unsigned char* buffer, size_t size) +{ + ucvector v; + v.data = buffer; + v.allocsize = v.size = size; + return v; +} + +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_PNG +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + +/*free string pointer and set it to NULL*/ +static void string_cleanup(char** out) +{ + lodepng_free(*out); + *out = NULL; +} + +/*also appends null termination character*/ +static char* alloc_string_sized(const char* in, size_t insize) +{ + char* out = (char*) lodepng_malloc(insize + 1); + if (out) { + lodepng_memcpy(out, in, insize); + out[insize] = 0; + } + return out; +} + +/* dynamically allocates a new string with a copy of the null terminated input + * text */ +static char* alloc_string(const char* in) +{ + return alloc_string_sized(in, lodepng_strlen(in)); +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +#endif /*LODEPNG_COMPILE_PNG*/ + +/* ////////////////////////////////////////////////////////////////////////// */ + +#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_PNG) +static unsigned lodepng_read32bitInt(const unsigned char* buffer) +{ + return (((unsigned) buffer[0] << 24u) | ((unsigned) buffer[1] << 16u) + | ((unsigned) buffer[2] << 8u) | (unsigned) buffer[3]); +} +#endif /*defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_PNG)*/ + +#if defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_ENCODER) +/*buffer must have at least 4 allocated bytes available*/ +static void lodepng_set32bitInt(unsigned char* buffer, unsigned value) +{ + buffer[0] = (unsigned char) ((value >> 24) & 0xff); + buffer[1] = (unsigned char) ((value >> 16) & 0xff); + buffer[2] = (unsigned char) ((value >> 8) & 0xff); + buffer[3] = (unsigned char) ((value) & 0xff); +} +#endif /*defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_ENCODER)*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / File IO / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_DISK + +/* returns negative value on error. This should be pure C compatible, so no + * fstat. */ +static long lodepng_filesize(FILE* file) +{ + long size; + if (fseek(file, 0, SEEK_END) != 0) { + return -1; + } + size = ftell(file); + /* It may give LONG_MAX as directory size, this is invalid for us. */ + if (size == LONG_MAX) { + return -1; + } + if (fseek(file, 0, SEEK_SET) != 0) { + return -1; + } + return size; +} + +/* Allocates the output buffer to the file size and reads the file into it. + * Returns error code.*/ +static unsigned lodepng_load_file_(unsigned char** out, + size_t* outsize, + FILE* file) +{ + long size = lodepng_filesize(file); + if (size < 0) { + return 78; + } + *outsize = (size_t) size; + *out = (unsigned char*) lodepng_malloc((size_t) size); + if (!(*out) && size > 0) { + return 83; /*the above malloc failed*/ + } + if (fread(*out, 1, *outsize, file) != *outsize) { + return 78; + } + return 0; /*ok*/ +} + +unsigned lodepng_load_file(unsigned char** out, + size_t* outsize, + const char* filename) +{ + unsigned error; + FILE* file = fopen(filename, "rb"); + if (!file) { + return 78; + } + error = lodepng_load_file_(out, outsize, file); + fclose(file); + return error; +} + +/*write given buffer to the file, overwriting the file, it doesn't append to + * it.*/ +unsigned lodepng_save_file(const unsigned char* buffer, + size_t buffersize, + const char* filename) +{ + FILE* file = fopen(filename, "wb"); + if (!file) { + return 79; + } + fwrite(buffer, 1, buffersize, file); + fclose(file); + return 0; +} + +#endif /*LODEPNG_COMPILE_DISK*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* // End of common code and tools. Begin of Zlib related code. // */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_ZLIB +#ifdef LODEPNG_COMPILE_ENCODER + +typedef struct +{ + ucvector* data; + unsigned char bp; /*ok to overflow, indicates bit pos inside byte*/ +} LodePNGBitWriter; + +static void LodePNGBitWriter_init(LodePNGBitWriter* writer, ucvector* data) +{ + writer->data = data; + writer->bp = 0; +} + +/*TODO: this ignores potential out of memory errors*/ +#define WRITEBIT(writer, bit) \ + { \ + /* append new byte */ \ + if (((writer->bp) & 7u) == 0) { \ + if (!ucvector_resize(writer->data, writer->data->size + 1)) \ + return; \ + writer->data->data[writer->data->size - 1] = 0; \ + } \ + (writer->data->data[writer->data->size - 1]) \ + |= (bit << ((writer->bp) & 7u)); \ + ++writer->bp; \ + } + +/* LSB of value is written first, and LSB of bytes is used first */ +static void writeBits(LodePNGBitWriter* writer, unsigned value, size_t nbits) +{ + if (nbits + == 1) { /* compiler should statically compile this case if nbits == 1 */ + WRITEBIT(writer, value); + } else { + /* TODO: increase output size only once here rather than in each WRITEBIT */ + size_t i; + for (i = 0; i != nbits; ++i) { + WRITEBIT(writer, (unsigned char) ((value >> i) & 1)); + } + } +} + +/* This one is to use for adding huffman symbol, the value bits are written MSB + * first */ +static void writeBitsReversed(LodePNGBitWriter* writer, + unsigned value, + size_t nbits) +{ + size_t i; + for (i = 0; i != nbits; ++i) { + /* TODO: increase output size only once here rather than in each WRITEBIT */ + WRITEBIT(writer, (unsigned char) ((value >> (nbits - 1u - i)) & 1u)); + } +} +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_DECODER + +typedef struct +{ + const unsigned char* data; + size_t size; /*size of data in bytes*/ + size_t bitsize; /*size of data in bits, end of valid bp values, should be + 8*size*/ + size_t bp; + unsigned buffer; /*buffer for reading bits. NOTE: 'unsigned' must support at + least 32 bits*/ +} LodePNGBitReader; + +/* data size argument is in bytes. Returns error if size too large causing + * overflow */ +static unsigned LodePNGBitReader_init(LodePNGBitReader* reader, + const unsigned char* data, + size_t size) +{ + size_t temp; + reader->data = data; + reader->size = size; + /* size in bits, return error if overflow (if size_t is 32 bit this supports + * up to 500MB) */ + if (lodepng_mulofl(size, 8u, &reader->bitsize)) { + return 105; + } + /*ensure incremented bp can be compared to bitsize without overflow even when + it would be incremented 32 too much and trying to ensure 32 more bits*/ + if (lodepng_addofl(reader->bitsize, 64u, &temp)) { + return 105; + } + reader->bp = 0; + reader->buffer = 0; + return 0; /*ok*/ +} + +/* +ensureBits functions: +Ensures the reader can at least read nbits bits in one or more readBits calls, +safely even if not enough bits are available. +The nbits parameter is unused but is given for documentation purposes, error +checking for amount of bits must be done beforehand. +*/ + +/*See ensureBits documentation above. This one ensures up to 9 bits */ +static LODEPNG_INLINE void ensureBits9(LodePNGBitReader* reader, size_t nbits) +{ + size_t start = reader->bp >> 3u; + size_t size = reader->size; + if (start + 1u < size) { + reader->buffer = (unsigned) reader->data[start + 0] + | ((unsigned) reader->data[start + 1] << 8u); + reader->buffer >>= (reader->bp & 7u); + } else { + reader->buffer = 0; + if (start + 0u < size) { + reader->buffer = reader->data[start + 0]; + } + reader->buffer >>= (reader->bp & 7u); + } + (void) nbits; +} + +/*See ensureBits documentation above. This one ensures up to 17 bits */ +static LODEPNG_INLINE void ensureBits17(LodePNGBitReader* reader, size_t nbits) +{ + size_t start = reader->bp >> 3u; + size_t size = reader->size; + if (start + 2u < size) { + reader->buffer = (unsigned) reader->data[start + 0] + | ((unsigned) reader->data[start + 1] << 8u) + | ((unsigned) reader->data[start + 2] << 16u); + reader->buffer >>= (reader->bp & 7u); + } else { + reader->buffer = 0; + if (start + 0u < size) { + reader->buffer |= reader->data[start + 0]; + } + if (start + 1u < size) { + reader->buffer |= ((unsigned) reader->data[start + 1] << 8u); + } + reader->buffer >>= (reader->bp & 7u); + } + (void) nbits; +} + +/*See ensureBits documentation above. This one ensures up to 25 bits */ +static LODEPNG_INLINE void ensureBits25(LodePNGBitReader* reader, size_t nbits) +{ + size_t start = reader->bp >> 3u; + size_t size = reader->size; + if (start + 3u < size) { + reader->buffer = (unsigned) reader->data[start + 0] + | ((unsigned) reader->data[start + 1] << 8u) + | ((unsigned) reader->data[start + 2] << 16u) + | ((unsigned) reader->data[start + 3] << 24u); + reader->buffer >>= (reader->bp & 7u); + } else { + reader->buffer = 0; + if (start + 0u < size) { + reader->buffer |= reader->data[start + 0]; + } + if (start + 1u < size) { + reader->buffer |= ((unsigned) reader->data[start + 1] << 8u); + } + if (start + 2u < size) { + reader->buffer |= ((unsigned) reader->data[start + 2] << 16u); + } + reader->buffer >>= (reader->bp & 7u); + } + (void) nbits; +} + +/*See ensureBits documentation above. This one ensures up to 32 bits */ +static LODEPNG_INLINE void ensureBits32(LodePNGBitReader* reader, size_t nbits) +{ + size_t start = reader->bp >> 3u; + size_t size = reader->size; + if (start + 4u < size) { + reader->buffer = (unsigned) reader->data[start + 0] + | ((unsigned) reader->data[start + 1] << 8u) + | ((unsigned) reader->data[start + 2] << 16u) + | ((unsigned) reader->data[start + 3] << 24u); + reader->buffer >>= (reader->bp & 7u); + reader->buffer |= (((unsigned) reader->data[start + 4] << 24u) + << (8u - (reader->bp & 7u))); + } else { + reader->buffer = 0; + if (start + 0u < size) { + reader->buffer |= reader->data[start + 0]; + } + if (start + 1u < size) { + reader->buffer |= ((unsigned) reader->data[start + 1] << 8u); + } + if (start + 2u < size) { + reader->buffer |= ((unsigned) reader->data[start + 2] << 16u); + } + if (start + 3u < size) { + reader->buffer |= ((unsigned) reader->data[start + 3] << 24u); + } + reader->buffer >>= (reader->bp & 7u); + } + (void) nbits; +} + +/* Get bits without advancing the bit pointer. Must have enough bits available + * with ensureBits. Max nbits is 31. */ +static LODEPNG_INLINE unsigned peekBits(LodePNGBitReader* reader, size_t nbits) +{ + /* The shift allows nbits to be only up to 31. */ + return reader->buffer & ((1u << nbits) - 1u); +} + +/* Must have enough bits available with ensureBits */ +static LODEPNG_INLINE void advanceBits(LodePNGBitReader* reader, size_t nbits) +{ + reader->buffer >>= nbits; + reader->bp += nbits; +} + +/* Must have enough bits available with ensureBits */ +static LODEPNG_INLINE unsigned readBits(LodePNGBitReader* reader, size_t nbits) +{ + unsigned result = peekBits(reader, nbits); + advanceBits(reader, nbits); + return result; +} +#endif /*LODEPNG_COMPILE_DECODER*/ + +static unsigned reverseBits(unsigned bits, unsigned num) +{ + /*TODO: implement faster lookup table based version when needed*/ + unsigned i, result = 0; + for (i = 0; i < num; i++) { + result |= ((bits >> (num - i - 1u)) & 1u) << i; + } + return result; +} + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Deflate - Huffman / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#define FIRST_LENGTH_CODE_INDEX 257 +#define LAST_LENGTH_CODE_INDEX 285 +/*256 literals, the end code, some length codes, and 2 unused codes*/ +#define NUM_DEFLATE_CODE_SYMBOLS 288 +/*the distance codes have their own symbols, 30 used, 2 unused*/ +#define NUM_DISTANCE_SYMBOLS 32 +/*the code length codes. 0-15: code lengths, 16: copy previous 3-6 times, 17: + * 3-10 zeros, 18: 11-138 zeros*/ +#define NUM_CODE_LENGTH_CODES 19 + +/*the base lengths represented by codes 257-285*/ +static const unsigned LENGTHBASE[29] + = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, + 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}; + +/*the extra bits used by codes 257-285 (added to base length)*/ +static const unsigned LENGTHEXTRA[29] + = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, + 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; + +/*the base backwards distances (the bits of distance codes appear after length + * codes and use their own huffman tree)*/ +static const unsigned DISTANCEBASE[30] + = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, + 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, + 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; + +/*the extra bits of backwards distances (added to base)*/ +static const unsigned DISTANCEEXTRA[30] + = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, + 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; + +/*the order in which "code length alphabet code lengths" are stored as specified +by deflate, out of this the huffman tree of the dynamic huffman tree lengths is +generated*/ +static const unsigned CLCL_ORDER[NUM_CODE_LENGTH_CODES] + = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; + +/* ////////////////////////////////////////////////////////////////////////// */ + +/* +Huffman tree struct, containing multiple representations of the tree +*/ +typedef struct HuffmanTree +{ + unsigned* codes; /*the huffman codes (bit patterns representing the symbols)*/ + unsigned* lengths; /*the lengths of the huffman codes*/ + unsigned maxbitlen; /*maximum number of bits a single code can get*/ + unsigned numcodes; /*number of symbols in the alphabet = number of codes*/ + /* for reading only */ + unsigned char* table_len; /*length of symbol from lookup table, or max length + if secondary lookup needed*/ + unsigned short* table_value; /*value of symbol from lookup table, or pointer + to secondary table if needed*/ +} HuffmanTree; + +static void HuffmanTree_init(HuffmanTree* tree) +{ + tree->codes = 0; + tree->lengths = 0; + tree->table_len = 0; + tree->table_value = 0; +} + +static void HuffmanTree_cleanup(HuffmanTree* tree) +{ + lodepng_free(tree->codes); + lodepng_free(tree->lengths); + lodepng_free(tree->table_len); + lodepng_free(tree->table_value); +} + +/* amount of bits for first huffman table lookup (aka root bits), see + * HuffmanTree_makeTable and huffmanDecodeSymbol.*/ +/* values 8u and 9u work the fastest */ +#define FIRSTBITS 9u + +/* a symbol value too big to represent any valid symbol, to indicate reading +disallowed huffman bits combination, which is possible in case of only 0 or 1 +present symbols. */ +#define INVALIDSYMBOL 65535u + +/* make table for huffman decoding */ +static unsigned HuffmanTree_makeTable(HuffmanTree* tree) +{ + static const unsigned headsize = 1u << FIRSTBITS; /*size of the first table*/ + static const unsigned mask = (1u << FIRSTBITS) /*headsize*/ - 1u; + size_t i, numpresent, pointer, size; /*total table size*/ + unsigned* maxlens = (unsigned*) lodepng_malloc(headsize * sizeof(unsigned)); + if (!maxlens) { + return 83; /*alloc fail*/ + } + + /* compute maxlens: max total bit length of symbols sharing prefix in the + * first table*/ + lodepng_memset(maxlens, 0, headsize * sizeof(*maxlens)); + for (i = 0; i < tree->numcodes; i++) { + unsigned symbol = tree->codes[i]; + unsigned l = tree->lengths[i]; + unsigned index; + if (l <= FIRSTBITS) { + continue; /*symbols that fit in first table don't increase secondary table + size*/ + } + /*get the FIRSTBITS MSBs, the MSBs of the symbol are encoded first. See + * later comment about the reversing*/ + index = reverseBits(symbol >> (l - FIRSTBITS), FIRSTBITS); + maxlens[index] = LODEPNG_MAX(maxlens[index], l); + } + /* compute total table size: size of first table plus all secondary tables for + * symbols longer than FIRSTBITS */ + size = headsize; + for (i = 0; i < headsize; ++i) { + unsigned l = maxlens[i]; + if (l > FIRSTBITS) { + size += (((size_t) 1) << (l - FIRSTBITS)); + } + } + tree->table_len + = (unsigned char*) lodepng_malloc(size * sizeof(*tree->table_len)); + tree->table_value + = (unsigned short*) lodepng_malloc(size * sizeof(*tree->table_value)); + if (!tree->table_len || !tree->table_value) { + lodepng_free(maxlens); + /* freeing tree->table values is done at a higher scope */ + return 83; /*alloc fail*/ + } + /*initialize with an invalid length to indicate unused entries*/ + for (i = 0; i < size; ++i) { + tree->table_len[i] = 16; + } + + /*fill in the first table for long symbols: max prefix size and pointer to + * secondary tables*/ + pointer = headsize; + for (i = 0; i < headsize; ++i) { + unsigned l = maxlens[i]; + if (l <= FIRSTBITS) { + continue; + } + tree->table_len[i] = l; + tree->table_value[i] = (unsigned short) pointer; + pointer += (((size_t) 1) << (l - FIRSTBITS)); + } + lodepng_free(maxlens); + + /*fill in the first table for short symbols, or secondary table for long + * symbols*/ + numpresent = 0; + for (i = 0; i < tree->numcodes; ++i) { + unsigned l = tree->lengths[i]; + unsigned symbol, reverse; + if (l == 0) { + continue; + } + symbol = tree->codes[i]; /*the huffman bit pattern. i itself is the value.*/ + /*reverse bits, because the huffman bits are given in MSB first order but + * the bit reader reads LSB first*/ + reverse = reverseBits(symbol, l); + numpresent++; + + if (l <= FIRSTBITS) { + /*short symbol, fully in first table, replicated num times if l < + * FIRSTBITS*/ + unsigned num = 1u << (FIRSTBITS - l); + unsigned j; + for (j = 0; j < num; ++j) { + /*bit reader will read the l bits of symbol first, the remaining + * FIRSTBITS - l bits go to the MSB's*/ + unsigned index = reverse | (j << l); + if (tree->table_len[index] != 16) { + return 55; /*invalid tree: long symbol shares prefix with short + symbol*/ + } + tree->table_len[index] = l; + tree->table_value[index] = (unsigned short) i; + } + } else { + /*long symbol, shares prefix with other long symbols in first lookup + * table, needs second lookup*/ + /*the FIRSTBITS MSBs of the symbol are the first table index*/ + unsigned index = reverse & mask; + unsigned maxlen = tree->table_len[index]; + /*log2 of secondary table length, should be >= l - FIRSTBITS*/ + unsigned tablelen = maxlen - FIRSTBITS; + unsigned start + = tree->table_value[index]; /*starting index in secondary table*/ + unsigned num + = 1u << (tablelen - (l - FIRSTBITS)); /*amount of entries of this + symbol in secondary table*/ + unsigned j; + if (maxlen < l) { + return 55; /*invalid tree: long symbol shares prefix with short symbol*/ + } + for (j = 0; j < num; ++j) { + unsigned reverse2 = reverse >> FIRSTBITS; /* l - FIRSTBITS bits */ + unsigned index2 = start + (reverse2 | (j << (l - FIRSTBITS))); + tree->table_len[index2] = l; + tree->table_value[index2] = (unsigned short) i; + } + } + } + + if (numpresent < 2) { + /* In case of exactly 1 symbol, in theory the huffman symbol needs 0 bits, + but deflate uses 1 bit instead. In case of 0 symbols, no symbols can + appear at all, but such huffman tree could still exist (e.g. if distance + codes are never used). In both cases, not all symbols of the table will be + filled in. Fill them in with an invalid symbol value so returning them from + huffmanDecodeSymbol will cause error. */ + for (i = 0; i < size; ++i) { + if (tree->table_len[i] == 16) { + /* As length, use a value smaller than FIRSTBITS for the head table, + and a value larger than FIRSTBITS for the secondary table, to ensure + valid behavior for advanceBits when reading this symbol. */ + tree->table_len[i] = (i < headsize) ? 1 : (FIRSTBITS + 1); + tree->table_value[i] = INVALIDSYMBOL; + } + } + } else { + /* A good huffman tree has N * 2 - 1 nodes, of which N - 1 are internal + nodes. If that is not the case (due to too long length codes), the table + will not have been fully used, and this is an error (not all bit + combinations can be decoded): an oversubscribed huffman tree, indicated by + error 55. */ + for (i = 0; i < size; ++i) { + if (tree->table_len[i] == 16) { + return 55; + } + } + } + + return 0; +} + +/* +Second step for the ...makeFromLengths and ...makeFromFrequencies functions. +numcodes, lengths and maxbitlen must already be filled in correctly. return +value is error. +*/ +static unsigned HuffmanTree_makeFromLengths2(HuffmanTree* tree) +{ + unsigned* blcount; + unsigned* nextcode; + unsigned error = 0; + unsigned bits, n; + + tree->codes = (unsigned*) lodepng_malloc(tree->numcodes * sizeof(unsigned)); + blcount + = (unsigned*) lodepng_malloc((tree->maxbitlen + 1) * sizeof(unsigned)); + nextcode + = (unsigned*) lodepng_malloc((tree->maxbitlen + 1) * sizeof(unsigned)); + if (!tree->codes || !blcount || !nextcode) { + error = 83; /*alloc fail*/ + } + + if (!error) { + for (n = 0; n != tree->maxbitlen + 1; n++) { + blcount[n] = nextcode[n] = 0; + } + /*step 1: count number of instances of each code length*/ + for (bits = 0; bits != tree->numcodes; ++bits) { + ++blcount[tree->lengths[bits]]; + } + /*step 2: generate the nextcode values*/ + for (bits = 1; bits <= tree->maxbitlen; ++bits) { + nextcode[bits] = (nextcode[bits - 1] + blcount[bits - 1]) << 1u; + } + /*step 3: generate all the codes*/ + for (n = 0; n != tree->numcodes; ++n) { + if (tree->lengths[n] != 0) { + tree->codes[n] = nextcode[tree->lengths[n]]++; + /*remove superfluous bits from the code*/ + tree->codes[n] &= ((1u << tree->lengths[n]) - 1u); + } + } + } + + lodepng_free(blcount); + lodepng_free(nextcode); + + if (!error) { + error = HuffmanTree_makeTable(tree); + } + return error; +} + +/* +given the code lengths (as stored in the PNG file), generate the tree as defined +by Deflate. maxbitlen is the maximum bits that a code in the tree can have. +return value is error. +*/ +static unsigned HuffmanTree_makeFromLengths(HuffmanTree* tree, + const unsigned* bitlen, + size_t numcodes, + unsigned maxbitlen) +{ + unsigned i; + tree->lengths = (unsigned*) lodepng_malloc(numcodes * sizeof(unsigned)); + if (!tree->lengths) { + return 83; /*alloc fail*/ + } + for (i = 0; i != numcodes; ++i) { + tree->lengths[i] = bitlen[i]; + } + tree->numcodes = (unsigned) numcodes; /*number of symbols*/ + tree->maxbitlen = maxbitlen; + return HuffmanTree_makeFromLengths2(tree); +} + +#ifdef LODEPNG_COMPILE_ENCODER + +/*BPM: Boundary Package Merge, see "A Fast and Space-Economical Algorithm for +Length-Limited Coding", Jyrki Katajainen, Alistair Moffat, Andrew Turpin, +1995.*/ + +/*chain node for boundary package merge*/ +typedef struct BPMNode +{ + int weight; /*the sum of all weights in this chain*/ + unsigned index; /*index of this leaf node (called "count" in the paper)*/ + struct BPMNode* tail; /*the next nodes in this chain (null if last)*/ + int in_use; +} BPMNode; + +/*lists of chains*/ +typedef struct BPMLists +{ + /*memory pool*/ + unsigned memsize; + BPMNode* memory; + unsigned numfree; + unsigned nextfree; + BPMNode** freelist; + /*two heads of lookahead chains per list*/ + unsigned listsize; + BPMNode** chains0; + BPMNode** chains1; +} BPMLists; + +/*creates a new chain node with the given parameters, from the memory in the + * lists */ +static BPMNode* bpmnode_create(BPMLists* lists, + int weight, + unsigned index, + BPMNode* tail) +{ + unsigned i; + BPMNode* result; + + /*memory full, so garbage collect*/ + if (lists->nextfree >= lists->numfree) { + /*mark only those that are in use*/ + for (i = 0; i != lists->memsize; ++i) { + lists->memory[i].in_use = 0; + } + for (i = 0; i != lists->listsize; ++i) { + BPMNode* node; + for (node = lists->chains0[i]; node != 0; node = node->tail) { + node->in_use = 1; + } + for (node = lists->chains1[i]; node != 0; node = node->tail) { + node->in_use = 1; + } + } + /*collect those that are free*/ + lists->numfree = 0; + for (i = 0; i != lists->memsize; ++i) { + if (!lists->memory[i].in_use) { + lists->freelist[lists->numfree++] = &lists->memory[i]; + } + } + lists->nextfree = 0; + } + + result = lists->freelist[lists->nextfree++]; + result->weight = weight; + result->index = index; + result->tail = tail; + return result; +} + +/*sort the leaves with stable mergesort*/ +static void bpmnode_sort(BPMNode* leaves, size_t num) +{ + BPMNode* mem = (BPMNode*) lodepng_malloc(sizeof(*leaves) * num); + size_t width, counter = 0; + for (width = 1; width < num; width *= 2) { + BPMNode* a = (counter & 1) ? mem : leaves; + BPMNode* b = (counter & 1) ? leaves : mem; + size_t p; + for (p = 0; p < num; p += 2 * width) { + size_t q = (p + width > num) ? num : (p + width); + size_t r = (p + 2 * width > num) ? num : (p + 2 * width); + size_t i = p, j = q, k; + for (k = p; k < r; k++) { + if (i < q && (j >= r || a[i].weight <= a[j].weight)) { + b[k] = a[i++]; + } else { + b[k] = a[j++]; + } + } + } + counter++; + } + if (counter & 1) { + lodepng_memcpy(leaves, mem, sizeof(*leaves) * num); + } + lodepng_free(mem); +} + +/*Boundary Package Merge step, numpresent is the amount of leaves, and c is the + * current chain.*/ +static void boundaryPM(BPMLists* lists, + BPMNode* leaves, + size_t numpresent, + int c, + int num) +{ + unsigned lastindex = lists->chains1[c]->index; + + if (c == 0) { + if (lastindex >= numpresent) { + return; + } + lists->chains0[c] = lists->chains1[c]; + lists->chains1[c] + = bpmnode_create(lists, leaves[lastindex].weight, lastindex + 1, 0); + } else { + /*sum of the weights of the head nodes of the previous lookahead chains.*/ + int sum = lists->chains0[c - 1]->weight + lists->chains1[c - 1]->weight; + lists->chains0[c] = lists->chains1[c]; + if (lastindex < numpresent && sum > leaves[lastindex].weight) { + lists->chains1[c] = bpmnode_create(lists, + leaves[lastindex].weight, + lastindex + 1, + lists->chains1[c]->tail); + return; + } + lists->chains1[c] + = bpmnode_create(lists, sum, lastindex, lists->chains1[c - 1]); + /*in the end we are only interested in the chain of the last list, so no + need to recurse if we're at the last one (this gives measurable speedup)*/ + if (num + 1 < (int) (2 * numpresent - 2)) { + boundaryPM(lists, leaves, numpresent, c - 1, num); + boundaryPM(lists, leaves, numpresent, c - 1, num); + } + } +} + +unsigned lodepng_huffman_code_lengths(unsigned* lengths, + const unsigned* frequencies, + size_t numcodes, + unsigned maxbitlen) +{ + unsigned error = 0; + unsigned i; + size_t numpresent = 0; /*number of symbols with non-zero frequency*/ + BPMNode* leaves; /*the symbols, only those with > 0 frequency*/ + + if (numcodes == 0) { + return 80; /*error: a tree of 0 symbols is not supposed to be made*/ + } + if ((1u << maxbitlen) < (unsigned) numcodes) { + return 80; /*error: represent all symbols*/ + } + + leaves = (BPMNode*) lodepng_malloc(numcodes * sizeof(*leaves)); + if (!leaves) { + return 83; /*alloc fail*/ + } + + for (i = 0; i != numcodes; ++i) { + if (frequencies[i] > 0) { + leaves[numpresent].weight = (int) frequencies[i]; + leaves[numpresent].index = i; + ++numpresent; + } + } + + lodepng_memset(lengths, 0, numcodes * sizeof(*lengths)); + + /*ensure at least two present symbols. There should be at least one symbol + according to RFC 1951 section 3.2.7. Some decoders incorrectly require two. To + make these work as well ensure there are at least two symbols. The + Package-Merge code below also doesn't work correctly if there's only one + symbol, it'd give it the theoretical 0 bits but in practice zlib wants 1 bit*/ + if (numpresent == 0) { + lengths[0] = lengths[1] = 1; /*note that for RFC 1951 section 3.2.7, only + lengths[0] = 1 is needed*/ + } else if (numpresent == 1) { + lengths[leaves[0].index] = 1; + lengths[leaves[0].index == 0 ? 1 : 0] = 1; + } else { + BPMLists lists; + BPMNode* node; + + bpmnode_sort(leaves, numpresent); + + lists.listsize = maxbitlen; + lists.memsize = 2 * maxbitlen * (maxbitlen + 1); + lists.nextfree = 0; + lists.numfree = lists.memsize; + lists.memory + = (BPMNode*) lodepng_malloc(lists.memsize * sizeof(*lists.memory)); + lists.freelist + = (BPMNode**) lodepng_malloc(lists.memsize * sizeof(BPMNode*)); + lists.chains0 + = (BPMNode**) lodepng_malloc(lists.listsize * sizeof(BPMNode*)); + lists.chains1 + = (BPMNode**) lodepng_malloc(lists.listsize * sizeof(BPMNode*)); + if (!lists.memory || !lists.freelist || !lists.chains0 || !lists.chains1) { + error = 83; /*alloc fail*/ + } + + if (!error) { + for (i = 0; i != lists.memsize; ++i) { + lists.freelist[i] = &lists.memory[i]; + } + + bpmnode_create(&lists, leaves[0].weight, 1, 0); + bpmnode_create(&lists, leaves[1].weight, 2, 0); + + for (i = 0; i != lists.listsize; ++i) { + lists.chains0[i] = &lists.memory[0]; + lists.chains1[i] = &lists.memory[1]; + } + + /*each boundaryPM call adds one chain to the last list, and we need 2 * + * numpresent - 2 chains.*/ + for (i = 2; i != 2 * numpresent - 2; ++i) { + boundaryPM(&lists, leaves, numpresent, (int) maxbitlen - 1, (int) i); + } + + for (node = lists.chains1[maxbitlen - 1]; node; node = node->tail) { + for (i = 0; i != node->index; ++i) { + ++lengths[leaves[i].index]; + } + } + } + + lodepng_free(lists.memory); + lodepng_free(lists.freelist); + lodepng_free(lists.chains0); + lodepng_free(lists.chains1); + } + + lodepng_free(leaves); + return error; +} + +/*Create the Huffman tree given the symbol frequencies*/ +static unsigned HuffmanTree_makeFromFrequencies(HuffmanTree* tree, + const unsigned* frequencies, + size_t mincodes, + size_t numcodes, + unsigned maxbitlen) +{ + unsigned error = 0; + while (!frequencies[numcodes - 1] && numcodes > mincodes) { + --numcodes; /*trim zeroes*/ + } + tree->lengths = (unsigned*) lodepng_malloc(numcodes * sizeof(unsigned)); + if (!tree->lengths) { + return 83; /*alloc fail*/ + } + tree->maxbitlen = maxbitlen; + tree->numcodes = (unsigned) numcodes; /*number of symbols*/ + + error = lodepng_huffman_code_lengths( + tree->lengths, frequencies, numcodes, maxbitlen); + if (!error) { + error = HuffmanTree_makeFromLengths2(tree); + } + return error; +} +#endif /*LODEPNG_COMPILE_ENCODER*/ + +/*get the literal and length code tree of a deflated block with fixed tree, as + * per the deflate specification*/ +static unsigned generateFixedLitLenTree(HuffmanTree* tree) +{ + unsigned i, error = 0; + unsigned* bitlen + = (unsigned*) lodepng_malloc(NUM_DEFLATE_CODE_SYMBOLS * sizeof(unsigned)); + if (!bitlen) { + return 83; /*alloc fail*/ + } + + /*288 possible codes: 0-255=literals, 256=endcode, 257-285=lengthcodes, + * 286-287=unused*/ + for (i = 0; i <= 143; ++i) { + bitlen[i] = 8; + } + for (i = 144; i <= 255; ++i) { + bitlen[i] = 9; + } + for (i = 256; i <= 279; ++i) { + bitlen[i] = 7; + } + for (i = 280; i <= 287; ++i) { + bitlen[i] = 8; + } + + error + = HuffmanTree_makeFromLengths(tree, bitlen, NUM_DEFLATE_CODE_SYMBOLS, 15); + + lodepng_free(bitlen); + return error; +} + +/*get the distance code tree of a deflated block with fixed tree, as specified + * in the deflate specification*/ +static unsigned generateFixedDistanceTree(HuffmanTree* tree) +{ + unsigned i, error = 0; + unsigned* bitlen + = (unsigned*) lodepng_malloc(NUM_DISTANCE_SYMBOLS * sizeof(unsigned)); + if (!bitlen) { + return 83; /*alloc fail*/ + } + + /*there are 32 distance codes, but 30-31 are unused*/ + for (i = 0; i != NUM_DISTANCE_SYMBOLS; ++i) { + bitlen[i] = 5; + } + error = HuffmanTree_makeFromLengths(tree, bitlen, NUM_DISTANCE_SYMBOLS, 15); + + lodepng_free(bitlen); + return error; +} + +#ifdef LODEPNG_COMPILE_DECODER + +/* +returns the code. The bit reader must already have been ensured at least 15 bits +*/ +static unsigned huffmanDecodeSymbol(LodePNGBitReader* reader, + const HuffmanTree* codetree) +{ + unsigned short code = peekBits(reader, FIRSTBITS); + unsigned short l = codetree->table_len[code]; + unsigned short value = codetree->table_value[code]; + if (l <= FIRSTBITS) { + advanceBits(reader, l); + return value; + } else { + advanceBits(reader, FIRSTBITS); + value += peekBits(reader, l - FIRSTBITS); + advanceBits(reader, codetree->table_len[value] - FIRSTBITS); + return codetree->table_value[value]; + } +} +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_DECODER + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Inflator (Decompressor) / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/*get the tree of a deflated block with fixed tree, as specified in the deflate +specification Returns error code.*/ +static unsigned getTreeInflateFixed(HuffmanTree* tree_ll, HuffmanTree* tree_d) +{ + unsigned error = generateFixedLitLenTree(tree_ll); + if (error) { + return error; + } + return generateFixedDistanceTree(tree_d); +} + +/*get the tree of a deflated block with dynamic tree, the tree itself is also + * Huffman compressed with a known tree*/ +static unsigned getTreeInflateDynamic(HuffmanTree* tree_ll, + HuffmanTree* tree_d, + LodePNGBitReader* reader) +{ + /*make sure that length values that aren't filled in will be 0, or a wrong + * tree will be generated*/ + unsigned error = 0; + unsigned n, HLIT, HDIST, HCLEN, i; + + /*see comments in deflateDynamic for explanation of the context and these + * variables, it is analogous*/ + unsigned* bitlen_ll = 0; /*lit,len code lengths*/ + unsigned* bitlen_d = 0; /*dist code lengths*/ + /*code length code lengths ("clcl"), the bit lengths of the huffman tree used + * to compress bitlen_ll and bitlen_d*/ + unsigned* bitlen_cl = 0; + HuffmanTree tree_cl; /*the code tree for code length codes (the huffman tree + for compressed huffman trees)*/ + + if (reader->bitsize - reader->bp < 14) { + return 49; /*error: the bit pointer is or will go past the memory*/ + } + ensureBits17(reader, 14); + + /*number of literal/length codes + 257. Unlike the spec, the value 257 is + * added to it here already*/ + HLIT = readBits(reader, 5) + 257; + /*number of distance codes. Unlike the spec, the value 1 is added to it here + * already*/ + HDIST = readBits(reader, 5) + 1; + /*number of code length codes. Unlike the spec, the value 4 is added to it + * here already*/ + HCLEN = readBits(reader, 4) + 4; + + bitlen_cl + = (unsigned*) lodepng_malloc(NUM_CODE_LENGTH_CODES * sizeof(unsigned)); + if (!bitlen_cl) { + return 83 /*alloc fail*/; + } + + HuffmanTree_init(&tree_cl); + + while (!error) { + /*read the code length codes out of 3 * (amount of code length codes) bits*/ + if (lodepng_gtofl(reader->bp, HCLEN * 3, reader->bitsize)) { + ERROR_BREAK(50); /*error: the bit pointer is or will go past the memory*/ + } + for (i = 0; i != HCLEN; ++i) { + ensureBits9(reader, 3); /*out of bounds already checked above */ + bitlen_cl[CLCL_ORDER[i]] = readBits(reader, 3); + } + for (i = HCLEN; i != NUM_CODE_LENGTH_CODES; ++i) { + bitlen_cl[CLCL_ORDER[i]] = 0; + } + + error = HuffmanTree_makeFromLengths( + &tree_cl, bitlen_cl, NUM_CODE_LENGTH_CODES, 7); + if (error) { + break; + } + + /*now we can use this tree to read the lengths for the tree that this + * function will return*/ + bitlen_ll = (unsigned*) lodepng_malloc(NUM_DEFLATE_CODE_SYMBOLS + * sizeof(unsigned)); + bitlen_d + = (unsigned*) lodepng_malloc(NUM_DISTANCE_SYMBOLS * sizeof(unsigned)); + if (!bitlen_ll || !bitlen_d) { + ERROR_BREAK(83 /*alloc fail*/); + } + lodepng_memset(bitlen_ll, 0, NUM_DEFLATE_CODE_SYMBOLS * sizeof(*bitlen_ll)); + lodepng_memset(bitlen_d, 0, NUM_DISTANCE_SYMBOLS * sizeof(*bitlen_d)); + + /*i is the current symbol we're reading in the part that contains the code + * lengths of lit/len and dist codes*/ + i = 0; + while (i < HLIT + HDIST) { + unsigned code; + ensureBits25( + reader, + 22); /* up to 15 bits for huffman code, up to 7 extra bits below*/ + code = huffmanDecodeSymbol(reader, &tree_cl); + if (code <= 15) /*a length code*/ { + if (i < HLIT) { + bitlen_ll[i] = code; + } else { + bitlen_d[i - HLIT] = code; + } + ++i; + } else if (code == 16) /*repeat previous*/ { + unsigned replength + = 3; /*read in the 2 bits that indicate repeat length (3-6)*/ + unsigned value; /*set value to the previous code*/ + + if (i == 0) { + ERROR_BREAK(54); /*can't repeat previous if i is 0*/ + } + + replength += readBits(reader, 2); + + if (i < HLIT + 1) { + value = bitlen_ll[i - 1]; + } else { + value = bitlen_d[i - HLIT - 1]; + } + /*repeat this value in the next lengths*/ + for (n = 0; n < replength; ++n) { + if (i >= HLIT + HDIST) { + ERROR_BREAK(13); /*error: i is larger than the amount of codes*/ + } + if (i < HLIT) { + bitlen_ll[i] = value; + } else { + bitlen_d[i - HLIT] = value; + } + ++i; + } + } else if (code == 17) /*repeat "0" 3-10 times*/ { + unsigned replength = 3; /*read in the bits that indicate repeat length*/ + replength += readBits(reader, 3); + + /*repeat this value in the next lengths*/ + for (n = 0; n < replength; ++n) { + if (i >= HLIT + HDIST) { + ERROR_BREAK(14); /*error: i is larger than the amount of codes*/ + } + + if (i < HLIT) { + bitlen_ll[i] = 0; + } else { + bitlen_d[i - HLIT] = 0; + } + ++i; + } + } else if (code == 18) /*repeat "0" 11-138 times*/ { + unsigned replength + = 11; /*read in the bits that indicate repeat length*/ + replength += readBits(reader, 7); + + /*repeat this value in the next lengths*/ + for (n = 0; n < replength; ++n) { + if (i >= HLIT + HDIST) { + ERROR_BREAK(15); /*error: i is larger than the amount of codes*/ + } + + if (i < HLIT) { + bitlen_ll[i] = 0; + } else { + bitlen_d[i - HLIT] = 0; + } + ++i; + } + } else /*if(code == INVALIDSYMBOL)*/ { + ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ + } + /*check if any of the ensureBits above went out of bounds*/ + if (reader->bp > reader->bitsize) { + /*return error code 10 or 11 depending on the situation that happened in + huffmanDecodeSymbol (10=no endcode, 11=wrong jump outside of tree)*/ + /* TODO: revise error codes 10,11,50: the above comment is no longer + * valid */ + ERROR_BREAK(50); /*error, bit pointer jumps past memory*/ + } + } + if (error) { + break; + } + + if (bitlen_ll[256] == 0) { + ERROR_BREAK(64); /*the length of the end code 256 must be larger than 0*/ + } + + /*now we've finally got HLIT and HDIST, so generate the code trees, and the + * function is done*/ + error = HuffmanTree_makeFromLengths( + tree_ll, bitlen_ll, NUM_DEFLATE_CODE_SYMBOLS, 15); + if (error) { + break; + } + error = HuffmanTree_makeFromLengths( + tree_d, bitlen_d, NUM_DISTANCE_SYMBOLS, 15); + + break; /*end of error-while*/ + } + + lodepng_free(bitlen_cl); + lodepng_free(bitlen_ll); + lodepng_free(bitlen_d); + HuffmanTree_cleanup(&tree_cl); + + return error; +} + +/*inflate a block with dynamic of fixed Huffman tree. btype must be 1 or 2.*/ +static unsigned inflateHuffmanBlock(ucvector* out, + LodePNGBitReader* reader, + unsigned btype, + size_t max_output_size) +{ + unsigned error = 0; + HuffmanTree tree_ll; /*the huffman tree for literal and length codes*/ + HuffmanTree tree_d; /*the huffman tree for distance codes*/ + const size_t reserved_size + = 260; /* must be at least 258 for max length, and a few extra for adding + a few extra literals */ + int done = 0; + + if (!ucvector_reserve(out, out->size + reserved_size)) { + return 83; /*alloc fail*/ + } + + HuffmanTree_init(&tree_ll); + HuffmanTree_init(&tree_d); + + if (btype == 1) { + error = getTreeInflateFixed(&tree_ll, &tree_d); + } else { /*if(btype == 2)*/ + error = getTreeInflateDynamic(&tree_ll, &tree_d, reader); + } + + while ( + !error + && !done) /*decode all symbols until end reached, breaks at end code*/ { + /*code_ll is literal, length or end code*/ + unsigned code_ll; + /* ensure enough bits for 2 huffman code reads (15 bits each): if the first + is a literal, a second literal is read at once. This appears to be slightly + faster, than ensuring 20 bits here for 1 huffman symbol and the potential 5 + extra bits for the length symbol.*/ + ensureBits32(reader, 30); + code_ll = huffmanDecodeSymbol(reader, &tree_ll); + if (code_ll <= 255) { + /*slightly faster code path if multiple literals in a row*/ + out->data[out->size++] = (unsigned char) code_ll; + code_ll = huffmanDecodeSymbol(reader, &tree_ll); + } + if (code_ll <= 255) /*literal symbol*/ { + out->data[out->size++] = (unsigned char) code_ll; + } else if (code_ll >= FIRST_LENGTH_CODE_INDEX + && code_ll <= LAST_LENGTH_CODE_INDEX) /*length code*/ { + unsigned code_d, distance; + unsigned numextrabits_l, + numextrabits_d; /*extra bits for length and distance*/ + size_t start, backward, length; + + /*part 1: get length base*/ + length = LENGTHBASE[code_ll - FIRST_LENGTH_CODE_INDEX]; + + /*part 2: get extra bits and add the value of that to length*/ + numextrabits_l = LENGTHEXTRA[code_ll - FIRST_LENGTH_CODE_INDEX]; + if (numextrabits_l != 0) { + /* bits already ensured above */ + ensureBits25(reader, 5); + length += readBits(reader, numextrabits_l); + } + + /*part 3: get distance code*/ + ensureBits32(reader, 28); /* up to 15 for the huffman symbol, up to 13 for + the extra bits */ + code_d = huffmanDecodeSymbol(reader, &tree_d); + if (code_d > 29) { + if (code_d <= 31) { + ERROR_BREAK( + 18); /*error: invalid distance code (30-31 are never used)*/ + } else /* if(code_d == INVALIDSYMBOL) */ { + ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ + } + } + distance = DISTANCEBASE[code_d]; + + /*part 4: get extra bits from distance*/ + numextrabits_d = DISTANCEEXTRA[code_d]; + if (numextrabits_d != 0) { + /* bits already ensured above */ + distance += readBits(reader, numextrabits_d); + } + + /*part 5: fill in all the out[n] values based on the length and dist*/ + start = out->size; + if (distance > start) { + ERROR_BREAK(52); /*too long backward distance*/ + } + backward = start - distance; + + out->size += length; + if (distance < length) { + size_t forward; + lodepng_memcpy(out->data + start, out->data + backward, distance); + start += distance; + for (forward = distance; forward < length; ++forward) { + out->data[start++] = out->data[backward++]; + } + } else { + lodepng_memcpy(out->data + start, out->data + backward, length); + } + } else if (code_ll == 256) { + done = 1; /*end code, finish the loop*/ + } else /*if(code_ll == INVALIDSYMBOL)*/ { + ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ + } + if (out->allocsize - out->size < reserved_size) { + if (!ucvector_reserve(out, out->size + reserved_size)) { + ERROR_BREAK(83); /*alloc fail*/ + } + } + /*check if any of the ensureBits above went out of bounds*/ + if (reader->bp > reader->bitsize) { + /*return error code 10 or 11 depending on the situation that happened in + huffmanDecodeSymbol (10=no endcode, 11=wrong jump outside of tree)*/ + /* TODO: revise error codes 10,11,50: the above comment is no longer valid + */ + ERROR_BREAK(51); /*error, bit pointer jumps past memory*/ + } + if (max_output_size && out->size > max_output_size) { + ERROR_BREAK(109); /*error, larger than max size*/ + } + } + + HuffmanTree_cleanup(&tree_ll); + HuffmanTree_cleanup(&tree_d); + + return error; +} + +static unsigned inflateNoCompression(ucvector* out, + LodePNGBitReader* reader, + const LodePNGDecompressSettings* settings) +{ + size_t bytepos; + size_t size = reader->size; + unsigned LEN, NLEN, error = 0; + + /*go to first boundary of byte*/ + bytepos = (reader->bp + 7u) >> 3u; + + /*read LEN (2 bytes) and NLEN (2 bytes)*/ + if (bytepos + 4 >= size) { + return 52; /*error, bit pointer will jump past memory*/ + } + LEN = (unsigned) reader->data[bytepos] + + ((unsigned) reader->data[bytepos + 1] << 8u); + bytepos += 2; + NLEN = (unsigned) reader->data[bytepos] + + ((unsigned) reader->data[bytepos + 1] << 8u); + bytepos += 2; + + /*check if 16-bit NLEN is really the one's complement of LEN*/ + if (!settings->ignore_nlen && LEN + NLEN != 65535) { + return 21; /*error: NLEN is not one's complement of LEN*/ + } + + if (!ucvector_resize(out, out->size + LEN)) { + return 83; /*alloc fail*/ + } + + /*read the literal data: LEN bytes are now stored in the out buffer*/ + if (bytepos + LEN > size) { + return 23; /*error: reading outside of in buffer*/ + } + + /*out->data can be NULL (when LEN is zero), and arithmetics on NULL ptr is + * undefined*/ + if (LEN) { + lodepng_memcpy(out->data + out->size - LEN, reader->data + bytepos, LEN); + bytepos += LEN; + } + + reader->bp = bytepos << 3u; + + return error; +} + +static unsigned lodepng_inflatev(ucvector* out, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings) +{ + unsigned BFINAL = 0; + LodePNGBitReader reader; + unsigned error = LodePNGBitReader_init(&reader, in, insize); + + if (error) { + return error; + } + + while (!BFINAL) { + unsigned BTYPE; + if (reader.bitsize - reader.bp < 3) { + return 52; /*error, bit pointer will jump past memory*/ + } + ensureBits9(&reader, 3); + BFINAL = readBits(&reader, 1); + BTYPE = readBits(&reader, 2); + + if (BTYPE == 3) { + return 20; /*error: invalid BTYPE*/ + } else if (BTYPE == 0) { + error = inflateNoCompression(out, &reader, settings); /*no compression*/ + } else { + error = inflateHuffmanBlock( + out, + &reader, + BTYPE, + settings->max_output_size); /*compression, BTYPE 01 or 10*/ + } + if (!error && settings->max_output_size + && out->size > settings->max_output_size) { + error = 109; + } + if (error) { + break; + } + } + + return error; +} + +unsigned lodepng_inflate(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings) +{ + ucvector v = ucvector_init(*out, *outsize); + unsigned error = lodepng_inflatev(&v, in, insize, settings); + *out = v.data; + *outsize = v.size; + return error; +} + +static unsigned inflatev(ucvector* out, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings) +{ + if (settings->custom_inflate) { + unsigned error = settings->custom_inflate( + &out->data, &out->size, in, insize, settings); + out->allocsize = out->size; + if (error) { + /*the custom inflate is allowed to have its own error codes, however, we + * translate it to code 110*/ + error = 110; + /*if there's a max output size, and the custom zlib returned error, then + * indicate that error instead*/ + if (settings->max_output_size && out->size > settings->max_output_size) { + error = 109; + } + } + return error; + } else { + return lodepng_inflatev(out, in, insize, settings); + } +} + +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Deflator (Compressor) / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +static const unsigned MAX_SUPPORTED_DEFLATE_LENGTH = 258; + +/*search the index in the array, that has the largest value smaller than or +equal to the given value, given array must be sorted (if no value is smaller, it +returns the size of the given array)*/ +static size_t searchCodeIndex(const unsigned* array, + size_t array_size, + size_t value) +{ + /*binary search (only small gain over linear). TODO: use CPU log2 instruction + * for getting symbols instead*/ + size_t left = 1; + size_t right = array_size - 1; + + while (left <= right) { + size_t mid = (left + right) >> 1; + if (array[mid] >= value) { + right = mid - 1; + } else { + left = mid + 1; + } + } + if (left >= array_size || array[left] > value) { + left--; + } + return left; +} + +static void addLengthDistance(uivector* values, size_t length, size_t distance) +{ + /*values in encoded vector are those used by deflate: + 0-255: literal bytes + 256: end + 257-285: length/distance pair (length code, followed by extra length bits, + distance code, extra distance bits) 286-287: invalid*/ + + unsigned length_code = (unsigned) searchCodeIndex(LENGTHBASE, 29, length); + unsigned extra_length = (unsigned) (length - LENGTHBASE[length_code]); + unsigned dist_code = (unsigned) searchCodeIndex(DISTANCEBASE, 30, distance); + unsigned extra_distance = (unsigned) (distance - DISTANCEBASE[dist_code]); + + size_t pos = values->size; + /*TODO: return error when this fails (out of memory)*/ + unsigned ok = uivector_resize(values, values->size + 4); + if (ok) { + values->data[pos + 0] = length_code + FIRST_LENGTH_CODE_INDEX; + values->data[pos + 1] = extra_length; + values->data[pos + 2] = dist_code; + values->data[pos + 3] = extra_distance; + } +} + +/*3 bytes of data get encoded into two bytes. The hash cannot use more than 3 +bytes as input because 3 is the minimum match length for deflate*/ +static const unsigned HASH_NUM_VALUES = 65536; +static const unsigned HASH_BIT_MASK + = 65535; /*HASH_NUM_VALUES - 1, but C90 does not like that as initializer*/ + +typedef struct Hash +{ + int* head; /*hash value to head circular pos - can be outdated if went around + window*/ + /*circular pos to prev circular pos*/ + unsigned short* chain; + int* val; /*circular pos to hash value*/ + + /*TODO: do this not only for zeros but for any repeated byte. However for PNG + it's always going to be the zeros that dominate, so not important for PNG*/ + int* headz; /*similar to head, but for chainz*/ + unsigned short* chainz; /*those with same amount of zeros*/ + unsigned short* zeros; /*length of zeros streak, used as a second hash chain*/ +} Hash; + +static unsigned hash_init(Hash* hash, unsigned windowsize) +{ + unsigned i; + hash->head = (int*) lodepng_malloc(sizeof(int) * HASH_NUM_VALUES); + hash->val = (int*) lodepng_malloc(sizeof(int) * windowsize); + hash->chain + = (unsigned short*) lodepng_malloc(sizeof(unsigned short) * windowsize); + + hash->zeros + = (unsigned short*) lodepng_malloc(sizeof(unsigned short) * windowsize); + hash->headz + = (int*) lodepng_malloc(sizeof(int) * (MAX_SUPPORTED_DEFLATE_LENGTH + 1)); + hash->chainz + = (unsigned short*) lodepng_malloc(sizeof(unsigned short) * windowsize); + + if (!hash->head || !hash->chain || !hash->val || !hash->headz || !hash->chainz + || !hash->zeros) { + return 83; /*alloc fail*/ + } + + /*initialize hash table*/ + for (i = 0; i != HASH_NUM_VALUES; ++i) { + hash->head[i] = -1; + } + for (i = 0; i != windowsize; ++i) { + hash->val[i] = -1; + } + for (i = 0; i != windowsize; ++i) { + hash->chain[i] = i; /*same value as index indicates uninitialized*/ + } + + for (i = 0; i <= MAX_SUPPORTED_DEFLATE_LENGTH; ++i) { + hash->headz[i] = -1; + } + for (i = 0; i != windowsize; ++i) { + hash->chainz[i] = i; /*same value as index indicates uninitialized*/ + } + + return 0; +} + +static void hash_cleanup(Hash* hash) +{ + lodepng_free(hash->head); + lodepng_free(hash->val); + lodepng_free(hash->chain); + + lodepng_free(hash->zeros); + lodepng_free(hash->headz); + lodepng_free(hash->chainz); +} + +static unsigned getHash(const unsigned char* data, size_t size, size_t pos) +{ + unsigned result = 0; + if (pos + 2 < size) { + /*A simple shift and xor hash is used. Since the data of PNGs is dominated + by zeroes due to the filters, a better hash does not have a significant + effect on speed in traversing the chain, and causes more time spend on + calculating the hash.*/ + result ^= ((unsigned) data[pos + 0] << 0u); + result ^= ((unsigned) data[pos + 1] << 4u); + result ^= ((unsigned) data[pos + 2] << 8u); + } else { + size_t amount, i; + if (pos >= size) { + return 0; + } + amount = size - pos; + for (i = 0; i != amount; ++i) { + result ^= ((unsigned) data[pos + i] << (i * 8u)); + } + } + return result & HASH_BIT_MASK; +} + +static unsigned countZeros(const unsigned char* data, size_t size, size_t pos) +{ + const unsigned char* start = data + pos; + const unsigned char* end = start + MAX_SUPPORTED_DEFLATE_LENGTH; + if (end > data + size) { + end = data + size; + } + data = start; + while (data != end && *data == 0) { + ++data; + } + /*subtracting two addresses returned as 32-bit number (max value is + * MAX_SUPPORTED_DEFLATE_LENGTH)*/ + return (unsigned) (data - start); +} + +/*wpos = pos & (windowsize - 1)*/ +static void updateHashChain(Hash* hash, + size_t wpos, + unsigned hashval, + unsigned short numzeros) +{ + hash->val[wpos] = (int) hashval; + if (hash->head[hashval] != -1) { + hash->chain[wpos] = hash->head[hashval]; + } + hash->head[hashval] = (int) wpos; + + hash->zeros[wpos] = numzeros; + if (hash->headz[numzeros] != -1) { + hash->chainz[wpos] = hash->headz[numzeros]; + } + hash->headz[numzeros] = (int) wpos; +} + +/* +LZ77-encode the data. Return value is error code. The input are raw bytes, the +output is in the form of unsigned integers with codes representing for example +literal bytes, or length/distance pairs. It uses a hash table technique to let +it encode faster. When doing LZ77 encoding, a sliding window (of windowsize) is +used, and all past bytes in that window can be used as the "dictionary". A brute +force search through all possible distances would be slow, and this hash +technique is one out of several ways to speed this up. +*/ +static unsigned encodeLZ77(uivector* out, + Hash* hash, + const unsigned char* in, + size_t inpos, + size_t insize, + unsigned windowsize, + unsigned minmatch, + unsigned nicematch, + unsigned lazymatching) +{ + size_t pos; + unsigned i, error = 0; + /*for large window lengths, assume the user wants no compression loss. + * Otherwise, max hash chain length speedup.*/ + unsigned maxchainlength = windowsize >= 8192 ? windowsize : windowsize / 8u; + unsigned maxlazymatch + = windowsize >= 8192 ? MAX_SUPPORTED_DEFLATE_LENGTH : 64; + + unsigned usezeros = 1; /*not sure if setting it to false for windowsize < 8192 + is better or worse*/ + unsigned numzeros = 0; + + unsigned offset; /*the offset represents the distance in LZ77 terminology*/ + unsigned length; + unsigned lazy = 0; + unsigned lazylength = 0, lazyoffset = 0; + unsigned hashval; + unsigned current_offset, current_length; + unsigned prev_offset; + const unsigned char *lastptr, *foreptr, *backptr; + unsigned hashpos; + + if (windowsize == 0 || windowsize > 32768) { + return 60; /*error: windowsize smaller/larger than allowed*/ + } + if ((windowsize & (windowsize - 1)) != 0) { + return 90; /*error: must be power of two*/ + } + + if (nicematch > MAX_SUPPORTED_DEFLATE_LENGTH) { + nicematch = MAX_SUPPORTED_DEFLATE_LENGTH; + } + + for (pos = inpos; pos < insize; ++pos) { + size_t wpos + = pos & (windowsize - 1); /*position for in 'circular' hash buffers*/ + unsigned chainlength = 0; + + hashval = getHash(in, insize, pos); + + if (usezeros && hashval == 0) { + if (numzeros == 0) { + numzeros = countZeros(in, insize, pos); + } else if (pos + numzeros > insize || in[pos + numzeros - 1] != 0) { + --numzeros; + } + } else { + numzeros = 0; + } + + updateHashChain(hash, wpos, hashval, numzeros); + + /*the length and offset found for the current position*/ + length = 0; + offset = 0; + + hashpos = hash->chain[wpos]; + + lastptr = &in[insize < pos + MAX_SUPPORTED_DEFLATE_LENGTH + ? insize + : pos + MAX_SUPPORTED_DEFLATE_LENGTH]; + + /*search for the longest string*/ + prev_offset = 0; + for (;;) { + if (chainlength++ >= maxchainlength) { + break; + } + current_offset + = (unsigned) (hashpos <= wpos ? wpos - hashpos + : wpos - hashpos + windowsize); + + if (current_offset < prev_offset) { + break; /*stop when went completely around the circular buffer*/ + } + prev_offset = current_offset; + if (current_offset > 0) { + /*test the next characters*/ + foreptr = &in[pos]; + backptr = &in[pos - current_offset]; + + /*common case in PNGs is lots of zeros. Quickly skip over them as a + * speedup*/ + if (numzeros >= 3) { + unsigned skip = hash->zeros[hashpos]; + if (skip > numzeros) { + skip = numzeros; + } + backptr += skip; + foreptr += skip; + } + + while (foreptr != lastptr + && *backptr == *foreptr) /*maximum supported length by deflate is + max length*/ + { + ++backptr; + ++foreptr; + } + current_length = (unsigned) (foreptr - &in[pos]); + + if (current_length > length) { + length = current_length; /*the longest length*/ + offset = current_offset; /*the offset that is related to this longest + length*/ + /*jump out once a length of max length is found (speed gain). This + also jumps out if length is MAX_SUPPORTED_DEFLATE_LENGTH*/ + if (current_length >= nicematch) { + break; + } + } + } + + if (hashpos == hash->chain[hashpos]) { + break; + } + + if (numzeros >= 3 && length > numzeros) { + hashpos = hash->chainz[hashpos]; + if (hash->zeros[hashpos] != numzeros) { + break; + } + } else { + hashpos = hash->chain[hashpos]; + /*outdated hash value, happens if particular value was not encountered + * in whole last window*/ + if (hash->val[hashpos] != (int) hashval) { + break; + } + } + } + + if (lazymatching) { + if (!lazy && length >= 3 && length <= maxlazymatch + && length < MAX_SUPPORTED_DEFLATE_LENGTH) { + lazy = 1; + lazylength = length; + lazyoffset = offset; + continue; /*try the next byte*/ + } + if (lazy) { + lazy = 0; + if (pos == 0) { + ERROR_BREAK(81); + } + if (length > lazylength + 1) { + /*push the previous character as literal*/ + if (!uivector_push_back(out, in[pos - 1])) { + ERROR_BREAK(83 /*alloc fail*/); + } + } else { + length = lazylength; + offset = lazyoffset; + hash->head[hashval] = -1; /*the same hashchain update will be done, + this ensures no wrong alteration*/ + hash->headz[numzeros] = -1; /*idem*/ + --pos; + } + } + } + if (length >= 3 && offset > windowsize) { + ERROR_BREAK(86 /*too big (or overflown negative) offset*/); + } + + /*encode it as length/distance pair or literal value*/ + if (length < 3) /*only lengths of 3 or higher are supported as + length/distance pair*/ + { + if (!uivector_push_back(out, in[pos])) { + ERROR_BREAK(83 /*alloc fail*/); + } + } else if (length < minmatch || (length == 3 && offset > 4096)) { + /*compensate for the fact that longer offsets have more extra bits, a + length of only 3 may be not worth it then*/ + if (!uivector_push_back(out, in[pos])) { + ERROR_BREAK(83 /*alloc fail*/); + } + } else { + addLengthDistance(out, length, offset); + for (i = 1; i < length; ++i) { + ++pos; + wpos = pos & (windowsize - 1); + hashval = getHash(in, insize, pos); + if (usezeros && hashval == 0) { + if (numzeros == 0) { + numzeros = countZeros(in, insize, pos); + } else if (pos + numzeros > insize || in[pos + numzeros - 1] != 0) { + --numzeros; + } + } else { + numzeros = 0; + } + updateHashChain(hash, wpos, hashval, numzeros); + } + } + } /*end of the loop through each character of input*/ + + return error; +} + +/* /////////////////////////////////////////////////////////////////////////// + */ + +static unsigned deflateNoCompression(ucvector* out, + const unsigned char* data, + size_t datasize) +{ + /*non compressed deflate block data: 1 bit BFINAL,2 bits BTYPE,(5 bits): it + jumps to start of next byte, 2 bytes LEN, 2 bytes NLEN, LEN bytes literal + DATA*/ + + size_t i, numdeflateblocks = (datasize + 65534u) / 65535u; + size_t datapos = 0; + for (i = 0; i != numdeflateblocks; ++i) { + unsigned BFINAL, BTYPE, LEN, NLEN; + unsigned char firstbyte; + size_t pos = out->size; + + BFINAL = (i == numdeflateblocks - 1); + BTYPE = 0; + + LEN = 65535; + if (datasize - datapos < 65535u) { + LEN = (unsigned) datasize - (unsigned) datapos; + } + NLEN = 65535 - LEN; + + if (!ucvector_resize(out, out->size + LEN + 5)) { + return 83; /*alloc fail*/ + } + + firstbyte = (unsigned char) (BFINAL + ((BTYPE & 1u) << 1u) + + ((BTYPE & 2u) << 1u)); + out->data[pos + 0] = firstbyte; + out->data[pos + 1] = (unsigned char) (LEN & 255); + out->data[pos + 2] = (unsigned char) (LEN >> 8u); + out->data[pos + 3] = (unsigned char) (NLEN & 255); + out->data[pos + 4] = (unsigned char) (NLEN >> 8u); + lodepng_memcpy(out->data + pos + 5, data + datapos, LEN); + datapos += LEN; + } + + return 0; +} + +/* +write the lz77-encoded data, which has lit, len and dist codes, to compressed +stream using huffman trees. tree_ll: the tree for lit and len codes. tree_d: the +tree for distance codes. +*/ +static void writeLZ77data(LodePNGBitWriter* writer, + const uivector* lz77_encoded, + const HuffmanTree* tree_ll, + const HuffmanTree* tree_d) +{ + size_t i = 0; + for (i = 0; i != lz77_encoded->size; ++i) { + unsigned val = lz77_encoded->data[i]; + writeBitsReversed(writer, tree_ll->codes[val], tree_ll->lengths[val]); + if (val > 256) /*for a length code, 3 more things have to be added*/ { + unsigned length_index = val - FIRST_LENGTH_CODE_INDEX; + unsigned n_length_extra_bits = LENGTHEXTRA[length_index]; + unsigned length_extra_bits = lz77_encoded->data[++i]; + + unsigned distance_code = lz77_encoded->data[++i]; + + unsigned distance_index = distance_code; + unsigned n_distance_extra_bits = DISTANCEEXTRA[distance_index]; + unsigned distance_extra_bits = lz77_encoded->data[++i]; + + writeBits(writer, length_extra_bits, n_length_extra_bits); + writeBitsReversed( + writer, tree_d->codes[distance_code], tree_d->lengths[distance_code]); + writeBits(writer, distance_extra_bits, n_distance_extra_bits); + } + } +} + +/*Deflate for a block of type "dynamic", that is, with freely, optimally, + * created huffman trees*/ +static unsigned deflateDynamic(LodePNGBitWriter* writer, + Hash* hash, + const unsigned char* data, + size_t datapos, + size_t dataend, + const LodePNGCompressSettings* settings, + unsigned final) +{ + unsigned error = 0; + + /* + A block is compressed as follows: The PNG data is lz77 encoded, resulting in + literal bytes and length/distance pairs. This is then huffman compressed with + two huffman trees. One huffman tree is used for the lit and len values ("ll"), + another huffman tree is used for the dist values ("d"). These two trees are + stored using their code lengths, and to compress even more these code lengths + are also run-length encoded and huffman compressed. This gives a huffman tree + of code lengths "cl". The code lengths used to describe this third tree are + the code length code lengths ("clcl"). + */ + + /*The lz77 encoded data, represented with integers since there will also be + * length and distance codes in it*/ + uivector lz77_encoded; + HuffmanTree tree_ll; /*tree for lit,len values*/ + HuffmanTree tree_d; /*tree for distance codes*/ + HuffmanTree tree_cl; /*tree for encoding the code lengths representing tree_ll + and tree_d*/ + unsigned* frequencies_ll = 0; /*frequency of lit,len codes*/ + unsigned* frequencies_d = 0; /*frequency of dist codes*/ + unsigned* frequencies_cl = 0; /*frequency of code length codes*/ + unsigned* bitlen_lld = 0; /*lit,len,dist code lengths (int bits), literally + (without repeat codes).*/ + unsigned* bitlen_lld_e = 0; /*bitlen_lld encoded with repeat codes (this is a + rudimentary run length compression)*/ + size_t datasize = dataend - datapos; + + /* + If we could call "bitlen_cl" the the code length code lengths ("clcl"), that + is the bit lengths of codes to represent tree_cl in CLCL_ORDER, then due to + the huffman compression of huffman tree representations ("two levels"), there + are some analogies: bitlen_lld is to tree_cl what data is to tree_ll and + tree_d. bitlen_lld_e is to bitlen_lld what lz77_encoded is to data. bitlen_cl + is to bitlen_lld_e what bitlen_lld is to lz77_encoded. + */ + + unsigned BFINAL = final; + size_t i; + size_t numcodes_ll, numcodes_d, numcodes_lld, numcodes_lld_e, numcodes_cl; + unsigned HLIT, HDIST, HCLEN; + + uivector_init(&lz77_encoded); + HuffmanTree_init(&tree_ll); + HuffmanTree_init(&tree_d); + HuffmanTree_init(&tree_cl); + /* could fit on stack, but >1KB is on the larger side so allocate instead */ + frequencies_ll = (unsigned*) lodepng_malloc(286 * sizeof(*frequencies_ll)); + frequencies_d = (unsigned*) lodepng_malloc(30 * sizeof(*frequencies_d)); + frequencies_cl = (unsigned*) lodepng_malloc(NUM_CODE_LENGTH_CODES + * sizeof(*frequencies_cl)); + + if (!frequencies_ll || !frequencies_d || !frequencies_cl) { + error = 83; /*alloc fail*/ + } + + /*This while loop never loops due to a break at the end, it is here to + allow breaking out of it to the cleanup phase on error conditions.*/ + while (!error) { + lodepng_memset(frequencies_ll, 0, 286 * sizeof(*frequencies_ll)); + lodepng_memset(frequencies_d, 0, 30 * sizeof(*frequencies_d)); + lodepng_memset( + frequencies_cl, 0, NUM_CODE_LENGTH_CODES * sizeof(*frequencies_cl)); + + if (settings->use_lz77) { + error = encodeLZ77(&lz77_encoded, + hash, + data, + datapos, + dataend, + settings->windowsize, + settings->minmatch, + settings->nicematch, + settings->lazymatching); + if (error) { + break; + } + } else { + if (!uivector_resize(&lz77_encoded, datasize)) { + ERROR_BREAK(83 /*alloc fail*/); + } + for (i = datapos; i < dataend; ++i) { + lz77_encoded.data[i - datapos] + = data[i]; /*no LZ77, but still will be Huffman compressed*/ + } + } + + /*Count the frequencies of lit, len and dist codes*/ + for (i = 0; i != lz77_encoded.size; ++i) { + unsigned symbol = lz77_encoded.data[i]; + ++frequencies_ll[symbol]; + if (symbol > 256) { + unsigned dist = lz77_encoded.data[i + 2]; + ++frequencies_d[dist]; + i += 3; + } + } + frequencies_ll[256] + = 1; /*there will be exactly 1 end code, at the end of the block*/ + + /*Make both huffman trees, one for the lit and len codes, one for the dist + * codes*/ + error = HuffmanTree_makeFromFrequencies( + &tree_ll, frequencies_ll, 257, 286, 15); + if (error) { + break; + } + /*2, not 1, is chosen for mincodes: some buggy PNG decoders require at least + * 2 symbols in the dist tree*/ + error = HuffmanTree_makeFromFrequencies(&tree_d, frequencies_d, 2, 30, 15); + if (error) { + break; + } + + numcodes_ll = LODEPNG_MIN(tree_ll.numcodes, 286); + numcodes_d = LODEPNG_MIN(tree_d.numcodes, 30); + /*store the code lengths of both generated trees in bitlen_lld*/ + numcodes_lld = numcodes_ll + numcodes_d; + bitlen_lld = (unsigned*) lodepng_malloc(numcodes_lld * sizeof(*bitlen_lld)); + /*numcodes_lld_e never needs more size than bitlen_lld*/ + bitlen_lld_e + = (unsigned*) lodepng_malloc(numcodes_lld * sizeof(*bitlen_lld_e)); + if (!bitlen_lld || !bitlen_lld_e) { + ERROR_BREAK(83); /*alloc fail*/ + } + numcodes_lld_e = 0; + + for (i = 0; i != numcodes_ll; ++i) { + bitlen_lld[i] = tree_ll.lengths[i]; + } + for (i = 0; i != numcodes_d; ++i) { + bitlen_lld[numcodes_ll + i] = tree_d.lengths[i]; + } + + /*run-length compress bitlen_ldd into bitlen_lld_e by using repeat codes 16 + (copy length 3-6 times), 17 (3-10 zeroes), 18 (11-138 zeroes)*/ + for (i = 0; i != numcodes_lld; ++i) { + unsigned j = 0; /*amount of repetitions*/ + while (i + j + 1 < numcodes_lld + && bitlen_lld[i + j + 1] == bitlen_lld[i]) { + ++j; + } + + if (bitlen_lld[i] == 0 && j >= 2) /*repeat code for zeroes*/ { + ++j; /*include the first zero*/ + if (j <= 10) /*repeat code 17 supports max 10 zeroes*/ { + bitlen_lld_e[numcodes_lld_e++] = 17; + bitlen_lld_e[numcodes_lld_e++] = j - 3; + } else /*repeat code 18 supports max 138 zeroes*/ { + if (j > 138) { + j = 138; + } + bitlen_lld_e[numcodes_lld_e++] = 18; + bitlen_lld_e[numcodes_lld_e++] = j - 11; + } + i += (j - 1); + } else if (j >= 3) /*repeat code for value other than zero*/ { + size_t k; + unsigned num = j / 6u, rest = j % 6u; + bitlen_lld_e[numcodes_lld_e++] = bitlen_lld[i]; + for (k = 0; k < num; ++k) { + bitlen_lld_e[numcodes_lld_e++] = 16; + bitlen_lld_e[numcodes_lld_e++] = 6 - 3; + } + if (rest >= 3) { + bitlen_lld_e[numcodes_lld_e++] = 16; + bitlen_lld_e[numcodes_lld_e++] = rest - 3; + } else { + j -= rest; + } + i += j; + } else /*too short to benefit from repeat code*/ { + bitlen_lld_e[numcodes_lld_e++] = bitlen_lld[i]; + } + } + + /*generate tree_cl, the huffmantree of huffmantrees*/ + for (i = 0; i != numcodes_lld_e; ++i) { + ++frequencies_cl[bitlen_lld_e[i]]; + /*after a repeat code come the bits that specify the number of + repetitions, those don't need to be in the frequencies_cl calculation*/ + if (bitlen_lld_e[i] >= 16) { + ++i; + } + } + + error = HuffmanTree_makeFromFrequencies(&tree_cl, + frequencies_cl, + NUM_CODE_LENGTH_CODES, + NUM_CODE_LENGTH_CODES, + 7); + if (error) { + break; + } + + /*compute amount of code-length-code-lengths to output*/ + numcodes_cl = NUM_CODE_LENGTH_CODES; + /*trim zeros at the end (using CLCL_ORDER), but minimum size must be 4 (see + * HCLEN below)*/ + while (numcodes_cl > 4u + && tree_cl.lengths[CLCL_ORDER[numcodes_cl - 1u]] == 0) { + numcodes_cl--; + } + + /* + Write everything into the output + + After the BFINAL and BTYPE, the dynamic block consists out of the following: + - 5 bits HLIT, 5 bits HDIST, 4 bits HCLEN + - (HCLEN+4)*3 bits code lengths of code length alphabet + - HLIT + 257 code lengths of lit/length alphabet (encoded using the code + length alphabet, + possible repetition codes 16, 17, 18) + - HDIST + 1 code lengths of distance alphabet (encoded using the code length + alphabet, + possible repetition codes 16, 17, 18) + - compressed data + - 256 (end code) + */ + + /*Write block type*/ + writeBits(writer, BFINAL, 1); + writeBits(writer, 0, 1); /*first bit of BTYPE "dynamic"*/ + writeBits(writer, 1, 1); /*second bit of BTYPE "dynamic"*/ + + /*write the HLIT, HDIST and HCLEN values*/ + /*all three sizes take trimmed ending zeroes into account, done either by + HuffmanTree_makeFromFrequencies or in the loop for numcodes_cl above, which + saves space. */ + HLIT = (unsigned) (numcodes_ll - 257); + HDIST = (unsigned) (numcodes_d - 1); + HCLEN = (unsigned) (numcodes_cl - 4); + writeBits(writer, HLIT, 5); + writeBits(writer, HDIST, 5); + writeBits(writer, HCLEN, 4); + + /*write the code lengths of the code length alphabet ("bitlen_cl")*/ + for (i = 0; i != numcodes_cl; ++i) { + writeBits(writer, tree_cl.lengths[CLCL_ORDER[i]], 3); + } + + /*write the lengths of the lit/len AND the dist alphabet*/ + for (i = 0; i != numcodes_lld_e; ++i) { + writeBitsReversed(writer, + tree_cl.codes[bitlen_lld_e[i]], + tree_cl.lengths[bitlen_lld_e[i]]); + /*extra bits of repeat codes*/ + if (bitlen_lld_e[i] == 16) { + writeBits(writer, bitlen_lld_e[++i], 2); + } else if (bitlen_lld_e[i] == 17) { + writeBits(writer, bitlen_lld_e[++i], 3); + } else if (bitlen_lld_e[i] == 18) { + writeBits(writer, bitlen_lld_e[++i], 7); + } + } + + /*write the compressed data symbols*/ + writeLZ77data(writer, &lz77_encoded, &tree_ll, &tree_d); + /*error: the length of the end code 256 must be larger than 0*/ + if (tree_ll.lengths[256] == 0) { + ERROR_BREAK(64); + } + + /*write the end code*/ + writeBitsReversed(writer, tree_ll.codes[256], tree_ll.lengths[256]); + + break; /*end of error-while*/ + } + + /*cleanup*/ + uivector_cleanup(&lz77_encoded); + HuffmanTree_cleanup(&tree_ll); + HuffmanTree_cleanup(&tree_d); + HuffmanTree_cleanup(&tree_cl); + lodepng_free(frequencies_ll); + lodepng_free(frequencies_d); + lodepng_free(frequencies_cl); + lodepng_free(bitlen_lld); + lodepng_free(bitlen_lld_e); + + return error; +} + +static unsigned deflateFixed(LodePNGBitWriter* writer, + Hash* hash, + const unsigned char* data, + size_t datapos, + size_t dataend, + const LodePNGCompressSettings* settings, + unsigned final) +{ + HuffmanTree tree_ll; /*tree for literal values and length codes*/ + HuffmanTree tree_d; /*tree for distance codes*/ + + unsigned BFINAL = final; + unsigned error = 0; + size_t i; + + HuffmanTree_init(&tree_ll); + HuffmanTree_init(&tree_d); + + error = generateFixedLitLenTree(&tree_ll); + if (!error) { + error = generateFixedDistanceTree(&tree_d); + } + + if (!error) { + writeBits(writer, BFINAL, 1); + writeBits(writer, 1, 1); /*first bit of BTYPE*/ + writeBits(writer, 0, 1); /*second bit of BTYPE*/ + + if (settings->use_lz77) /*LZ77 encoded*/ { + uivector lz77_encoded; + uivector_init(&lz77_encoded); + error = encodeLZ77(&lz77_encoded, + hash, + data, + datapos, + dataend, + settings->windowsize, + settings->minmatch, + settings->nicematch, + settings->lazymatching); + if (!error) { + writeLZ77data(writer, &lz77_encoded, &tree_ll, &tree_d); + } + uivector_cleanup(&lz77_encoded); + } else /*no LZ77, but still will be Huffman compressed*/ { + for (i = datapos; i < dataend; ++i) { + writeBitsReversed( + writer, tree_ll.codes[data[i]], tree_ll.lengths[data[i]]); + } + } + /*add END code*/ + if (!error) { + writeBitsReversed(writer, tree_ll.codes[256], tree_ll.lengths[256]); + } + } + + /*cleanup*/ + HuffmanTree_cleanup(&tree_ll); + HuffmanTree_cleanup(&tree_d); + + return error; +} + +static unsigned lodepng_deflatev(ucvector* out, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings* settings) +{ + unsigned error = 0; + size_t i, blocksize, numdeflateblocks; + Hash hash; + LodePNGBitWriter writer; + + LodePNGBitWriter_init(&writer, out); + + if (settings->btype > 2) { + return 61; + } else if (settings->btype == 0) { + return deflateNoCompression(out, in, insize); + } else if (settings->btype == 1) { + blocksize = insize; + } else /*if(settings->btype == 2)*/ { + /*on PNGs, deflate blocks of 65-262k seem to give most dense encoding*/ + blocksize = insize / 8u + 8; + if (blocksize < 65536) { + blocksize = 65536; + } + if (blocksize > 262144) { + blocksize = 262144; + } + } + + numdeflateblocks = (insize + blocksize - 1) / blocksize; + if (numdeflateblocks == 0) { + numdeflateblocks = 1; + } + + error = hash_init(&hash, settings->windowsize); + + if (!error) { + for (i = 0; i != numdeflateblocks && !error; ++i) { + unsigned final = (i == numdeflateblocks - 1); + size_t start = i * blocksize; + size_t end = start + blocksize; + if (end > insize) { + end = insize; + } + + if (settings->btype == 1) { + error = deflateFixed(&writer, &hash, in, start, end, settings, final); + } else if (settings->btype == 2) { + error = deflateDynamic(&writer, &hash, in, start, end, settings, final); + } + } + } + + hash_cleanup(&hash); + + return error; +} + +unsigned lodepng_deflate(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings* settings) +{ + ucvector v = ucvector_init(*out, *outsize); + unsigned error = lodepng_deflatev(&v, in, insize, settings); + *out = v.data; + *outsize = v.size; + return error; +} + +static unsigned deflate(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings* settings) +{ + if (settings->custom_deflate) { + unsigned error + = settings->custom_deflate(out, outsize, in, insize, settings); + /*the custom deflate is allowed to have its own error codes, however, we + * translate it to code 111*/ + return error ? 111 : 0; + } else { + return lodepng_deflate(out, outsize, in, insize, settings); + } +} + +#endif /*LODEPNG_COMPILE_DECODER*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Adler32 / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +static unsigned update_adler32(unsigned adler, + const unsigned char* data, + unsigned len) +{ + unsigned s1 = adler & 0xffffu; + unsigned s2 = (adler >> 16u) & 0xffffu; + + while (len != 0u) { + unsigned i; + /*at least 5552 sums can be done before the sums overflow, saving a lot of + * module divisions*/ + unsigned amount = len > 5552u ? 5552u : len; + len -= amount; + for (i = 0; i != amount; ++i) { + s1 += (*data++); + s2 += s1; + } + s1 %= 65521u; + s2 %= 65521u; + } + + return (s2 << 16u) | s1; +} + +/*Return the adler32 of the bytes data[0..len-1]*/ +static unsigned adler32(const unsigned char* data, unsigned len) +{ + return update_adler32(1u, data, len); +} + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Zlib / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_DECODER + +static unsigned lodepng_zlib_decompressv( + ucvector* out, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings) +{ + unsigned error = 0; + unsigned CM, CINFO, FDICT; + + if (insize < 2) { + return 53; /*error, size of zlib data too small*/ + } + /*read information from zlib header*/ + if ((in[0] * 256 + in[1]) % 31 != 0) { + /*error: 256 * in[0] + in[1] must be a multiple of 31, the FCHECK value is + * supposed to be made that way*/ + return 24; + } + + CM = in[0] & 15; + CINFO = (in[0] >> 4) & 15; + /*FCHECK = in[1] & 31;*/ /*FCHECK is already tested above*/ + FDICT = (in[1] >> 5) & 1; + /*FLEVEL = (in[1] >> 6) & 3;*/ /*FLEVEL is not used here*/ + + if (CM != 8 || CINFO > 7) { + /*error: only compression method 8: inflate with sliding window of 32k is + * supported by the PNG spec*/ + return 25; + } + if (FDICT != 0) { + /*error: the specification of PNG says about the zlib stream: + "The additional flags shall not specify a preset dictionary."*/ + return 26; + } + + error = inflatev(out, in + 2, insize - 2, settings); + if (error) { + return error; + } + + if (!settings->ignore_adler32) { + unsigned ADLER32 = lodepng_read32bitInt(&in[insize - 4]); + unsigned checksum = adler32(out->data, (unsigned) (out->size)); + if (checksum != ADLER32) { + return 58; /*error, adler checksum not correct, data must be corrupted*/ + } + } + + return 0; /*no error*/ +} + +unsigned lodepng_zlib_decompress(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings) +{ + ucvector v = ucvector_init(*out, *outsize); + unsigned error = lodepng_zlib_decompressv(&v, in, insize, settings); + *out = v.data; + *outsize = v.size; + return error; +} + +/*expected_size is expected output size, to avoid intermediate allocations. Set + * to 0 if not known. */ +static unsigned zlib_decompress(unsigned char** out, + size_t* outsize, + size_t expected_size, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings) +{ + unsigned error; + if (settings->custom_zlib) { + error = settings->custom_zlib(out, outsize, in, insize, settings); + if (error) { + /*the custom zlib is allowed to have its own error codes, however, we + * translate it to code 110*/ + error = 110; + /*if there's a max output size, and the custom zlib returned error, then + * indicate that error instead*/ + if (settings->max_output_size && *outsize > settings->max_output_size) { + error = 109; + } + } + } else { + ucvector v = ucvector_init(*out, *outsize); + if (expected_size) { + /*reserve the memory to avoid intermediate reallocations*/ + ucvector_resize(&v, *outsize + expected_size); + v.size = *outsize; + } + error = lodepng_zlib_decompressv(&v, in, insize, settings); + *out = v.data; + *outsize = v.size; + } + return error; +} + +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER + +unsigned lodepng_zlib_compress(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings* settings) +{ + size_t i; + unsigned error; + unsigned char* deflatedata = 0; + size_t deflatesize = 0; + + error = deflate(&deflatedata, &deflatesize, in, insize, settings); + + *out = NULL; + *outsize = 0; + if (!error) { + *outsize = deflatesize + 6; + *out = (unsigned char*) lodepng_malloc(*outsize); + if (!*out) { + error = 83; /*alloc fail*/ + } + } + + if (!error) { + unsigned ADLER32 = adler32(in, (unsigned) insize); + /*zlib data: 1 byte CMF (CM+CINFO), 1 byte FLG, deflate data, 4 byte ADLER32 + * checksum of the Decompressed data*/ + unsigned CMF = 120; /*0b01111000: CM 8, CINFO 7. With CINFO 7, any window + size up to 32768 can be used.*/ + unsigned FLEVEL = 0; + unsigned FDICT = 0; + unsigned CMFFLG = 256 * CMF + FDICT * 32 + FLEVEL * 64; + unsigned FCHECK = 31 - CMFFLG % 31; + CMFFLG += FCHECK; + + (*out)[0] = (unsigned char) (CMFFLG >> 8); + (*out)[1] = (unsigned char) (CMFFLG & 255); + for (i = 0; i != deflatesize; ++i) { + (*out)[i + 2] = deflatedata[i]; + } + lodepng_set32bitInt(&(*out)[*outsize - 4], ADLER32); + } + + lodepng_free(deflatedata); + return error; +} + +/* compress using the default or custom zlib function */ +static unsigned zlib_compress(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings* settings) +{ + if (settings->custom_zlib) { + unsigned error = settings->custom_zlib(out, outsize, in, insize, settings); + /*the custom zlib is allowed to have its own error codes, however, we + * translate it to code 111*/ + return error ? 111 : 0; + } else { + return lodepng_zlib_compress(out, outsize, in, insize, settings); + } +} + +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#else /*no LODEPNG_COMPILE_ZLIB*/ + +#ifdef LODEPNG_COMPILE_DECODER +static unsigned zlib_decompress(unsigned char** out, + size_t* outsize, + size_t expected_size, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings) +{ + if (!settings->custom_zlib) { + return 87; /*no custom zlib function provided */ + } + (void) expected_size; + return settings->custom_zlib(out, outsize, in, insize, settings); +} +#endif /*LODEPNG_COMPILE_DECODER*/ +#ifdef LODEPNG_COMPILE_ENCODER +static unsigned zlib_compress(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings* settings) +{ + if (!settings->custom_zlib) { + return 87; /*no custom zlib function provided */ + } + return settings->custom_zlib(out, outsize, in, insize, settings); +} +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#endif /*LODEPNG_COMPILE_ZLIB*/ + +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_ENCODER + +/*this is a good tradeoff between speed and compression ratio*/ +#define DEFAULT_WINDOWSIZE 2048 + +void lodepng_compress_settings_init(LodePNGCompressSettings* settings) +{ + /*compress with dynamic huffman tree (not in the mathematical sense, just not + * the predefined one)*/ + settings->btype = 2; + settings->use_lz77 = 1; + settings->windowsize = DEFAULT_WINDOWSIZE; + settings->minmatch = 3; + settings->nicematch = 128; + settings->lazymatching = 1; + + settings->custom_zlib = 0; + settings->custom_deflate = 0; + settings->custom_context = 0; +} + +const LodePNGCompressSettings lodepng_default_compress_settings + = {2, 1, DEFAULT_WINDOWSIZE, 3, 128, 1, 0, 0, 0}; + +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_DECODER + +void lodepng_decompress_settings_init(LodePNGDecompressSettings* settings) +{ + settings->ignore_adler32 = 0; + settings->ignore_nlen = 0; + settings->max_output_size = 0; + + settings->custom_zlib = 0; + settings->custom_inflate = 0; + settings->custom_context = 0; +} + +const LodePNGDecompressSettings lodepng_default_decompress_settings + = {0, 0, 0, 0, 0, 0}; + +#endif /*LODEPNG_COMPILE_DECODER*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* // End of Zlib related code. Begin of PNG related code. // */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_PNG + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / CRC32 / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_CRC + +static const unsigned lodepng_crc32_table0[256] + = {0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, + 0x706af48fu, 0xe963a535u, 0x9e6495a3u, 0x0edb8832u, 0x79dcb8a4u, + 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu, 0xe7b82d07u, + 0x90bf1d91u, 0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, + 0x1adad47du, 0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u, 0x136c9856u, + 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu, 0x14015c4fu, 0x63066cd9u, + 0xfa0f3d63u, 0x8d080df5u, 0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u, + 0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu, + 0x35b5a8fau, 0x42b2986cu, 0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, + 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u, 0x26d930acu, 0x51de003au, + 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, + 0xb8bda50fu, 0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, + 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu, 0xb6662d3du, 0x76dc4190u, + 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu, + 0x9fbfe4a5u, 0xe8b8d433u, 0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, + 0xe10e9818u, 0x7f6a0dbbu, 0x086d3d2du, 0x91646c97u, 0xe6635c01u, + 0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu, 0x6c0695edu, + 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u, 0x65b0d9c6u, 0x12b7e950u, + 0x8bbeb8eau, 0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, + 0xfbd44c65u, 0x4db26158u, 0x3ab551ceu, 0xa3bc0074u, 0xd4bb30e2u, + 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu, 0x4369e96au, + 0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, + 0xaa0a4c5fu, 0xdd0d7cc9u, 0x5005713cu, 0x270241aau, 0xbe0b1010u, + 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u, 0xce61e49fu, + 0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, + 0x2eb40d81u, 0xb7bd5c3bu, 0xc0ba6cadu, 0xedb88320u, 0x9abfb3b6u, + 0x03b6e20cu, 0x74b1d29au, 0xead54739u, 0x9dd277afu, 0x04db2615u, + 0x73dc1683u, 0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u, + 0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u, 0xf00f9344u, + 0x8708a3d2u, 0x1e01f268u, 0x6906c2feu, 0xf762575du, 0x806567cbu, + 0x196c3671u, 0x6e6b06e7u, 0xfed41b76u, 0x89d32be0u, 0x10da7a5au, + 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u, + 0xd6d6a3e8u, 0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, + 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu, 0xd80d2bdau, 0xaf0a1b4cu, + 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu, + 0x4669be79u, 0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, + 0xcc0c7795u, 0xbb0b4703u, 0x220216b9u, 0x5505262fu, 0xc5ba3bbeu, + 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u, 0xb5d0cf31u, + 0x2cd99e8bu, 0x5bdeae1du, 0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, + 0x026d930au, 0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u, + 0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu, 0x0cb61b38u, 0x92d28e9bu, + 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u, 0x86d3d2d4u, 0xf1d4e242u, + 0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, + 0x18b74777u, 0x88085ae6u, 0xff0f6a70u, 0x66063bcau, 0x11010b5cu, + 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u, 0xa00ae278u, + 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, + 0x4969474du, 0x3e6e77dbu, 0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, + 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u, 0x47b2cf7fu, 0x30b5ffe9u, + 0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u, + 0xcdd70693u, 0x54de5729u, 0x23d967bfu, 0xb3667a2eu, 0xc4614ab8u, + 0x5d681b02u, 0x2a6f2b94u, 0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, + 0x2d02ef8du}; + +static const unsigned lodepng_crc32_table1[256] + = {0x00000000u, 0x191b3141u, 0x32366282u, 0x2b2d53c3u, 0x646cc504u, + 0x7d77f445u, 0x565aa786u, 0x4f4196c7u, 0xc8d98a08u, 0xd1c2bb49u, + 0xfaefe88au, 0xe3f4d9cbu, 0xacb54f0cu, 0xb5ae7e4du, 0x9e832d8eu, + 0x87981ccfu, 0x4ac21251u, 0x53d92310u, 0x78f470d3u, 0x61ef4192u, + 0x2eaed755u, 0x37b5e614u, 0x1c98b5d7u, 0x05838496u, 0x821b9859u, + 0x9b00a918u, 0xb02dfadbu, 0xa936cb9au, 0xe6775d5du, 0xff6c6c1cu, + 0xd4413fdfu, 0xcd5a0e9eu, 0x958424a2u, 0x8c9f15e3u, 0xa7b24620u, + 0xbea97761u, 0xf1e8e1a6u, 0xe8f3d0e7u, 0xc3de8324u, 0xdac5b265u, + 0x5d5daeaau, 0x44469febu, 0x6f6bcc28u, 0x7670fd69u, 0x39316baeu, + 0x202a5aefu, 0x0b07092cu, 0x121c386du, 0xdf4636f3u, 0xc65d07b2u, + 0xed705471u, 0xf46b6530u, 0xbb2af3f7u, 0xa231c2b6u, 0x891c9175u, + 0x9007a034u, 0x179fbcfbu, 0x0e848dbau, 0x25a9de79u, 0x3cb2ef38u, + 0x73f379ffu, 0x6ae848beu, 0x41c51b7du, 0x58de2a3cu, 0xf0794f05u, + 0xe9627e44u, 0xc24f2d87u, 0xdb541cc6u, 0x94158a01u, 0x8d0ebb40u, + 0xa623e883u, 0xbf38d9c2u, 0x38a0c50du, 0x21bbf44cu, 0x0a96a78fu, + 0x138d96ceu, 0x5ccc0009u, 0x45d73148u, 0x6efa628bu, 0x77e153cau, + 0xbabb5d54u, 0xa3a06c15u, 0x888d3fd6u, 0x91960e97u, 0xded79850u, + 0xc7cca911u, 0xece1fad2u, 0xf5facb93u, 0x7262d75cu, 0x6b79e61du, + 0x4054b5deu, 0x594f849fu, 0x160e1258u, 0x0f152319u, 0x243870dau, + 0x3d23419bu, 0x65fd6ba7u, 0x7ce65ae6u, 0x57cb0925u, 0x4ed03864u, + 0x0191aea3u, 0x188a9fe2u, 0x33a7cc21u, 0x2abcfd60u, 0xad24e1afu, + 0xb43fd0eeu, 0x9f12832du, 0x8609b26cu, 0xc94824abu, 0xd05315eau, + 0xfb7e4629u, 0xe2657768u, 0x2f3f79f6u, 0x362448b7u, 0x1d091b74u, + 0x04122a35u, 0x4b53bcf2u, 0x52488db3u, 0x7965de70u, 0x607eef31u, + 0xe7e6f3feu, 0xfefdc2bfu, 0xd5d0917cu, 0xcccba03du, 0x838a36fau, + 0x9a9107bbu, 0xb1bc5478u, 0xa8a76539u, 0x3b83984bu, 0x2298a90au, + 0x09b5fac9u, 0x10aecb88u, 0x5fef5d4fu, 0x46f46c0eu, 0x6dd93fcdu, + 0x74c20e8cu, 0xf35a1243u, 0xea412302u, 0xc16c70c1u, 0xd8774180u, + 0x9736d747u, 0x8e2de606u, 0xa500b5c5u, 0xbc1b8484u, 0x71418a1au, + 0x685abb5bu, 0x4377e898u, 0x5a6cd9d9u, 0x152d4f1eu, 0x0c367e5fu, + 0x271b2d9cu, 0x3e001cddu, 0xb9980012u, 0xa0833153u, 0x8bae6290u, + 0x92b553d1u, 0xddf4c516u, 0xc4eff457u, 0xefc2a794u, 0xf6d996d5u, + 0xae07bce9u, 0xb71c8da8u, 0x9c31de6bu, 0x852aef2au, 0xca6b79edu, + 0xd37048acu, 0xf85d1b6fu, 0xe1462a2eu, 0x66de36e1u, 0x7fc507a0u, + 0x54e85463u, 0x4df36522u, 0x02b2f3e5u, 0x1ba9c2a4u, 0x30849167u, + 0x299fa026u, 0xe4c5aeb8u, 0xfdde9ff9u, 0xd6f3cc3au, 0xcfe8fd7bu, + 0x80a96bbcu, 0x99b25afdu, 0xb29f093eu, 0xab84387fu, 0x2c1c24b0u, + 0x350715f1u, 0x1e2a4632u, 0x07317773u, 0x4870e1b4u, 0x516bd0f5u, + 0x7a468336u, 0x635db277u, 0xcbfad74eu, 0xd2e1e60fu, 0xf9ccb5ccu, + 0xe0d7848du, 0xaf96124au, 0xb68d230bu, 0x9da070c8u, 0x84bb4189u, + 0x03235d46u, 0x1a386c07u, 0x31153fc4u, 0x280e0e85u, 0x674f9842u, + 0x7e54a903u, 0x5579fac0u, 0x4c62cb81u, 0x8138c51fu, 0x9823f45eu, + 0xb30ea79du, 0xaa1596dcu, 0xe554001bu, 0xfc4f315au, 0xd7626299u, + 0xce7953d8u, 0x49e14f17u, 0x50fa7e56u, 0x7bd72d95u, 0x62cc1cd4u, + 0x2d8d8a13u, 0x3496bb52u, 0x1fbbe891u, 0x06a0d9d0u, 0x5e7ef3ecu, + 0x4765c2adu, 0x6c48916eu, 0x7553a02fu, 0x3a1236e8u, 0x230907a9u, + 0x0824546au, 0x113f652bu, 0x96a779e4u, 0x8fbc48a5u, 0xa4911b66u, + 0xbd8a2a27u, 0xf2cbbce0u, 0xebd08da1u, 0xc0fdde62u, 0xd9e6ef23u, + 0x14bce1bdu, 0x0da7d0fcu, 0x268a833fu, 0x3f91b27eu, 0x70d024b9u, + 0x69cb15f8u, 0x42e6463bu, 0x5bfd777au, 0xdc656bb5u, 0xc57e5af4u, + 0xee530937u, 0xf7483876u, 0xb809aeb1u, 0xa1129ff0u, 0x8a3fcc33u, + 0x9324fd72u}; + +static const unsigned lodepng_crc32_table2[256] + = {0x00000000u, 0x01c26a37u, 0x0384d46eu, 0x0246be59u, 0x0709a8dcu, + 0x06cbc2ebu, 0x048d7cb2u, 0x054f1685u, 0x0e1351b8u, 0x0fd13b8fu, + 0x0d9785d6u, 0x0c55efe1u, 0x091af964u, 0x08d89353u, 0x0a9e2d0au, + 0x0b5c473du, 0x1c26a370u, 0x1de4c947u, 0x1fa2771eu, 0x1e601d29u, + 0x1b2f0bacu, 0x1aed619bu, 0x18abdfc2u, 0x1969b5f5u, 0x1235f2c8u, + 0x13f798ffu, 0x11b126a6u, 0x10734c91u, 0x153c5a14u, 0x14fe3023u, + 0x16b88e7au, 0x177ae44du, 0x384d46e0u, 0x398f2cd7u, 0x3bc9928eu, + 0x3a0bf8b9u, 0x3f44ee3cu, 0x3e86840bu, 0x3cc03a52u, 0x3d025065u, + 0x365e1758u, 0x379c7d6fu, 0x35dac336u, 0x3418a901u, 0x3157bf84u, + 0x3095d5b3u, 0x32d36beau, 0x331101ddu, 0x246be590u, 0x25a98fa7u, + 0x27ef31feu, 0x262d5bc9u, 0x23624d4cu, 0x22a0277bu, 0x20e69922u, + 0x2124f315u, 0x2a78b428u, 0x2bbade1fu, 0x29fc6046u, 0x283e0a71u, + 0x2d711cf4u, 0x2cb376c3u, 0x2ef5c89au, 0x2f37a2adu, 0x709a8dc0u, + 0x7158e7f7u, 0x731e59aeu, 0x72dc3399u, 0x7793251cu, 0x76514f2bu, + 0x7417f172u, 0x75d59b45u, 0x7e89dc78u, 0x7f4bb64fu, 0x7d0d0816u, + 0x7ccf6221u, 0x798074a4u, 0x78421e93u, 0x7a04a0cau, 0x7bc6cafdu, + 0x6cbc2eb0u, 0x6d7e4487u, 0x6f38fadeu, 0x6efa90e9u, 0x6bb5866cu, + 0x6a77ec5bu, 0x68315202u, 0x69f33835u, 0x62af7f08u, 0x636d153fu, + 0x612bab66u, 0x60e9c151u, 0x65a6d7d4u, 0x6464bde3u, 0x662203bau, + 0x67e0698du, 0x48d7cb20u, 0x4915a117u, 0x4b531f4eu, 0x4a917579u, + 0x4fde63fcu, 0x4e1c09cbu, 0x4c5ab792u, 0x4d98dda5u, 0x46c49a98u, + 0x4706f0afu, 0x45404ef6u, 0x448224c1u, 0x41cd3244u, 0x400f5873u, + 0x4249e62au, 0x438b8c1du, 0x54f16850u, 0x55330267u, 0x5775bc3eu, + 0x56b7d609u, 0x53f8c08cu, 0x523aaabbu, 0x507c14e2u, 0x51be7ed5u, + 0x5ae239e8u, 0x5b2053dfu, 0x5966ed86u, 0x58a487b1u, 0x5deb9134u, + 0x5c29fb03u, 0x5e6f455au, 0x5fad2f6du, 0xe1351b80u, 0xe0f771b7u, + 0xe2b1cfeeu, 0xe373a5d9u, 0xe63cb35cu, 0xe7fed96bu, 0xe5b86732u, + 0xe47a0d05u, 0xef264a38u, 0xeee4200fu, 0xeca29e56u, 0xed60f461u, + 0xe82fe2e4u, 0xe9ed88d3u, 0xebab368au, 0xea695cbdu, 0xfd13b8f0u, + 0xfcd1d2c7u, 0xfe976c9eu, 0xff5506a9u, 0xfa1a102cu, 0xfbd87a1bu, + 0xf99ec442u, 0xf85cae75u, 0xf300e948u, 0xf2c2837fu, 0xf0843d26u, + 0xf1465711u, 0xf4094194u, 0xf5cb2ba3u, 0xf78d95fau, 0xf64fffcdu, + 0xd9785d60u, 0xd8ba3757u, 0xdafc890eu, 0xdb3ee339u, 0xde71f5bcu, + 0xdfb39f8bu, 0xddf521d2u, 0xdc374be5u, 0xd76b0cd8u, 0xd6a966efu, + 0xd4efd8b6u, 0xd52db281u, 0xd062a404u, 0xd1a0ce33u, 0xd3e6706au, + 0xd2241a5du, 0xc55efe10u, 0xc49c9427u, 0xc6da2a7eu, 0xc7184049u, + 0xc25756ccu, 0xc3953cfbu, 0xc1d382a2u, 0xc011e895u, 0xcb4dafa8u, + 0xca8fc59fu, 0xc8c97bc6u, 0xc90b11f1u, 0xcc440774u, 0xcd866d43u, + 0xcfc0d31au, 0xce02b92du, 0x91af9640u, 0x906dfc77u, 0x922b422eu, + 0x93e92819u, 0x96a63e9cu, 0x976454abu, 0x9522eaf2u, 0x94e080c5u, + 0x9fbcc7f8u, 0x9e7eadcfu, 0x9c381396u, 0x9dfa79a1u, 0x98b56f24u, + 0x99770513u, 0x9b31bb4au, 0x9af3d17du, 0x8d893530u, 0x8c4b5f07u, + 0x8e0de15eu, 0x8fcf8b69u, 0x8a809decu, 0x8b42f7dbu, 0x89044982u, + 0x88c623b5u, 0x839a6488u, 0x82580ebfu, 0x801eb0e6u, 0x81dcdad1u, + 0x8493cc54u, 0x8551a663u, 0x8717183au, 0x86d5720du, 0xa9e2d0a0u, + 0xa820ba97u, 0xaa6604ceu, 0xaba46ef9u, 0xaeeb787cu, 0xaf29124bu, + 0xad6fac12u, 0xacadc625u, 0xa7f18118u, 0xa633eb2fu, 0xa4755576u, + 0xa5b73f41u, 0xa0f829c4u, 0xa13a43f3u, 0xa37cfdaau, 0xa2be979du, + 0xb5c473d0u, 0xb40619e7u, 0xb640a7beu, 0xb782cd89u, 0xb2cddb0cu, + 0xb30fb13bu, 0xb1490f62u, 0xb08b6555u, 0xbbd72268u, 0xba15485fu, + 0xb853f606u, 0xb9919c31u, 0xbcde8ab4u, 0xbd1ce083u, 0xbf5a5edau, + 0xbe9834edu}; + +static const unsigned lodepng_crc32_table3[256] + = {0x00000000u, 0xb8bc6765u, 0xaa09c88bu, 0x12b5afeeu, 0x8f629757u, + 0x37def032u, 0x256b5fdcu, 0x9dd738b9u, 0xc5b428efu, 0x7d084f8au, + 0x6fbde064u, 0xd7018701u, 0x4ad6bfb8u, 0xf26ad8ddu, 0xe0df7733u, + 0x58631056u, 0x5019579fu, 0xe8a530fau, 0xfa109f14u, 0x42acf871u, + 0xdf7bc0c8u, 0x67c7a7adu, 0x75720843u, 0xcdce6f26u, 0x95ad7f70u, + 0x2d111815u, 0x3fa4b7fbu, 0x8718d09eu, 0x1acfe827u, 0xa2738f42u, + 0xb0c620acu, 0x087a47c9u, 0xa032af3eu, 0x188ec85bu, 0x0a3b67b5u, + 0xb28700d0u, 0x2f503869u, 0x97ec5f0cu, 0x8559f0e2u, 0x3de59787u, + 0x658687d1u, 0xdd3ae0b4u, 0xcf8f4f5au, 0x7733283fu, 0xeae41086u, + 0x525877e3u, 0x40edd80du, 0xf851bf68u, 0xf02bf8a1u, 0x48979fc4u, + 0x5a22302au, 0xe29e574fu, 0x7f496ff6u, 0xc7f50893u, 0xd540a77du, + 0x6dfcc018u, 0x359fd04eu, 0x8d23b72bu, 0x9f9618c5u, 0x272a7fa0u, + 0xbafd4719u, 0x0241207cu, 0x10f48f92u, 0xa848e8f7u, 0x9b14583du, + 0x23a83f58u, 0x311d90b6u, 0x89a1f7d3u, 0x1476cf6au, 0xaccaa80fu, + 0xbe7f07e1u, 0x06c36084u, 0x5ea070d2u, 0xe61c17b7u, 0xf4a9b859u, + 0x4c15df3cu, 0xd1c2e785u, 0x697e80e0u, 0x7bcb2f0eu, 0xc377486bu, + 0xcb0d0fa2u, 0x73b168c7u, 0x6104c729u, 0xd9b8a04cu, 0x446f98f5u, + 0xfcd3ff90u, 0xee66507eu, 0x56da371bu, 0x0eb9274du, 0xb6054028u, + 0xa4b0efc6u, 0x1c0c88a3u, 0x81dbb01au, 0x3967d77fu, 0x2bd27891u, + 0x936e1ff4u, 0x3b26f703u, 0x839a9066u, 0x912f3f88u, 0x299358edu, + 0xb4446054u, 0x0cf80731u, 0x1e4da8dfu, 0xa6f1cfbau, 0xfe92dfecu, + 0x462eb889u, 0x549b1767u, 0xec277002u, 0x71f048bbu, 0xc94c2fdeu, + 0xdbf98030u, 0x6345e755u, 0x6b3fa09cu, 0xd383c7f9u, 0xc1366817u, + 0x798a0f72u, 0xe45d37cbu, 0x5ce150aeu, 0x4e54ff40u, 0xf6e89825u, + 0xae8b8873u, 0x1637ef16u, 0x048240f8u, 0xbc3e279du, 0x21e91f24u, + 0x99557841u, 0x8be0d7afu, 0x335cb0cau, 0xed59b63bu, 0x55e5d15eu, + 0x47507eb0u, 0xffec19d5u, 0x623b216cu, 0xda874609u, 0xc832e9e7u, + 0x708e8e82u, 0x28ed9ed4u, 0x9051f9b1u, 0x82e4565fu, 0x3a58313au, + 0xa78f0983u, 0x1f336ee6u, 0x0d86c108u, 0xb53aa66du, 0xbd40e1a4u, + 0x05fc86c1u, 0x1749292fu, 0xaff54e4au, 0x322276f3u, 0x8a9e1196u, + 0x982bbe78u, 0x2097d91du, 0x78f4c94bu, 0xc048ae2eu, 0xd2fd01c0u, + 0x6a4166a5u, 0xf7965e1cu, 0x4f2a3979u, 0x5d9f9697u, 0xe523f1f2u, + 0x4d6b1905u, 0xf5d77e60u, 0xe762d18eu, 0x5fdeb6ebu, 0xc2098e52u, + 0x7ab5e937u, 0x680046d9u, 0xd0bc21bcu, 0x88df31eau, 0x3063568fu, + 0x22d6f961u, 0x9a6a9e04u, 0x07bda6bdu, 0xbf01c1d8u, 0xadb46e36u, + 0x15080953u, 0x1d724e9au, 0xa5ce29ffu, 0xb77b8611u, 0x0fc7e174u, + 0x9210d9cdu, 0x2aacbea8u, 0x38191146u, 0x80a57623u, 0xd8c66675u, + 0x607a0110u, 0x72cfaefeu, 0xca73c99bu, 0x57a4f122u, 0xef189647u, + 0xfdad39a9u, 0x45115eccu, 0x764dee06u, 0xcef18963u, 0xdc44268du, + 0x64f841e8u, 0xf92f7951u, 0x41931e34u, 0x5326b1dau, 0xeb9ad6bfu, + 0xb3f9c6e9u, 0x0b45a18cu, 0x19f00e62u, 0xa14c6907u, 0x3c9b51beu, + 0x842736dbu, 0x96929935u, 0x2e2efe50u, 0x2654b999u, 0x9ee8defcu, + 0x8c5d7112u, 0x34e11677u, 0xa9362eceu, 0x118a49abu, 0x033fe645u, + 0xbb838120u, 0xe3e09176u, 0x5b5cf613u, 0x49e959fdu, 0xf1553e98u, + 0x6c820621u, 0xd43e6144u, 0xc68bceaau, 0x7e37a9cfu, 0xd67f4138u, + 0x6ec3265du, 0x7c7689b3u, 0xc4caeed6u, 0x591dd66fu, 0xe1a1b10au, + 0xf3141ee4u, 0x4ba87981u, 0x13cb69d7u, 0xab770eb2u, 0xb9c2a15cu, + 0x017ec639u, 0x9ca9fe80u, 0x241599e5u, 0x36a0360bu, 0x8e1c516eu, + 0x866616a7u, 0x3eda71c2u, 0x2c6fde2cu, 0x94d3b949u, 0x090481f0u, + 0xb1b8e695u, 0xa30d497bu, 0x1bb12e1eu, 0x43d23e48u, 0xfb6e592du, + 0xe9dbf6c3u, 0x516791a6u, 0xccb0a91fu, 0x740cce7au, 0x66b96194u, + 0xde0506f1u}; + +static const unsigned lodepng_crc32_table4[256] + = {0x00000000u, 0x3d6029b0u, 0x7ac05360u, 0x47a07ad0u, 0xf580a6c0u, + 0xc8e08f70u, 0x8f40f5a0u, 0xb220dc10u, 0x30704bc1u, 0x0d106271u, + 0x4ab018a1u, 0x77d03111u, 0xc5f0ed01u, 0xf890c4b1u, 0xbf30be61u, + 0x825097d1u, 0x60e09782u, 0x5d80be32u, 0x1a20c4e2u, 0x2740ed52u, + 0x95603142u, 0xa80018f2u, 0xefa06222u, 0xd2c04b92u, 0x5090dc43u, + 0x6df0f5f3u, 0x2a508f23u, 0x1730a693u, 0xa5107a83u, 0x98705333u, + 0xdfd029e3u, 0xe2b00053u, 0xc1c12f04u, 0xfca106b4u, 0xbb017c64u, + 0x866155d4u, 0x344189c4u, 0x0921a074u, 0x4e81daa4u, 0x73e1f314u, + 0xf1b164c5u, 0xccd14d75u, 0x8b7137a5u, 0xb6111e15u, 0x0431c205u, + 0x3951ebb5u, 0x7ef19165u, 0x4391b8d5u, 0xa121b886u, 0x9c419136u, + 0xdbe1ebe6u, 0xe681c256u, 0x54a11e46u, 0x69c137f6u, 0x2e614d26u, + 0x13016496u, 0x9151f347u, 0xac31daf7u, 0xeb91a027u, 0xd6f18997u, + 0x64d15587u, 0x59b17c37u, 0x1e1106e7u, 0x23712f57u, 0x58f35849u, + 0x659371f9u, 0x22330b29u, 0x1f532299u, 0xad73fe89u, 0x9013d739u, + 0xd7b3ade9u, 0xead38459u, 0x68831388u, 0x55e33a38u, 0x124340e8u, + 0x2f236958u, 0x9d03b548u, 0xa0639cf8u, 0xe7c3e628u, 0xdaa3cf98u, + 0x3813cfcbu, 0x0573e67bu, 0x42d39cabu, 0x7fb3b51bu, 0xcd93690bu, + 0xf0f340bbu, 0xb7533a6bu, 0x8a3313dbu, 0x0863840au, 0x3503adbau, + 0x72a3d76au, 0x4fc3fedau, 0xfde322cau, 0xc0830b7au, 0x872371aau, + 0xba43581au, 0x9932774du, 0xa4525efdu, 0xe3f2242du, 0xde920d9du, + 0x6cb2d18du, 0x51d2f83du, 0x167282edu, 0x2b12ab5du, 0xa9423c8cu, + 0x9422153cu, 0xd3826fecu, 0xeee2465cu, 0x5cc29a4cu, 0x61a2b3fcu, + 0x2602c92cu, 0x1b62e09cu, 0xf9d2e0cfu, 0xc4b2c97fu, 0x8312b3afu, + 0xbe729a1fu, 0x0c52460fu, 0x31326fbfu, 0x7692156fu, 0x4bf23cdfu, + 0xc9a2ab0eu, 0xf4c282beu, 0xb362f86eu, 0x8e02d1deu, 0x3c220dceu, + 0x0142247eu, 0x46e25eaeu, 0x7b82771eu, 0xb1e6b092u, 0x8c869922u, + 0xcb26e3f2u, 0xf646ca42u, 0x44661652u, 0x79063fe2u, 0x3ea64532u, + 0x03c66c82u, 0x8196fb53u, 0xbcf6d2e3u, 0xfb56a833u, 0xc6368183u, + 0x74165d93u, 0x49767423u, 0x0ed60ef3u, 0x33b62743u, 0xd1062710u, + 0xec660ea0u, 0xabc67470u, 0x96a65dc0u, 0x248681d0u, 0x19e6a860u, + 0x5e46d2b0u, 0x6326fb00u, 0xe1766cd1u, 0xdc164561u, 0x9bb63fb1u, + 0xa6d61601u, 0x14f6ca11u, 0x2996e3a1u, 0x6e369971u, 0x5356b0c1u, + 0x70279f96u, 0x4d47b626u, 0x0ae7ccf6u, 0x3787e546u, 0x85a73956u, + 0xb8c710e6u, 0xff676a36u, 0xc2074386u, 0x4057d457u, 0x7d37fde7u, + 0x3a978737u, 0x07f7ae87u, 0xb5d77297u, 0x88b75b27u, 0xcf1721f7u, + 0xf2770847u, 0x10c70814u, 0x2da721a4u, 0x6a075b74u, 0x576772c4u, + 0xe547aed4u, 0xd8278764u, 0x9f87fdb4u, 0xa2e7d404u, 0x20b743d5u, + 0x1dd76a65u, 0x5a7710b5u, 0x67173905u, 0xd537e515u, 0xe857cca5u, + 0xaff7b675u, 0x92979fc5u, 0xe915e8dbu, 0xd475c16bu, 0x93d5bbbbu, + 0xaeb5920bu, 0x1c954e1bu, 0x21f567abu, 0x66551d7bu, 0x5b3534cbu, + 0xd965a31au, 0xe4058aaau, 0xa3a5f07au, 0x9ec5d9cau, 0x2ce505dau, + 0x11852c6au, 0x562556bau, 0x6b457f0au, 0x89f57f59u, 0xb49556e9u, + 0xf3352c39u, 0xce550589u, 0x7c75d999u, 0x4115f029u, 0x06b58af9u, + 0x3bd5a349u, 0xb9853498u, 0x84e51d28u, 0xc34567f8u, 0xfe254e48u, + 0x4c059258u, 0x7165bbe8u, 0x36c5c138u, 0x0ba5e888u, 0x28d4c7dfu, + 0x15b4ee6fu, 0x521494bfu, 0x6f74bd0fu, 0xdd54611fu, 0xe03448afu, + 0xa794327fu, 0x9af41bcfu, 0x18a48c1eu, 0x25c4a5aeu, 0x6264df7eu, + 0x5f04f6ceu, 0xed242adeu, 0xd044036eu, 0x97e479beu, 0xaa84500eu, + 0x4834505du, 0x755479edu, 0x32f4033du, 0x0f942a8du, 0xbdb4f69du, + 0x80d4df2du, 0xc774a5fdu, 0xfa148c4du, 0x78441b9cu, 0x4524322cu, + 0x028448fcu, 0x3fe4614cu, 0x8dc4bd5cu, 0xb0a494ecu, 0xf704ee3cu, + 0xca64c78cu}; + +static const unsigned lodepng_crc32_table5[256] + = {0x00000000u, 0xcb5cd3a5u, 0x4dc8a10bu, 0x869472aeu, 0x9b914216u, + 0x50cd91b3u, 0xd659e31du, 0x1d0530b8u, 0xec53826du, 0x270f51c8u, + 0xa19b2366u, 0x6ac7f0c3u, 0x77c2c07bu, 0xbc9e13deu, 0x3a0a6170u, + 0xf156b2d5u, 0x03d6029bu, 0xc88ad13eu, 0x4e1ea390u, 0x85427035u, + 0x9847408du, 0x531b9328u, 0xd58fe186u, 0x1ed33223u, 0xef8580f6u, + 0x24d95353u, 0xa24d21fdu, 0x6911f258u, 0x7414c2e0u, 0xbf481145u, + 0x39dc63ebu, 0xf280b04eu, 0x07ac0536u, 0xccf0d693u, 0x4a64a43du, + 0x81387798u, 0x9c3d4720u, 0x57619485u, 0xd1f5e62bu, 0x1aa9358eu, + 0xebff875bu, 0x20a354feu, 0xa6372650u, 0x6d6bf5f5u, 0x706ec54du, + 0xbb3216e8u, 0x3da66446u, 0xf6fab7e3u, 0x047a07adu, 0xcf26d408u, + 0x49b2a6a6u, 0x82ee7503u, 0x9feb45bbu, 0x54b7961eu, 0xd223e4b0u, + 0x197f3715u, 0xe82985c0u, 0x23755665u, 0xa5e124cbu, 0x6ebdf76eu, + 0x73b8c7d6u, 0xb8e41473u, 0x3e7066ddu, 0xf52cb578u, 0x0f580a6cu, + 0xc404d9c9u, 0x4290ab67u, 0x89cc78c2u, 0x94c9487au, 0x5f959bdfu, + 0xd901e971u, 0x125d3ad4u, 0xe30b8801u, 0x28575ba4u, 0xaec3290au, + 0x659ffaafu, 0x789aca17u, 0xb3c619b2u, 0x35526b1cu, 0xfe0eb8b9u, + 0x0c8e08f7u, 0xc7d2db52u, 0x4146a9fcu, 0x8a1a7a59u, 0x971f4ae1u, + 0x5c439944u, 0xdad7ebeau, 0x118b384fu, 0xe0dd8a9au, 0x2b81593fu, + 0xad152b91u, 0x6649f834u, 0x7b4cc88cu, 0xb0101b29u, 0x36846987u, + 0xfdd8ba22u, 0x08f40f5au, 0xc3a8dcffu, 0x453cae51u, 0x8e607df4u, + 0x93654d4cu, 0x58399ee9u, 0xdeadec47u, 0x15f13fe2u, 0xe4a78d37u, + 0x2ffb5e92u, 0xa96f2c3cu, 0x6233ff99u, 0x7f36cf21u, 0xb46a1c84u, + 0x32fe6e2au, 0xf9a2bd8fu, 0x0b220dc1u, 0xc07ede64u, 0x46eaaccau, + 0x8db67f6fu, 0x90b34fd7u, 0x5bef9c72u, 0xdd7beedcu, 0x16273d79u, + 0xe7718facu, 0x2c2d5c09u, 0xaab92ea7u, 0x61e5fd02u, 0x7ce0cdbau, + 0xb7bc1e1fu, 0x31286cb1u, 0xfa74bf14u, 0x1eb014d8u, 0xd5ecc77du, + 0x5378b5d3u, 0x98246676u, 0x852156ceu, 0x4e7d856bu, 0xc8e9f7c5u, + 0x03b52460u, 0xf2e396b5u, 0x39bf4510u, 0xbf2b37beu, 0x7477e41bu, + 0x6972d4a3u, 0xa22e0706u, 0x24ba75a8u, 0xefe6a60du, 0x1d661643u, + 0xd63ac5e6u, 0x50aeb748u, 0x9bf264edu, 0x86f75455u, 0x4dab87f0u, + 0xcb3ff55eu, 0x006326fbu, 0xf135942eu, 0x3a69478bu, 0xbcfd3525u, + 0x77a1e680u, 0x6aa4d638u, 0xa1f8059du, 0x276c7733u, 0xec30a496u, + 0x191c11eeu, 0xd240c24bu, 0x54d4b0e5u, 0x9f886340u, 0x828d53f8u, + 0x49d1805du, 0xcf45f2f3u, 0x04192156u, 0xf54f9383u, 0x3e134026u, + 0xb8873288u, 0x73dbe12du, 0x6eded195u, 0xa5820230u, 0x2316709eu, + 0xe84aa33bu, 0x1aca1375u, 0xd196c0d0u, 0x5702b27eu, 0x9c5e61dbu, + 0x815b5163u, 0x4a0782c6u, 0xcc93f068u, 0x07cf23cdu, 0xf6999118u, + 0x3dc542bdu, 0xbb513013u, 0x700de3b6u, 0x6d08d30eu, 0xa65400abu, + 0x20c07205u, 0xeb9ca1a0u, 0x11e81eb4u, 0xdab4cd11u, 0x5c20bfbfu, + 0x977c6c1au, 0x8a795ca2u, 0x41258f07u, 0xc7b1fda9u, 0x0ced2e0cu, + 0xfdbb9cd9u, 0x36e74f7cu, 0xb0733dd2u, 0x7b2fee77u, 0x662adecfu, + 0xad760d6au, 0x2be27fc4u, 0xe0beac61u, 0x123e1c2fu, 0xd962cf8au, + 0x5ff6bd24u, 0x94aa6e81u, 0x89af5e39u, 0x42f38d9cu, 0xc467ff32u, + 0x0f3b2c97u, 0xfe6d9e42u, 0x35314de7u, 0xb3a53f49u, 0x78f9ececu, + 0x65fcdc54u, 0xaea00ff1u, 0x28347d5fu, 0xe368aefau, 0x16441b82u, + 0xdd18c827u, 0x5b8cba89u, 0x90d0692cu, 0x8dd55994u, 0x46898a31u, + 0xc01df89fu, 0x0b412b3au, 0xfa1799efu, 0x314b4a4au, 0xb7df38e4u, + 0x7c83eb41u, 0x6186dbf9u, 0xaada085cu, 0x2c4e7af2u, 0xe712a957u, + 0x15921919u, 0xdececabcu, 0x585ab812u, 0x93066bb7u, 0x8e035b0fu, + 0x455f88aau, 0xc3cbfa04u, 0x089729a1u, 0xf9c19b74u, 0x329d48d1u, + 0xb4093a7fu, 0x7f55e9dau, 0x6250d962u, 0xa90c0ac7u, 0x2f987869u, + 0xe4c4abccu}; + +static const unsigned lodepng_crc32_table6[256] + = {0x00000000u, 0xa6770bb4u, 0x979f1129u, 0x31e81a9du, 0xf44f2413u, + 0x52382fa7u, 0x63d0353au, 0xc5a73e8eu, 0x33ef4e67u, 0x959845d3u, + 0xa4705f4eu, 0x020754fau, 0xc7a06a74u, 0x61d761c0u, 0x503f7b5du, + 0xf64870e9u, 0x67de9cceu, 0xc1a9977au, 0xf0418de7u, 0x56368653u, + 0x9391b8ddu, 0x35e6b369u, 0x040ea9f4u, 0xa279a240u, 0x5431d2a9u, + 0xf246d91du, 0xc3aec380u, 0x65d9c834u, 0xa07ef6bau, 0x0609fd0eu, + 0x37e1e793u, 0x9196ec27u, 0xcfbd399cu, 0x69ca3228u, 0x582228b5u, + 0xfe552301u, 0x3bf21d8fu, 0x9d85163bu, 0xac6d0ca6u, 0x0a1a0712u, + 0xfc5277fbu, 0x5a257c4fu, 0x6bcd66d2u, 0xcdba6d66u, 0x081d53e8u, + 0xae6a585cu, 0x9f8242c1u, 0x39f54975u, 0xa863a552u, 0x0e14aee6u, + 0x3ffcb47bu, 0x998bbfcfu, 0x5c2c8141u, 0xfa5b8af5u, 0xcbb39068u, + 0x6dc49bdcu, 0x9b8ceb35u, 0x3dfbe081u, 0x0c13fa1cu, 0xaa64f1a8u, + 0x6fc3cf26u, 0xc9b4c492u, 0xf85cde0fu, 0x5e2bd5bbu, 0x440b7579u, + 0xe27c7ecdu, 0xd3946450u, 0x75e36fe4u, 0xb044516au, 0x16335adeu, + 0x27db4043u, 0x81ac4bf7u, 0x77e43b1eu, 0xd19330aau, 0xe07b2a37u, + 0x460c2183u, 0x83ab1f0du, 0x25dc14b9u, 0x14340e24u, 0xb2430590u, + 0x23d5e9b7u, 0x85a2e203u, 0xb44af89eu, 0x123df32au, 0xd79acda4u, + 0x71edc610u, 0x4005dc8du, 0xe672d739u, 0x103aa7d0u, 0xb64dac64u, + 0x87a5b6f9u, 0x21d2bd4du, 0xe47583c3u, 0x42028877u, 0x73ea92eau, + 0xd59d995eu, 0x8bb64ce5u, 0x2dc14751u, 0x1c295dccu, 0xba5e5678u, + 0x7ff968f6u, 0xd98e6342u, 0xe86679dfu, 0x4e11726bu, 0xb8590282u, + 0x1e2e0936u, 0x2fc613abu, 0x89b1181fu, 0x4c162691u, 0xea612d25u, + 0xdb8937b8u, 0x7dfe3c0cu, 0xec68d02bu, 0x4a1fdb9fu, 0x7bf7c102u, + 0xdd80cab6u, 0x1827f438u, 0xbe50ff8cu, 0x8fb8e511u, 0x29cfeea5u, + 0xdf879e4cu, 0x79f095f8u, 0x48188f65u, 0xee6f84d1u, 0x2bc8ba5fu, + 0x8dbfb1ebu, 0xbc57ab76u, 0x1a20a0c2u, 0x8816eaf2u, 0x2e61e146u, + 0x1f89fbdbu, 0xb9fef06fu, 0x7c59cee1u, 0xda2ec555u, 0xebc6dfc8u, + 0x4db1d47cu, 0xbbf9a495u, 0x1d8eaf21u, 0x2c66b5bcu, 0x8a11be08u, + 0x4fb68086u, 0xe9c18b32u, 0xd82991afu, 0x7e5e9a1bu, 0xefc8763cu, + 0x49bf7d88u, 0x78576715u, 0xde206ca1u, 0x1b87522fu, 0xbdf0599bu, + 0x8c184306u, 0x2a6f48b2u, 0xdc27385bu, 0x7a5033efu, 0x4bb82972u, + 0xedcf22c6u, 0x28681c48u, 0x8e1f17fcu, 0xbff70d61u, 0x198006d5u, + 0x47abd36eu, 0xe1dcd8dau, 0xd034c247u, 0x7643c9f3u, 0xb3e4f77du, + 0x1593fcc9u, 0x247be654u, 0x820cede0u, 0x74449d09u, 0xd23396bdu, + 0xe3db8c20u, 0x45ac8794u, 0x800bb91au, 0x267cb2aeu, 0x1794a833u, + 0xb1e3a387u, 0x20754fa0u, 0x86024414u, 0xb7ea5e89u, 0x119d553du, + 0xd43a6bb3u, 0x724d6007u, 0x43a57a9au, 0xe5d2712eu, 0x139a01c7u, + 0xb5ed0a73u, 0x840510eeu, 0x22721b5au, 0xe7d525d4u, 0x41a22e60u, + 0x704a34fdu, 0xd63d3f49u, 0xcc1d9f8bu, 0x6a6a943fu, 0x5b828ea2u, + 0xfdf58516u, 0x3852bb98u, 0x9e25b02cu, 0xafcdaab1u, 0x09baa105u, + 0xfff2d1ecu, 0x5985da58u, 0x686dc0c5u, 0xce1acb71u, 0x0bbdf5ffu, + 0xadcafe4bu, 0x9c22e4d6u, 0x3a55ef62u, 0xabc30345u, 0x0db408f1u, + 0x3c5c126cu, 0x9a2b19d8u, 0x5f8c2756u, 0xf9fb2ce2u, 0xc813367fu, + 0x6e643dcbu, 0x982c4d22u, 0x3e5b4696u, 0x0fb35c0bu, 0xa9c457bfu, + 0x6c636931u, 0xca146285u, 0xfbfc7818u, 0x5d8b73acu, 0x03a0a617u, + 0xa5d7ada3u, 0x943fb73eu, 0x3248bc8au, 0xf7ef8204u, 0x519889b0u, + 0x6070932du, 0xc6079899u, 0x304fe870u, 0x9638e3c4u, 0xa7d0f959u, + 0x01a7f2edu, 0xc400cc63u, 0x6277c7d7u, 0x539fdd4au, 0xf5e8d6feu, + 0x647e3ad9u, 0xc209316du, 0xf3e12bf0u, 0x55962044u, 0x90311ecau, + 0x3646157eu, 0x07ae0fe3u, 0xa1d90457u, 0x579174beu, 0xf1e67f0au, + 0xc00e6597u, 0x66796e23u, 0xa3de50adu, 0x05a95b19u, 0x34414184u, + 0x92364a30u}; + +static const unsigned lodepng_crc32_table7[256] + = {0x00000000u, 0xccaa009eu, 0x4225077du, 0x8e8f07e3u, 0x844a0efau, + 0x48e00e64u, 0xc66f0987u, 0x0ac50919u, 0xd3e51bb5u, 0x1f4f1b2bu, + 0x91c01cc8u, 0x5d6a1c56u, 0x57af154fu, 0x9b0515d1u, 0x158a1232u, + 0xd92012acu, 0x7cbb312bu, 0xb01131b5u, 0x3e9e3656u, 0xf23436c8u, + 0xf8f13fd1u, 0x345b3f4fu, 0xbad438acu, 0x767e3832u, 0xaf5e2a9eu, + 0x63f42a00u, 0xed7b2de3u, 0x21d12d7du, 0x2b142464u, 0xe7be24fau, + 0x69312319u, 0xa59b2387u, 0xf9766256u, 0x35dc62c8u, 0xbb53652bu, + 0x77f965b5u, 0x7d3c6cacu, 0xb1966c32u, 0x3f196bd1u, 0xf3b36b4fu, + 0x2a9379e3u, 0xe639797du, 0x68b67e9eu, 0xa41c7e00u, 0xaed97719u, + 0x62737787u, 0xecfc7064u, 0x205670fau, 0x85cd537du, 0x496753e3u, + 0xc7e85400u, 0x0b42549eu, 0x01875d87u, 0xcd2d5d19u, 0x43a25afau, + 0x8f085a64u, 0x562848c8u, 0x9a824856u, 0x140d4fb5u, 0xd8a74f2bu, + 0xd2624632u, 0x1ec846acu, 0x9047414fu, 0x5ced41d1u, 0x299dc2edu, + 0xe537c273u, 0x6bb8c590u, 0xa712c50eu, 0xadd7cc17u, 0x617dcc89u, + 0xeff2cb6au, 0x2358cbf4u, 0xfa78d958u, 0x36d2d9c6u, 0xb85dde25u, + 0x74f7debbu, 0x7e32d7a2u, 0xb298d73cu, 0x3c17d0dfu, 0xf0bdd041u, + 0x5526f3c6u, 0x998cf358u, 0x1703f4bbu, 0xdba9f425u, 0xd16cfd3cu, + 0x1dc6fda2u, 0x9349fa41u, 0x5fe3fadfu, 0x86c3e873u, 0x4a69e8edu, + 0xc4e6ef0eu, 0x084cef90u, 0x0289e689u, 0xce23e617u, 0x40ace1f4u, + 0x8c06e16au, 0xd0eba0bbu, 0x1c41a025u, 0x92cea7c6u, 0x5e64a758u, + 0x54a1ae41u, 0x980baedfu, 0x1684a93cu, 0xda2ea9a2u, 0x030ebb0eu, + 0xcfa4bb90u, 0x412bbc73u, 0x8d81bcedu, 0x8744b5f4u, 0x4beeb56au, + 0xc561b289u, 0x09cbb217u, 0xac509190u, 0x60fa910eu, 0xee7596edu, + 0x22df9673u, 0x281a9f6au, 0xe4b09ff4u, 0x6a3f9817u, 0xa6959889u, + 0x7fb58a25u, 0xb31f8abbu, 0x3d908d58u, 0xf13a8dc6u, 0xfbff84dfu, + 0x37558441u, 0xb9da83a2u, 0x7570833cu, 0x533b85dau, 0x9f918544u, + 0x111e82a7u, 0xddb48239u, 0xd7718b20u, 0x1bdb8bbeu, 0x95548c5du, + 0x59fe8cc3u, 0x80de9e6fu, 0x4c749ef1u, 0xc2fb9912u, 0x0e51998cu, + 0x04949095u, 0xc83e900bu, 0x46b197e8u, 0x8a1b9776u, 0x2f80b4f1u, + 0xe32ab46fu, 0x6da5b38cu, 0xa10fb312u, 0xabcaba0bu, 0x6760ba95u, + 0xe9efbd76u, 0x2545bde8u, 0xfc65af44u, 0x30cfafdau, 0xbe40a839u, + 0x72eaa8a7u, 0x782fa1beu, 0xb485a120u, 0x3a0aa6c3u, 0xf6a0a65du, + 0xaa4de78cu, 0x66e7e712u, 0xe868e0f1u, 0x24c2e06fu, 0x2e07e976u, + 0xe2ade9e8u, 0x6c22ee0bu, 0xa088ee95u, 0x79a8fc39u, 0xb502fca7u, + 0x3b8dfb44u, 0xf727fbdau, 0xfde2f2c3u, 0x3148f25du, 0xbfc7f5beu, + 0x736df520u, 0xd6f6d6a7u, 0x1a5cd639u, 0x94d3d1dau, 0x5879d144u, + 0x52bcd85du, 0x9e16d8c3u, 0x1099df20u, 0xdc33dfbeu, 0x0513cd12u, + 0xc9b9cd8cu, 0x4736ca6fu, 0x8b9ccaf1u, 0x8159c3e8u, 0x4df3c376u, + 0xc37cc495u, 0x0fd6c40bu, 0x7aa64737u, 0xb60c47a9u, 0x3883404au, + 0xf42940d4u, 0xfeec49cdu, 0x32464953u, 0xbcc94eb0u, 0x70634e2eu, + 0xa9435c82u, 0x65e95c1cu, 0xeb665bffu, 0x27cc5b61u, 0x2d095278u, + 0xe1a352e6u, 0x6f2c5505u, 0xa386559bu, 0x061d761cu, 0xcab77682u, + 0x44387161u, 0x889271ffu, 0x825778e6u, 0x4efd7878u, 0xc0727f9bu, + 0x0cd87f05u, 0xd5f86da9u, 0x19526d37u, 0x97dd6ad4u, 0x5b776a4au, + 0x51b26353u, 0x9d1863cdu, 0x1397642eu, 0xdf3d64b0u, 0x83d02561u, + 0x4f7a25ffu, 0xc1f5221cu, 0x0d5f2282u, 0x079a2b9bu, 0xcb302b05u, + 0x45bf2ce6u, 0x89152c78u, 0x50353ed4u, 0x9c9f3e4au, 0x121039a9u, + 0xdeba3937u, 0xd47f302eu, 0x18d530b0u, 0x965a3753u, 0x5af037cdu, + 0xff6b144au, 0x33c114d4u, 0xbd4e1337u, 0x71e413a9u, 0x7b211ab0u, + 0xb78b1a2eu, 0x39041dcdu, 0xf5ae1d53u, 0x2c8e0fffu, 0xe0240f61u, + 0x6eab0882u, 0xa201081cu, 0xa8c40105u, 0x646e019bu, 0xeae10678u, + 0x264b06e6u}; + +/* Computes the cyclic redundancy check as used by PNG chunks*/ +unsigned lodepng_crc32(const unsigned char* data, size_t length) +{ + /*Using the Slicing by Eight algorithm*/ + unsigned r = 0xffffffffu; + while (length >= 8) { + r = lodepng_crc32_table7[(data[0] ^ (r & 0xffu))] + ^ lodepng_crc32_table6[(data[1] ^ ((r >> 8) & 0xffu))] + ^ lodepng_crc32_table5[(data[2] ^ ((r >> 16) & 0xffu))] + ^ lodepng_crc32_table4[(data[3] ^ ((r >> 24) & 0xffu))] + ^ lodepng_crc32_table3[data[4]] ^ lodepng_crc32_table2[data[5]] + ^ lodepng_crc32_table1[data[6]] ^ lodepng_crc32_table0[data[7]]; + data += 8; + length -= 8; + } + while (length--) { + r = lodepng_crc32_table0[(r ^ *data++) & 0xffu] ^ (r >> 8); + } + return r ^ 0xffffffffu; +} +#else /* LODEPNG_COMPILE_CRC */ +/*in this case, the function is only declared here, and must be defined +externally so that it will be linked in. + +Example implementation that uses a much smaller lookup table for memory +constrained cases: + +unsigned lodepng_crc32(const unsigned char* data, size_t length) { + unsigned r = 0xffffffffu; + static const unsigned table[16] = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, +0x4db26158, 0x5005713c, 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, +0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c + }; + while(length--) { + r = table[(r ^ *data) & 0xf] ^ (r >> 4); + r = table[(r ^ (*data >> 4)) & 0xf] ^ (r >> 4); + data++; + } + return r ^ 0xffffffffu; +} +*/ +unsigned lodepng_crc32(const unsigned char* data, size_t length); +#endif /* LODEPNG_COMPILE_CRC */ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Reading and writing PNG color channel bits / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/* The color channel bits of less-than-8-bit pixels are read with the MSB of +bytes first, so LodePNGBitWriter and LodePNGBitReader can't be used for those. +*/ + +static unsigned char readBitFromReversedStream(size_t* bitpointer, + const unsigned char* bitstream) +{ + unsigned char result = (unsigned char) ((bitstream[(*bitpointer) >> 3] + >> (7 - ((*bitpointer) & 0x7))) + & 1); + ++(*bitpointer); + return result; +} + +/* TODO: make this faster */ +static unsigned readBitsFromReversedStream(size_t* bitpointer, + const unsigned char* bitstream, + size_t nbits) +{ + unsigned result = 0; + size_t i; + for (i = 0; i < nbits; ++i) { + result <<= 1u; + result |= (unsigned) readBitFromReversedStream(bitpointer, bitstream); + } + return result; +} + +static void setBitOfReversedStream(size_t* bitpointer, + unsigned char* bitstream, + unsigned char bit) +{ + /*the current bit in bitstream may be 0 or 1 for this to work*/ + if (bit == 0) { + bitstream[(*bitpointer) >> 3u] + &= (unsigned char) (~(1u << (7u - ((*bitpointer) & 7u)))); + } else { + bitstream[(*bitpointer) >> 3u] |= (1u << (7u - ((*bitpointer) & 7u))); + } + ++(*bitpointer); +} + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / PNG chunks / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +unsigned lodepng_chunk_length(const unsigned char* chunk) +{ + return lodepng_read32bitInt(chunk); +} + +void lodepng_chunk_type(char type[5], const unsigned char* chunk) +{ + unsigned i; + for (i = 0; i != 4; ++i) { + type[i] = (char) chunk[4 + i]; + } + type[4] = 0; /*null termination char*/ +} + +unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, + const char* type) +{ + if (lodepng_strlen(type) != 4) { + return 0; + } + return (chunk[4] == type[0] && chunk[5] == type[1] && chunk[6] == type[2] + && chunk[7] == type[3]); +} + +/* chunk type name must exist only out of alphabetic characters a-z or A-Z */ +static unsigned char lodepng_chunk_type_name_valid(const unsigned char* chunk) +{ + unsigned i; + for (i = 0; i != 4; ++i) { + char c = (char) chunk[4 + i]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) { + return 0; /* not valid */ + } + } + return 1; /* valid */ +} + +unsigned char lodepng_chunk_ancillary(const unsigned char* chunk) +{ + return ((chunk[4] & 32) != 0); +} + +unsigned char lodepng_chunk_private(const unsigned char* chunk) +{ + return ((chunk[5] & 32) != 0); +} + +/* this is an error if it is reserved: the third character must be uppercase in +the PNG standard, lowercasing this character is reserved for possible future +extension by the spec*/ +static unsigned char lodepng_chunk_reserved(const unsigned char* chunk) +{ + return ((chunk[6] & 32) != 0); +} + +unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk) +{ + return ((chunk[7] & 32) != 0); +} + +unsigned char* lodepng_chunk_data(unsigned char* chunk) +{ + return &chunk[8]; +} + +const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk) +{ + return &chunk[8]; +} + +unsigned lodepng_chunk_check_crc(const unsigned char* chunk) +{ + unsigned length = lodepng_chunk_length(chunk); + unsigned crc = lodepng_read32bitInt(&chunk[length + 8]); + /*the CRC is taken of the data and the 4 chunk type letters, not the length*/ + unsigned checksum = lodepng_crc32(&chunk[4], length + 4); + if (crc != checksum) { + return 1; + } else { + return 0; + } +} + +void lodepng_chunk_generate_crc(unsigned char* chunk) +{ + unsigned length = lodepng_chunk_length(chunk); + unsigned crc = lodepng_crc32(&chunk[4], length + 4); + lodepng_set32bitInt(chunk + 8 + length, crc); +} + +unsigned char* lodepng_chunk_next(unsigned char* chunk, unsigned char* end) +{ + size_t available_size = (size_t) (end - chunk); + if (chunk >= end || available_size < 12) { + return end; /*too small to contain a chunk*/ + } + if (chunk[0] == 0x89 && chunk[1] == 0x50 && chunk[2] == 0x4e + && chunk[3] == 0x47 && chunk[4] == 0x0d && chunk[5] == 0x0a + && chunk[6] == 0x1a && chunk[7] == 0x0a) { + /* Is PNG magic header at start of PNG file. Jump to first actual chunk. */ + return chunk + 8; + } else { + size_t total_chunk_length; + if (lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) { + return end; + } + if (total_chunk_length > available_size) { + return end; /*outside of range*/ + } + return chunk + total_chunk_length; + } +} + +const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk, + const unsigned char* end) +{ + size_t available_size = (size_t) (end - chunk); + if (chunk >= end || available_size < 12) { + return end; /*too small to contain a chunk*/ + } + if (chunk[0] == 0x89 && chunk[1] == 0x50 && chunk[2] == 0x4e + && chunk[3] == 0x47 && chunk[4] == 0x0d && chunk[5] == 0x0a + && chunk[6] == 0x1a && chunk[7] == 0x0a) { + /* Is PNG magic header at start of PNG file. Jump to first actual chunk. */ + return chunk + 8; + } else { + size_t total_chunk_length; + if (lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) { + return end; + } + if (total_chunk_length > available_size) { + return end; /*outside of range*/ + } + return chunk + total_chunk_length; + } +} + +unsigned char* lodepng_chunk_find(unsigned char* chunk, + unsigned char* end, + const char type[5]) +{ + for (;;) { + if (chunk >= end || end - chunk < 12) { + return 0; /* past file end: chunk + 12 > end */ + } + if (lodepng_chunk_type_equals(chunk, type)) { + return chunk; + } + chunk = lodepng_chunk_next(chunk, end); + } +} + +const unsigned char* lodepng_chunk_find_const(const unsigned char* chunk, + const unsigned char* end, + const char type[5]) +{ + for (;;) { + if (chunk >= end || end - chunk < 12) { + return 0; /* past file end: chunk + 12 > end */ + } + if (lodepng_chunk_type_equals(chunk, type)) { + return chunk; + } + chunk = lodepng_chunk_next_const(chunk, end); + } +} + +unsigned lodepng_chunk_append(unsigned char** out, + size_t* outsize, + const unsigned char* chunk) +{ + unsigned i; + size_t total_chunk_length, new_length; + unsigned char *chunk_start, *new_buffer; + + if (!lodepng_chunk_type_name_valid(chunk)) { + return 121; /* invalid chunk type name */ + } + if (lodepng_chunk_reserved(chunk)) { + return 122; /* invalid third lowercase character */ + } + + if (lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) { + return 77; + } + if (lodepng_addofl(*outsize, total_chunk_length, &new_length)) { + return 77; + } + + new_buffer = (unsigned char*) lodepng_realloc(*out, new_length); + if (!new_buffer) { + return 83; /*alloc fail*/ + } + (*out) = new_buffer; + (*outsize) = new_length; + chunk_start = &(*out)[new_length - total_chunk_length]; + + for (i = 0; i != total_chunk_length; ++i) { + chunk_start[i] = chunk[i]; + } + + return 0; +} + +/*Sets length and name and allocates the space for data and crc but does not +set data or crc yet. Returns the start of the chunk in chunk. The start of +the data is at chunk + 8. To finalize chunk, add the data, then use +lodepng_chunk_generate_crc */ +static unsigned lodepng_chunk_init(unsigned char** chunk, + ucvector* out, + size_t length, + const char* type) +{ + size_t new_length = out->size; + if (lodepng_addofl(new_length, length, &new_length)) { + return 77; + } + if (lodepng_addofl(new_length, 12, &new_length)) { + return 77; + } + if (!ucvector_resize(out, new_length)) { + return 83; /*alloc fail*/ + } + *chunk = out->data + new_length - length - 12u; + + /*1: length*/ + lodepng_set32bitInt(*chunk, (unsigned) length); + + /*2: chunk name (4 letters)*/ + lodepng_memcpy(*chunk + 4, type, 4); + + return 0; +} + +/* like lodepng_chunk_create but with custom allocsize */ +static unsigned lodepng_chunk_createv(ucvector* out, + size_t length, + const char* type, + const unsigned char* data) +{ + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, length, type)); + + /*3: the data*/ + lodepng_memcpy(chunk + 8, data, length); + + /*4: CRC (of the chunkname characters and the data)*/ + lodepng_chunk_generate_crc(chunk); + + return 0; +} + +unsigned lodepng_chunk_create(unsigned char** out, + size_t* outsize, + size_t length, + const char* type, + const unsigned char* data) +{ + ucvector v = ucvector_init(*out, *outsize); + unsigned error = lodepng_chunk_createv(&v, length, type, data); + *out = v.data; + *outsize = v.size; + return error; +} + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Color types, channels, bits / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/*checks if the colortype is valid and the bitdepth bd is allowed for this +colortype. Return value is a LodePNG error code.*/ +static unsigned checkColorValidity(LodePNGColorType colortype, unsigned bd) +{ + switch (colortype) { + case LCT_GREY: + if (!(bd == 1 || bd == 2 || bd == 4 || bd == 8 || bd == 16)) { + return 37; + } + break; + case LCT_RGB: + if (!(bd == 8 || bd == 16)) { + return 37; + } + break; + case LCT_PALETTE: + if (!(bd == 1 || bd == 2 || bd == 4 || bd == 8)) { + return 37; + } + break; + case LCT_GREY_ALPHA: + if (!(bd == 8 || bd == 16)) { + return 37; + } + break; + case LCT_RGBA: + if (!(bd == 8 || bd == 16)) { + return 37; + } + break; + case LCT_MAX_OCTET_VALUE: + return 31; /* invalid color type */ + default: + return 31; /* invalid color type */ + } + return 0; /*allowed color type / bits combination*/ +} + +static unsigned getNumColorChannels(LodePNGColorType colortype) +{ + switch (colortype) { + case LCT_GREY: + return 1; + case LCT_RGB: + return 3; + case LCT_PALETTE: + return 1; + case LCT_GREY_ALPHA: + return 2; + case LCT_RGBA: + return 4; + case LCT_MAX_OCTET_VALUE: + return 0; /* invalid color type */ + default: + return 0; /*invalid color type*/ + } +} + +static unsigned lodepng_get_bpp_lct(LodePNGColorType colortype, + unsigned bitdepth) +{ + /*bits per pixel is amount of channels * bits per channel*/ + return getNumColorChannels(colortype) * bitdepth; +} + +/* ////////////////////////////////////////////////////////////////////////// */ + +void lodepng_color_mode_init(LodePNGColorMode* info) +{ + info->key_defined = 0; + info->key_r = info->key_g = info->key_b = 0; + info->colortype = LCT_RGBA; + info->bitdepth = 8; + info->palette = 0; + info->palettesize = 0; +} + +/*allocates palette memory if needed, and initializes all colors to black*/ +static void lodepng_color_mode_alloc_palette(LodePNGColorMode* info) +{ + size_t i; + /*if the palette is already allocated, it will have size 1024 so no + * reallocation needed in that case*/ + /*the palette must have room for up to 256 colors with 4 bytes each.*/ + if (!info->palette) { + info->palette = (unsigned char*) lodepng_malloc(1024); + } + if (!info->palette) { + return; /*alloc fail*/ + } + for (i = 0; i != 256; ++i) { + /*Initialize all unused colors with black, the value used for invalid + palette indices. This is an error according to the PNG spec, but common PNG + decoders make it black instead. That makes color conversion slightly faster + due to no error handling needed.*/ + info->palette[i * 4 + 0] = 0; + info->palette[i * 4 + 1] = 0; + info->palette[i * 4 + 2] = 0; + info->palette[i * 4 + 3] = 255; + } +} + +void lodepng_color_mode_cleanup(LodePNGColorMode* info) +{ + lodepng_palette_clear(info); +} + +unsigned lodepng_color_mode_copy(LodePNGColorMode* dest, + const LodePNGColorMode* source) +{ + lodepng_color_mode_cleanup(dest); + lodepng_memcpy(dest, source, sizeof(LodePNGColorMode)); + if (source->palette) { + dest->palette = (unsigned char*) lodepng_malloc(1024); + if (!dest->palette && source->palettesize) { + return 83; /*alloc fail*/ + } + lodepng_memcpy(dest->palette, source->palette, source->palettesize * 4); + } + return 0; +} + +LodePNGColorMode lodepng_color_mode_make(LodePNGColorType colortype, + unsigned bitdepth) +{ + LodePNGColorMode result; + lodepng_color_mode_init(&result); + result.colortype = colortype; + result.bitdepth = bitdepth; + return result; +} + +static int lodepng_color_mode_equal(const LodePNGColorMode* a, + const LodePNGColorMode* b) +{ + size_t i; + if (a->colortype != b->colortype) { + return 0; + } + if (a->bitdepth != b->bitdepth) { + return 0; + } + if (a->key_defined != b->key_defined) { + return 0; + } + if (a->key_defined) { + if (a->key_r != b->key_r) { + return 0; + } + if (a->key_g != b->key_g) { + return 0; + } + if (a->key_b != b->key_b) { + return 0; + } + } + if (a->palettesize != b->palettesize) { + return 0; + } + for (i = 0; i != a->palettesize * 4; ++i) { + if (a->palette[i] != b->palette[i]) { + return 0; + } + } + return 1; +} + +void lodepng_palette_clear(LodePNGColorMode* info) +{ + if (info->palette) { + lodepng_free(info->palette); + } + info->palette = 0; + info->palettesize = 0; +} + +unsigned lodepng_palette_add(LodePNGColorMode* info, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a) +{ + if (!info->palette) /*allocate palette if empty*/ { + lodepng_color_mode_alloc_palette(info); + if (!info->palette) { + return 83; /*alloc fail*/ + } + } + if (info->palettesize >= 256) { + return 108; /*too many palette values*/ + } + info->palette[4 * info->palettesize + 0] = r; + info->palette[4 * info->palettesize + 1] = g; + info->palette[4 * info->palettesize + 2] = b; + info->palette[4 * info->palettesize + 3] = a; + ++info->palettesize; + return 0; +} + +/*calculate bits per pixel out of colortype and bitdepth*/ +unsigned lodepng_get_bpp(const LodePNGColorMode* info) +{ + return lodepng_get_bpp_lct(info->colortype, info->bitdepth); +} + +unsigned lodepng_get_channels(const LodePNGColorMode* info) +{ + return getNumColorChannels(info->colortype); +} + +unsigned lodepng_is_greyscale_type(const LodePNGColorMode* info) +{ + return info->colortype == LCT_GREY || info->colortype == LCT_GREY_ALPHA; +} + +unsigned lodepng_is_alpha_type(const LodePNGColorMode* info) +{ + return (info->colortype & 4) != 0; /*4 or 6*/ +} + +unsigned lodepng_is_palette_type(const LodePNGColorMode* info) +{ + return info->colortype == LCT_PALETTE; +} + +unsigned lodepng_has_palette_alpha(const LodePNGColorMode* info) +{ + size_t i; + for (i = 0; i != info->palettesize; ++i) { + if (info->palette[i * 4 + 3] < 255) { + return 1; + } + } + return 0; +} + +unsigned lodepng_can_have_alpha(const LodePNGColorMode* info) +{ + return info->key_defined || lodepng_is_alpha_type(info) + || lodepng_has_palette_alpha(info); +} + +static size_t lodepng_get_raw_size_lct(unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth) +{ + size_t bpp = lodepng_get_bpp_lct(colortype, bitdepth); + size_t n = (size_t) w * (size_t) h; + return ((n / 8u) * bpp) + ((n & 7u) * bpp + 7u) / 8u; +} + +size_t lodepng_get_raw_size(unsigned w, + unsigned h, + const LodePNGColorMode* color) +{ + return lodepng_get_raw_size_lct(w, h, color->colortype, color->bitdepth); +} + +#ifdef LODEPNG_COMPILE_PNG + +/*in an idat chunk, each scanline is a multiple of 8 bits, unlike the lodepng +output buffer, and in addition has one extra byte per line: the filter byte. So +this gives a larger result than lodepng_get_raw_size. Set h to 1 to get the size +of 1 row including filter byte. */ +static size_t lodepng_get_raw_size_idat(unsigned w, unsigned h, unsigned bpp) +{ + /* + 1 for the filter byte, and possibly plus padding bits per line. */ + /* Ignoring casts, the expression is equal to (w * bpp + 7) / 8 + 1, but + * avoids overflow of w * bpp */ + size_t line = ((size_t) (w / 8u) * bpp) + 1u + ((w & 7u) * bpp + 7u) / 8u; + return (size_t) h * line; +} + +#ifdef LODEPNG_COMPILE_DECODER +/*Safely checks whether size_t overflow can be caused due to amount of pixels. +This check is overcautious rather than precise. If this check indicates no +overflow, you can safely compute in a size_t (but not an unsigned): +-(size_t)w * (size_t)h * 8 +-amount of bytes in IDAT (including filter, padding and Adam7 bytes) +-amount of bytes in raw color model +Returns 1 if overflow possible, 0 if not. +*/ +static int lodepng_pixel_overflow(unsigned w, + unsigned h, + const LodePNGColorMode* pngcolor, + const LodePNGColorMode* rawcolor) +{ + size_t bpp + = LODEPNG_MAX(lodepng_get_bpp(pngcolor), lodepng_get_bpp(rawcolor)); + size_t numpixels, total; + size_t line; /* bytes per line in worst case */ + + if (lodepng_mulofl((size_t) w, (size_t) h, &numpixels)) { + return 1; + } + if (lodepng_mulofl(numpixels, 8, &total)) { + return 1; /* bit pointer with 8-bit color, or 8 bytes per channel color */ + } + + /* Bytes per scanline with the expression "(w / 8u) * bpp) + ((w & 7u) * bpp + + * 7u) / 8u" */ + if (lodepng_mulofl((size_t) (w / 8u), bpp, &line)) { + return 1; + } + if (lodepng_addofl(line, ((w & 7u) * bpp + 7u) / 8u, &line)) { + return 1; + } + + if (lodepng_addofl(line, 5, &line)) { + return 1; /* 5 bytes overhead per line: 1 filterbyte, 4 for Adam7 worst case + */ + } + if (lodepng_mulofl(line, h, &total)) { + return 1; /* Total bytes in worst case */ + } + + return 0; /* no overflow */ +} +#endif /*LODEPNG_COMPILE_DECODER*/ +#endif /*LODEPNG_COMPILE_PNG*/ + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + +static void LodePNGUnknownChunks_init(LodePNGInfo* info) +{ + unsigned i; + for (i = 0; i != 3; ++i) { + info->unknown_chunks_data[i] = 0; + } + for (i = 0; i != 3; ++i) { + info->unknown_chunks_size[i] = 0; + } +} + +static void LodePNGUnknownChunks_cleanup(LodePNGInfo* info) +{ + unsigned i; + for (i = 0; i != 3; ++i) { + lodepng_free(info->unknown_chunks_data[i]); + } +} + +static unsigned LodePNGUnknownChunks_copy(LodePNGInfo* dest, + const LodePNGInfo* src) +{ + unsigned i; + + LodePNGUnknownChunks_cleanup(dest); + + for (i = 0; i != 3; ++i) { + size_t j; + dest->unknown_chunks_size[i] = src->unknown_chunks_size[i]; + dest->unknown_chunks_data[i] + = (unsigned char*) lodepng_malloc(src->unknown_chunks_size[i]); + if (!dest->unknown_chunks_data[i] && dest->unknown_chunks_size[i]) { + return 83; /*alloc fail*/ + } + for (j = 0; j < src->unknown_chunks_size[i]; ++j) { + dest->unknown_chunks_data[i][j] = src->unknown_chunks_data[i][j]; + } + } + + return 0; +} + +/******************************************************************************/ + +static void LodePNGText_init(LodePNGInfo* info) +{ + info->text_num = 0; + info->text_keys = NULL; + info->text_strings = NULL; +} + +static void LodePNGText_cleanup(LodePNGInfo* info) +{ + size_t i; + for (i = 0; i != info->text_num; ++i) { + string_cleanup(&info->text_keys[i]); + string_cleanup(&info->text_strings[i]); + } + lodepng_free(info->text_keys); + lodepng_free(info->text_strings); +} + +static unsigned LodePNGText_copy(LodePNGInfo* dest, const LodePNGInfo* source) +{ + size_t i = 0; + dest->text_keys = NULL; + dest->text_strings = NULL; + dest->text_num = 0; + for (i = 0; i != source->text_num; ++i) { + CERROR_TRY_RETURN( + lodepng_add_text(dest, source->text_keys[i], source->text_strings[i])); + } + return 0; +} + +static unsigned lodepng_add_text_sized(LodePNGInfo* info, + const char* key, + const char* str, + size_t size) +{ + char** new_keys = (char**) (lodepng_realloc( + info->text_keys, sizeof(char*) * (info->text_num + 1))); + char** new_strings = (char**) (lodepng_realloc( + info->text_strings, sizeof(char*) * (info->text_num + 1))); + + if (new_keys) { + info->text_keys = new_keys; + } + if (new_strings) { + info->text_strings = new_strings; + } + + if (!new_keys || !new_strings) { + return 83; /*alloc fail*/ + } + + ++info->text_num; + info->text_keys[info->text_num - 1] = alloc_string(key); + info->text_strings[info->text_num - 1] = alloc_string_sized(str, size); + if (!info->text_keys[info->text_num - 1] + || !info->text_strings[info->text_num - 1]) { + return 83; /*alloc fail*/ + } + + return 0; +} + +unsigned lodepng_add_text(LodePNGInfo* info, const char* key, const char* str) +{ + return lodepng_add_text_sized(info, key, str, lodepng_strlen(str)); +} + +void lodepng_clear_text(LodePNGInfo* info) +{ + LodePNGText_cleanup(info); +} + +/******************************************************************************/ + +static void LodePNGIText_init(LodePNGInfo* info) +{ + info->itext_num = 0; + info->itext_keys = NULL; + info->itext_langtags = NULL; + info->itext_transkeys = NULL; + info->itext_strings = NULL; +} + +static void LodePNGIText_cleanup(LodePNGInfo* info) +{ + size_t i; + for (i = 0; i != info->itext_num; ++i) { + string_cleanup(&info->itext_keys[i]); + string_cleanup(&info->itext_langtags[i]); + string_cleanup(&info->itext_transkeys[i]); + string_cleanup(&info->itext_strings[i]); + } + lodepng_free(info->itext_keys); + lodepng_free(info->itext_langtags); + lodepng_free(info->itext_transkeys); + lodepng_free(info->itext_strings); +} + +static unsigned LodePNGIText_copy(LodePNGInfo* dest, const LodePNGInfo* source) +{ + size_t i = 0; + dest->itext_keys = NULL; + dest->itext_langtags = NULL; + dest->itext_transkeys = NULL; + dest->itext_strings = NULL; + dest->itext_num = 0; + for (i = 0; i != source->itext_num; ++i) { + CERROR_TRY_RETURN(lodepng_add_itext(dest, + source->itext_keys[i], + source->itext_langtags[i], + source->itext_transkeys[i], + source->itext_strings[i])); + } + return 0; +} + +void lodepng_clear_itext(LodePNGInfo* info) +{ + LodePNGIText_cleanup(info); +} + +static unsigned lodepng_add_itext_sized(LodePNGInfo* info, + const char* key, + const char* langtag, + const char* transkey, + const char* str, + size_t size) +{ + char** new_keys = (char**) (lodepng_realloc( + info->itext_keys, sizeof(char*) * (info->itext_num + 1))); + char** new_langtags = (char**) (lodepng_realloc( + info->itext_langtags, sizeof(char*) * (info->itext_num + 1))); + char** new_transkeys = (char**) (lodepng_realloc( + info->itext_transkeys, sizeof(char*) * (info->itext_num + 1))); + char** new_strings = (char**) (lodepng_realloc( + info->itext_strings, sizeof(char*) * (info->itext_num + 1))); + + if (new_keys) { + info->itext_keys = new_keys; + } + if (new_langtags) { + info->itext_langtags = new_langtags; + } + if (new_transkeys) { + info->itext_transkeys = new_transkeys; + } + if (new_strings) { + info->itext_strings = new_strings; + } + + if (!new_keys || !new_langtags || !new_transkeys || !new_strings) { + return 83; /*alloc fail*/ + } + + ++info->itext_num; + + info->itext_keys[info->itext_num - 1] = alloc_string(key); + info->itext_langtags[info->itext_num - 1] = alloc_string(langtag); + info->itext_transkeys[info->itext_num - 1] = alloc_string(transkey); + info->itext_strings[info->itext_num - 1] = alloc_string_sized(str, size); + + return 0; +} + +unsigned lodepng_add_itext(LodePNGInfo* info, + const char* key, + const char* langtag, + const char* transkey, + const char* str) +{ + return lodepng_add_itext_sized( + info, key, langtag, transkey, str, lodepng_strlen(str)); +} + +unsigned lodepng_set_icc(LodePNGInfo* info, + const char* name, + const unsigned char* profile, + unsigned profile_size) +{ + if (info->iccp_defined) { + lodepng_clear_icc(info); + } + + if (profile_size == 0) { + return 100; /*invalid ICC profile size*/ + } + + info->iccp_name = alloc_string(name); + if (!info->iccp_name) { + return 83; /*alloc fail*/ + } + + info->iccp_profile = (unsigned char*) lodepng_malloc(profile_size); + if (!info->iccp_profile) { + lodepng_free(info->iccp_name); + return 83; /*alloc fail*/ + } + + lodepng_memcpy(info->iccp_profile, profile, profile_size); + info->iccp_profile_size = profile_size; + info->iccp_defined = 1; + + return 0; /*ok*/ +} + +void lodepng_clear_icc(LodePNGInfo* info) +{ + string_cleanup(&info->iccp_name); + lodepng_free(info->iccp_profile); + info->iccp_profile = NULL; + info->iccp_profile_size = 0; + info->iccp_defined = 0; +} + +unsigned lodepng_set_exif(LodePNGInfo* info, + const unsigned char* exif, + unsigned exif_size) +{ + if (info->exif_defined) { + lodepng_clear_exif(info); + } + info->exif = (unsigned char*) lodepng_malloc(exif_size); + + if (!info->exif) { + return 83; /*alloc fail*/ + } + + lodepng_memcpy(info->exif, exif, exif_size); + info->exif_size = exif_size; + info->exif_defined = 1; + + return 0; /*ok*/ +} + +void lodepng_clear_exif(LodePNGInfo* info) +{ + lodepng_free(info->exif); + info->exif = NULL; + info->exif_size = 0; + info->exif_defined = 0; +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +void lodepng_info_init(LodePNGInfo* info) +{ + lodepng_color_mode_init(&info->color); + info->interlace_method = 0; + info->compression_method = 0; + info->filter_method = 0; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + info->background_defined = 0; + info->background_r = info->background_g = info->background_b = 0; + + LodePNGText_init(info); + LodePNGIText_init(info); + + info->time_defined = 0; + info->phys_defined = 0; + + info->gama_defined = 0; + info->chrm_defined = 0; + info->srgb_defined = 0; + info->iccp_defined = 0; + info->iccp_name = NULL; + info->iccp_profile = NULL; + info->cicp_defined = 0; + info->cicp_color_primaries = 0; + info->cicp_transfer_function = 0; + info->cicp_matrix_coefficients = 0; + info->cicp_video_full_range_flag = 0; + info->mdcv_defined = 0; + info->mdcv_red_x = 0; + info->mdcv_red_y = 0; + info->mdcv_green_x = 0; + info->mdcv_green_y = 0; + info->mdcv_blue_x = 0; + info->mdcv_blue_y = 0; + info->mdcv_white_x = 0; + info->mdcv_white_y = 0; + info->mdcv_max_luminance = 0; + info->mdcv_min_luminance = 0; + info->clli_defined = 0; + info->clli_max_cll = 0; + info->clli_max_fall = 0; + + info->exif_defined = 0; + info->exif = NULL; + info->exif_size = 0; + + info->sbit_defined = 0; + info->sbit_r = info->sbit_g = info->sbit_b = info->sbit_a = 0; + + LodePNGUnknownChunks_init(info); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} + +void lodepng_info_cleanup(LodePNGInfo* info) +{ + lodepng_color_mode_cleanup(&info->color); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + LodePNGText_cleanup(info); + LodePNGIText_cleanup(info); + + lodepng_clear_icc(info); + lodepng_clear_exif(info); + + LodePNGUnknownChunks_cleanup(info); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} + +unsigned lodepng_info_copy(LodePNGInfo* dest, const LodePNGInfo* source) +{ + lodepng_info_cleanup(dest); + lodepng_memcpy(dest, source, sizeof(LodePNGInfo)); + lodepng_color_mode_init(&dest->color); + CERROR_TRY_RETURN(lodepng_color_mode_copy(&dest->color, &source->color)); + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + CERROR_TRY_RETURN(LodePNGText_copy(dest, source)); + CERROR_TRY_RETURN(LodePNGIText_copy(dest, source)); + if (source->iccp_defined) { + dest->iccp_defined + = 0; /*the memcpy above set this to 1 while it shouldn't*/ + CERROR_TRY_RETURN(lodepng_set_icc(dest, + source->iccp_name, + source->iccp_profile, + source->iccp_profile_size)); + } + if (source->exif_defined) { + dest->exif_defined + = 0; /*the memcpy above set this to 1 while it shouldn't*/ + CERROR_TRY_RETURN(lodepng_set_exif(dest, source->exif, source->exif_size)); + } + + LodePNGUnknownChunks_init(dest); + CERROR_TRY_RETURN(LodePNGUnknownChunks_copy(dest, source)); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + return 0; +} + +/* ////////////////////////////////////////////////////////////////////////// */ + +/*index: bitgroup index, bits: bitgroup size(1, 2 or 4), in: bitgroup value, + * out: octet array to add bits to*/ +static void addColorBits(unsigned char* out, + size_t index, + unsigned bits, + unsigned in) +{ + unsigned m = bits == 1 ? 7 : bits == 2 ? 3 : 1; /*8 / bits - 1*/ + /*p = the partial index in the byte, e.g. with 4 palettebits it is 0 for first + * half or 1 for second half*/ + unsigned p = index & m; + in &= (1u << bits) - 1u; /*filter out any other bits of the input value*/ + in = in << (bits * (m - p)); + if (p == 0) { + out[index * bits / 8u] = in; + } else { + out[index * bits / 8u] |= in; + } +} + +typedef struct ColorTree ColorTree; + +/* +One node of a color tree +This is the data structure used to count the number of unique colors and to get +a palette index for a color. It's like an octree, but because the alpha channel +is used too, each node has 16 instead of 8 children. +*/ +struct ColorTree +{ + ColorTree* children[16]; /*up to 16 pointers to ColorTree of next level*/ + int index; /*the payload. Only has a meaningful value if this is in the last + level*/ +}; + +static void color_tree_init(ColorTree* tree) +{ + lodepng_memset(tree->children, 0, 16 * sizeof(*tree->children)); + tree->index = -1; +} + +static void color_tree_cleanup(ColorTree* tree) +{ + int i; + for (i = 0; i != 16; ++i) { + if (tree->children[i]) { + color_tree_cleanup(tree->children[i]); + lodepng_free(tree->children[i]); + } + } +} + +/*returns -1 if color not present, its index otherwise*/ +static int color_tree_get(ColorTree* tree, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a) +{ + int bit = 0; + for (bit = 0; bit < 8; ++bit) { + int i = 8 * ((r >> bit) & 1) + 4 * ((g >> bit) & 1) + 2 * ((b >> bit) & 1) + + 1 * ((a >> bit) & 1); + if (!tree->children[i]) { + return -1; + } else { + tree = tree->children[i]; + } + } + return tree ? tree->index : -1; +} + +#ifdef LODEPNG_COMPILE_ENCODER +static int color_tree_has(ColorTree* tree, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a) +{ + return color_tree_get(tree, r, g, b, a) >= 0; +} +#endif /*LODEPNG_COMPILE_ENCODER*/ + +/*color is not allowed to already exist. +Index should be >= 0 (it's signed to be compatible with using -1 for "doesn't +exist") Returns error code, or 0 if ok*/ +static unsigned color_tree_add(ColorTree* tree, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a, + unsigned index) +{ + int bit; + for (bit = 0; bit < 8; ++bit) { + int i = 8 * ((r >> bit) & 1) + 4 * ((g >> bit) & 1) + 2 * ((b >> bit) & 1) + + 1 * ((a >> bit) & 1); + if (!tree->children[i]) { + tree->children[i] = (ColorTree*) lodepng_malloc(sizeof(ColorTree)); + if (!tree->children[i]) { + return 83; /*alloc fail*/ + } + color_tree_init(tree->children[i]); + } + tree = tree->children[i]; + } + tree->index = (int) index; + return 0; +} + +/*put a pixel, given its RGBA color, into image of any color type*/ +static unsigned rgba8ToPixel(unsigned char* out, + size_t i, + const LodePNGColorMode* mode, + ColorTree* tree /*for palette*/, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a) +{ + if (mode->colortype == LCT_GREY) { + unsigned char gray = r; /*((unsigned short)r + g + b) / 3u;*/ + if (mode->bitdepth == 8) { + out[i] = gray; + } else if (mode->bitdepth == 16) { + out[i * 2 + 0] = out[i * 2 + 1] = gray; + } else { + /*take the most significant bits of gray*/ + gray = ((unsigned) gray >> (8u - mode->bitdepth)) + & ((1u << mode->bitdepth) - 1u); + addColorBits(out, i, mode->bitdepth, gray); + } + } else if (mode->colortype == LCT_RGB) { + if (mode->bitdepth == 8) { + out[i * 3 + 0] = r; + out[i * 3 + 1] = g; + out[i * 3 + 2] = b; + } else { + out[i * 6 + 0] = out[i * 6 + 1] = r; + out[i * 6 + 2] = out[i * 6 + 3] = g; + out[i * 6 + 4] = out[i * 6 + 5] = b; + } + } else if (mode->colortype == LCT_PALETTE) { + int index = color_tree_get(tree, r, g, b, a); + if (index < 0) { + return 82; /*color not in palette*/ + } + if (mode->bitdepth == 8) { + out[i] = index; + } else { + addColorBits(out, i, mode->bitdepth, (unsigned) index); + } + } else if (mode->colortype == LCT_GREY_ALPHA) { + unsigned char gray = r; /*((unsigned short)r + g + b) / 3u;*/ + if (mode->bitdepth == 8) { + out[i * 2 + 0] = gray; + out[i * 2 + 1] = a; + } else if (mode->bitdepth == 16) { + out[i * 4 + 0] = out[i * 4 + 1] = gray; + out[i * 4 + 2] = out[i * 4 + 3] = a; + } + } else if (mode->colortype == LCT_RGBA) { + if (mode->bitdepth == 8) { + out[i * 4 + 0] = r; + out[i * 4 + 1] = g; + out[i * 4 + 2] = b; + out[i * 4 + 3] = a; + } else { + out[i * 8 + 0] = out[i * 8 + 1] = r; + out[i * 8 + 2] = out[i * 8 + 3] = g; + out[i * 8 + 4] = out[i * 8 + 5] = b; + out[i * 8 + 6] = out[i * 8 + 7] = a; + } + } + + return 0; /*no error*/ +} + +/*put a pixel, given its RGBA16 color, into image of any color 16-bitdepth + * type*/ +static void rgba16ToPixel(unsigned char* out, + size_t i, + const LodePNGColorMode* mode, + unsigned short r, + unsigned short g, + unsigned short b, + unsigned short a) +{ + if (mode->colortype == LCT_GREY) { + unsigned short gray = r; /*((unsigned)r + g + b) / 3u;*/ + out[i * 2 + 0] = (gray >> 8) & 255; + out[i * 2 + 1] = gray & 255; + } else if (mode->colortype == LCT_RGB) { + out[i * 6 + 0] = (r >> 8) & 255; + out[i * 6 + 1] = r & 255; + out[i * 6 + 2] = (g >> 8) & 255; + out[i * 6 + 3] = g & 255; + out[i * 6 + 4] = (b >> 8) & 255; + out[i * 6 + 5] = b & 255; + } else if (mode->colortype == LCT_GREY_ALPHA) { + unsigned short gray = r; /*((unsigned)r + g + b) / 3u;*/ + out[i * 4 + 0] = (gray >> 8) & 255; + out[i * 4 + 1] = gray & 255; + out[i * 4 + 2] = (a >> 8) & 255; + out[i * 4 + 3] = a & 255; + } else if (mode->colortype == LCT_RGBA) { + out[i * 8 + 0] = (r >> 8) & 255; + out[i * 8 + 1] = r & 255; + out[i * 8 + 2] = (g >> 8) & 255; + out[i * 8 + 3] = g & 255; + out[i * 8 + 4] = (b >> 8) & 255; + out[i * 8 + 5] = b & 255; + out[i * 8 + 6] = (a >> 8) & 255; + out[i * 8 + 7] = a & 255; + } +} + +/*Get RGBA8 color of pixel with index i (y * width + x) from the raw image with + * given color type.*/ +static void getPixelColorRGBA8(unsigned char* r, + unsigned char* g, + unsigned char* b, + unsigned char* a, + const unsigned char* in, + size_t i, + const LodePNGColorMode* mode) +{ + if (mode->colortype == LCT_GREY) { + if (mode->bitdepth == 8) { + *r = *g = *b = in[i]; + if (mode->key_defined && *r == mode->key_r) { + *a = 0; + } else { + *a = 255; + } + } else if (mode->bitdepth == 16) { + *r = *g = *b = in[i * 2 + 0]; + if (mode->key_defined + && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r) { + *a = 0; + } else { + *a = 255; + } + } else { + unsigned highest = ((1U << mode->bitdepth) + - 1U); /*highest possible value for this bit depth*/ + size_t j = i * mode->bitdepth; + unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); + *r = *g = *b = (value * 255) / highest; + if (mode->key_defined && value == mode->key_r) { + *a = 0; + } else { + *a = 255; + } + } + } else if (mode->colortype == LCT_RGB) { + if (mode->bitdepth == 8) { + *r = in[i * 3 + 0]; + *g = in[i * 3 + 1]; + *b = in[i * 3 + 2]; + if (mode->key_defined && *r == mode->key_r && *g == mode->key_g + && *b == mode->key_b) { + *a = 0; + } else { + *a = 255; + } + } else { + *r = in[i * 6 + 0]; + *g = in[i * 6 + 2]; + *b = in[i * 6 + 4]; + if (mode->key_defined + && 256U * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r + && 256U * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g + && 256U * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b) { + *a = 0; + } else { + *a = 255; + } + } + } else if (mode->colortype == LCT_PALETTE) { + unsigned index; + if (mode->bitdepth == 8) { + index = in[i]; + } else { + size_t j = i * mode->bitdepth; + index = readBitsFromReversedStream(&j, in, mode->bitdepth); + } + /*out of bounds of palette not checked: see + * lodepng_color_mode_alloc_palette.*/ + *r = mode->palette[index * 4 + 0]; + *g = mode->palette[index * 4 + 1]; + *b = mode->palette[index * 4 + 2]; + *a = mode->palette[index * 4 + 3]; + } else if (mode->colortype == LCT_GREY_ALPHA) { + if (mode->bitdepth == 8) { + *r = *g = *b = in[i * 2 + 0]; + *a = in[i * 2 + 1]; + } else { + *r = *g = *b = in[i * 4 + 0]; + *a = in[i * 4 + 2]; + } + } else if (mode->colortype == LCT_RGBA) { + if (mode->bitdepth == 8) { + *r = in[i * 4 + 0]; + *g = in[i * 4 + 1]; + *b = in[i * 4 + 2]; + *a = in[i * 4 + 3]; + } else { + *r = in[i * 8 + 0]; + *g = in[i * 8 + 2]; + *b = in[i * 8 + 4]; + *a = in[i * 8 + 6]; + } + } +} + +/*Similar to getPixelColorRGBA8, but with all the for loops inside of the color +mode test cases, optimized to convert the colors much faster, when converting +to the common case of RGBA with 8 bit per channel. buffer must be RGBA with +enough memory.*/ +static void getPixelColorsRGBA8(unsigned char* LODEPNG_RESTRICT buffer, + size_t numpixels, + const unsigned char* LODEPNG_RESTRICT in, + const LodePNGColorMode* mode) +{ + unsigned num_channels = 4; + size_t i; + if (mode->colortype == LCT_GREY) { + if (mode->bitdepth == 8) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i]; + buffer[3] = 255; + } + if (mode->key_defined) { + buffer -= numpixels * num_channels; + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + if (buffer[0] == mode->key_r) { + buffer[3] = 0; + } + } + } + } else if (mode->bitdepth == 16) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 2]; + buffer[3] + = mode->key_defined + && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r + ? 0 + : 255; + } + } else { + unsigned highest = ((1U << mode->bitdepth) + - 1U); /*highest possible value for this bit depth*/ + size_t j = 0; + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); + buffer[0] = buffer[1] = buffer[2] = (value * 255) / highest; + buffer[3] = mode->key_defined && value == mode->key_r ? 0 : 255; + } + } + } else if (mode->colortype == LCT_RGB) { + if (mode->bitdepth == 8) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + lodepng_memcpy(buffer, &in[i * 3], 3); + buffer[3] = 255; + } + if (mode->key_defined) { + buffer -= numpixels * num_channels; + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + if (buffer[0] == mode->key_r && buffer[1] == mode->key_g + && buffer[2] == mode->key_b) { + buffer[3] = 0; + } + } + } + } else { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = in[i * 6 + 0]; + buffer[1] = in[i * 6 + 2]; + buffer[2] = in[i * 6 + 4]; + buffer[3] + = mode->key_defined + && 256U * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r + && 256U * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g + && 256U * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b + ? 0 + : 255; + } + } + } else if (mode->colortype == LCT_PALETTE) { + if (mode->bitdepth == 8) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned index = in[i]; + /*out of bounds of palette not checked: see + * lodepng_color_mode_alloc_palette.*/ + lodepng_memcpy(buffer, &mode->palette[index * 4], 4); + } + } else { + size_t j = 0; + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned index = readBitsFromReversedStream(&j, in, mode->bitdepth); + /*out of bounds of palette not checked: see + * lodepng_color_mode_alloc_palette.*/ + lodepng_memcpy(buffer, &mode->palette[index * 4], 4); + } + } + } else if (mode->colortype == LCT_GREY_ALPHA) { + if (mode->bitdepth == 8) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 2 + 0]; + buffer[3] = in[i * 2 + 1]; + } + } else { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 4 + 0]; + buffer[3] = in[i * 4 + 2]; + } + } + } else if (mode->colortype == LCT_RGBA) { + if (mode->bitdepth == 8) { + lodepng_memcpy(buffer, in, numpixels * 4); + } else { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = in[i * 8 + 0]; + buffer[1] = in[i * 8 + 2]; + buffer[2] = in[i * 8 + 4]; + buffer[3] = in[i * 8 + 6]; + } + } + } +} + +/*Similar to getPixelColorsRGBA8, but with 3-channel RGB output.*/ +static void getPixelColorsRGB8(unsigned char* LODEPNG_RESTRICT buffer, + size_t numpixels, + const unsigned char* LODEPNG_RESTRICT in, + const LodePNGColorMode* mode) +{ + const unsigned num_channels = 3; + size_t i; + if (mode->colortype == LCT_GREY) { + if (mode->bitdepth == 8) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i]; + } + } else if (mode->bitdepth == 16) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 2]; + } + } else { + unsigned highest = ((1U << mode->bitdepth) + - 1U); /*highest possible value for this bit depth*/ + size_t j = 0; + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); + buffer[0] = buffer[1] = buffer[2] = (value * 255) / highest; + } + } + } else if (mode->colortype == LCT_RGB) { + if (mode->bitdepth == 8) { + lodepng_memcpy(buffer, in, numpixels * 3); + } else { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = in[i * 6 + 0]; + buffer[1] = in[i * 6 + 2]; + buffer[2] = in[i * 6 + 4]; + } + } + } else if (mode->colortype == LCT_PALETTE) { + if (mode->bitdepth == 8) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned index = in[i]; + /*out of bounds of palette not checked: see + * lodepng_color_mode_alloc_palette.*/ + lodepng_memcpy(buffer, &mode->palette[index * 4], 3); + } + } else { + size_t j = 0; + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned index = readBitsFromReversedStream(&j, in, mode->bitdepth); + /*out of bounds of palette not checked: see + * lodepng_color_mode_alloc_palette.*/ + lodepng_memcpy(buffer, &mode->palette[index * 4], 3); + } + } + } else if (mode->colortype == LCT_GREY_ALPHA) { + if (mode->bitdepth == 8) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 2 + 0]; + } + } else { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 4 + 0]; + } + } + } else if (mode->colortype == LCT_RGBA) { + if (mode->bitdepth == 8) { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + lodepng_memcpy(buffer, &in[i * 4], 3); + } + } else { + for (i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = in[i * 8 + 0]; + buffer[1] = in[i * 8 + 2]; + buffer[2] = in[i * 8 + 4]; + } + } + } +} + +/*Get RGBA16 color of pixel with index i (y * width + x) from the raw image with +given color type, but the given color type must be 16-bit itself.*/ +static void getPixelColorRGBA16(unsigned short* r, + unsigned short* g, + unsigned short* b, + unsigned short* a, + const unsigned char* in, + size_t i, + const LodePNGColorMode* mode) +{ + if (mode->colortype == LCT_GREY) { + *r = *g = *b = 256 * in[i * 2 + 0] + in[i * 2 + 1]; + if (mode->key_defined + && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r) { + *a = 0; + } else { + *a = 65535; + } + } else if (mode->colortype == LCT_RGB) { + *r = 256u * in[i * 6 + 0] + in[i * 6 + 1]; + *g = 256u * in[i * 6 + 2] + in[i * 6 + 3]; + *b = 256u * in[i * 6 + 4] + in[i * 6 + 5]; + if (mode->key_defined && 256u * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r + && 256u * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g + && 256u * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b) { + *a = 0; + } else { + *a = 65535; + } + } else if (mode->colortype == LCT_GREY_ALPHA) { + *r = *g = *b = 256u * in[i * 4 + 0] + in[i * 4 + 1]; + *a = 256u * in[i * 4 + 2] + in[i * 4 + 3]; + } else if (mode->colortype == LCT_RGBA) { + *r = 256u * in[i * 8 + 0] + in[i * 8 + 1]; + *g = 256u * in[i * 8 + 2] + in[i * 8 + 3]; + *b = 256u * in[i * 8 + 4] + in[i * 8 + 5]; + *a = 256u * in[i * 8 + 6] + in[i * 8 + 7]; + } +} + +unsigned lodepng_convert(unsigned char* out, + const unsigned char* in, + const LodePNGColorMode* mode_out, + const LodePNGColorMode* mode_in, + unsigned w, + unsigned h) +{ + size_t i; + ColorTree tree; + size_t numpixels = (size_t) w * (size_t) h; + unsigned error = 0; + + if (mode_in->colortype == LCT_PALETTE && !mode_in->palette) { + return 107; /* error: must provide palette if input mode is palette */ + } + + if (lodepng_color_mode_equal(mode_out, mode_in)) { + size_t numbytes = lodepng_get_raw_size(w, h, mode_in); + lodepng_memcpy(out, in, numbytes); + return 0; + } + + if (mode_out->colortype == LCT_PALETTE) { + size_t palettesize = mode_out->palettesize; + const unsigned char* palette = mode_out->palette; + size_t palsize = (size_t) 1u << mode_out->bitdepth; + /*if the user specified output palette but did not give the values, assume + they want the values of the input color type (assuming that one is palette). + Note that we never create a new palette ourselves.*/ + if (palettesize == 0) { + palettesize = mode_in->palettesize; + palette = mode_in->palette; + /*if the input was also palette with same bitdepth, then the color types + are also equal, so copy literally. This to preserve the exact indices that + were in the PNG even in case there are duplicate colors in the palette.*/ + if (mode_in->colortype == LCT_PALETTE + && mode_in->bitdepth == mode_out->bitdepth) { + size_t numbytes = lodepng_get_raw_size(w, h, mode_in); + lodepng_memcpy(out, in, numbytes); + return 0; + } + } + if (palettesize < palsize) { + palsize = palettesize; + } + color_tree_init(&tree); + for (i = 0; i != palsize; ++i) { + const unsigned char* p = &palette[i * 4]; + error = color_tree_add(&tree, p[0], p[1], p[2], p[3], (unsigned) i); + if (error) { + break; + } + } + } + + if (!error) { + if (mode_in->bitdepth == 16 && mode_out->bitdepth == 16) { + for (i = 0; i != numpixels; ++i) { + unsigned short r = 0, g = 0, b = 0, a = 0; + getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); + rgba16ToPixel(out, i, mode_out, r, g, b, a); + } + } else if (mode_out->bitdepth == 8 && mode_out->colortype == LCT_RGBA) { + getPixelColorsRGBA8(out, numpixels, in, mode_in); + } else if (mode_out->bitdepth == 8 && mode_out->colortype == LCT_RGB) { + getPixelColorsRGB8(out, numpixels, in, mode_in); + } else { + unsigned char r = 0, g = 0, b = 0, a = 0; + for (i = 0; i != numpixels; ++i) { + getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); + error = rgba8ToPixel(out, i, mode_out, &tree, r, g, b, a); + if (error) { + break; + } + } + } + } + + if (mode_out->colortype == LCT_PALETTE) { + color_tree_cleanup(&tree); + } + + return error; +} + +/* Converts a single rgb color without alpha from one type to another, color +bits truncated to their bitdepth. In case of single channel (gray or palette), +only the r channel is used. Slow function, do not use to process all pixels of +an image. Alpha channel not supported on purpose: this is for bKGD, supporting +alpha may prevent it from finding a color in the palette, from the specification +it looks like bKGD should ignore the alpha values of the palette since it can +use any palette index but doesn't have an alpha channel. Idem with ignoring +color key. */ +unsigned lodepng_convert_rgb(unsigned* r_out, + unsigned* g_out, + unsigned* b_out, + unsigned r_in, + unsigned g_in, + unsigned b_in, + const LodePNGColorMode* mode_out, + const LodePNGColorMode* mode_in) +{ + unsigned r = 0, g = 0, b = 0; + unsigned mul + = 65535 / ((1u << mode_in->bitdepth) - 1u); /*65535, 21845, 4369, 257, 1*/ + unsigned shift = 16 - mode_out->bitdepth; + + if (mode_in->colortype == LCT_GREY || mode_in->colortype == LCT_GREY_ALPHA) { + r = g = b = r_in * mul; + } else if (mode_in->colortype == LCT_RGB || mode_in->colortype == LCT_RGBA) { + r = r_in * mul; + g = g_in * mul; + b = b_in * mul; + } else if (mode_in->colortype == LCT_PALETTE) { + if (r_in >= mode_in->palettesize) { + return 82; + } + r = mode_in->palette[r_in * 4 + 0] * 257u; + g = mode_in->palette[r_in * 4 + 1] * 257u; + b = mode_in->palette[r_in * 4 + 2] * 257u; + } else { + return 31; + } + + /* now convert to output format */ + if (mode_out->colortype == LCT_GREY + || mode_out->colortype == LCT_GREY_ALPHA) { + *r_out = r >> shift; + } else if (mode_out->colortype == LCT_RGB + || mode_out->colortype == LCT_RGBA) { + *r_out = r >> shift; + *g_out = g >> shift; + *b_out = b >> shift; + } else if (mode_out->colortype == LCT_PALETTE) { + unsigned i; + /* a 16-bit color cannot be in the palette */ + if ((r >> 8) != (r & 255) || (g >> 8) != (g & 255) + || (b >> 8) != (b & 255)) { + return 82; + } + for (i = 0; i < mode_out->palettesize; i++) { + unsigned j = i * 4; + if ((r >> 8) == mode_out->palette[j + 0] + && (g >> 8) == mode_out->palette[j + 1] + && (b >> 8) == mode_out->palette[j + 2]) { + *r_out = i; + return 0; + } + } + return 82; + } else { + return 31; + } + + return 0; +} + +#ifdef LODEPNG_COMPILE_ENCODER + +void lodepng_color_stats_init(LodePNGColorStats* stats) +{ + /*stats*/ + stats->colored = 0; + stats->key = 0; + stats->key_r = stats->key_g = stats->key_b = 0; + stats->alpha = 0; + stats->numcolors = 0; + stats->bits = 1; + stats->numpixels = 0; + /*settings*/ + stats->allow_palette = 1; + stats->allow_greyscale = 1; +} + +/*function used for debug purposes with C++*/ +/*void printColorStats(LodePNGColorStats* p) { + std::cout << "colored: " << (int)p->colored << ", "; + std::cout << "key: " << (int)p->key << ", "; + std::cout << "key_r: " << (int)p->key_r << ", "; + std::cout << "key_g: " << (int)p->key_g << ", "; + std::cout << "key_b: " << (int)p->key_b << ", "; + std::cout << "alpha: " << (int)p->alpha << ", "; + std::cout << "numcolors: " << (int)p->numcolors << ", "; + std::cout << "bits: " << (int)p->bits << std::endl; +}*/ + +/*Returns how many bits needed to represent given value (max 8 bit)*/ +static unsigned getValueRequiredBits(unsigned char value) +{ + if (value == 0 || value == 255) { + return 1; + } + /*The scaling of 2-bit and 4-bit values uses multiples of 85 and 17*/ + if (value % 17 == 0) { + return value % 85 == 0 ? 2 : 4; + } + return 8; +} + +/*stats must already have been inited. */ +unsigned lodepng_compute_color_stats(LodePNGColorStats* stats, + const unsigned char* in, + unsigned w, + unsigned h, + const LodePNGColorMode* mode_in) +{ + size_t i; + ColorTree tree; + size_t numpixels = (size_t) w * (size_t) h; + unsigned error = 0; + + /* mark things as done already if it would be impossible to have a more + * expensive case */ + unsigned colored_done = lodepng_is_greyscale_type(mode_in) ? 1 : 0; + unsigned alpha_done = lodepng_can_have_alpha(mode_in) ? 0 : 1; + unsigned numcolors_done = 0; + unsigned bpp = lodepng_get_bpp(mode_in); + unsigned bits_done = (stats->bits == 1 && bpp == 1) ? 1 : 0; + unsigned sixteen = 0; /* whether the input image is 16 bit */ + unsigned maxnumcolors = 257; + if (bpp <= 8) { + maxnumcolors = LODEPNG_MIN(257, stats->numcolors + (1u << bpp)); + } + + stats->numpixels += numpixels; + + /*if palette not allowed, no need to compute numcolors*/ + if (!stats->allow_palette) { + numcolors_done = 1; + } + + color_tree_init(&tree); + + /*If the stats was already filled in from previous data, fill its palette in + tree and mark things as done already if we know they are the most expensive + case already*/ + if (stats->alpha) { + alpha_done = 1; + } + if (stats->colored) { + colored_done = 1; + } + if (stats->bits == 16) { + numcolors_done = 1; + } + if (stats->bits >= bpp) { + bits_done = 1; + } + if (stats->numcolors >= maxnumcolors) { + numcolors_done = 1; + } + + if (!numcolors_done) { + for (i = 0; i < stats->numcolors; i++) { + const unsigned char* color = &stats->palette[i * 4]; + error = color_tree_add( + &tree, color[0], color[1], color[2], color[3], (unsigned) i); + if (error) { + goto cleanup; + } + } + } + + /*Check if the 16-bit input is truly 16-bit*/ + if (mode_in->bitdepth == 16 && !sixteen) { + unsigned short r = 0, g = 0, b = 0, a = 0; + for (i = 0; i != numpixels; ++i) { + getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); + if ((r & 255) != ((r >> 8) & 255) || (g & 255) != ((g >> 8) & 255) + || (b & 255) != ((b >> 8) & 255) + || (a & 255) != ((a >> 8) & 255)) /*first and second byte differ*/ { + stats->bits = 16; + sixteen = 1; + bits_done = 1; + numcolors_done = 1; /*counting colors no longer useful, palette doesn't + support 16-bit*/ + break; + } + } + } + + if (sixteen) { + unsigned short r = 0, g = 0, b = 0, a = 0; + + for (i = 0; i != numpixels; ++i) { + getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); + + if (!colored_done && (r != g || r != b)) { + stats->colored = 1; + colored_done = 1; + } + + if (!alpha_done) { + unsigned matchkey + = (r == stats->key_r && g == stats->key_g && b == stats->key_b); + if (a != 65535 && (a != 0 || (stats->key && !matchkey))) { + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + } else if (a == 0 && !stats->alpha && !stats->key) { + stats->key = 1; + stats->key_r = r; + stats->key_g = g; + stats->key_b = b; + } else if (a == 65535 && stats->key && matchkey) { + /* Color key cannot be used if an opaque pixel also has that RGB + * color. */ + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + } + } + if (alpha_done && numcolors_done && colored_done && bits_done) { + break; + } + } + + if (stats->key && !stats->alpha) { + for (i = 0; i != numpixels; ++i) { + getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); + if (a != 0 && r == stats->key_r && g == stats->key_g + && b == stats->key_b) { + /* Color key cannot be used if an opaque pixel also has that RGB + * color. */ + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + } + } + } + } else /* < 16-bit */ { + unsigned char r = 0, g = 0, b = 0, a = 0; + unsigned char pr = 0, pg = 0, pb = 0, pa = 0; + for (i = 0; i != numpixels; ++i) { + getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); + + /*skip if color same as before, this speeds up large non-photographic + images with many same colors by avoiding 'color_tree_has' below */ + if (i != 0 && r == pr && g == pg && b == pb && a == pa) { + continue; + } + pr = r; + pg = g; + pb = b; + pa = a; + + if (!bits_done && stats->bits < 8) { + /*only r is checked, < 8 bits is only relevant for grayscale*/ + unsigned bits = getValueRequiredBits(r); + if (bits > stats->bits) { + stats->bits = bits; + } + } + bits_done = (stats->bits >= bpp); + + if (!colored_done && (r != g || r != b)) { + stats->colored = 1; + colored_done = 1; + if (stats->bits < 8) { + stats->bits + = 8; /*PNG has no colored modes with less than 8-bit per channel*/ + } + } + + if (!alpha_done) { + unsigned matchkey + = (r == stats->key_r && g == stats->key_g && b == stats->key_b); + if (a != 255 && (a != 0 || (stats->key && !matchkey))) { + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + if (stats->bits < 8) { + stats->bits = 8; /*PNG has no alphachannel modes with less than + 8-bit per channel*/ + } + } else if (a == 0 && !stats->alpha && !stats->key) { + stats->key = 1; + stats->key_r = r; + stats->key_g = g; + stats->key_b = b; + } else if (a == 255 && stats->key && matchkey) { + /* Color key cannot be used if an opaque pixel also has that RGB + * color. */ + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + if (stats->bits < 8) { + stats->bits = 8; /*PNG has no alphachannel modes with less than + 8-bit per channel*/ + } + } + } + + if (!numcolors_done) { + if (!color_tree_has(&tree, r, g, b, a)) { + error = color_tree_add(&tree, r, g, b, a, stats->numcolors); + if (error) { + goto cleanup; + } + if (stats->numcolors < 256) { + unsigned char* p = stats->palette; + unsigned n = stats->numcolors; + p[n * 4 + 0] = r; + p[n * 4 + 1] = g; + p[n * 4 + 2] = b; + p[n * 4 + 3] = a; + } + ++stats->numcolors; + numcolors_done = stats->numcolors >= maxnumcolors; + } + } + + if (alpha_done && numcolors_done && colored_done && bits_done) { + break; + } + } + + if (stats->key && !stats->alpha) { + for (i = 0; i != numpixels; ++i) { + getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); + if (a != 0 && r == stats->key_r && g == stats->key_g + && b == stats->key_b) { + /* Color key cannot be used if an opaque pixel also has that RGB + * color. */ + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + if (stats->bits < 8) { + stats->bits = 8; /*PNG has no alphachannel modes with less than + 8-bit per channel*/ + } + } + } + } + + /*make the stats's key always 16-bit for consistency - repeat each byte + * twice*/ + stats->key_r += (stats->key_r << 8); + stats->key_g += (stats->key_g << 8); + stats->key_b += (stats->key_b << 8); + } + +cleanup: + color_tree_cleanup(&tree); + return error; +} + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +/*Adds a single color to the color stats. The stats must already have been +inited. The color must be given as 16-bit (with 2 bytes repeating for 8-bit and +65535 for opaque alpha channel). This function is expensive, do not call it for +all pixels of an image but only for a few additional values. */ +static unsigned lodepng_color_stats_add(LodePNGColorStats* stats, + unsigned r, + unsigned g, + unsigned b, + unsigned a) +{ + unsigned error = 0; + unsigned char image[8]; + LodePNGColorMode mode; + lodepng_color_mode_init(&mode); + image[0] = r >> 8; + image[1] = r; + image[2] = g >> 8; + image[3] = g; + image[4] = b >> 8; + image[5] = b; + image[6] = a >> 8; + image[7] = a; + mode.bitdepth = 16; + mode.colortype = LCT_RGBA; + error = lodepng_compute_color_stats(stats, image, 1, 1, &mode); + lodepng_color_mode_cleanup(&mode); + return error; +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +/*Computes a minimal PNG color model that can contain all colors as indicated by +the stats. The stats should be computed with lodepng_compute_color_stats. +mode_in is raw color profile of the image the stats were computed on, to copy +palette order from when relevant. Minimal PNG color model means the color type +and bit depth that gives smallest amount of bits in the output image, e.g. gray +if only grayscale pixels, palette if less than 256 colors, color key if only +single transparent color, ... This is used if auto_convert is enabled (it is by +default). +*/ +static unsigned auto_choose_color(LodePNGColorMode* mode_out, + const LodePNGColorMode* mode_in, + const LodePNGColorStats* stats) +{ + unsigned error = 0; + unsigned palettebits; + size_t i, n; + size_t numpixels = stats->numpixels; + unsigned palette_ok, gray_ok; + + unsigned alpha = stats->alpha; + unsigned key = stats->key; + unsigned bits = stats->bits; + + mode_out->key_defined = 0; + + if (key && numpixels <= 16) { + alpha = 1; /*too few pixels to justify tRNS chunk overhead*/ + key = 0; + if (bits < 8) { + bits = 8; /*PNG has no alphachannel modes with less than 8-bit per + channel*/ + } + } + + gray_ok = !stats->colored; + if (!stats->allow_greyscale) { + gray_ok = 0; + } + if (!gray_ok && bits < 8) { + bits = 8; + } + + n = stats->numcolors; + palettebits = n <= 2 ? 1 : (n <= 4 ? 2 : (n <= 16 ? 4 : 8)); + palette_ok = n <= 256 && bits <= 8 + && n != 0; /*n==0 means likely numcolors wasn't computed*/ + if (numpixels < n * 2) { + palette_ok + = 0; /*don't add palette overhead if image has only a few pixels*/ + } + if (gray_ok && !alpha && bits <= palettebits) { + palette_ok = 0; /*gray is less overhead*/ + } + if (!stats->allow_palette) { + palette_ok = 0; + } + + if (palette_ok) { + const unsigned char* p = stats->palette; + lodepng_palette_clear(mode_out); /*remove potential earlier palette*/ + for (i = 0; i != stats->numcolors; ++i) { + error = lodepng_palette_add( + mode_out, p[i * 4 + 0], p[i * 4 + 1], p[i * 4 + 2], p[i * 4 + 3]); + if (error) { + break; + } + } + + mode_out->colortype = LCT_PALETTE; + mode_out->bitdepth = palettebits; + + if (mode_in->colortype == LCT_PALETTE + && mode_in->palettesize >= mode_out->palettesize + && mode_in->bitdepth == mode_out->bitdepth) { + /*If input should have same palette colors, keep original to preserve its + * order and prevent conversion*/ + lodepng_color_mode_cleanup( + mode_out); /*clears palette, keeps the above set colortype and + bitdepth fields as-is*/ + lodepng_color_mode_copy(mode_out, mode_in); + } + } else /*8-bit or 16-bit per channel*/ { + mode_out->bitdepth = bits; + mode_out->colortype = alpha ? (gray_ok ? LCT_GREY_ALPHA : LCT_RGBA) + : (gray_ok ? LCT_GREY : LCT_RGB); + if (key) { + unsigned mask = (1u << mode_out->bitdepth) + - 1u; /*stats always uses 16-bit, mask converts it*/ + mode_out->key_r = stats->key_r & mask; + mode_out->key_g = stats->key_g & mask; + mode_out->key_b = stats->key_b & mask; + mode_out->key_defined = 1; + } + } + + return error; +} + +#endif /* #ifdef LODEPNG_COMPILE_ENCODER */ + +/*Paeth predictor, used by PNG filter type 4*/ +static unsigned char paethPredictor(unsigned char a, + unsigned char b, + unsigned char c) +{ + /* the subtractions of unsigned char cast it to a signed type. + With gcc, short is faster than int, with clang int is as fast (as of april + 2023)*/ + short pa = (b - c) < 0 ? -(b - c) : (b - c); + short pb = (a - c) < 0 ? -(a - c) : (a - c); + /* writing it out like this compiles to something faster than introducing a + * temp variable*/ + short pc = (a + b - c - c) < 0 ? -(a + b - c - c) : (a + b - c - c); + /* return input value associated with smallest of pa, pb, pc (with certain + * priority if equal) */ + if (pb < pa) { + a = b; + pa = pb; + } + return (pc < pa) ? c : a; +} + +/*shared values used by multiple Adam7 related functions*/ + +static const unsigned ADAM7_IX[7] = {0, 4, 0, 2, 0, 1, 0}; /*x start values*/ +static const unsigned ADAM7_IY[7] = {0, 0, 4, 0, 2, 0, 1}; /*y start values*/ +static const unsigned ADAM7_DX[7] = {8, 8, 4, 4, 2, 2, 1}; /*x delta values*/ +static const unsigned ADAM7_DY[7] = {8, 8, 8, 4, 4, 2, 2}; /*y delta values*/ + +/* +Outputs various dimensions and positions in the image related to the Adam7 +reduced images. passw: output containing the width of the 7 passes passh: output +containing the height of the 7 passes filter_passstart: output containing the +index of the start and end of each reduced image with filter bytes +padded_passstart output containing the index of the start and end of each + reduced image when without filter bytes but with padded scanlines +passstart: output containing the index of the start and end of each reduced + image without padding between scanlines, but still padding between the images +w, h: width and height of non-interlaced image +bpp: bits per pixel +"padded" is only relevant if bpp is less than 8 and a scanline or image does not + end at a full byte +*/ +static void Adam7_getpassvalues(unsigned passw[7], + unsigned passh[7], + size_t filter_passstart[8], + size_t padded_passstart[8], + size_t passstart[8], + unsigned w, + unsigned h, + unsigned bpp) +{ + /*the passstart values have 8 values: the 8th one indicates the byte after the + * end of the 7th (= last) pass*/ + unsigned i; + + /*calculate width and height in pixels of each pass*/ + for (i = 0; i != 7; ++i) { + passw[i] = (w + ADAM7_DX[i] - ADAM7_IX[i] - 1) / ADAM7_DX[i]; + passh[i] = (h + ADAM7_DY[i] - ADAM7_IY[i] - 1) / ADAM7_DY[i]; + if (passw[i] == 0) { + passh[i] = 0; + } + if (passh[i] == 0) { + passw[i] = 0; + } + } + + filter_passstart[0] = padded_passstart[0] = passstart[0] = 0; + for (i = 0; i != 7; ++i) { + /*if passw[i] is 0, it's 0 bytes, not 1 (no filtertype-byte)*/ + filter_passstart[i + 1] + = filter_passstart[i] + + ((passw[i] && passh[i]) + ? passh[i] * (1u + (passw[i] * bpp + 7u) / 8u) + : 0); + /*bits padded if needed to fill full byte at end of each scanline*/ + padded_passstart[i + 1] + = padded_passstart[i] + passh[i] * ((passw[i] * bpp + 7u) / 8u); + /*only padded at end of reduced image*/ + passstart[i + 1] = passstart[i] + (passh[i] * passw[i] * bpp + 7u) / 8u; + } +} + +#ifdef LODEPNG_COMPILE_DECODER + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / PNG Decoder / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/*read the information from the header and store it in the LodePNGInfo. return + * value is error*/ +unsigned lodepng_inspect(unsigned* w, + unsigned* h, + LodePNGState* state, + const unsigned char* in, + size_t insize) +{ + unsigned width, height; + LodePNGInfo* info = &state->info_png; + if (insize == 0 || in == 0) { + CERROR_RETURN_ERROR(state->error, 48); /*error: the given data is empty*/ + } + if (insize < 33) { + CERROR_RETURN_ERROR(state->error, 27); /*error: the data length is smaller + than the length of a PNG header*/ + } + + /*when decoding a new PNG image, make sure all parameters created after + * previous decoding are reset*/ + /* TODO: remove this. One should use a new LodePNGState for new sessions */ + lodepng_info_cleanup(info); + lodepng_info_init(info); + + if (in[0] != 137 || in[1] != 80 || in[2] != 78 || in[3] != 71 || in[4] != 13 + || in[5] != 10 || in[6] != 26 || in[7] != 10) { + CERROR_RETURN_ERROR( + state->error, + 28); /*error: the first 8 bytes are not the correct PNG signature*/ + } + if (lodepng_chunk_length(in + 8) != 13) { + CERROR_RETURN_ERROR(state->error, + 94); /*error: header size must be 13 bytes*/ + } + if (!lodepng_chunk_type_equals(in + 8, "IHDR")) { + CERROR_RETURN_ERROR(state->error, + 29); /*error: it doesn't start with a IHDR chunk!*/ + } + + /*read the values given in the header*/ + width = lodepng_read32bitInt(&in[16]); + height = lodepng_read32bitInt(&in[20]); + /*TODO: remove the undocumented feature that allows to give null pointers to + * width or height*/ + if (w) { + *w = width; + } + if (h) { + *h = height; + } + info->color.bitdepth = in[24]; + info->color.colortype = (LodePNGColorType) in[25]; + info->compression_method = in[26]; + info->filter_method = in[27]; + info->interlace_method = in[28]; + + /*errors returned only after the parsing so other values are still output*/ + + /*error: invalid image size*/ + if (width == 0 || height == 0) { + CERROR_RETURN_ERROR(state->error, 93); + } + /*error: invalid colortype or bitdepth combination*/ + state->error + = checkColorValidity(info->color.colortype, info->color.bitdepth); + if (state->error) { + return state->error; + } + /*error: only compression method 0 is allowed in the specification*/ + if (info->compression_method != 0) { + CERROR_RETURN_ERROR(state->error, 32); + } + /*error: only filter method 0 is allowed in the specification*/ + if (info->filter_method != 0) { + CERROR_RETURN_ERROR(state->error, 33); + } + /*error: only interlace methods 0 and 1 exist in the specification*/ + if (info->interlace_method > 1) { + CERROR_RETURN_ERROR(state->error, 34); + } + + if (!state->decoder.ignore_crc) { + unsigned crc = lodepng_read32bitInt(&in[29]); + unsigned checksum = lodepng_crc32(&in[12], 17); + if (crc != checksum) { + CERROR_RETURN_ERROR(state->error, 57); /*invalid CRC*/ + } + } + + return state->error; +} + +static unsigned unfilterScanline(unsigned char* recon, + const unsigned char* scanline, + const unsigned char* precon, + size_t bytewidth, + unsigned char filterType, + size_t length) +{ + /* + For PNG filter method 0 + unfilter a PNG image scanline by scanline. when the pixels are smaller than 1 + byte, the filter works byte per byte (bytewidth = 1) precon is the previous + unfiltered scanline, recon the result, scanline the current one the incoming + scanlines do NOT include the filtertype byte, that one is given in the + parameter filterType instead recon and scanline MAY be the same memory + address! precon must be disjoint. + */ + + size_t i; + switch (filterType) { + case 0: + for (i = 0; i != length; ++i) { + recon[i] = scanline[i]; + } + break; + case 1: { + size_t j = 0; + for (i = 0; i != bytewidth; ++i) { + recon[i] = scanline[i]; + } + for (i = bytewidth; i != length; ++i, ++j) { + recon[i] = scanline[i] + recon[j]; + } + break; + } + case 2: + if (precon) { + for (i = 0; i != length; ++i) { + recon[i] = scanline[i] + precon[i]; + } + } else { + for (i = 0; i != length; ++i) { + recon[i] = scanline[i]; + } + } + break; + case 3: + if (precon) { + size_t j = 0; + for (i = 0; i != bytewidth; ++i) { + recon[i] = scanline[i] + (precon[i] >> 1u); + } + /* Unroll independent paths of this predictor. A 6x and 8x version is + also possible but that adds too much code. Whether this speeds up + anything depends on compiler and settings. */ + if (bytewidth >= 4) { + for (; i + 3 < length; i += 4, j += 4) { + unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1], + s2 = scanline[i + 2], s3 = scanline[i + 3]; + unsigned char r0 = recon[j + 0], r1 = recon[j + 1], + r2 = recon[j + 2], r3 = recon[j + 3]; + unsigned char p0 = precon[i + 0], p1 = precon[i + 1], + p2 = precon[i + 2], p3 = precon[i + 3]; + recon[i + 0] = s0 + ((r0 + p0) >> 1u); + recon[i + 1] = s1 + ((r1 + p1) >> 1u); + recon[i + 2] = s2 + ((r2 + p2) >> 1u); + recon[i + 3] = s3 + ((r3 + p3) >> 1u); + } + } else if (bytewidth >= 3) { + for (; i + 2 < length; i += 3, j += 3) { + unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1], + s2 = scanline[i + 2]; + unsigned char r0 = recon[j + 0], r1 = recon[j + 1], + r2 = recon[j + 2]; + unsigned char p0 = precon[i + 0], p1 = precon[i + 1], + p2 = precon[i + 2]; + recon[i + 0] = s0 + ((r0 + p0) >> 1u); + recon[i + 1] = s1 + ((r1 + p1) >> 1u); + recon[i + 2] = s2 + ((r2 + p2) >> 1u); + } + } else if (bytewidth >= 2) { + for (; i + 1 < length; i += 2, j += 2) { + unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1]; + unsigned char r0 = recon[j + 0], r1 = recon[j + 1]; + unsigned char p0 = precon[i + 0], p1 = precon[i + 1]; + recon[i + 0] = s0 + ((r0 + p0) >> 1u); + recon[i + 1] = s1 + ((r1 + p1) >> 1u); + } + } + for (; i != length; ++i, ++j) { + recon[i] = scanline[i] + ((recon[j] + precon[i]) >> 1u); + } + } else { + size_t j = 0; + for (i = 0; i != bytewidth; ++i) { + recon[i] = scanline[i]; + } + for (i = bytewidth; i != length; ++i, ++j) { + recon[i] = scanline[i] + (recon[j] >> 1u); + } + } + break; + case 4: + if (precon) { + /* Unroll independent paths of this predictor. Whether this speeds up + anything depends on compiler and settings. */ + if (bytewidth == 8) { + unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; + unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; + unsigned char a4, b4 = 0, c4, d4 = 0, a5, b5 = 0, c5, d5 = 0; + unsigned char a6, b6 = 0, c6, d6 = 0, a7, b7 = 0, c7, d7 = 0; + for (i = 0; i + 7 < length; i += 8) { + c0 = b0; + c1 = b1; + c2 = b2; + c3 = b3; + c4 = b4; + c5 = b5; + c6 = b6; + c7 = b7; + b0 = precon[i + 0]; + b1 = precon[i + 1]; + b2 = precon[i + 2]; + b3 = precon[i + 3]; + b4 = precon[i + 4]; + b5 = precon[i + 5]; + b6 = precon[i + 6]; + b7 = precon[i + 7]; + a0 = d0; + a1 = d1; + a2 = d2; + a3 = d3; + a4 = d4; + a5 = d5; + a6 = d6; + a7 = d7; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); + d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); + d4 = scanline[i + 4] + paethPredictor(a4, b4, c4); + d5 = scanline[i + 5] + paethPredictor(a5, b5, c5); + d6 = scanline[i + 6] + paethPredictor(a6, b6, c6); + d7 = scanline[i + 7] + paethPredictor(a7, b7, c7); + recon[i + 0] = d0; + recon[i + 1] = d1; + recon[i + 2] = d2; + recon[i + 3] = d3; + recon[i + 4] = d4; + recon[i + 5] = d5; + recon[i + 6] = d6; + recon[i + 7] = d7; + } + } else if (bytewidth == 6) { + unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; + unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; + unsigned char a4, b4 = 0, c4, d4 = 0, a5, b5 = 0, c5, d5 = 0; + for (i = 0; i + 5 < length; i += 6) { + c0 = b0; + c1 = b1; + c2 = b2; + c3 = b3; + c4 = b4; + c5 = b5; + b0 = precon[i + 0]; + b1 = precon[i + 1]; + b2 = precon[i + 2]; + b3 = precon[i + 3]; + b4 = precon[i + 4]; + b5 = precon[i + 5]; + a0 = d0; + a1 = d1; + a2 = d2; + a3 = d3; + a4 = d4; + a5 = d5; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); + d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); + d4 = scanline[i + 4] + paethPredictor(a4, b4, c4); + d5 = scanline[i + 5] + paethPredictor(a5, b5, c5); + recon[i + 0] = d0; + recon[i + 1] = d1; + recon[i + 2] = d2; + recon[i + 3] = d3; + recon[i + 4] = d4; + recon[i + 5] = d5; + } + } else if (bytewidth == 4) { + unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; + unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; + for (i = 0; i + 3 < length; i += 4) { + c0 = b0; + c1 = b1; + c2 = b2; + c3 = b3; + b0 = precon[i + 0]; + b1 = precon[i + 1]; + b2 = precon[i + 2]; + b3 = precon[i + 3]; + a0 = d0; + a1 = d1; + a2 = d2; + a3 = d3; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); + d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); + recon[i + 0] = d0; + recon[i + 1] = d1; + recon[i + 2] = d2; + recon[i + 3] = d3; + } + } else if (bytewidth == 3) { + unsigned char a0, b0 = 0, c0, d0 = 0; + unsigned char a1, b1 = 0, c1, d1 = 0; + unsigned char a2, b2 = 0, c2, d2 = 0; + for (i = 0; i + 2 < length; i += 3) { + c0 = b0; + c1 = b1; + c2 = b2; + b0 = precon[i + 0]; + b1 = precon[i + 1]; + b2 = precon[i + 2]; + a0 = d0; + a1 = d1; + a2 = d2; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); + recon[i + 0] = d0; + recon[i + 1] = d1; + recon[i + 2] = d2; + } + } else if (bytewidth == 2) { + unsigned char a0, b0 = 0, c0, d0 = 0; + unsigned char a1, b1 = 0, c1, d1 = 0; + for (i = 0; i + 1 < length; i += 2) { + c0 = b0; + c1 = b1; + b0 = precon[i + 0]; + b1 = precon[i + 1]; + a0 = d0; + a1 = d1; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + recon[i + 0] = d0; + recon[i + 1] = d1; + } + } else if (bytewidth == 1) { + unsigned char a, b = 0, c, d = 0; + for (i = 0; i != length; ++i) { + c = b; + b = precon[i]; + a = d; + d = scanline[i] + paethPredictor(a, b, c); + recon[i] = d; + } + } else { + /* Normally not a possible case, but this would handle it correctly */ + for (i = 0; i != bytewidth; ++i) { + recon[i] = (scanline[i] + precon[i]); /*paethPredictor(0, precon[i], + 0) is always precon[i]*/ + } + } + /* finish any remaining bytes */ + for (; i != length; ++i) { + recon[i] + = (scanline[i] + + paethPredictor( + recon[i - bytewidth], precon[i], precon[i - bytewidth])); + } + } else { + size_t j = 0; + for (i = 0; i != bytewidth; ++i) { + recon[i] = scanline[i]; + } + for (i = bytewidth; i != length; ++i, ++j) { + /*paethPredictor(recon[i - bytewidth], 0, 0) is always recon[i - + * bytewidth]*/ + recon[i] = (scanline[i] + recon[j]); + } + } + break; + default: + return 36; /*error: invalid filter type given*/ + } + return 0; +} + +static unsigned unfilter(unsigned char* out, + const unsigned char* in, + unsigned w, + unsigned h, + unsigned bpp) +{ + /* + For PNG filter method 0 + this function unfilters a single image (e.g. without interlacing this is + called once, with Adam7 seven times) out must have enough bytes allocated + already, in must have the scanlines + 1 filtertype byte per scanline w and h + are image dimensions or dimensions of reduced image, bpp is bits per pixel in + and out are allowed to be the same memory address (but aren't the same size + since in has the extra filter bytes) + */ + + unsigned y; + unsigned char* prevline = 0; + + /*bytewidth is used for filtering, is 1 when bpp < 8, number of bytes per + * pixel otherwise*/ + size_t bytewidth = (bpp + 7u) / 8u; + /*the width of a scanline in bytes, not including the filter type*/ + size_t linebytes = lodepng_get_raw_size_idat(w, 1, bpp) - 1u; + + for (y = 0; y < h; ++y) { + size_t outindex = linebytes * y; + size_t inindex + = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ + unsigned char filterType = in[inindex]; + + CERROR_TRY_RETURN(unfilterScanline(&out[outindex], + &in[inindex + 1], + prevline, + bytewidth, + filterType, + linebytes)); + + prevline = &out[outindex]; + } + + return 0; +} + +/* +in: Adam7 interlaced image, with no padding bits between scanlines, but between + reduced images so that each reduced image starts at a byte. +out: the same pixels, but re-ordered so that they're now a non-interlaced image +with size w*h bpp: bits per pixel out has the following size in bits: w * h * +bpp. in is possibly bigger due to padding bits between reduced images. out must +be big enough AND must be 0 everywhere if bpp < 8 in the current implementation +(because that's likely a little bit faster) +NOTE: comments about padding bits are only relevant if bpp < 8 +*/ +static void Adam7_deinterlace(unsigned char* out, + const unsigned char* in, + unsigned w, + unsigned h, + unsigned bpp) +{ + unsigned passw[7], passh[7]; + size_t filter_passstart[8], padded_passstart[8], passstart[8]; + unsigned i; + + Adam7_getpassvalues( + passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); + + if (bpp >= 8) { + for (i = 0; i != 7; ++i) { + unsigned x, y, b; + size_t bytewidth = bpp / 8u; + for (y = 0; y < passh[i]; ++y) { + for (x = 0; x < passw[i]; ++x) { + size_t pixelinstart = passstart[i] + (y * passw[i] + x) * bytewidth; + size_t pixeloutstart + = ((ADAM7_IY[i] + (size_t) y * ADAM7_DY[i]) * (size_t) w + + ADAM7_IX[i] + (size_t) x * ADAM7_DX[i]) + * bytewidth; + for (b = 0; b < bytewidth; ++b) { + out[pixeloutstart + b] = in[pixelinstart + b]; + } + } + } + } + } else /*bpp < 8: Adam7 with pixels < 8 bit is a bit trickier: with bit + pointers*/ + { + for (i = 0; i != 7; ++i) { + unsigned x, y, b; + unsigned ilinebits = bpp * passw[i]; + unsigned olinebits = bpp * w; + size_t obp, ibp; /*bit pointers (for out and in buffer)*/ + for (y = 0; y < passh[i]; ++y) { + for (x = 0; x < passw[i]; ++x) { + ibp = (8 * passstart[i]) + (y * ilinebits + x * bpp); + obp = (ADAM7_IY[i] + (size_t) y * ADAM7_DY[i]) * olinebits + + (ADAM7_IX[i] + (size_t) x * ADAM7_DX[i]) * bpp; + for (b = 0; b < bpp; ++b) { + unsigned char bit = readBitFromReversedStream(&ibp, in); + setBitOfReversedStream(&obp, out, bit); + } + } + } + } + } +} + +static void removePaddingBits(unsigned char* out, + const unsigned char* in, + size_t olinebits, + size_t ilinebits, + unsigned h) +{ + /* + After filtering there are still padding bits if scanlines have non multiple of + 8 bit amounts. They need to be removed (except at last scanline of + (Adam7-reduced) image) before working with pure image buffers for the Adam7 + code, the color convert code and the output to the user. in and out are + allowed to be the same buffer, in may also be higher but still overlapping; in + must have >= ilinebits*h bits, out must have >= olinebits*h bits, olinebits + must be <= ilinebits also used to move bits after earlier such operations + happened, e.g. in a sequence of reduced images from Adam7 only useful if + (ilinebits - olinebits) is a value in the range 1..7 + */ + unsigned y; + size_t diff = ilinebits - olinebits; + size_t ibp = 0, obp = 0; /*input and output bit pointers*/ + for (y = 0; y < h; ++y) { + size_t x; + for (x = 0; x < olinebits; ++x) { + unsigned char bit = readBitFromReversedStream(&ibp, in); + setBitOfReversedStream(&obp, out, bit); + } + ibp += diff; + } +} + +/*out must be buffer big enough to contain full image, and in must contain the +full decompressed data from the IDAT chunks (with filter index bytes and +possible padding bits) return value is error*/ +static unsigned postProcessScanlines(unsigned char* out, + unsigned char* in, + unsigned w, + unsigned h, + const LodePNGInfo* info_png) +{ + /* + This function converts the filtered-padded-interlaced data into pure 2D image + buffer with the PNG's colortype. Steps: + *) if no Adam7: 1) unfilter 2) remove padding bits (= possible extra bits per + scanline if bpp < 8) + *) if adam7: 1) 7x unfilter 2) 7x remove padding bits 3) Adam7_deinterlace + NOTE: the in buffer will be overwritten with intermediate data! + */ + unsigned bpp = lodepng_get_bpp(&info_png->color); + if (bpp == 0) { + return 31; /*error: invalid colortype*/ + } + + if (info_png->interlace_method == 0) { + if (bpp < 8 && w * bpp != ((w * bpp + 7u) / 8u) * 8u) { + CERROR_TRY_RETURN(unfilter(in, in, w, h, bpp)); + removePaddingBits(out, in, w * bpp, ((w * bpp + 7u) / 8u) * 8u, h); + } + /*we can immediately filter into the out buffer, no other steps needed*/ + else { + CERROR_TRY_RETURN(unfilter(out, in, w, h, bpp)); + } + } else /*interlace_method is 1 (Adam7)*/ { + unsigned passw[7], passh[7]; + size_t filter_passstart[8], padded_passstart[8], passstart[8]; + unsigned i; + + Adam7_getpassvalues( + passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); + + for (i = 0; i != 7; ++i) { + CERROR_TRY_RETURN(unfilter(&in[padded_passstart[i]], + &in[filter_passstart[i]], + passw[i], + passh[i], + bpp)); + /*TODO: possible efficiency improvement: if in this reduced image the bits + fit nicely in 1 scanline, move bytes instead of bits or move not at all*/ + if (bpp < 8) { + /*remove padding bits in scanlines; after this there still may be + padding bits between the different reduced images: each reduced image + still starts nicely at a byte*/ + removePaddingBits(&in[passstart[i]], + &in[padded_passstart[i]], + passw[i] * bpp, + ((passw[i] * bpp + 7u) / 8u) * 8u, + passh[i]); + } + } + + Adam7_deinterlace(out, in, w, h, bpp); + } + + return 0; +} + +static unsigned readChunk_PLTE(LodePNGColorMode* color, + const unsigned char* data, + size_t chunkLength) +{ + unsigned pos = 0, i; + color->palettesize = chunkLength / 3u; + if (color->palettesize == 0 || color->palettesize > 256) { + return 38; /*error: palette too small or big*/ + } + lodepng_color_mode_alloc_palette(color); + if (!color->palette && color->palettesize) { + color->palettesize = 0; + return 83; /*alloc fail*/ + } + + for (i = 0; i != color->palettesize; ++i) { + color->palette[4 * i + 0] = data[pos++]; /*R*/ + color->palette[4 * i + 1] = data[pos++]; /*G*/ + color->palette[4 * i + 2] = data[pos++]; /*B*/ + color->palette[4 * i + 3] = 255; /*alpha*/ + } + + return 0; /* OK */ +} + +static unsigned readChunk_tRNS(LodePNGColorMode* color, + const unsigned char* data, + size_t chunkLength) +{ + unsigned i; + if (color->colortype == LCT_PALETTE) { + /*error: more alpha values given than there are palette entries*/ + if (chunkLength > color->palettesize) { + return 39; + } + + for (i = 0; i != chunkLength; ++i) { + color->palette[4 * i + 3] = data[i]; + } + } else if (color->colortype == LCT_GREY) { + /*error: this chunk must be 2 bytes for grayscale image*/ + if (chunkLength != 2) { + return 30; + } + + color->key_defined = 1; + color->key_r = color->key_g = color->key_b = 256u * data[0] + data[1]; + } else if (color->colortype == LCT_RGB) { + /*error: this chunk must be 6 bytes for RGB image*/ + if (chunkLength != 6) { + return 41; + } + + color->key_defined = 1; + color->key_r = 256u * data[0] + data[1]; + color->key_g = 256u * data[2] + data[3]; + color->key_b = 256u * data[4] + data[5]; + } else { + return 42; /*error: tRNS chunk not allowed for other color models*/ + } + + return 0; /* OK */ +} + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +/*background color chunk (bKGD)*/ +static unsigned readChunk_bKGD(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (info->color.colortype == LCT_PALETTE) { + /*error: this chunk must be 1 byte for indexed color image*/ + if (chunkLength != 1) { + return 43; + } + + /*error: invalid palette index, or maybe this chunk appeared before PLTE*/ + if (data[0] >= info->color.palettesize) { + return 103; + } + + info->background_defined = 1; + info->background_r = info->background_g = info->background_b = data[0]; + } else if (info->color.colortype == LCT_GREY + || info->color.colortype == LCT_GREY_ALPHA) { + /*error: this chunk must be 2 bytes for grayscale image*/ + if (chunkLength != 2) { + return 44; + } + + /*the values are truncated to bitdepth in the PNG file*/ + info->background_defined = 1; + info->background_r = info->background_g = info->background_b + = 256u * data[0] + data[1]; + } else if (info->color.colortype == LCT_RGB + || info->color.colortype == LCT_RGBA) { + /*error: this chunk must be 6 bytes for grayscale image*/ + if (chunkLength != 6) { + return 45; + } + + /*the values are truncated to bitdepth in the PNG file*/ + info->background_defined = 1; + info->background_r = 256u * data[0] + data[1]; + info->background_g = 256u * data[2] + data[3]; + info->background_b = 256u * data[4] + data[5]; + } + + return 0; /* OK */ +} + +/*text chunk (tEXt)*/ +static unsigned readChunk_tEXt(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + unsigned error = 0; + char *key = 0, *str = 0; + + while (!error) /*not really a while loop, only used to break on error*/ { + unsigned length, string2_begin; + + length = 0; + while (length < chunkLength && data[length] != 0) { + ++length; + } + /*even though it's not allowed by the standard, no error is thrown if + there's no null termination char, if the text is empty*/ + if (length < 1 || length > 79) { + CERROR_BREAK(error, 89); /*keyword too short or long*/ + } + + key = (char*) lodepng_malloc(length + 1); + if (!key) { + CERROR_BREAK(error, 83); /*alloc fail*/ + } + + lodepng_memcpy(key, data, length); + key[length] = 0; + + string2_begin = length + 1; /*skip keyword null terminator*/ + + length = (unsigned) (chunkLength < string2_begin + ? 0 + : chunkLength - string2_begin); + str = (char*) lodepng_malloc(length + 1); + if (!str) { + CERROR_BREAK(error, 83); /*alloc fail*/ + } + + lodepng_memcpy(str, data + string2_begin, length); + str[length] = 0; + + error = lodepng_add_text(info, key, str); + + break; + } + + lodepng_free(key); + lodepng_free(str); + + return error; +} + +/*compressed text chunk (zTXt)*/ +static unsigned readChunk_zTXt(LodePNGInfo* info, + const LodePNGDecoderSettings* decoder, + const unsigned char* data, + size_t chunkLength) +{ + unsigned error = 0; + + /*copy the object to change parameters in it*/ + LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; + + unsigned length, string2_begin; + char* key = 0; + unsigned char* str = 0; + size_t size = 0; + + while (!error) /*not really a while loop, only used to break on error*/ { + for (length = 0; length < chunkLength && data[length] != 0; ++length) + ; + if (length + 2 >= chunkLength) { + CERROR_BREAK(error, 75); /*no null termination, corrupt?*/ + } + if (length < 1 || length > 79) { + CERROR_BREAK(error, 89); /*keyword too short or long*/ + } + + key = (char*) lodepng_malloc(length + 1); + if (!key) { + CERROR_BREAK(error, 83); /*alloc fail*/ + } + + lodepng_memcpy(key, data, length); + key[length] = 0; + + if (data[length + 1] != 0) { + CERROR_BREAK(error, 72); /*the 0 byte indicating compression must be 0*/ + } + + string2_begin = length + 2; + if (string2_begin > chunkLength) { + CERROR_BREAK(error, 75); /*no null termination, corrupt?*/ + } + + length = (unsigned) chunkLength - string2_begin; + zlibsettings.max_output_size = decoder->max_text_size; + /*will fail if zlib error, e.g. if length is too small*/ + error = zlib_decompress( + &str, &size, 0, &data[string2_begin], length, &zlibsettings); + /*error: compressed text larger than decoder->max_text_size*/ + if (error && size > zlibsettings.max_output_size) { + error = 112; + } + if (error) { + break; + } + error = lodepng_add_text_sized(info, key, (char*) str, size); + break; + } + + lodepng_free(key); + lodepng_free(str); + + return error; +} + +/*international text chunk (iTXt)*/ +static unsigned readChunk_iTXt(LodePNGInfo* info, + const LodePNGDecoderSettings* decoder, + const unsigned char* data, + size_t chunkLength) +{ + unsigned error = 0; + unsigned i; + + /*copy the object to change parameters in it*/ + LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; + + unsigned length, begin, compressed; + char *key = 0, *langtag = 0, *transkey = 0; + + while (!error) /*not really a while loop, only used to break on error*/ { + /*Quick check if the chunk length isn't too small. Even without check + it'd still fail with other error checks below if it's too short. This just + gives a different error code.*/ + if (chunkLength < 5) { + CERROR_BREAK(error, 30); /*iTXt chunk too short*/ + } + + /*read the key*/ + for (length = 0; length < chunkLength && data[length] != 0; ++length) + ; + if (length + 3 >= chunkLength) { + CERROR_BREAK(error, 75); /*no null termination char, corrupt?*/ + } + if (length < 1 || length > 79) { + CERROR_BREAK(error, 89); /*keyword too short or long*/ + } + + key = (char*) lodepng_malloc(length + 1); + if (!key) { + CERROR_BREAK(error, 83); /*alloc fail*/ + } + + lodepng_memcpy(key, data, length); + key[length] = 0; + + /*read the compression method*/ + compressed = data[length + 1]; + if (data[length + 2] != 0) { + CERROR_BREAK(error, 72); /*the 0 byte indicating compression must be 0*/ + } + + /*even though it's not allowed by the standard, no error is thrown if + there's no null termination char, if the text is empty for the next 3 + texts*/ + + /*read the langtag*/ + begin = length + 3; + length = 0; + for (i = begin; i < chunkLength && data[i] != 0; ++i) { + ++length; + } + + langtag = (char*) lodepng_malloc(length + 1); + if (!langtag) { + CERROR_BREAK(error, 83); /*alloc fail*/ + } + + lodepng_memcpy(langtag, data + begin, length); + langtag[length] = 0; + + /*read the transkey*/ + begin += length + 1; + length = 0; + for (i = begin; i < chunkLength && data[i] != 0; ++i) { + ++length; + } + + transkey = (char*) lodepng_malloc(length + 1); + if (!transkey) { + CERROR_BREAK(error, 83); /*alloc fail*/ + } + + lodepng_memcpy(transkey, data + begin, length); + transkey[length] = 0; + + /*read the actual text*/ + begin += length + 1; + + length + = (unsigned) chunkLength < begin ? 0 : (unsigned) chunkLength - begin; + + if (compressed) { + unsigned char* str = 0; + size_t size = 0; + zlibsettings.max_output_size = decoder->max_text_size; + /*will fail if zlib error, e.g. if length is too small*/ + error = zlib_decompress( + &str, &size, 0, &data[begin], length, &zlibsettings); + /*error: compressed text larger than decoder->max_text_size*/ + if (error && size > zlibsettings.max_output_size) { + error = 112; + } + if (!error) { + error = lodepng_add_itext_sized( + info, key, langtag, transkey, (char*) str, size); + } + lodepng_free(str); + } else { + error = lodepng_add_itext_sized( + info, key, langtag, transkey, (const char*) (data + begin), length); + } + + break; + } + + lodepng_free(key); + lodepng_free(langtag); + lodepng_free(transkey); + + return error; +} + +static unsigned readChunk_tIME(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (chunkLength != 7) { + return 73; /*invalid tIME chunk size*/ + } + + info->time_defined = 1; + info->time.year = 256u * data[0] + data[1]; + info->time.month = data[2]; + info->time.day = data[3]; + info->time.hour = data[4]; + info->time.minute = data[5]; + info->time.second = data[6]; + + return 0; /* OK */ +} + +static unsigned readChunk_pHYs(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (chunkLength != 9) { + return 74; /*invalid pHYs chunk size*/ + } + + info->phys_defined = 1; + info->phys_x + = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; + info->phys_y + = 16777216u * data[4] + 65536u * data[5] + 256u * data[6] + data[7]; + info->phys_unit = data[8]; + + return 0; /* OK */ +} + +static unsigned readChunk_gAMA(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (chunkLength != 4) { + return 96; /*invalid gAMA chunk size*/ + } + + info->gama_defined = 1; + info->gama_gamma + = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; + + return 0; /* OK */ +} + +static unsigned readChunk_cHRM(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (chunkLength != 32) { + return 97; /*invalid cHRM chunk size*/ + } + + info->chrm_defined = 1; + info->chrm_white_x + = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; + info->chrm_white_y + = 16777216u * data[4] + 65536u * data[5] + 256u * data[6] + data[7]; + info->chrm_red_x + = 16777216u * data[8] + 65536u * data[9] + 256u * data[10] + data[11]; + info->chrm_red_y + = 16777216u * data[12] + 65536u * data[13] + 256u * data[14] + data[15]; + info->chrm_green_x + = 16777216u * data[16] + 65536u * data[17] + 256u * data[18] + data[19]; + info->chrm_green_y + = 16777216u * data[20] + 65536u * data[21] + 256u * data[22] + data[23]; + info->chrm_blue_x + = 16777216u * data[24] + 65536u * data[25] + 256u * data[26] + data[27]; + info->chrm_blue_y + = 16777216u * data[28] + 65536u * data[29] + 256u * data[30] + data[31]; + + return 0; /* OK */ +} + +static unsigned readChunk_sRGB(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (chunkLength != 1) { + return 98; /*invalid sRGB chunk size (this one is never ignored)*/ + } + + info->srgb_defined = 1; + info->srgb_intent = data[0]; + + return 0; /* OK */ +} + +static unsigned readChunk_iCCP(LodePNGInfo* info, + const LodePNGDecoderSettings* decoder, + const unsigned char* data, + size_t chunkLength) +{ + unsigned error = 0; + unsigned i; + size_t size = 0; + /*copy the object to change parameters in it*/ + LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; + + unsigned length, string2_begin; + + if (info->iccp_defined) { + lodepng_clear_icc(info); + } + info->iccp_defined = 1; + + for (length = 0; length < chunkLength && data[length] != 0; ++length) + ; + if (length + 2 >= chunkLength) { + return 75; /*no null termination, corrupt?*/ + } + if (length < 1 || length > 79) { + return 89; /*keyword too short or long*/ + } + + info->iccp_name = (char*) lodepng_malloc(length + 1); + if (!info->iccp_name) { + return 83; /*alloc fail*/ + } + + info->iccp_name[length] = 0; + for (i = 0; i != length; ++i) { + info->iccp_name[i] = (char) data[i]; + } + + if (data[length + 1] != 0) { + return 72; /*the 0 byte indicating compression must be 0*/ + } + + string2_begin = length + 2; + if (string2_begin > chunkLength) { + return 75; /*no null termination, corrupt?*/ + } + + length = (unsigned) chunkLength - string2_begin; + zlibsettings.max_output_size = decoder->max_icc_size; + error = zlib_decompress(&info->iccp_profile, + &size, + 0, + &data[string2_begin], + length, + &zlibsettings); + /*error: ICC profile larger than decoder->max_icc_size*/ + if (error && size > zlibsettings.max_output_size) { + error = 113; + } + info->iccp_profile_size = (unsigned) size; + if (!error && !info->iccp_profile_size) { + error = 100; /*invalid ICC profile size*/ + } + return error; +} + +static unsigned readChunk_cICP(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (chunkLength != 4) { + return 117; /*invalid cICP chunk size*/ + } + + info->cicp_defined = 1; + /* No error checking for value ranges is done here, that is up to a CICP + handling library, not the PNG decoding. Just pass on the metadata. */ + info->cicp_color_primaries = data[0]; + info->cicp_transfer_function = data[1]; + info->cicp_matrix_coefficients = data[2]; + info->cicp_video_full_range_flag = data[3]; + + return 0; /* OK */ +} + +static unsigned readChunk_mDCV(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (chunkLength != 24) { + return 119; /*invalid mDCV chunk size*/ + } + + info->mdcv_defined = 1; + info->mdcv_red_x = 256u * data[0] + data[1]; + info->mdcv_red_y = 256u * data[2] + data[3]; + info->mdcv_green_x = 256u * data[4] + data[5]; + info->mdcv_green_y = 256u * data[6] + data[7]; + info->mdcv_blue_x = 256u * data[8] + data[9]; + info->mdcv_blue_y = 256u * data[10] + data[11]; + info->mdcv_white_x = 256u * data[12] + data[13]; + info->mdcv_white_y = 256u * data[14] + data[15]; + info->mdcv_max_luminance + = 16777216u * data[16] + 65536u * data[17] + 256u * data[18] + data[19]; + info->mdcv_min_luminance + = 16777216u * data[20] + 65536u * data[21] + 256u * data[22] + data[23]; + + return 0; /* OK */ +} + +static unsigned readChunk_cLLI(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + if (chunkLength != 8) { + return 120; /*invalid cLLI chunk size*/ + } + + info->clli_defined = 1; + info->clli_max_cll + = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; + info->clli_max_fall + = 16777216u * data[4] + 65536u * data[5] + 256u * data[6] + data[7]; + + return 0; /* OK */ +} + +static unsigned readChunk_eXIf(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + return lodepng_set_exif(info, data, (unsigned) chunkLength); +} + +/*significant bits chunk (sBIT)*/ +static unsigned readChunk_sBIT(LodePNGInfo* info, + const unsigned char* data, + size_t chunkLength) +{ + unsigned bitdepth + = (info->color.colortype == LCT_PALETTE) ? 8 : info->color.bitdepth; + if (info->color.colortype == LCT_GREY) { + /*error: this chunk must be 1 bytes for grayscale image*/ + if (chunkLength != 1) { + return 114; + } + if (data[0] == 0 || data[0] > bitdepth) { + return 115; + } + info->sbit_defined = 1; + info->sbit_r = info->sbit_g = info->sbit_b + = data[0]; /*setting g and b is not required, but sensible*/ + } else if (info->color.colortype == LCT_RGB + || info->color.colortype == LCT_PALETTE) { + /*error: this chunk must be 3 bytes for RGB and palette image*/ + if (chunkLength != 3) { + return 114; + } + if (data[0] == 0 || data[1] == 0 || data[2] == 0) { + return 115; + } + if (data[0] > bitdepth || data[1] > bitdepth || data[2] > bitdepth) { + return 115; + } + info->sbit_defined = 1; + info->sbit_r = data[0]; + info->sbit_g = data[1]; + info->sbit_b = data[2]; + } else if (info->color.colortype == LCT_GREY_ALPHA) { + /*error: this chunk must be 2 byte for grayscale with alpha image*/ + if (chunkLength != 2) { + return 114; + } + if (data[0] == 0 || data[1] == 0) { + return 115; + } + if (data[0] > bitdepth || data[1] > bitdepth) { + return 115; + } + info->sbit_defined = 1; + info->sbit_r = info->sbit_g = info->sbit_b + = data[0]; /*setting g and b is not required, but sensible*/ + info->sbit_a = data[1]; + } else if (info->color.colortype == LCT_RGBA) { + /*error: this chunk must be 4 bytes for grayscale image*/ + if (chunkLength != 4) { + return 114; + } + if (data[0] == 0 || data[1] == 0 || data[2] == 0 || data[3] == 0) { + return 115; + } + if (data[0] > bitdepth || data[1] > bitdepth || data[2] > bitdepth + || data[3] > bitdepth) { + return 115; + } + info->sbit_defined = 1; + info->sbit_r = data[0]; + info->sbit_g = data[1]; + info->sbit_b = data[2]; + info->sbit_a = data[3]; + } + + return 0; /* OK */ +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +unsigned lodepng_inspect_chunk(LodePNGState* state, + size_t pos, + const unsigned char* in, + size_t insize) +{ + const unsigned char* chunk = in + pos; + unsigned chunkLength; + const unsigned char* data; + unsigned unhandled = 0; + unsigned error = 0; + + if (pos + 4 > insize) { + return 30; + } + chunkLength = lodepng_chunk_length(chunk); + if (chunkLength > 2147483647) { + return 63; + } + data = lodepng_chunk_data_const(chunk); + if (chunkLength + 12 > insize - pos) { + return 30; + } + + if (lodepng_chunk_type_equals(chunk, "PLTE")) { + error = readChunk_PLTE(&state->info_png.color, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "tRNS")) { + error = readChunk_tRNS(&state->info_png.color, data, chunkLength); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + } else if (lodepng_chunk_type_equals(chunk, "bKGD")) { + error = readChunk_bKGD(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "tEXt")) { + error = readChunk_tEXt(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "zTXt")) { + error + = readChunk_zTXt(&state->info_png, &state->decoder, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "iTXt")) { + error + = readChunk_iTXt(&state->info_png, &state->decoder, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "tIME")) { + error = readChunk_tIME(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "pHYs")) { + error = readChunk_pHYs(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "gAMA")) { + error = readChunk_gAMA(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "cHRM")) { + error = readChunk_cHRM(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "sRGB")) { + error = readChunk_sRGB(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "iCCP")) { + error + = readChunk_iCCP(&state->info_png, &state->decoder, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "cICP")) { + error = readChunk_cICP(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "mDCV")) { + error = readChunk_mDCV(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "cLLI")) { + error = readChunk_cLLI(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "eXIf")) { + error = readChunk_eXIf(&state->info_png, data, chunkLength); + } else if (lodepng_chunk_type_equals(chunk, "sBIT")) { + error = readChunk_sBIT(&state->info_png, data, chunkLength); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } else { + /* unhandled chunk is ok (is not an error) */ + unhandled = 1; + } + + if (!error && !unhandled && !state->decoder.ignore_crc) { + if (lodepng_chunk_check_crc(chunk)) { + return 57; /*invalid CRC*/ + } + } + + return error; +} + +/*read a PNG, the result will be in the same color type as the PNG (hence + * "generic")*/ +static void decodeGeneric(unsigned char** out, + unsigned* w, + unsigned* h, + LodePNGState* state, + const unsigned char* in, + size_t insize) +{ + unsigned char IEND = 0; + const unsigned char* chunk; /*points to beginning of next chunk*/ + unsigned char* idat; /*the data from idat chunks, zlib compressed*/ + size_t idatsize = 0; + unsigned char* scanlines = 0; + size_t scanlines_size = 0, expected_size = 0; + size_t outsize = 0; + + /*for unknown chunk order*/ + unsigned unknown = 0; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + unsigned critical_pos = 1; /*1 = after IHDR, 2 = after PLTE, 3 = after IDAT*/ +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + + /* safe output values in case error happens */ + *out = 0; + *w = *h = 0; + + state->error = lodepng_inspect( + w, + h, + state, + in, + insize); /*reads header and resets other parameters in state->info_png*/ + if (state->error) { + return; + } + + if (lodepng_pixel_overflow( + *w, *h, &state->info_png.color, &state->info_raw)) { + CERROR_RETURN(state->error, + 92); /*overflow possible due to amount of pixels*/ + } + + /*the input filesize is a safe upper bound for the sum of idat chunks size*/ + idat = (unsigned char*) lodepng_malloc(insize); + if (!idat) { + CERROR_RETURN(state->error, 83); /*alloc fail*/ + } + + chunk = &in[33]; /*first byte of the first chunk after the header*/ + + /*loop through the chunks, ignoring unknown chunks and stopping at IEND chunk. + IDAT data is put at the start of the in buffer*/ + while (!IEND && !state->error) { + unsigned chunkLength; + const unsigned char* data; /*the data in the chunk*/ + size_t pos = (size_t) (chunk - in); + + /*error: next chunk out of bounds of the in buffer*/ + if (chunk < in || pos + 12 > insize) { + if (state->decoder.ignore_end) { + break; /*other errors may still happen though*/ + } + CERROR_BREAK(state->error, 30); + } + + /*length of the data of the chunk, excluding the 12 bytes for length, chunk + * type and CRC*/ + chunkLength = lodepng_chunk_length(chunk); + /*error: chunk length larger than the max PNG chunk size*/ + if (chunkLength > 2147483647) { + if (state->decoder.ignore_end) { + break; /*other errors may still happen though*/ + } + CERROR_BREAK(state->error, 63); + } + + if (pos + (size_t) chunkLength + 12 > insize + || pos + (size_t) chunkLength + 12 < pos) { + CERROR_BREAK(state->error, + 64); /*error: size of the in buffer too small to contain next + chunk (or int overflow)*/ + } + + data = lodepng_chunk_data_const(chunk); + + unknown = 0; + + /*IDAT chunk, containing compressed image data*/ + if (lodepng_chunk_type_equals(chunk, "IDAT")) { + size_t newsize; + if (lodepng_addofl(idatsize, chunkLength, &newsize)) { + CERROR_BREAK(state->error, 95); + } + if (newsize > insize) { + CERROR_BREAK(state->error, 95); + } + lodepng_memcpy(idat + idatsize, data, chunkLength); + idatsize += chunkLength; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + critical_pos = 3; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } else if (lodepng_chunk_type_equals(chunk, "IEND")) { + /*IEND chunk*/ + IEND = 1; + } else if (lodepng_chunk_type_equals(chunk, "PLTE")) { + /*palette chunk (PLTE)*/ + state->error = readChunk_PLTE(&state->info_png.color, data, chunkLength); + if (state->error) { + break; + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + critical_pos = 2; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } else if (lodepng_chunk_type_equals(chunk, "tRNS")) { + /*palette transparency chunk (tRNS). Even though this one is an ancillary + chunk , it is still compiled in without 'LODEPNG_COMPILE_ANCILLARY_CHUNKS' + because it contains essential color information that affects the alpha + channel of pixels. */ + state->error = readChunk_tRNS(&state->info_png.color, data, chunkLength); + if (state->error) { + break; + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*background color chunk (bKGD)*/ + } else if (lodepng_chunk_type_equals(chunk, "bKGD")) { + state->error = readChunk_bKGD(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "tEXt")) { + /*text chunk (tEXt)*/ + if (state->decoder.read_text_chunks) { + state->error = readChunk_tEXt(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } + } else if (lodepng_chunk_type_equals(chunk, "zTXt")) { + /*compressed text chunk (zTXt)*/ + if (state->decoder.read_text_chunks) { + state->error = readChunk_zTXt( + &state->info_png, &state->decoder, data, chunkLength); + if (state->error) { + break; + } + } + } else if (lodepng_chunk_type_equals(chunk, "iTXt")) { + /*international text chunk (iTXt)*/ + if (state->decoder.read_text_chunks) { + state->error = readChunk_iTXt( + &state->info_png, &state->decoder, data, chunkLength); + if (state->error) { + break; + } + } + } else if (lodepng_chunk_type_equals(chunk, "tIME")) { + state->error = readChunk_tIME(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "pHYs")) { + state->error = readChunk_pHYs(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "gAMA")) { + state->error = readChunk_gAMA(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "cHRM")) { + state->error = readChunk_cHRM(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "sRGB")) { + state->error = readChunk_sRGB(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "iCCP")) { + state->error = readChunk_iCCP( + &state->info_png, &state->decoder, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "cICP")) { + state->error = readChunk_cICP(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "mDCV")) { + state->error = readChunk_mDCV(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "cLLI")) { + state->error = readChunk_cLLI(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "eXIf")) { + state->error = readChunk_eXIf(&state->info_png, data, chunkLength); + if (state->error) { + break; + } + } else if (lodepng_chunk_type_equals(chunk, "sBIT")) { + state->error = readChunk_sBIT(&state->info_png, data, chunkLength); + if (state->error) { + break; + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } else /*it's not an implemented chunk type, so ignore it: skip over the + data*/ + { + if (!lodepng_chunk_type_name_valid(chunk)) { + CERROR_BREAK(state->error, 121); /* invalid chunk type name */ + } + if (lodepng_chunk_reserved(chunk)) { + CERROR_BREAK(state->error, 122); /* invalid third lowercase character */ + } + + /*error: unknown critical chunk (5th bit of first byte of chunk type is + * 0)*/ + if (!state->decoder.ignore_critical && !lodepng_chunk_ancillary(chunk)) { + CERROR_BREAK(state->error, 69); + } + + unknown = 1; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if (state->decoder.remember_unknown_chunks) { + state->error = lodepng_chunk_append( + &state->info_png.unknown_chunks_data[critical_pos - 1], + &state->info_png.unknown_chunks_size[critical_pos - 1], + chunk); + if (state->error) { + break; + } + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } + + if (!state->decoder.ignore_crc + && !unknown) /*check CRC if wanted, only on known chunk types*/ { + if (lodepng_chunk_check_crc(chunk)) { + CERROR_BREAK(state->error, 57); /*invalid CRC*/ + } + } + + if (!IEND) { + chunk = lodepng_chunk_next_const(chunk, in + insize); + } + } + + if (!state->error && state->info_png.color.colortype == LCT_PALETTE + && !state->info_png.color.palette) { + state->error = 106; /* error: PNG file must have PLTE chunk if color type is + palette */ + } + + if (!state->error) { + /*predict output size, to allocate exact size for output buffer to avoid + more dynamic allocation. If the decompressed size does not match the + prediction, the image must be corrupt.*/ + if (state->info_png.interlace_method == 0) { + unsigned bpp = lodepng_get_bpp(&state->info_png.color); + expected_size = lodepng_get_raw_size_idat(*w, *h, bpp); + } else { + unsigned bpp = lodepng_get_bpp(&state->info_png.color); + /*Adam-7 interlaced: expected size is the sum of the 7 sub-images sizes*/ + expected_size = 0; + expected_size + += lodepng_get_raw_size_idat((*w + 7) >> 3, (*h + 7) >> 3, bpp); + if (*w > 4) { + expected_size + += lodepng_get_raw_size_idat((*w + 3) >> 3, (*h + 7) >> 3, bpp); + } + expected_size + += lodepng_get_raw_size_idat((*w + 3) >> 2, (*h + 3) >> 3, bpp); + if (*w > 2) { + expected_size + += lodepng_get_raw_size_idat((*w + 1) >> 2, (*h + 3) >> 2, bpp); + } + expected_size + += lodepng_get_raw_size_idat((*w + 1) >> 1, (*h + 1) >> 2, bpp); + if (*w > 1) { + expected_size + += lodepng_get_raw_size_idat((*w + 0) >> 1, (*h + 1) >> 1, bpp); + } + expected_size += lodepng_get_raw_size_idat((*w + 0), (*h + 0) >> 1, bpp); + } + + state->error = zlib_decompress(&scanlines, + &scanlines_size, + expected_size, + idat, + idatsize, + &state->decoder.zlibsettings); + } + if (!state->error && scanlines_size != expected_size) { + state->error = 91; /*decompressed size doesn't match prediction*/ + } + lodepng_free(idat); + + if (!state->error) { + outsize = lodepng_get_raw_size(*w, *h, &state->info_png.color); + *out = (unsigned char*) lodepng_malloc(outsize); + if (!*out) { + state->error = 83; /*alloc fail*/ + } + } + if (!state->error) { + lodepng_memset(*out, 0, outsize); + state->error + = postProcessScanlines(*out, scanlines, *w, *h, &state->info_png); + } + lodepng_free(scanlines); +} + +unsigned lodepng_decode(unsigned char** out, + unsigned* w, + unsigned* h, + LodePNGState* state, + const unsigned char* in, + size_t insize) +{ + *out = 0; + decodeGeneric(out, w, h, state, in, insize); + if (state->error) { + return state->error; + } + if (!state->decoder.color_convert + || lodepng_color_mode_equal(&state->info_raw, &state->info_png.color)) { + /*same color type, no copying or converting of data needed*/ + /*store the info_png color settings on the info_raw so that the info_raw + still reflects what colortype the raw image has to the end user*/ + if (!state->decoder.color_convert) { + state->error + = lodepng_color_mode_copy(&state->info_raw, &state->info_png.color); + if (state->error) { + return state->error; + } + } + } else { /*color conversion needed*/ + unsigned char* data = *out; + size_t outsize; + + /*TODO: check if this works according to the statement in the documentation: + "The converter can convert from grayscale input color type, to 8-bit + grayscale or grayscale with alpha"*/ + if (!(state->info_raw.colortype == LCT_RGB + || state->info_raw.colortype == LCT_RGBA) + && !(state->info_raw.bitdepth == 8)) { + return 56; /*unsupported color mode conversion*/ + } + + outsize = lodepng_get_raw_size(*w, *h, &state->info_raw); + *out = (unsigned char*) lodepng_malloc(outsize); + if (!(*out)) { + state->error = 83; /*alloc fail*/ + } else { + state->error = lodepng_convert( + *out, data, &state->info_raw, &state->info_png.color, *w, *h); + } + lodepng_free(data); + } + return state->error; +} + +unsigned lodepng_decode_memory(unsigned char** out, + unsigned* w, + unsigned* h, + const unsigned char* in, + size_t insize, + LodePNGColorType colortype, + unsigned bitdepth) +{ + unsigned error; + LodePNGState state; + lodepng_state_init(&state); + state.info_raw.colortype = colortype; + state.info_raw.bitdepth = bitdepth; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*disable reading things that this function doesn't output*/ + state.decoder.read_text_chunks = 0; + state.decoder.remember_unknown_chunks = 0; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + error = lodepng_decode(out, w, h, &state, in, insize); + lodepng_state_cleanup(&state); + return error; +} + +unsigned lodepng_decode32(unsigned char** out, + unsigned* w, + unsigned* h, + const unsigned char* in, + size_t insize) +{ + return lodepng_decode_memory(out, w, h, in, insize, LCT_RGBA, 8); +} + +unsigned lodepng_decode24(unsigned char** out, + unsigned* w, + unsigned* h, + const unsigned char* in, + size_t insize) +{ + return lodepng_decode_memory(out, w, h, in, insize, LCT_RGB, 8); +} + +#ifdef LODEPNG_COMPILE_DISK +unsigned lodepng_decode_file(unsigned char** out, + unsigned* w, + unsigned* h, + const char* filename, + LodePNGColorType colortype, + unsigned bitdepth) +{ + unsigned char* buffer = 0; + size_t buffersize; + unsigned error; + /* safe output values in case error happens */ + *out = 0; + *w = *h = 0; + error = lodepng_load_file(&buffer, &buffersize, filename); + if (!error) { + error = lodepng_decode_memory( + out, w, h, buffer, buffersize, colortype, bitdepth); + } + lodepng_free(buffer); + return error; +} + +unsigned lodepng_decode32_file(unsigned char** out, + unsigned* w, + unsigned* h, + const char* filename) +{ + return lodepng_decode_file(out, w, h, filename, LCT_RGBA, 8); +} + +unsigned lodepng_decode24_file(unsigned char** out, + unsigned* w, + unsigned* h, + const char* filename) +{ + return lodepng_decode_file(out, w, h, filename, LCT_RGB, 8); +} +#endif /*LODEPNG_COMPILE_DISK*/ + +void lodepng_decoder_settings_init(LodePNGDecoderSettings* settings) +{ + settings->color_convert = 1; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + settings->read_text_chunks = 1; + settings->remember_unknown_chunks = 0; + settings->max_text_size = 16777216; + settings->max_icc_size = 16777216; /* 16MB is much more than enough for any + reasonable ICC profile */ +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + settings->ignore_crc = 0; + settings->ignore_critical = 0; + settings->ignore_end = 0; + lodepng_decompress_settings_init(&settings->zlibsettings); +} + +#endif /*LODEPNG_COMPILE_DECODER*/ + +#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) + +void lodepng_state_init(LodePNGState* state) +{ +#ifdef LODEPNG_COMPILE_DECODER + lodepng_decoder_settings_init(&state->decoder); +#endif /*LODEPNG_COMPILE_DECODER*/ +#ifdef LODEPNG_COMPILE_ENCODER + lodepng_encoder_settings_init(&state->encoder); +#endif /*LODEPNG_COMPILE_ENCODER*/ + lodepng_color_mode_init(&state->info_raw); + lodepng_info_init(&state->info_png); + state->error = 1; +} + +void lodepng_state_cleanup(LodePNGState* state) +{ + lodepng_color_mode_cleanup(&state->info_raw); + lodepng_info_cleanup(&state->info_png); +} + +void lodepng_state_copy(LodePNGState* dest, const LodePNGState* source) +{ + lodepng_state_cleanup(dest); + *dest = *source; + lodepng_color_mode_init(&dest->info_raw); + lodepng_info_init(&dest->info_png); + dest->error = lodepng_color_mode_copy(&dest->info_raw, &source->info_raw); + if (dest->error) { + return; + } + dest->error = lodepng_info_copy(&dest->info_png, &source->info_png); + if (dest->error) { + return; + } +} + +#endif /* defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) \ + */ + +#ifdef LODEPNG_COMPILE_ENCODER + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / PNG Encoder / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +static unsigned writeSignature(ucvector* out) +{ + size_t pos = out->size; + const unsigned char signature[] = {137, 80, 78, 71, 13, 10, 26, 10}; + /*8 bytes PNG signature, aka the magic bytes*/ + if (!ucvector_resize(out, out->size + 8)) { + return 83; /*alloc fail*/ + } + lodepng_memcpy(out->data + pos, signature, 8); + return 0; +} + +static unsigned addChunk_IHDR(ucvector* out, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth, + unsigned interlace_method) +{ + unsigned char *chunk, *data; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 13, "IHDR")); + data = chunk + 8; + + lodepng_set32bitInt(data + 0, w); /*width*/ + lodepng_set32bitInt(data + 4, h); /*height*/ + data[8] = (unsigned char) bitdepth; /*bit depth*/ + data[9] = (unsigned char) colortype; /*color type*/ + data[10] = 0; /*compression method*/ + data[11] = 0; /*filter method*/ + data[12] = interlace_method; /*interlace method*/ + + lodepng_chunk_generate_crc(chunk); + return 0; +} + +/* only adds the chunk if needed (there is a key or palette with alpha) */ +static unsigned addChunk_PLTE(ucvector* out, const LodePNGColorMode* info) +{ + unsigned char* chunk; + size_t i, j = 8; + + if (info->palettesize == 0 || info->palettesize > 256) { + return 68; /*invalid palette size, it is only allowed to be 1-256*/ + } + + CERROR_TRY_RETURN( + lodepng_chunk_init(&chunk, out, info->palettesize * 3, "PLTE")); + + for (i = 0; i != info->palettesize; ++i) { + /*add all channels except alpha channel*/ + chunk[j++] = info->palette[i * 4 + 0]; + chunk[j++] = info->palette[i * 4 + 1]; + chunk[j++] = info->palette[i * 4 + 2]; + } + + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_tRNS(ucvector* out, const LodePNGColorMode* info) +{ + unsigned char* chunk = 0; + + if (info->colortype == LCT_PALETTE) { + size_t i, amount = info->palettesize; + /*the tail of palette values that all have 255 as alpha, does not have to be + * encoded*/ + for (i = info->palettesize; i != 0; --i) { + if (info->palette[4 * (i - 1) + 3] != 255) { + break; + } + --amount; + } + if (amount) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, amount, "tRNS")); + /*add the alpha channel values from the palette*/ + for (i = 0; i != amount; ++i) { + chunk[8 + i] = info->palette[4 * i + 3]; + } + } + } else if (info->colortype == LCT_GREY) { + if (info->key_defined) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "tRNS")); + chunk[8] = (unsigned char) (info->key_r >> 8); + chunk[9] = (unsigned char) (info->key_r & 255); + } + } else if (info->colortype == LCT_RGB) { + if (info->key_defined) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 6, "tRNS")); + chunk[8] = (unsigned char) (info->key_r >> 8); + chunk[9] = (unsigned char) (info->key_r & 255); + chunk[10] = (unsigned char) (info->key_g >> 8); + chunk[11] = (unsigned char) (info->key_g & 255); + chunk[12] = (unsigned char) (info->key_b >> 8); + chunk[13] = (unsigned char) (info->key_b & 255); + } + } + + if (chunk) { + lodepng_chunk_generate_crc(chunk); + } + return 0; +} + +static unsigned addChunk_IDAT(ucvector* out, + const unsigned char* data, + size_t datasize, + LodePNGCompressSettings* zlibsettings) +{ + unsigned error = 0; + unsigned char* zlib = 0; + size_t pos = 0; + size_t zlibsize = 0; + /* max chunk length allowed by the specification is 2147483647 bytes */ + const size_t max_chunk_length = 2147483647u; + + error = zlib_compress(&zlib, &zlibsize, data, datasize, zlibsettings); + while (!error) { + if (zlibsize - pos > max_chunk_length) { + error = lodepng_chunk_createv(out, max_chunk_length, "IDAT", zlib + pos); + pos += max_chunk_length; + } else { + error = lodepng_chunk_createv(out, zlibsize - pos, "IDAT", zlib + pos); + break; + } + } + lodepng_free(zlib); + return error; +} + +static unsigned addChunk_IEND(ucvector* out) +{ + return lodepng_chunk_createv(out, 0, "IEND", 0); +} + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + +static unsigned addChunk_tEXt(ucvector* out, + const char* keyword, + const char* textstring) +{ + unsigned char* chunk = 0; + size_t keysize = lodepng_strlen(keyword), + textsize = lodepng_strlen(textstring); + size_t size = keysize + 1 + textsize; + if (keysize < 1 || keysize > 79) { + return 89; /*error: invalid keyword size*/ + } + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, size, "tEXt")); + lodepng_memcpy(chunk + 8, keyword, keysize); + chunk[8 + keysize] = 0; /*null termination char*/ + lodepng_memcpy(chunk + 9 + keysize, textstring, textsize); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_zTXt(ucvector* out, + const char* keyword, + const char* textstring, + LodePNGCompressSettings* zlibsettings) +{ + unsigned error = 0; + unsigned char* chunk = 0; + unsigned char* compressed = 0; + size_t compressedsize = 0; + size_t textsize = lodepng_strlen(textstring); + size_t keysize = lodepng_strlen(keyword); + if (keysize < 1 || keysize > 79) { + return 89; /*error: invalid keyword size*/ + } + + error = zlib_compress(&compressed, + &compressedsize, + (const unsigned char*) textstring, + textsize, + zlibsettings); + if (!error) { + size_t size = keysize + 2 + compressedsize; + error = lodepng_chunk_init(&chunk, out, size, "zTXt"); + } + if (!error) { + lodepng_memcpy(chunk + 8, keyword, keysize); + chunk[8 + keysize] = 0; /*null termination char*/ + chunk[9 + keysize] = 0; /*compression method: 0*/ + lodepng_memcpy(chunk + 10 + keysize, compressed, compressedsize); + lodepng_chunk_generate_crc(chunk); + } + + lodepng_free(compressed); + return error; +} + +static unsigned addChunk_iTXt(ucvector* out, + unsigned compress, + const char* keyword, + const char* langtag, + const char* transkey, + const char* textstring, + LodePNGCompressSettings* zlibsettings) +{ + unsigned error = 0; + unsigned char* chunk = 0; + unsigned char* compressed = 0; + size_t compressedsize = 0; + size_t textsize = lodepng_strlen(textstring); + size_t keysize = lodepng_strlen(keyword), langsize = lodepng_strlen(langtag), + transsize = lodepng_strlen(transkey); + + if (keysize < 1 || keysize > 79) { + return 89; /*error: invalid keyword size*/ + } + + if (compress) { + error = zlib_compress(&compressed, + &compressedsize, + (const unsigned char*) textstring, + textsize, + zlibsettings); + } + if (!error) { + size_t size = keysize + 3 + langsize + 1 + transsize + 1 + + (compress ? compressedsize : textsize); + error = lodepng_chunk_init(&chunk, out, size, "iTXt"); + } + if (!error) { + size_t pos = 8; + lodepng_memcpy(chunk + pos, keyword, keysize); + pos += keysize; + chunk[pos++] = 0; /*null termination char*/ + chunk[pos++] = (compress ? 1 : 0); /*compression flag*/ + chunk[pos++] = 0; /*compression method: 0*/ + lodepng_memcpy(chunk + pos, langtag, langsize); + pos += langsize; + chunk[pos++] = 0; /*null termination char*/ + lodepng_memcpy(chunk + pos, transkey, transsize); + pos += transsize; + chunk[pos++] = 0; /*null termination char*/ + if (compress) { + lodepng_memcpy(chunk + pos, compressed, compressedsize); + } else { + lodepng_memcpy(chunk + pos, textstring, textsize); + } + lodepng_chunk_generate_crc(chunk); + } + + lodepng_free(compressed); + return error; +} + +static unsigned addChunk_bKGD(ucvector* out, const LodePNGInfo* info) +{ + unsigned char* chunk = 0; + if (info->color.colortype == LCT_GREY + || info->color.colortype == LCT_GREY_ALPHA) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "bKGD")); + chunk[8] = (unsigned char) (info->background_r >> 8); + chunk[9] = (unsigned char) (info->background_r & 255); + } else if (info->color.colortype == LCT_RGB + || info->color.colortype == LCT_RGBA) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 6, "bKGD")); + chunk[8] = (unsigned char) (info->background_r >> 8); + chunk[9] = (unsigned char) (info->background_r & 255); + chunk[10] = (unsigned char) (info->background_g >> 8); + chunk[11] = (unsigned char) (info->background_g & 255); + chunk[12] = (unsigned char) (info->background_b >> 8); + chunk[13] = (unsigned char) (info->background_b & 255); + } else if (info->color.colortype == LCT_PALETTE) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 1, "bKGD")); + chunk[8] = (unsigned char) (info->background_r & 255); /*palette index*/ + } + if (chunk) { + lodepng_chunk_generate_crc(chunk); + } + return 0; +} + +static unsigned addChunk_tIME(ucvector* out, const LodePNGTime* time) +{ + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 7, "tIME")); + chunk[8] = (unsigned char) (time->year >> 8); + chunk[9] = (unsigned char) (time->year & 255); + chunk[10] = (unsigned char) time->month; + chunk[11] = (unsigned char) time->day; + chunk[12] = (unsigned char) time->hour; + chunk[13] = (unsigned char) time->minute; + chunk[14] = (unsigned char) time->second; + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_pHYs(ucvector* out, const LodePNGInfo* info) +{ + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 9, "pHYs")); + lodepng_set32bitInt(chunk + 8, info->phys_x); + lodepng_set32bitInt(chunk + 12, info->phys_y); + chunk[16] = info->phys_unit; + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_gAMA(ucvector* out, const LodePNGInfo* info) +{ + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "gAMA")); + lodepng_set32bitInt(chunk + 8, info->gama_gamma); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_cHRM(ucvector* out, const LodePNGInfo* info) +{ + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 32, "cHRM")); + lodepng_set32bitInt(chunk + 8, info->chrm_white_x); + lodepng_set32bitInt(chunk + 12, info->chrm_white_y); + lodepng_set32bitInt(chunk + 16, info->chrm_red_x); + lodepng_set32bitInt(chunk + 20, info->chrm_red_y); + lodepng_set32bitInt(chunk + 24, info->chrm_green_x); + lodepng_set32bitInt(chunk + 28, info->chrm_green_y); + lodepng_set32bitInt(chunk + 32, info->chrm_blue_x); + lodepng_set32bitInt(chunk + 36, info->chrm_blue_y); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_sRGB(ucvector* out, const LodePNGInfo* info) +{ + unsigned char data = info->srgb_intent; + return lodepng_chunk_createv(out, 1, "sRGB", &data); +} + +static unsigned addChunk_iCCP(ucvector* out, + const LodePNGInfo* info, + LodePNGCompressSettings* zlibsettings) +{ + unsigned error = 0; + unsigned char* chunk = 0; + unsigned char* compressed = 0; + size_t compressedsize = 0; + size_t keysize = lodepng_strlen(info->iccp_name); + + if (keysize < 1 || keysize > 79) { + return 89; /*error: invalid keyword size*/ + } + error = zlib_compress(&compressed, + &compressedsize, + info->iccp_profile, + info->iccp_profile_size, + zlibsettings); + if (!error) { + size_t size = keysize + 2 + compressedsize; + error = lodepng_chunk_init(&chunk, out, size, "iCCP"); + } + if (!error) { + lodepng_memcpy(chunk + 8, info->iccp_name, keysize); + chunk[8 + keysize] = 0; /*null termination char*/ + chunk[9 + keysize] = 0; /*compression method: 0*/ + lodepng_memcpy(chunk + 10 + keysize, compressed, compressedsize); + lodepng_chunk_generate_crc(chunk); + } + + lodepng_free(compressed); + return error; +} + +static unsigned addChunk_cICP(ucvector* out, const LodePNGInfo* info) +{ + unsigned char* chunk; + /* Allow up to 255 since they are bytes. The ITU-R-BT.709 spec has a more + restricted set of valid values for each field, but that's up to the error + handling of a CICP library, not the PNG encoding/decoding, to manage. */ + if (info->cicp_color_primaries > 255) { + return 116; + } + if (info->cicp_transfer_function > 255) { + return 116; + } + if (info->cicp_matrix_coefficients > 255) { + return 116; + } + if (info->cicp_video_full_range_flag > 255) { + return 116; + } + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "cICP")); + chunk[8 + 0] = (unsigned char) info->cicp_color_primaries; + chunk[8 + 1] = (unsigned char) info->cicp_transfer_function; + chunk[8 + 2] = (unsigned char) info->cicp_matrix_coefficients; + chunk[8 + 3] = (unsigned char) info->cicp_video_full_range_flag; + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_mDCV(ucvector* out, const LodePNGInfo* info) +{ + unsigned char* chunk; + /* Allow up to 65535 since they are 16-bit ints. */ + if (info->mdcv_red_x > 65535) { + return 118; + } + if (info->mdcv_red_y > 65535) { + return 118; + } + if (info->mdcv_green_x > 65535) { + return 118; + } + if (info->mdcv_green_y > 65535) { + return 118; + } + if (info->mdcv_blue_x > 65535) { + return 118; + } + if (info->mdcv_blue_y > 65535) { + return 118; + } + if (info->mdcv_white_x > 65535) { + return 118; + } + if (info->mdcv_white_y > 65535) { + return 118; + } + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 24, "mDCV")); + chunk[8 + 0] = (unsigned char) ((info->mdcv_red_x) >> 8u); + chunk[8 + 1] = (unsigned char) (info->mdcv_red_x); + chunk[8 + 2] = (unsigned char) ((info->mdcv_red_y) >> 8u); + chunk[8 + 3] = (unsigned char) (info->mdcv_red_y); + chunk[8 + 4] = (unsigned char) ((info->mdcv_green_x) >> 8u); + chunk[8 + 5] = (unsigned char) (info->mdcv_green_x); + chunk[8 + 6] = (unsigned char) ((info->mdcv_green_y) >> 8u); + chunk[8 + 7] = (unsigned char) (info->mdcv_green_y); + chunk[8 + 8] = (unsigned char) ((info->mdcv_blue_x) >> 8u); + chunk[8 + 9] = (unsigned char) (info->mdcv_blue_x); + chunk[8 + 10] = (unsigned char) ((info->mdcv_blue_y) >> 8u); + chunk[8 + 11] = (unsigned char) (info->mdcv_blue_y); + chunk[8 + 12] = (unsigned char) ((info->mdcv_white_x) >> 8u); + chunk[8 + 13] = (unsigned char) (info->mdcv_white_x); + chunk[8 + 14] = (unsigned char) ((info->mdcv_white_y) >> 8u); + chunk[8 + 15] = (unsigned char) (info->mdcv_white_y); + lodepng_set32bitInt(chunk + 8 + 16, info->mdcv_max_luminance); + lodepng_set32bitInt(chunk + 8 + 20, info->mdcv_min_luminance); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_cLLI(ucvector* out, const LodePNGInfo* info) +{ + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 8, "cLLI")); + lodepng_set32bitInt(chunk + 8 + 0, info->clli_max_cll); + lodepng_set32bitInt(chunk + 8 + 4, info->clli_max_fall); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_eXIf(ucvector* out, const LodePNGInfo* info) +{ + return lodepng_chunk_createv(out, info->exif_size, "eXIf", info->exif); +} + +static unsigned addChunk_sBIT(ucvector* out, const LodePNGInfo* info) +{ + unsigned bitdepth + = (info->color.colortype == LCT_PALETTE) ? 8 : info->color.bitdepth; + unsigned char* chunk = 0; + if (info->color.colortype == LCT_GREY) { + if (info->sbit_r == 0 || info->sbit_r > bitdepth) { + return 115; + } + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 1, "sBIT")); + chunk[8] = info->sbit_r; + } else if (info->color.colortype == LCT_RGB + || info->color.colortype == LCT_PALETTE) { + if (info->sbit_r == 0 || info->sbit_g == 0 || info->sbit_b == 0) { + return 115; + } + if (info->sbit_r > bitdepth || info->sbit_g > bitdepth + || info->sbit_b > bitdepth) { + return 115; + } + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 3, "sBIT")); + chunk[8] = info->sbit_r; + chunk[9] = info->sbit_g; + chunk[10] = info->sbit_b; + } else if (info->color.colortype == LCT_GREY_ALPHA) { + if (info->sbit_r == 0 || info->sbit_a == 0) { + return 115; + } + if (info->sbit_r > bitdepth || info->sbit_a > bitdepth) { + return 115; + } + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "sBIT")); + chunk[8] = info->sbit_r; + chunk[9] = info->sbit_a; + } else if (info->color.colortype == LCT_RGBA) { + if (info->sbit_r == 0 || info->sbit_g == 0 || info->sbit_b == 0 + || info->sbit_a == 0 || info->sbit_r > bitdepth + || info->sbit_g > bitdepth || info->sbit_b > bitdepth + || info->sbit_a > bitdepth) { + return 115; + } + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "sBIT")); + chunk[8] = info->sbit_r; + chunk[9] = info->sbit_g; + chunk[10] = info->sbit_b; + chunk[11] = info->sbit_a; + } + if (chunk) { + lodepng_chunk_generate_crc(chunk); + } + return 0; +} + +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +static void filterScanline(unsigned char* out, + const unsigned char* scanline, + const unsigned char* prevline, + size_t length, + size_t bytewidth, + unsigned char filterType) +{ + size_t i; + switch (filterType) { + case 0: /*None*/ + for (i = 0; i != length; ++i) { + out[i] = scanline[i]; + } + break; + case 1: /*Sub*/ + for (i = 0; i != bytewidth; ++i) { + out[i] = scanline[i]; + } + for (i = bytewidth; i < length; ++i) { + out[i] = scanline[i] - scanline[i - bytewidth]; + } + break; + case 2: /*Up*/ + if (prevline) { + for (i = 0; i != length; ++i) { + out[i] = scanline[i] - prevline[i]; + } + } else { + for (i = 0; i != length; ++i) { + out[i] = scanline[i]; + } + } + break; + case 3: /*Average*/ + if (prevline) { + for (i = 0; i != bytewidth; ++i) { + out[i] = scanline[i] - (prevline[i] >> 1); + } + for (i = bytewidth; i < length; ++i) { + out[i] = scanline[i] - ((scanline[i - bytewidth] + prevline[i]) >> 1); + } + } else { + for (i = 0; i != bytewidth; ++i) { + out[i] = scanline[i]; + } + for (i = bytewidth; i < length; ++i) { + out[i] = scanline[i] - (scanline[i - bytewidth] >> 1); + } + } + break; + case 4: /*Paeth*/ + if (prevline) { + /*paethPredictor(0, prevline[i], 0) is always prevline[i]*/ + for (i = 0; i != bytewidth; ++i) { + out[i] = (scanline[i] - prevline[i]); + } + for (i = bytewidth; i < length; ++i) { + out[i] = (scanline[i] + - paethPredictor(scanline[i - bytewidth], + prevline[i], + prevline[i - bytewidth])); + } + } else { + for (i = 0; i != bytewidth; ++i) { + out[i] = scanline[i]; + } + /*paethPredictor(scanline[i - bytewidth], 0, 0) is always scanline[i - + * bytewidth]*/ + for (i = bytewidth; i < length; ++i) { + out[i] = (scanline[i] - scanline[i - bytewidth]); + } + } + break; + default: + return; /*invalid filter type given*/ + } +} + +/* integer binary logarithm, max return value is 31 */ +static size_t ilog2(size_t i) +{ + size_t result = 0; + if (i >= 65536) { + result += 16; + i >>= 16; + } + if (i >= 256) { + result += 8; + i >>= 8; + } + if (i >= 16) { + result += 4; + i >>= 4; + } + if (i >= 4) { + result += 2; + i >>= 2; + } + if (i >= 2) { + result += 1; /*i >>= 1;*/ + } + return result; +} + +/* integer approximation for i * log2(i), helper function for LFS_ENTROPY */ +static size_t ilog2i(size_t i) +{ + size_t l; + if (i == 0) { + return 0; + } + l = ilog2(i); + /* approximate i*log2(i): l is integer logarithm, ((i - (1u << l)) << 1u) + linearly approximates the missing fractional part multiplied by i */ + return i * l + ((i - (((size_t) 1) << l)) << 1u); +} + +static unsigned filter(unsigned char* out, + const unsigned char* in, + unsigned w, + unsigned h, + const LodePNGColorMode* color, + const LodePNGEncoderSettings* settings) +{ + /* + For PNG filter method 0 + out must be a buffer with as size: h + (w * h * bpp + 7u) / 8u, because there + are the scanlines with 1 extra byte per scanline + */ + + unsigned bpp = lodepng_get_bpp(color); + /*the width of a scanline in bytes, not including the filter type*/ + size_t linebytes = lodepng_get_raw_size_idat(w, 1, bpp) - 1u; + + /*bytewidth is used for filtering, is 1 when bpp < 8, number of bytes per + * pixel otherwise*/ + size_t bytewidth = (bpp + 7u) / 8u; + const unsigned char* prevline = 0; + unsigned x, y; + unsigned error = 0; + LodePNGFilterStrategy strategy = settings->filter_strategy; + + if (settings->filter_palette_zero + && (color->colortype == LCT_PALETTE || color->bitdepth < 8)) { + /*if the filter_palette_zero setting is enabled, override the filter + strategy with zero for all scanlines for palette and less-than-8-bitdepth + images*/ + strategy = LFS_ZERO; + } + + if (bpp == 0) { + return 31; /*error: invalid color type*/ + } + + if (strategy >= LFS_ZERO && strategy <= LFS_FOUR) { + unsigned char type = (unsigned char) strategy; + for (y = 0; y != h; ++y) { + size_t outindex + = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ + size_t inindex = linebytes * y; + out[outindex] = type; /*filter type byte*/ + filterScanline(&out[outindex + 1], + &in[inindex], + prevline, + linebytes, + bytewidth, + type); + prevline = &in[inindex]; + } + } else if (strategy == LFS_MINSUM) { + /*adaptive filtering: independently for each row, try all five filter types + and select the one that produces the smallest sum of absolute values per + row.*/ + unsigned char* + attempt[5]; /*five filtering attempts, one for each filter type*/ + size_t smallest = 0; + unsigned char type, bestType = 0; + + for (type = 0; type != 5; ++type) { + attempt[type] = (unsigned char*) lodepng_malloc(linebytes); + if (!attempt[type]) { + error = 83; /*alloc fail*/ + } + } + + if (!error) { + for (y = 0; y != h; ++y) { + /*try the 5 filter types*/ + for (type = 0; type != 5; ++type) { + size_t sum = 0; + filterScanline(attempt[type], + &in[y * linebytes], + prevline, + linebytes, + bytewidth, + type); + + /*calculate the sum of the result*/ + if (type == 0) { + for (x = 0; x != linebytes; ++x) { + sum += (unsigned char) (attempt[type][x]); + } + } else { + for (x = 0; x != linebytes; ++x) { + /*For differences, each byte should be treated as signed, values + above 127 are negative (converted to signed char). Filtertype 0 + isn't a difference though, so use unsigned there. This means + filtertype 0 is almost never chosen, but that is justified.*/ + unsigned char s = attempt[type][x]; + sum += s < 128 ? s : (255U - s); + } + } + + /*check if this is smallest sum (or if type == 0 it's the first case + * so always store the values)*/ + if (type == 0 || sum < smallest) { + bestType = type; + smallest = sum; + } + } + + prevline = &in[y * linebytes]; + + /*now fill the out values*/ + out[y * (linebytes + 1)] + = bestType; /*the first byte of a scanline will be the filter type*/ + for (x = 0; x != linebytes; ++x) { + out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; + } + } + } + + for (type = 0; type != 5; ++type) { + lodepng_free(attempt[type]); + } + } else if (strategy == LFS_ENTROPY) { + unsigned char* + attempt[5]; /*five filtering attempts, one for each filter type*/ + size_t bestSum = 0; + unsigned type, bestType = 0; + unsigned count[256]; + + for (type = 0; type != 5; ++type) { + attempt[type] = (unsigned char*) lodepng_malloc(linebytes); + if (!attempt[type]) { + error = 83; /*alloc fail*/ + } + } + + if (!error) { + for (y = 0; y != h; ++y) { + /*try the 5 filter types*/ + for (type = 0; type != 5; ++type) { + size_t sum = 0; + filterScanline(attempt[type], + &in[y * linebytes], + prevline, + linebytes, + bytewidth, + type); + lodepng_memset(count, 0, 256 * sizeof(*count)); + for (x = 0; x != linebytes; ++x) { + ++count[attempt[type][x]]; + } + ++count[type]; /*the filter type itself is part of the scanline*/ + for (x = 0; x != 256; ++x) { + sum += ilog2i(count[x]); + } + /*check if this is smallest sum (or if type == 0 it's the first case + * so always store the values)*/ + if (type == 0 || sum > bestSum) { + bestType = type; + bestSum = sum; + } + } + + prevline = &in[y * linebytes]; + + /*now fill the out values*/ + out[y * (linebytes + 1)] + = bestType; /*the first byte of a scanline will be the filter type*/ + for (x = 0; x != linebytes; ++x) { + out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; + } + } + } + + for (type = 0; type != 5; ++type) { + lodepng_free(attempt[type]); + } + } else if (strategy == LFS_PREDEFINED) { + for (y = 0; y != h; ++y) { + size_t outindex + = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ + size_t inindex = linebytes * y; + unsigned char type = settings->predefined_filters[y]; + out[outindex] = type; /*filter type byte*/ + filterScanline(&out[outindex + 1], + &in[inindex], + prevline, + linebytes, + bytewidth, + type); + prevline = &in[inindex]; + } + } else if (strategy == LFS_BRUTE_FORCE) { + /*brute force filter chooser. + deflate the scanline after every filter attempt to see which one deflates + best. This is very slow and gives only slightly smaller, sometimes even + larger, result*/ + size_t size[5]; + unsigned char* + attempt[5]; /*five filtering attempts, one for each filter type*/ + size_t smallest = 0; + unsigned type = 0, bestType = 0; + unsigned char* dummy; + LodePNGCompressSettings zlibsettings; + lodepng_memcpy(&zlibsettings, + &settings->zlibsettings, + sizeof(LodePNGCompressSettings)); + /*use fixed tree on the attempts so that the tree is not adapted to the + filtertype on purpose, to simulate the true case where the tree is the same + for the whole image. Sometimes it gives better result with dynamic tree + anyway. Using the fixed tree sometimes gives worse, but in rare cases better + compression. It does make this a bit less slow, so it's worth doing this.*/ + zlibsettings.btype = 1; + /*a custom encoder likely doesn't read the btype setting and is optimized + for complete PNG images only, so disable it*/ + zlibsettings.custom_zlib = 0; + zlibsettings.custom_deflate = 0; + for (type = 0; type != 5; ++type) { + attempt[type] = (unsigned char*) lodepng_malloc(linebytes); + if (!attempt[type]) { + error = 83; /*alloc fail*/ + } + } + if (!error) { + for (y = 0; y != h; ++y) /*try the 5 filter types*/ { + for (type = 0; type != 5; ++type) { + unsigned testsize = (unsigned) linebytes; + /*if(testsize > 8) testsize /= 8;*/ /*it already works good enough by + testing a part of the row*/ + + filterScanline(attempt[type], + &in[y * linebytes], + prevline, + linebytes, + bytewidth, + type); + size[type] = 0; + dummy = 0; + zlib_compress( + &dummy, &size[type], attempt[type], testsize, &zlibsettings); + lodepng_free(dummy); + /*check if this is smallest size (or if type == 0 it's the first case + * so always store the values)*/ + if (type == 0 || size[type] < smallest) { + bestType = type; + smallest = size[type]; + } + } + prevline = &in[y * linebytes]; + out[y * (linebytes + 1)] + = bestType; /*the first byte of a scanline will be the filter type*/ + for (x = 0; x != linebytes; ++x) { + out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; + } + } + } + for (type = 0; type != 5; ++type) { + lodepng_free(attempt[type]); + } + } else { + return 88; /* unknown filter strategy */ + } + + return error; +} + +static void addPaddingBits(unsigned char* out, + const unsigned char* in, + size_t olinebits, + size_t ilinebits, + unsigned h) +{ + /*The opposite of the removePaddingBits function + olinebits must be >= ilinebits*/ + unsigned y; + size_t diff = olinebits - ilinebits; + size_t obp = 0, ibp = 0; /*bit pointers*/ + for (y = 0; y != h; ++y) { + size_t x; + for (x = 0; x < ilinebits; ++x) { + unsigned char bit = readBitFromReversedStream(&ibp, in); + setBitOfReversedStream(&obp, out, bit); + } + /*obp += diff; --> no, fill in some value in the padding bits too, to avoid + "Use of uninitialised value of size ###" warning from valgrind*/ + for (x = 0; x != diff; ++x) { + setBitOfReversedStream(&obp, out, 0); + } + } +} + +/* +in: non-interlaced image with size w*h +out: the same pixels, but re-ordered according to PNG's Adam7 interlacing, with + no padding bits between scanlines, but between reduced images so that each + reduced image starts at a byte. +bpp: bits per pixel +there are no padding bits, not between scanlines, not between reduced images +in has the following size in bits: w * h * bpp. +out is possibly bigger due to padding bits between reduced images +NOTE: comments about padding bits are only relevant if bpp < 8 +*/ +static void Adam7_interlace(unsigned char* out, + const unsigned char* in, + unsigned w, + unsigned h, + unsigned bpp) +{ + unsigned passw[7], passh[7]; + size_t filter_passstart[8], padded_passstart[8], passstart[8]; + unsigned i; + + Adam7_getpassvalues( + passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); + + if (bpp >= 8) { + for (i = 0; i != 7; ++i) { + unsigned x, y, b; + size_t bytewidth = bpp / 8u; + for (y = 0; y < passh[i]; ++y) { + for (x = 0; x < passw[i]; ++x) { + size_t pixelinstart = ((ADAM7_IY[i] + y * ADAM7_DY[i]) * w + + ADAM7_IX[i] + x * ADAM7_DX[i]) + * bytewidth; + size_t pixeloutstart = passstart[i] + (y * passw[i] + x) * bytewidth; + for (b = 0; b < bytewidth; ++b) { + out[pixeloutstart + b] = in[pixelinstart + b]; + } + } + } + } + } else /*bpp < 8: Adam7 with pixels < 8 bit is a bit trickier: with bit + pointers*/ + { + for (i = 0; i != 7; ++i) { + unsigned x, y, b; + unsigned ilinebits = bpp * passw[i]; + unsigned olinebits = bpp * w; + size_t obp, ibp; /*bit pointers (for out and in buffer)*/ + for (y = 0; y < passh[i]; ++y) { + for (x = 0; x < passw[i]; ++x) { + ibp = (ADAM7_IY[i] + y * ADAM7_DY[i]) * olinebits + + (ADAM7_IX[i] + x * ADAM7_DX[i]) * bpp; + obp = (8 * passstart[i]) + (y * ilinebits + x * bpp); + for (b = 0; b < bpp; ++b) { + unsigned char bit = readBitFromReversedStream(&ibp, in); + setBitOfReversedStream(&obp, out, bit); + } + } + } + } + } +} + +/*out must be buffer big enough to contain uncompressed IDAT chunk data, and in +must contain the full image. return value is error**/ +static unsigned preProcessScanlines(unsigned char** out, + size_t* outsize, + const unsigned char* in, + unsigned w, + unsigned h, + const LodePNGInfo* info_png, + const LodePNGEncoderSettings* settings) +{ + /* + This function converts the pure 2D image with the PNG's colortype, into + filtered-padded-interlaced data. Steps: + *) if no Adam7: 1) add padding bits (= possible extra bits per scanline if bpp + < 8) 2) filter + *) if adam7: 1) Adam7_interlace 2) 7x add padding bits 3) 7x filter + */ + size_t bpp = lodepng_get_bpp(&info_png->color); + unsigned error = 0; + if (info_png->interlace_method == 0) { + /*image size plus an extra byte per scanline + possible padding bits*/ + *outsize = (size_t) h + ((size_t) h * (((size_t) w * bpp + 7u) / 8u)); + *out = (unsigned char*) lodepng_malloc(*outsize); + if (!(*out) && (*outsize)) { + error = 83; /*alloc fail*/ + } + + if (!error) { + /*non multiple of 8 bits per scanline, padding bits needed per scanline*/ + if (bpp < 8 && (size_t) w * bpp != (((size_t) w * bpp + 7u) / 8u) * 8u) { + unsigned char* padded + = (unsigned char*) lodepng_malloc(h * ((w * bpp + 7u) / 8u)); + if (!padded) { + error = 83; /*alloc fail*/ + } + if (!error) { + addPaddingBits(padded, + in, + (((size_t) w * bpp + 7u) / 8u) * 8u, + (size_t) w * bpp, + h); + error = filter(*out, padded, w, h, &info_png->color, settings); + } + lodepng_free(padded); + } else { + /*we can immediately filter into the out buffer, no other steps needed*/ + error = filter(*out, in, w, h, &info_png->color, settings); + } + } + } else /*interlace_method is 1 (Adam7)*/ { + unsigned passw[7], passh[7]; + size_t filter_passstart[8], padded_passstart[8], passstart[8]; + unsigned char* adam7; + + Adam7_getpassvalues(passw, + passh, + filter_passstart, + padded_passstart, + passstart, + w, + h, + (unsigned) bpp); + + *outsize = filter_passstart[7]; /*image size plus an extra byte per scanline + + possible padding bits*/ + *out = (unsigned char*) lodepng_malloc(*outsize); + if (!(*out)) { + error = 83; /*alloc fail*/ + } + + adam7 = (unsigned char*) lodepng_malloc(passstart[7]); + if (!adam7 && passstart[7]) { + error = 83; /*alloc fail*/ + } + + if (!error) { + unsigned i; + + Adam7_interlace(adam7, in, w, h, (unsigned) bpp); + for (i = 0; i != 7; ++i) { + if (bpp < 8) { + unsigned char* padded = (unsigned char*) lodepng_malloc( + padded_passstart[i + 1] - padded_passstart[i]); + if (!padded) { + ERROR_BREAK(83); /*alloc fail*/ + } + addPaddingBits(padded, + &adam7[passstart[i]], + (((size_t) passw[i] * bpp + 7u) / 8u) * 8u, + (size_t) passw[i] * bpp, + passh[i]); + error = filter(&(*out)[filter_passstart[i]], + padded, + passw[i], + passh[i], + &info_png->color, + settings); + lodepng_free(padded); + } else { + error = filter(&(*out)[filter_passstart[i]], + &adam7[padded_passstart[i]], + passw[i], + passh[i], + &info_png->color, + settings); + } + + if (error) { + break; + } + } + } + + lodepng_free(adam7); + } + + return error; +} + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +static unsigned addUnknownChunks(ucvector* out, + unsigned char* data, + size_t datasize) +{ + unsigned char* inchunk = data; + while ((size_t) (inchunk - data) < datasize) { + CERROR_TRY_RETURN(lodepng_chunk_append(&out->data, &out->size, inchunk)); + out->allocsize = out->size; /*fix the allocsize again*/ + inchunk = lodepng_chunk_next(inchunk, data + datasize); + } + return 0; +} + +static unsigned isGrayICCProfile(const unsigned char* profile, unsigned size) +{ + /* + It is a gray profile if bytes 16-19 are "GRAY", rgb profile if bytes 16-19 + are "RGB ". We do not perform any full parsing of the ICC profile here, other + than check those 4 bytes to grayscale profile. Other than that, validity of + the profile is not checked. This is needed only because the PNG specification + requires using a non-gray color model if there is an ICC profile with "RGB " + (sadly limiting compression opportunities if the input data is grayscale RGB + data), and requires using a gray color model if it is "GRAY". + */ + if (size < 20) { + return 0; + } + return profile[16] == 'G' && profile[17] == 'R' && profile[18] == 'A' + && profile[19] == 'Y'; +} + +static unsigned isRGBICCProfile(const unsigned char* profile, unsigned size) +{ + /* See comment in isGrayICCProfile*/ + if (size < 20) { + return 0; + } + return profile[16] == 'R' && profile[17] == 'G' && profile[18] == 'B' + && profile[19] == ' '; +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +unsigned lodepng_encode(unsigned char** out, + size_t* outsize, + const unsigned char* image, + unsigned w, + unsigned h, + LodePNGState* state) +{ + unsigned char* data = 0; /*uncompressed version of the IDAT chunk data*/ + size_t datasize = 0; + ucvector outv = ucvector_init(NULL, 0); + LodePNGInfo info; + const LodePNGInfo* info_png = &state->info_png; + LodePNGColorMode auto_color; + + lodepng_info_init(&info); + lodepng_color_mode_init(&auto_color); + + /*provide some proper output values if error will happen*/ + *out = 0; + *outsize = 0; + state->error = 0; + + /*check input values validity*/ + if ((info_png->color.colortype == LCT_PALETTE || state->encoder.force_palette) + && (info_png->color.palettesize == 0 + || info_png->color.palettesize > 256)) { + /*this error is returned even if auto_convert is enabled and thus encoder + could generate the palette by itself: while allowing this could be possible + in theory, it may complicate the code or edge cases, and always requiring to + give a palette when setting this color type is a simpler contract*/ + state->error = 68; /*invalid palette size, it is only allowed to be 1-256*/ + goto cleanup; + } + if (state->encoder.zlibsettings.btype > 2) { + state->error = 61; /*error: invalid btype*/ + goto cleanup; + } + if (info_png->interlace_method > 1) { + state->error = 71; /*error: invalid interlace mode*/ + goto cleanup; + } + state->error + = checkColorValidity(info_png->color.colortype, info_png->color.bitdepth); + if (state->error) { + goto cleanup; /*error: invalid color type given*/ + } + state->error + = checkColorValidity(state->info_raw.colortype, state->info_raw.bitdepth); + if (state->error) { + goto cleanup; /*error: invalid color type given*/ + } + + /* color convert and compute scanline filter types */ + lodepng_info_copy(&info, &state->info_png); + if (state->encoder.auto_convert) { + LodePNGColorStats stats; + unsigned allow_convert = 1; + lodepng_color_stats_init(&stats); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if (info_png->iccp_defined + && isGrayICCProfile(info_png->iccp_profile, + info_png->iccp_profile_size)) { + /*the PNG specification does not allow to use palette with a GRAY ICC + profile, even if the palette has only gray colors, so disallow it.*/ + stats.allow_palette = 0; + } + if (info_png->iccp_defined + && isRGBICCProfile(info_png->iccp_profile, + info_png->iccp_profile_size)) { + /*the PNG specification does not allow to use grayscale color with RGB ICC + * profile, so disallow gray.*/ + stats.allow_greyscale = 0; + } +#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ + state->error + = lodepng_compute_color_stats(&stats, image, w, h, &state->info_raw); + if (state->error) { + goto cleanup; + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if (info_png->background_defined) { + /*the background chunk's color must be taken into account as well*/ + unsigned r = 0, g = 0, b = 0; + LodePNGColorMode mode16 = lodepng_color_mode_make(LCT_RGB, 16); + lodepng_convert_rgb(&r, + &g, + &b, + info_png->background_r, + info_png->background_g, + info_png->background_b, + &mode16, + &info_png->color); + state->error = lodepng_color_stats_add(&stats, r, g, b, 65535); + if (state->error) { + goto cleanup; + } + } +#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ + state->error = auto_choose_color(&auto_color, &state->info_raw, &stats); + if (state->error) { + goto cleanup; + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if (info_png->sbit_defined) { + /*if sbit is defined, due to strict requirements of which sbit values can + be present for which color modes, auto_convert can't be done in many + cases. However, do support a few cases here. + TODO: more conversions may be possible, and it may also be possible to get + a more appropriate color type out of auto_choose_color if knowledge about + sbit is used beforehand + */ + unsigned sbit_max = LODEPNG_MAX( + LODEPNG_MAX(LODEPNG_MAX(info_png->sbit_r, info_png->sbit_g), + info_png->sbit_b), + info_png->sbit_a); + unsigned equal + = (!info_png->sbit_g || info_png->sbit_g == info_png->sbit_r) + && (!info_png->sbit_b || info_png->sbit_b == info_png->sbit_r) + && (!info_png->sbit_a || info_png->sbit_a == info_png->sbit_r); + allow_convert = 0; + if (info.color.colortype == LCT_PALETTE + && auto_color.colortype == LCT_PALETTE) { + /* input and output are palette, and in this case it may happen that + palette data is expected to be copied from info_raw into the info_png */ + allow_convert = 1; + } + /*going from 8-bit RGB to palette (or 16-bit as long as sbit_max <= 8) is + possible since both are 8-bit RGB for sBIT's purposes*/ + if (info.color.colortype == LCT_RGB && auto_color.colortype == LCT_PALETTE + && sbit_max <= 8) { + allow_convert = 1; + } + /*going from 8-bit RGBA to palette is also ok but only if sbit_a is + * exactly 8*/ + if (info.color.colortype == LCT_RGBA + && auto_color.colortype == LCT_PALETTE && info_png->sbit_a == 8 + && sbit_max <= 8) { + allow_convert = 1; + } + /*going from 16-bit RGB(A) to 8-bit RGB(A) is ok if all sbit values are <= + * 8*/ + if ((info.color.colortype == LCT_RGB || info.color.colortype == LCT_RGBA) + && info.color.bitdepth == 16 + && auto_color.colortype == info.color.colortype + && auto_color.bitdepth == 8 && sbit_max <= 8) { + allow_convert = 1; + } + /*going to less channels is ok if all bit values are equal (all possible + values in sbit, as well as the chosen bitdepth of the result). Due to + how auto_convert works, we already know that auto_color.colortype has + less than or equal amount of channels than info.colortype. Palette is + not used here. This conversion is not allowed if info_png->sbit_r < + auto_color.bitdepth, because specifically for alpha, non-presence of an + sbit value heavily implies that alpha's bit depth is equal to the PNG + bit depth (rather than the bit depths set in the r, g and b sbit values, + by how the PNG specification describes handling tRNS chunk case with + sBIT), so be conservative here about ignoring user input.*/ + if (info.color.colortype != LCT_PALETTE + && auto_color.colortype != LCT_PALETTE && equal + && info_png->sbit_r == auto_color.bitdepth) { + allow_convert = 1; + } + } +#endif + if (state->encoder.force_palette) { + if (info.color.colortype != LCT_GREY + && info.color.colortype != LCT_GREY_ALPHA + && (auto_color.colortype == LCT_GREY + || auto_color.colortype == LCT_GREY_ALPHA)) { + /*user speficially forced a PLTE palette, so cannot convert to grayscale + types because the PNG specification only allows writing a suggested + palette in PLTE for truecolor types*/ + allow_convert = 0; + } + } + if (allow_convert) { + lodepng_color_mode_copy(&info.color, &auto_color); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*also convert the background chunk*/ + if (info_png->background_defined) { + if (lodepng_convert_rgb(&info.background_r, + &info.background_g, + &info.background_b, + info_png->background_r, + info_png->background_g, + info_png->background_b, + &info.color, + &info_png->color)) { + state->error = 104; + goto cleanup; + } + } +#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ + } + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if (info_png->iccp_defined) { + unsigned gray_icc + = isGrayICCProfile(info_png->iccp_profile, info_png->iccp_profile_size); + unsigned rgb_icc + = isRGBICCProfile(info_png->iccp_profile, info_png->iccp_profile_size); + unsigned gray_png = info.color.colortype == LCT_GREY + || info.color.colortype == LCT_GREY_ALPHA; + if (!gray_icc && !rgb_icc) { + state->error = 100; /* Disallowed profile color type for PNG */ + goto cleanup; + } + if (gray_icc != gray_png) { + /*Not allowed to use RGB/RGBA/palette with GRAY ICC profile or vice versa, + or in case of auto_convert, it wasn't possible to find appropriate model*/ + state->error = state->encoder.auto_convert ? 102 : 101; + goto cleanup; + } + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + if (!lodepng_color_mode_equal(&state->info_raw, &info.color)) { + unsigned char* converted; + size_t size + = ((size_t) w * (size_t) h * (size_t) lodepng_get_bpp(&info.color) + 7u) + / 8u; + + converted = (unsigned char*) lodepng_malloc(size); + if (!converted && size) { + state->error = 83; /*alloc fail*/ + } + if (!state->error) { + state->error = lodepng_convert( + converted, image, &info.color, &state->info_raw, w, h); + } + if (!state->error) { + state->error = preProcessScanlines( + &data, &datasize, converted, w, h, &info, &state->encoder); + } + lodepng_free(converted); + if (state->error) { + goto cleanup; + } + } else { + state->error = preProcessScanlines( + &data, &datasize, image, w, h, &info, &state->encoder); + if (state->error) { + goto cleanup; + } + } + + /* output all PNG chunks */ { +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + size_t i; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + /*write signature and chunks*/ + state->error = writeSignature(&outv); + if (state->error) { + goto cleanup; + } + /*IHDR*/ + state->error = addChunk_IHDR(&outv, + w, + h, + info.color.colortype, + info.color.bitdepth, + info.interlace_method); + if (state->error) { + goto cleanup; + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*unknown chunks between IHDR and PLTE*/ + if (info.unknown_chunks_data[0]) { + state->error = addUnknownChunks( + &outv, info.unknown_chunks_data[0], info.unknown_chunks_size[0]); + if (state->error) { + goto cleanup; + } + } + /*color profile chunks must come before PLTE */ + if (info.cicp_defined) { + state->error = addChunk_cICP(&outv, &info); + if (state->error) { + goto cleanup; + } + } + if (info.mdcv_defined) { + state->error = addChunk_mDCV(&outv, &info); + if (state->error) { + goto cleanup; + } + } + if (info.clli_defined) { + state->error = addChunk_cLLI(&outv, &info); + if (state->error) { + goto cleanup; + } + } + if (info.iccp_defined) { + state->error = addChunk_iCCP(&outv, &info, &state->encoder.zlibsettings); + if (state->error) { + goto cleanup; + } + } + if (info.srgb_defined) { + state->error = addChunk_sRGB(&outv, &info); + if (state->error) { + goto cleanup; + } + } + if (info.gama_defined) { + state->error = addChunk_gAMA(&outv, &info); + if (state->error) { + goto cleanup; + } + } + if (info.chrm_defined) { + state->error = addChunk_cHRM(&outv, &info); + if (state->error) { + goto cleanup; + } + } + if (info_png->sbit_defined) { + state->error = addChunk_sBIT(&outv, &info); + if (state->error) { + goto cleanup; + } + } + if (info.exif_defined) { + state->error = addChunk_eXIf(&outv, &info); + if (state->error) { + goto cleanup; + } + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + /*PLTE*/ + if (info.color.colortype == LCT_PALETTE) { + state->error = addChunk_PLTE(&outv, &info.color); + if (state->error) { + goto cleanup; + } + } + if (state->encoder.force_palette + && (info.color.colortype == LCT_RGB + || info.color.colortype == LCT_RGBA)) { + /*force_palette means: write suggested palette for truecolor in PLTE + * chunk*/ + state->error = addChunk_PLTE(&outv, &info.color); + if (state->error) { + goto cleanup; + } + } + /*tRNS (this will only add if when necessary) */ + state->error = addChunk_tRNS(&outv, &info.color); + if (state->error) { + goto cleanup; + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*bKGD (must come between PLTE and the IDAt chunks*/ + if (info.background_defined) { + state->error = addChunk_bKGD(&outv, &info); + if (state->error) { + goto cleanup; + } + } + /*pHYs (must come before the IDAT chunks)*/ + if (info.phys_defined) { + state->error = addChunk_pHYs(&outv, &info); + if (state->error) { + goto cleanup; + } + } + + /*unknown chunks between PLTE and IDAT*/ + if (info.unknown_chunks_data[1]) { + state->error = addUnknownChunks( + &outv, info.unknown_chunks_data[1], info.unknown_chunks_size[1]); + if (state->error) { + goto cleanup; + } + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + /*IDAT (multiple IDAT chunks must be consecutive)*/ + state->error + = addChunk_IDAT(&outv, data, datasize, &state->encoder.zlibsettings); + if (state->error) { + goto cleanup; + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*tIME*/ + if (info.time_defined) { + state->error = addChunk_tIME(&outv, &info.time); + if (state->error) { + goto cleanup; + } + } + /*tEXt and/or zTXt*/ + for (i = 0; i != info.text_num; ++i) { + if (lodepng_strlen(info.text_keys[i]) > 79) { + state->error = 66; /*text chunk too large*/ + goto cleanup; + } + if (lodepng_strlen(info.text_keys[i]) < 1) { + state->error = 67; /*text chunk too small*/ + goto cleanup; + } + if (state->encoder.text_compression) { + state->error = addChunk_zTXt(&outv, + info.text_keys[i], + info.text_strings[i], + &state->encoder.zlibsettings); + if (state->error) { + goto cleanup; + } + } else { + state->error + = addChunk_tEXt(&outv, info.text_keys[i], info.text_strings[i]); + if (state->error) { + goto cleanup; + } + } + } + /*LodePNG version id in text chunk*/ + if (state->encoder.add_id) { + unsigned already_added_id_text = 0; + for (i = 0; i != info.text_num; ++i) { + const char* k = info.text_keys[i]; + /* Could use strcmp, but we're not calling or reimplementing this C + * library function for this use only */ + if (k[0] == 'L' && k[1] == 'o' && k[2] == 'd' && k[3] == 'e' + && k[4] == 'P' && k[5] == 'N' && k[6] == 'G' && k[7] == '\0') { + already_added_id_text = 1; + break; + } + } + if (already_added_id_text == 0) { + state->error = addChunk_tEXt( + &outv, + "LodePNG", + LODEPNG_VERSION_STRING); /*it's shorter as tEXt than as zTXt chunk*/ + if (state->error) { + goto cleanup; + } + } + } + /*iTXt*/ + for (i = 0; i != info.itext_num; ++i) { + if (lodepng_strlen(info.itext_keys[i]) > 79) { + state->error = 66; /*text chunk too large*/ + goto cleanup; + } + if (lodepng_strlen(info.itext_keys[i]) < 1) { + state->error = 67; /*text chunk too small*/ + goto cleanup; + } + state->error = addChunk_iTXt(&outv, + state->encoder.text_compression, + info.itext_keys[i], + info.itext_langtags[i], + info.itext_transkeys[i], + info.itext_strings[i], + &state->encoder.zlibsettings); + if (state->error) { + goto cleanup; + } + } + + /*unknown chunks between IDAT and IEND*/ + if (info.unknown_chunks_data[2]) { + state->error = addUnknownChunks( + &outv, info.unknown_chunks_data[2], info.unknown_chunks_size[2]); + if (state->error) { + goto cleanup; + } + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + state->error = addChunk_IEND(&outv); + if (state->error) { + goto cleanup; + } + } + +cleanup: + lodepng_info_cleanup(&info); + lodepng_free(data); + lodepng_color_mode_cleanup(&auto_color); + + /*instead of cleaning the vector up, give it to the output*/ + *out = outv.data; + *outsize = outv.size; + + return state->error; +} + +unsigned lodepng_encode_memory(unsigned char** out, + size_t* outsize, + const unsigned char* image, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth) +{ + unsigned error; + LodePNGState state; + lodepng_state_init(&state); + state.info_raw.colortype = colortype; + state.info_raw.bitdepth = bitdepth; + state.info_png.color.colortype = colortype; + state.info_png.color.bitdepth = bitdepth; + lodepng_encode(out, outsize, image, w, h, &state); + error = state.error; + lodepng_state_cleanup(&state); + return error; +} + +unsigned lodepng_encode32(unsigned char** out, + size_t* outsize, + const unsigned char* image, + unsigned w, + unsigned h) +{ + return lodepng_encode_memory(out, outsize, image, w, h, LCT_RGBA, 8); +} + +unsigned lodepng_encode24(unsigned char** out, + size_t* outsize, + const unsigned char* image, + unsigned w, + unsigned h) +{ + return lodepng_encode_memory(out, outsize, image, w, h, LCT_RGB, 8); +} + +#ifdef LODEPNG_COMPILE_DISK +unsigned lodepng_encode_file(const char* filename, + const unsigned char* image, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth) +{ + unsigned char* buffer; + size_t buffersize; + unsigned error = lodepng_encode_memory( + &buffer, &buffersize, image, w, h, colortype, bitdepth); + if (!error) { + error = lodepng_save_file(buffer, buffersize, filename); + } + lodepng_free(buffer); + return error; +} + +unsigned lodepng_encode32_file(const char* filename, + const unsigned char* image, + unsigned w, + unsigned h) +{ + return lodepng_encode_file(filename, image, w, h, LCT_RGBA, 8); +} + +unsigned lodepng_encode24_file(const char* filename, + const unsigned char* image, + unsigned w, + unsigned h) +{ + return lodepng_encode_file(filename, image, w, h, LCT_RGB, 8); +} +#endif /*LODEPNG_COMPILE_DISK*/ + +void lodepng_encoder_settings_init(LodePNGEncoderSettings* settings) +{ + lodepng_compress_settings_init(&settings->zlibsettings); + settings->filter_palette_zero = 1; + settings->filter_strategy = LFS_MINSUM; + settings->auto_convert = 1; + settings->force_palette = 0; + settings->predefined_filters = 0; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + settings->add_id = 0; + settings->text_compression = 1; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} + +#endif /*LODEPNG_COMPILE_ENCODER*/ +#endif /*LODEPNG_COMPILE_PNG*/ + +#ifdef LODEPNG_COMPILE_ERROR_TEXT +/* +This returns the description of a numerical error code in English. This is also +the documentation of all the error codes. +*/ +const char* lodepng_error_text(unsigned code) +{ + switch (code) { + case 0: + return "no error, everything went ok"; + case 1: + return "nothing done yet"; /*the Encoder/Decoder has done nothing yet, + error checking makes no sense yet*/ + case 10: + return "end of input memory reached without huffman end code"; /*while + huffman + decoding*/ + case 11: + return "error in code tree made it jump outside of huffman tree"; /*while + huffman + decoding*/ + case 13: + return "problem while processing dynamic deflate block"; + case 14: + return "problem while processing dynamic deflate block"; + case 15: + return "problem while processing dynamic deflate block"; + /*this error could happen if there are only 0 or 1 symbols present in the + * huffman code:*/ + case 16: + return "invalid code while processing dynamic deflate block"; + case 17: + return "end of out buffer memory reached while inflating"; + case 18: + return "invalid distance code while inflating"; + case 19: + return "end of out buffer memory reached while inflating"; + case 20: + return "invalid deflate block BTYPE encountered while decoding"; + case 21: + return "NLEN is not ones complement of LEN in a deflate block"; + + /*end of out buffer memory reached while inflating: + This can happen if the inflated deflate data is longer than the amount of + bytes required to fill up all the pixels of the image, given the color depth + and image dimensions. Something that doesn't happen in a normal, well + encoded, PNG image.*/ + case 22: + return "end of out buffer memory reached while inflating"; + case 23: + return "end of in buffer memory reached while inflating"; + case 24: + return "invalid FCHECK in zlib header"; + case 25: + return "invalid compression method in zlib header"; + case 26: + return "FDICT encountered in zlib header while it's not used for PNG"; + case 27: + return "PNG file is smaller than a PNG header"; + /*Checks the magic file header, the first 8 bytes of the PNG file*/ + case 28: + return "incorrect PNG signature, it's no PNG or corrupted"; + case 29: + return "first chunk is not the header chunk"; + case 30: + return "chunk length too large, chunk broken off at end of file"; + case 31: + return "illegal PNG color type or bpp"; + case 32: + return "illegal PNG compression method"; + case 33: + return "illegal PNG filter method"; + case 34: + return "illegal PNG interlace method"; + case 35: + return "chunk length of a chunk is too large or the chunk too small"; + case 36: + return "illegal PNG filter type encountered"; + case 37: + return "illegal bit depth for this color type given"; + case 38: + return "the palette is too small or too big"; /*0, or more than 256 + colors*/ + case 39: + return "tRNS chunk before PLTE or has more entries than palette size"; + case 40: + return "tRNS chunk has wrong size for grayscale image"; + case 41: + return "tRNS chunk has wrong size for RGB image"; + case 42: + return "tRNS chunk appeared while it was not allowed for this color type"; + case 43: + return "bKGD chunk has wrong size for palette image"; + case 44: + return "bKGD chunk has wrong size for grayscale image"; + case 45: + return "bKGD chunk has wrong size for RGB image"; + case 48: + return "empty input buffer given to decoder. Maybe caused by " + "non-existing file?"; + case 49: + return "jumped past memory while generating dynamic huffman tree"; + case 50: + return "jumped past memory while generating dynamic huffman tree"; + case 51: + return "jumped past memory while inflating huffman block"; + case 52: + return "jumped past memory while inflating"; + case 53: + return "size of zlib data too small"; + case 54: + return "repeat symbol in tree while there was no value symbol yet"; + /*jumped past tree while generating huffman tree, this could be when the + tree will have more leaves than symbols after generating it out of the + given lengths. They call this an oversubscribed dynamic bit lengths tree in + zlib.*/ + case 55: + return "jumped past tree while generating huffman tree"; + case 56: + return "given output image colortype or bitdepth not supported for color " + "conversion"; + case 57: + return "invalid CRC encountered (checking CRC can be disabled)"; + case 58: + return "invalid ADLER32 encountered (checking ADLER32 can be disabled)"; + case 59: + return "requested color conversion not supported"; + case 60: + return "invalid window size given in the settings of the encoder (must " + "be 0-32768)"; + case 61: + return "invalid BTYPE given in the settings of the encoder (only 0, 1 " + "and 2 are allowed)"; + /*LodePNG leaves the choice of RGB to grayscale conversion formula to the + * user.*/ + case 62: + return "conversion from color to grayscale not supported"; + /*(2^31-1)*/ + case 63: + return "length of a chunk too long, max allowed for PNG is 2147483647 " + "bytes per chunk"; + /*this would result in the inability of a deflated block to ever contain an + * end code. It must be at least 1.*/ + case 64: + return "the length of the END symbol 256 in the Huffman tree is 0"; + case 66: + return "the length of a text chunk keyword given to the encoder is " + "longer than the maximum of 79 bytes"; + case 67: + return "the length of a text chunk keyword given to the encoder is " + "smaller than the minimum of 1 byte"; + case 68: + return "tried to encode a PLTE chunk with a palette that has less than 1 " + "or more than 256 colors"; + case 69: + return "unknown chunk type with 'critical' flag encountered by the " + "decoder"; + case 71: + return "invalid interlace mode given to encoder (must be 0 or 1)"; + case 72: + return "while decoding, invalid compression method encountering in zTXt " + "or iTXt chunk (it must be 0)"; + case 73: + return "invalid tIME chunk size"; + case 74: + return "invalid pHYs chunk size"; + /*length could be wrong, or data chopped off*/ + case 75: + return "no null termination char found while decoding text chunk"; + case 76: + return "iTXt chunk too short to contain required bytes"; + case 77: + return "integer overflow in buffer size"; + case 78: + return "failed to open file for reading"; /*file doesn't exist or couldn't + be opened for reading*/ + case 79: + return "failed to open file for writing"; + case 80: + return "tried creating a tree of 0 symbols"; + case 81: + return "lazy matching at pos 0 is impossible"; + case 82: + return "color conversion to palette requested while a color isn't in " + "palette, or index out of bounds"; + case 83: + return "memory allocation failed"; + case 84: + return "given image too small to contain all pixels to be encoded"; + case 86: + return "impossible offset in lz77 encoding (internal bug)"; + case 87: + return "must provide custom zlib function pointer if " + "LODEPNG_COMPILE_ZLIB is not defined"; + case 88: + return "invalid filter strategy given for " + "LodePNGEncoderSettings.filter_strategy"; + case 89: + return "text chunk keyword too short or long: must have size 1-79"; + /*the windowsize in the LodePNGCompressSettings. Requiring POT(==> & instead + * of %) makes encoding 12% faster.*/ + case 90: + return "windowsize must be a power of two"; + case 91: + return "invalid decompressed idat size"; + case 92: + return "integer overflow due to too many pixels"; + case 93: + return "zero width or height is invalid"; + case 94: + return "header chunk must have a size of 13 bytes"; + case 95: + return "integer overflow with combined idat chunk size"; + case 96: + return "invalid gAMA chunk size"; + case 97: + return "invalid cHRM chunk size"; + case 98: + return "invalid sRGB chunk size"; + case 99: + return "invalid sRGB rendering intent"; + case 100: + return "invalid ICC profile color type, the PNG specification only " + "allows RGB or GRAY"; + case 101: + return "PNG specification does not allow RGB ICC profile on gray color " + "types and vice versa"; + case 102: + return "not allowed to set grayscale ICC profile with colored pixels by " + "PNG specification"; + case 103: + return "invalid palette index in bKGD chunk. Maybe it came before PLTE " + "chunk?"; + case 104: + return "invalid bKGD color while encoding (e.g. palette index out of " + "range)"; + case 105: + return "integer overflow of bitsize"; + case 106: + return "PNG file must have PLTE chunk if color type is palette"; + case 107: + return "color convert from palette mode requested without setting the " + "palette data in it"; + case 108: + return "tried to add more than 256 values to a palette"; + /*this limit can be configured in LodePNGDecompressSettings*/ + case 109: + return "tried to decompress zlib or deflate data larger than desired " + "max_output_size"; + case 110: + return "custom zlib or inflate decompression failed"; + case 111: + return "custom zlib or deflate compression failed"; + /*max text size limit can be configured in LodePNGDecoderSettings. This + error prevents unreasonable memory consumption when decoding due to + impossibly large text sizes.*/ + case 112: + return "compressed text unreasonably large"; + /*max ICC size limit can be configured in LodePNGDecoderSettings. This error + prevents unreasonable memory consumption when decoding due to impossibly + large ICC profile*/ + case 113: + return "ICC profile unreasonably large"; + case 114: + return "sBIT chunk has wrong size for the color type of the image"; + case 115: + return "sBIT value out of range"; + case 116: + return "cICP value out of range"; + case 117: + return "invalid cICP chunk size"; + case 118: + return "mDCV value out of range"; + case 119: + return "invalid mDCV chunk size"; + case 120: + return "invalid cLLI chunk size"; + case 121: + return "invalid chunk type name: may only contain [a-zA-Z]"; + case 122: + return "invalid chunk type name: third character must be uppercase"; + } + return "unknown error code"; +} +#endif /*LODEPNG_COMPILE_ERROR_TEXT*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* // C++ Wrapper // */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_CPP +namespace lodepng { + +#ifdef LODEPNG_COMPILE_DISK +/* Resizes the vector to the file size and reads the file into it. Returns error + * code.*/ +static unsigned load_file_(std::vector& buffer, FILE* file) +{ + long size = lodepng_filesize(file); + if (size < 0) { + return 78; + } + buffer.resize((size_t) size); + if (size == 0) { + return 0; /*ok*/ + } + if (fread(&buffer[0], 1, buffer.size(), file) != buffer.size()) { + return 78; + } + return 0; /*ok*/ +} + +unsigned load_file(std::vector& buffer, + const std::string& filename) +{ + unsigned error; + FILE* file = fopen(filename.c_str(), "rb"); + if (!file) { + return 78; + } + error = load_file_(buffer, file); + fclose(file); + return error; +} + +/*write given buffer to the file, overwriting the file, it doesn't append to + * it.*/ +unsigned save_file(const std::vector& buffer, + const std::string& filename) +{ + return lodepng_save_file( + buffer.empty() ? 0 : &buffer[0], buffer.size(), filename.c_str()); +} +#endif /* LODEPNG_COMPILE_DISK */ + +#ifdef LODEPNG_COMPILE_ZLIB +#ifdef LODEPNG_COMPILE_DECODER +unsigned decompress(std::vector& out, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings& settings) +{ + unsigned char* buffer = 0; + size_t buffersize = 0; + unsigned error + = zlib_decompress(&buffer, &buffersize, 0, in, insize, &settings); + if (buffer) { + out.insert(out.end(), buffer, &buffer[buffersize]); + lodepng_free(buffer); + } + return error; +} + +unsigned decompress(std::vector& out, + const std::vector& in, + const LodePNGDecompressSettings& settings) +{ + return decompress(out, in.empty() ? 0 : &in[0], in.size(), settings); +} +#endif /* LODEPNG_COMPILE_DECODER */ + +#ifdef LODEPNG_COMPILE_ENCODER +unsigned compress(std::vector& out, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings& settings) +{ + unsigned char* buffer = 0; + size_t buffersize = 0; + unsigned error = zlib_compress(&buffer, &buffersize, in, insize, &settings); + if (buffer) { + out.insert(out.end(), buffer, &buffer[buffersize]); + lodepng_free(buffer); + } + return error; +} + +unsigned compress(std::vector& out, + const std::vector& in, + const LodePNGCompressSettings& settings) +{ + return compress(out, in.empty() ? 0 : &in[0], in.size(), settings); +} +#endif /* LODEPNG_COMPILE_ENCODER */ +#endif /* LODEPNG_COMPILE_ZLIB */ + +#ifdef LODEPNG_COMPILE_PNG + +State::State() +{ + lodepng_state_init(this); +} + +State::State(const State& other) +{ + lodepng_state_init(this); + lodepng_state_copy(this, &other); +} + +State::~State() +{ + lodepng_state_cleanup(this); +} + +State& State::operator=(const State& other) +{ + lodepng_state_copy(this, &other); + return *this; +} + +#ifdef LODEPNG_COMPILE_DECODER + +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + const unsigned char* in, + size_t insize, + LodePNGColorType colortype, + unsigned bitdepth) +{ + unsigned char* buffer = 0; + unsigned error + = lodepng_decode_memory(&buffer, &w, &h, in, insize, colortype, bitdepth); + if (buffer && !error) { + State state; + state.info_raw.colortype = colortype; + state.info_raw.bitdepth = bitdepth; + size_t buffersize = lodepng_get_raw_size(w, h, &state.info_raw); + out.insert(out.end(), buffer, &buffer[buffersize]); + } + lodepng_free(buffer); + return error; +} + +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + const std::vector& in, + LodePNGColorType colortype, + unsigned bitdepth) +{ + return decode(out, + w, + h, + in.empty() ? 0 : &in[0], + (unsigned) in.size(), + colortype, + bitdepth); +} + +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + State& state, + const unsigned char* in, + size_t insize) +{ + unsigned char* buffer = NULL; + unsigned error = lodepng_decode(&buffer, &w, &h, &state, in, insize); + if (buffer && !error) { + size_t buffersize = lodepng_get_raw_size(w, h, &state.info_raw); + out.insert(out.end(), buffer, &buffer[buffersize]); + } + lodepng_free(buffer); + return error; +} + +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + State& state, + const std::vector& in) +{ + return decode(out, w, h, state, in.empty() ? 0 : &in[0], in.size()); +} + +#ifdef LODEPNG_COMPILE_DISK +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + const std::string& filename, + LodePNGColorType colortype, + unsigned bitdepth) +{ + std::vector buffer; + /* safe output values in case error happens */ + w = h = 0; + unsigned error = load_file(buffer, filename); + if (error) { + return error; + } + return decode(out, w, h, buffer, colortype, bitdepth); +} +#endif /* LODEPNG_COMPILE_DECODER */ +#endif /* LODEPNG_COMPILE_DISK */ + +#ifdef LODEPNG_COMPILE_ENCODER +unsigned encode(std::vector& out, + const unsigned char* in, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth) +{ + unsigned char* buffer; + size_t buffersize; + unsigned error = lodepng_encode_memory( + &buffer, &buffersize, in, w, h, colortype, bitdepth); + if (buffer) { + out.insert(out.end(), buffer, &buffer[buffersize]); + lodepng_free(buffer); + } + return error; +} + +unsigned encode(std::vector& out, + const std::vector& in, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth) +{ + if (lodepng_get_raw_size_lct(w, h, colortype, bitdepth) > in.size()) { + return 84; + } + return encode(out, in.empty() ? 0 : &in[0], w, h, colortype, bitdepth); +} + +unsigned encode(std::vector& out, + const unsigned char* in, + unsigned w, + unsigned h, + State& state) +{ + unsigned char* buffer; + size_t buffersize; + unsigned error = lodepng_encode(&buffer, &buffersize, in, w, h, &state); + if (buffer) { + out.insert(out.end(), buffer, &buffer[buffersize]); + lodepng_free(buffer); + } + return error; +} + +unsigned encode(std::vector& out, + const std::vector& in, + unsigned w, + unsigned h, + State& state) +{ + if (lodepng_get_raw_size(w, h, &state.info_raw) > in.size()) { + return 84; + } + return encode(out, in.empty() ? 0 : &in[0], w, h, state); +} + +#ifdef LODEPNG_COMPILE_DISK +unsigned encode(const std::string& filename, + const unsigned char* in, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth) +{ + std::vector buffer; + unsigned error = encode(buffer, in, w, h, colortype, bitdepth); + if (!error) { + error = save_file(buffer, filename); + } + return error; +} + +unsigned encode(const std::string& filename, + const std::vector& in, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth) +{ + if (lodepng_get_raw_size_lct(w, h, colortype, bitdepth) > in.size()) { + return 84; + } + return encode(filename, in.empty() ? 0 : &in[0], w, h, colortype, bitdepth); +} +#endif /* LODEPNG_COMPILE_DISK */ +#endif /* LODEPNG_COMPILE_ENCODER */ +#endif /* LODEPNG_COMPILE_PNG */ +} /* namespace lodepng */ +#endif /*LODEPNG_COMPILE_CPP*/ diff --git a/third-party/lodepng/lodepng.h b/third-party/lodepng/lodepng.h new file mode 100644 index 00000000000..59b7bf7b7ed --- /dev/null +++ b/third-party/lodepng/lodepng.h @@ -0,0 +1,2475 @@ +/* +LodePNG version 20250506 + +Copyright (c) 2005-2025 Lode Vandevenne + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ + +#ifndef LODEPNG_H +#define LODEPNG_H + +#include /*for size_t*/ + +extern const char* LODEPNG_VERSION_STRING; + +/* +The following #defines are used to create code sections. They can be disabled +to disable code sections, which can give faster compile time and smaller binary. +The "NO_COMPILE" defines are designed to be used to pass as defines to the +compiler command to disable them without modifying this header, e.g. +-DLODEPNG_NO_COMPILE_ZLIB for gcc or clang. +*/ +/*deflate & zlib. If disabled, you must specify alternative zlib functions in +the custom_zlib field of the compress and decompress settings*/ +#ifndef LODEPNG_NO_COMPILE_ZLIB +/*pass -DLODEPNG_NO_COMPILE_ZLIB to the compiler to disable this, or comment out + * LODEPNG_COMPILE_ZLIB below*/ +#define LODEPNG_COMPILE_ZLIB +#endif + +/*png encoder and png decoder*/ +#ifndef LODEPNG_NO_COMPILE_PNG +/*pass -DLODEPNG_NO_COMPILE_PNG to the compiler to disable this, or comment out + * LODEPNG_COMPILE_PNG below*/ +#define LODEPNG_COMPILE_PNG +#endif + +/*deflate&zlib decoder and png decoder*/ +#ifndef LODEPNG_NO_COMPILE_DECODER +/*pass -DLODEPNG_NO_COMPILE_DECODER to the compiler to disable this, or comment + * out LODEPNG_COMPILE_DECODER below*/ +#define LODEPNG_COMPILE_DECODER +#endif + +/*deflate&zlib encoder and png encoder*/ +#ifndef LODEPNG_NO_COMPILE_ENCODER +/*pass -DLODEPNG_NO_COMPILE_ENCODER to the compiler to disable this, or comment + * out LODEPNG_COMPILE_ENCODER below*/ +#define LODEPNG_COMPILE_ENCODER +#endif + +/*the optional built in harddisk file loading and saving functions*/ +#ifndef LODEPNG_NO_COMPILE_DISK +/*pass -DLODEPNG_NO_COMPILE_DISK to the compiler to disable this, or comment out + * LODEPNG_COMPILE_DISK below*/ +#define LODEPNG_COMPILE_DISK +#endif + +/*support for chunks other than IHDR, IDAT, PLTE, tRNS, IEND: ancillary and + * unknown chunks*/ +#ifndef LODEPNG_NO_COMPILE_ANCILLARY_CHUNKS +/*pass -DLODEPNG_NO_COMPILE_ANCILLARY_CHUNKS to the compiler to disable this, +or comment out LODEPNG_COMPILE_ANCILLARY_CHUNKS below*/ +#define LODEPNG_COMPILE_ANCILLARY_CHUNKS +#endif + +/*ability to convert error numerical codes to English text string*/ +#ifndef LODEPNG_NO_COMPILE_ERROR_TEXT +/*pass -DLODEPNG_NO_COMPILE_ERROR_TEXT to the compiler to disable this, +or comment out LODEPNG_COMPILE_ERROR_TEXT below*/ +#define LODEPNG_COMPILE_ERROR_TEXT +#endif + +/*Compile the default allocators (C's free, malloc and realloc). If you disable +this, you can define the functions lodepng_free, lodepng_malloc and +lodepng_realloc in your source files with custom allocators.*/ +#ifndef LODEPNG_NO_COMPILE_ALLOCATORS +/*pass -DLODEPNG_NO_COMPILE_ALLOCATORS to the compiler to disable the built-in +ones, or comment out LODEPNG_COMPILE_ALLOCATORS below*/ +#define LODEPNG_COMPILE_ALLOCATORS +#endif + +/*Disable built-in CRC function, in that case a custom implementation of +lodepng_crc32 must be defined externally so that it can be linked in. +The default built-in CRC code comes with 8KB of lookup tables, so for memory +constrained environment you may want it disabled and provide a much smaller +implementation externally as said above. You can find such an example +implementation in a comment in the lodepng.c(pp) file in the 'else' case of the +searchable LODEPNG_COMPILE_CRC section.*/ +#ifndef LODEPNG_NO_COMPILE_CRC +/*pass -DLODEPNG_NO_COMPILE_CRC to the compiler to disable the built-in one, +or comment out LODEPNG_COMPILE_CRC below*/ +#define LODEPNG_COMPILE_CRC +#endif + +/*compile the C++ version (you can disable the C++ wrapper here even when + * compiling for C++)*/ +#ifdef __cplusplus +#ifndef LODEPNG_NO_COMPILE_CPP +/*pass -DLODEPNG_NO_COMPILE_CPP to the compiler to disable C++ (not needed if a +C-only compiler), or comment out LODEPNG_COMPILE_CPP below*/ +#define LODEPNG_COMPILE_CPP +#endif +#endif + +#ifdef LODEPNG_COMPILE_CPP +#include +#include +#endif /*LODEPNG_COMPILE_CPP*/ + +#ifdef LODEPNG_COMPILE_PNG +/*The PNG color types (also used for raw image).*/ +typedef enum LodePNGColorType +{ + LCT_GREY = 0, /*grayscale: 1,2,4,8,16 bit*/ + LCT_RGB = 2, /*RGB: 8,16 bit*/ + LCT_PALETTE = 3, /*palette: 1,2,4,8 bit*/ + LCT_GREY_ALPHA = 4, /*grayscale with alpha: 8,16 bit*/ + LCT_RGBA = 6, /*RGB with alpha: 8,16 bit*/ + /*LCT_MAX_OCTET_VALUE lets the compiler allow this enum to represent any + invalid byte value from 0 to 255 that could be present in an invalid PNG file + header. Do not use, compare with or set the name LCT_MAX_OCTET_VALUE, instead + either use the valid color type names above, or numeric values like 1 or 7 + when checking for particular disallowed color type byte values, or cast to + integer to print it.*/ + LCT_MAX_OCTET_VALUE = 255 +} LodePNGColorType; + +#ifdef LODEPNG_COMPILE_DECODER +/* +Converts PNG data in memory to raw pixel data. +out: Output parameter. Pointer to buffer that will contain the raw pixel data. + After decoding, its size is w * h * (bytes per pixel) bytes larger than + initially. Bytes per pixel depends on colortype and bitdepth. + Must be freed after usage with free(*out). + Note: for 16-bit per channel colors, uses big endian format like PNG does. +w: Output parameter. Pointer to width of pixel data. +h: Output parameter. Pointer to height of pixel data. +in: Memory buffer with the PNG file. +insize: size of the in buffer. +colortype: the desired color type for the raw output image. See explanation on +PNG color types. bitdepth: the desired bit depth for the raw output image. See +explanation on PNG color types. Return value: LodePNG error code (0 means no +error). +*/ +unsigned lodepng_decode_memory(unsigned char** out, + unsigned* w, + unsigned* h, + const unsigned char* in, + size_t insize, + LodePNGColorType colortype, + unsigned bitdepth); + +/*Same as lodepng_decode_memory, but always decodes to 32-bit RGBA raw image*/ +unsigned lodepng_decode32(unsigned char** out, + unsigned* w, + unsigned* h, + const unsigned char* in, + size_t insize); + +/*Same as lodepng_decode_memory, but always decodes to 24-bit RGB raw image*/ +unsigned lodepng_decode24(unsigned char** out, + unsigned* w, + unsigned* h, + const unsigned char* in, + size_t insize); + +#ifdef LODEPNG_COMPILE_DISK +/* +Load PNG from disk, from file with given name. +Same as the other decode functions, but instead takes a filename as input. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory.*/ +unsigned lodepng_decode_file(unsigned char** out, + unsigned* w, + unsigned* h, + const char* filename, + LodePNGColorType colortype, + unsigned bitdepth); + +/*Same as lodepng_decode_file, but always decodes to 32-bit RGBA raw image. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory.*/ +unsigned lodepng_decode32_file(unsigned char** out, + unsigned* w, + unsigned* h, + const char* filename); + +/*Same as lodepng_decode_file, but always decodes to 24-bit RGB raw image. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory.*/ +unsigned lodepng_decode24_file(unsigned char** out, + unsigned* w, + unsigned* h, + const char* filename); +#endif /*LODEPNG_COMPILE_DISK*/ +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/* +Converts raw pixel data into a PNG image in memory. The colortype and bitdepth + of the output PNG image cannot be chosen, they are automatically determined + by the colortype, bitdepth and content of the input pixel data. + Note: for 16-bit per channel colors, needs big endian format like PNG does. +out: Output parameter. Pointer to buffer that will contain the PNG image data. + Must be freed after usage with free(*out). +outsize: Output parameter. Pointer to the size in bytes of the out buffer. +image: The raw pixel data to encode. The size of this buffer should be + w * h * (bytes per pixel), bytes per pixel depends on colortype and +bitdepth. w: width of the raw pixel data in pixels. h: height of the raw pixel +data in pixels. colortype: the color type of the raw input image. See +explanation on PNG color types. bitdepth: the bit depth of the raw input image. +See explanation on PNG color types. Return value: LodePNG error code (0 means no +error). +*/ +unsigned lodepng_encode_memory(unsigned char** out, + size_t* outsize, + const unsigned char* image, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth); + +/*Same as lodepng_encode_memory, but always encodes from 32-bit RGBA raw + * image.*/ +unsigned lodepng_encode32(unsigned char** out, + size_t* outsize, + const unsigned char* image, + unsigned w, + unsigned h); + +/*Same as lodepng_encode_memory, but always encodes from 24-bit RGB raw image.*/ +unsigned lodepng_encode24(unsigned char** out, + size_t* outsize, + const unsigned char* image, + unsigned w, + unsigned h); + +#ifdef LODEPNG_COMPILE_DISK +/* +Converts raw pixel data into a PNG file on disk. +Same as the other encode functions, but instead takes a filename as output. + +NOTE: This overwrites existing files without warning! + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory.*/ +unsigned lodepng_encode_file(const char* filename, + const unsigned char* image, + unsigned w, + unsigned h, + LodePNGColorType colortype, + unsigned bitdepth); + +/*Same as lodepng_encode_file, but always encodes from 32-bit RGBA raw image. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory.*/ +unsigned lodepng_encode32_file(const char* filename, + const unsigned char* image, + unsigned w, + unsigned h); + +/*Same as lodepng_encode_file, but always encodes from 24-bit RGB raw image. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory.*/ +unsigned lodepng_encode24_file(const char* filename, + const unsigned char* image, + unsigned w, + unsigned h); +#endif /*LODEPNG_COMPILE_DISK*/ +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_CPP +namespace lodepng { +#ifdef LODEPNG_COMPILE_DECODER +/*Same as lodepng_decode_memory, but decodes to an std::vector. The colortype +is the format to output the pixels to. Default is RGBA 8-bit per channel.*/ +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + const unsigned char* in, + size_t insize, + LodePNGColorType colortype = LCT_RGBA, + unsigned bitdepth = 8); +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + const std::vector& in, + LodePNGColorType colortype = LCT_RGBA, + unsigned bitdepth = 8); +#ifdef LODEPNG_COMPILE_DISK +/* +Converts PNG file from disk to raw pixel data in memory. +Same as the other decode functions, but instead takes a filename as input. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory. +*/ +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + const std::string& filename, + LodePNGColorType colortype = LCT_RGBA, + unsigned bitdepth = 8); +#endif /* LODEPNG_COMPILE_DISK */ +#endif /* LODEPNG_COMPILE_DECODER */ + +#ifdef LODEPNG_COMPILE_ENCODER +/*Same as lodepng_encode_memory, but encodes to an std::vector. colortype +is that of the raw input data. The output PNG color type will be auto chosen.*/ +unsigned encode(std::vector& out, + const unsigned char* in, + unsigned w, + unsigned h, + LodePNGColorType colortype = LCT_RGBA, + unsigned bitdepth = 8); +unsigned encode(std::vector& out, + const std::vector& in, + unsigned w, + unsigned h, + LodePNGColorType colortype = LCT_RGBA, + unsigned bitdepth = 8); +#ifdef LODEPNG_COMPILE_DISK +/* +Converts 32-bit RGBA raw pixel data into a PNG file on disk. +Same as the other encode functions, but instead takes a filename as output. + +NOTE: This overwrites existing files without warning! + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory. +*/ +unsigned encode(const std::string& filename, + const unsigned char* in, + unsigned w, + unsigned h, + LodePNGColorType colortype = LCT_RGBA, + unsigned bitdepth = 8); +unsigned encode(const std::string& filename, + const std::vector& in, + unsigned w, + unsigned h, + LodePNGColorType colortype = LCT_RGBA, + unsigned bitdepth = 8); +#endif /* LODEPNG_COMPILE_DISK */ +#endif /* LODEPNG_COMPILE_ENCODER */ +} /* namespace lodepng */ +#endif /*LODEPNG_COMPILE_CPP*/ +#endif /*LODEPNG_COMPILE_PNG*/ + +#ifdef LODEPNG_COMPILE_ERROR_TEXT +/*Returns an English description of the numerical error code.*/ +const char* lodepng_error_text(unsigned code); +#endif /*LODEPNG_COMPILE_ERROR_TEXT*/ + +#ifdef LODEPNG_COMPILE_DECODER +/*Settings for zlib decompression*/ +typedef struct LodePNGDecompressSettings LodePNGDecompressSettings; +struct LodePNGDecompressSettings +{ + /* Check LodePNGDecoderSettings for more ignorable errors such as ignore_crc + */ + unsigned ignore_adler32; /*if 1, continue and don't give an error message if + the Adler32 checksum is corrupted*/ + unsigned + ignore_nlen; /*ignore complement of len checksum in uncompressed blocks*/ + + /*Maximum decompressed size, beyond this the decoder may (and is encouraged + to) stop decoding, return an error, output a data size > max_output_size and + all the data up to that point. This is not hard limit nor a guarantee, but can + prevent excessive memory usage. This setting is ignored by the PNG decoder, + but is used by the deflate/zlib decoder and can be used by custom ones. Set to + 0 to impose no limit (the default).*/ + size_t max_output_size; + + /*use custom zlib decoder instead of built in one (default: null). + Should return 0 if success, any non-0 if error (numeric value not exposed).*/ + unsigned (*custom_zlib)(unsigned char**, + size_t*, + const unsigned char*, + size_t, + const LodePNGDecompressSettings*); + /*use custom deflate decoder instead of built in one (default: null) + if custom_zlib is not null, custom_inflate is ignored (the zlib format uses + deflate). Should return 0 if success, any non-0 if error (numeric value not + exposed).*/ + unsigned (*custom_inflate)(unsigned char**, + size_t*, + const unsigned char*, + size_t, + const LodePNGDecompressSettings*); + + const void* custom_context; /*optional custom settings for custom functions*/ +}; + +extern const LodePNGDecompressSettings lodepng_default_decompress_settings; +void lodepng_decompress_settings_init(LodePNGDecompressSettings* settings); +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/* +Settings for zlib compression. Tweaking these settings tweaks the balance +between speed and compression ratio. +*/ +typedef struct LodePNGCompressSettings LodePNGCompressSettings; +struct LodePNGCompressSettings /*deflate = compress*/ +{ + /*LZ77 related settings*/ + unsigned btype; /*the block type for LZ (0, 1, 2 or 3, see zlib standard). + Should be 2 for proper compression.*/ + unsigned use_lz77; /*whether or not to use LZ77. Should be 1 for proper + compression.*/ + unsigned windowsize; /*must be a power of two <= 32768. higher compresses more + but is slower. Default value: 2048.*/ + unsigned minmatch; /*minimum lz77 length. 3 is normally best, 6 can be better + for some PNGs. Default: 0*/ + unsigned nicematch; /*stop searching if >= this length found. Set to 258 for + best compression. Default: 128*/ + unsigned lazymatching; /*use lazy matching: better compression but a bit + slower. Default: true*/ + + /*use custom zlib encoder instead of built in one (default: null)*/ + unsigned (*custom_zlib)(unsigned char**, + size_t*, + const unsigned char*, + size_t, + const LodePNGCompressSettings*); + /*use custom deflate encoder instead of built in one (default: null) + if custom_zlib is used, custom_deflate is ignored since only the built in + zlib function will call custom_deflate*/ + unsigned (*custom_deflate)(unsigned char**, + size_t*, + const unsigned char*, + size_t, + const LodePNGCompressSettings*); + + const void* custom_context; /*optional custom settings for custom functions*/ +}; + +extern const LodePNGCompressSettings lodepng_default_compress_settings; +void lodepng_compress_settings_init(LodePNGCompressSettings* settings); +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_PNG +/* +Color mode of an image. Contains all information required to decode the pixel +bits to RGBA colors. This information is the same as used in the PNG file +format, and is used both for PNG and raw image data in LodePNG. +*/ +typedef struct LodePNGColorMode +{ + /*header (IHDR)*/ + LodePNGColorType colortype; /*color type, see PNG standard or documentation + further in this header file*/ + unsigned bitdepth; /*bits per sample, see PNG standard or documentation + further in this header file*/ + + /* + palette (PLTE and tRNS) + + Dynamically allocated with the colors of the palette, including alpha. + This field may not be allocated directly, use lodepng_color_mode_init first, + then lodepng_palette_add per color to correctly initialize it (to ensure size + of exactly 1024 bytes). + + The alpha channels must be set as well, set them to 255 for opaque images. + + When decoding, with the default settings you can ignore this palette, since + LodePNG already fills the palette colors in the pixels of the raw RGBA output, + but when decoding to the original PNG color mode it is needed to reconstruct + the colors. + + The palette is only supported for color type 3. + */ + unsigned char* palette; /*palette in RGBARGBA... order. Must be either 0, or + when allocated must have 1024 bytes*/ + size_t palettesize; /*palette size in number of colors (amount of used bytes + is 4 * palettesize)*/ + + /* + transparent color key (tRNS) + + This color uses the same bit depth as the bitdepth value in this struct, which + can be 1-bit to 16-bit. For grayscale PNGs, r, g and b will all 3 be set to + the same. + + When decoding, by default you can ignore this information, since LodePNG sets + pixels with this key to transparent already in the raw RGBA output. + + The color key is only supported for color types 0 and 2. + */ + unsigned + key_defined; /*is a transparent color key given? 0 = false, 1 = true*/ + unsigned key_r; /*red/grayscale component of color key*/ + unsigned key_g; /*green component of color key*/ + unsigned key_b; /*blue component of color key*/ +} LodePNGColorMode; + +/*init, cleanup and copy functions to use with this struct*/ +void lodepng_color_mode_init(LodePNGColorMode* info); +void lodepng_color_mode_cleanup(LodePNGColorMode* info); +/*return value is error code (0 means no error)*/ +unsigned lodepng_color_mode_copy(LodePNGColorMode* dest, + const LodePNGColorMode* source); +/* Makes a temporary LodePNGColorMode that does not need cleanup (no palette) */ +LodePNGColorMode lodepng_color_mode_make(LodePNGColorType colortype, + unsigned bitdepth); + +void lodepng_palette_clear(LodePNGColorMode* info); +/*add 1 color to the palette*/ +unsigned lodepng_palette_add(LodePNGColorMode* info, + unsigned char r, + unsigned char g, + unsigned char b, + unsigned char a); + +/*get the total amount of bits per pixel, based on colortype and bitdepth in the + * struct*/ +unsigned lodepng_get_bpp(const LodePNGColorMode* info); +/*get the amount of color channels used, based on colortype in the struct. +If a palette is used, it counts as 1 channel.*/ +unsigned lodepng_get_channels(const LodePNGColorMode* info); +/*is it a grayscale type? (only colortype 0 or 4)*/ +unsigned lodepng_is_greyscale_type(const LodePNGColorMode* info); +/*has it got an alpha channel? (only colortype 2 or 6)*/ +unsigned lodepng_is_alpha_type(const LodePNGColorMode* info); +/*has it got a palette? (only colortype 3)*/ +unsigned lodepng_is_palette_type(const LodePNGColorMode* info); +/*only returns true if there is a palette and there is a value in the palette +with alpha < 255. Loops through the palette to check this.*/ +unsigned lodepng_has_palette_alpha(const LodePNGColorMode* info); +/* +Check if the given color info indicates the possibility of having non-opaque +pixels in the PNG image. Returns true if the image can have translucent or +invisible pixels (it still be opaque if it doesn't use such pixels). Returns +false if the image can only have opaque pixels. In detail, it returns true only +if it's a color type with alpha, or has a palette with non-opaque values, or if +"key_defined" is true. +*/ +unsigned lodepng_can_have_alpha(const LodePNGColorMode* info); +/*Returns the byte size of a raw image buffer with given width, height and color + * mode*/ +size_t lodepng_get_raw_size(unsigned w, + unsigned h, + const LodePNGColorMode* color); + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +/*The information of a Time chunk in PNG.*/ +typedef struct LodePNGTime +{ + unsigned year; /*2 bytes used (0-65535)*/ + unsigned month; /*1-12*/ + unsigned day; /*1-31*/ + unsigned hour; /*0-23*/ + unsigned minute; /*0-59*/ + unsigned second; /*0-60 (to allow for leap seconds)*/ +} LodePNGTime; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +/*Information about the PNG image, except pixels, width and height.*/ +typedef struct LodePNGInfo +{ + /*header (IHDR), palette (PLTE) and transparency (tRNS) chunks*/ + unsigned + compression_method; /*compression method of the original file. Always 0.*/ + unsigned filter_method; /*filter method of the original file*/ + unsigned interlace_method; /*interlace method of the original file: 0=none, + 1=Adam7*/ + LodePNGColorMode + color; /*color type and bits, palette and transparency of the PNG file*/ + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /* + Suggested background color chunk (bKGD) + + This uses the same color mode and bit depth as the PNG (except no alpha + channel), with values truncated to the bit depth in the unsigned integer. + + For grayscale and palette PNGs, the value is stored in background_r. The + values in background_g and background_b are then unused. The decoder will set + them equal to background_r, the encoder ignores them in this case. + + When decoding, you may get these in a different color mode than the one you + requested for the raw pixels: the colortype and bitdepth defined by + info_png.color, that is the ones defined in the header of the PNG image, are + used. + + When encoding with auto_convert, you must use the color model defined in + info_png.color for these values. The encoder normally ignores info_png.color + when auto_convert is on, but will use it to interpret these values (and + convert copies of them to its chosen color model). + + When encoding, avoid setting this to an expensive color, such as a non-gray + value when the image is gray, or the compression will be worse since it will + be forced to write the PNG with a more expensive color mode (when auto_convert + is on). + + The decoder does not use this background color to edit the color of pixels. + This is a completely optional metadata feature. + */ + unsigned background_defined; /*is a suggested background color given?*/ + unsigned + background_r; /*red/gray/palette component of suggested background color*/ + unsigned background_g; /*green component of suggested background color*/ + unsigned background_b; /*blue component of suggested background color*/ + + /* + Non-international text chunks (tEXt and zTXt) + + The char** arrays each contain num strings. The actual messages are in + text_strings, while text_keys are keywords that give a short description what + the actual text represents, e.g. Title, Author, Description, or anything else. + + All the string fields below including strings, keys, names and language tags + are null terminated. The PNG specification uses null characters for the keys, + names and tags, and forbids null characters to appear in the main text which + is why we can use null termination everywhere here. + + A keyword is minimum 1 character and maximum 79 characters long (plus the + additional null terminator). It's discouraged to use a single line length + longer than 79 characters for texts. + + Don't allocate these text buffers yourself. Use the init/cleanup functions + correctly and use lodepng_add_text and lodepng_clear_text. + + Standard text chunk keywords and strings are encoded using Latin-1. + */ + size_t text_num; /*the amount of texts in these char** buffers (there may be + more texts in itext)*/ + char** text_keys; /*the keyword of a text chunk (e.g. "Comment")*/ + char** text_strings; /*the actual text*/ + + /* + International text chunks (iTXt) + Similar to the non-international text chunks, but with additional strings + "langtags" and "transkeys", and the following text encodings are used: + keys: Latin-1, langtags: ASCII, transkeys and strings: UTF-8. + keys must be 1-79 characters (plus the additional null terminator), the other + strings are any length. + */ + size_t itext_num; /*the amount of international texts in this PNG*/ + char** itext_keys; /*the English keyword of the text chunk (e.g. "Comment")*/ + char** itext_langtags; /*language tag for this text's language, ISO/IEC 646 + string, e.g. ISO 639 language tag*/ + char** itext_transkeys; /*keyword translated to the international language - + UTF-8 string*/ + char** itext_strings; /*the actual international text - UTF-8 string*/ + + /* + Optional exif metadata in exif_size bytes. + Don't allocate this buffer yourself. Use the init/cleanup functions + correctly and use lodepng_set_exif and lodepng_clear_exif. + The exif data is in exif-encoded form but without JPEG markers, starting with + the 'II' or 'MM' marker that indicates endianness. It's up to an exif handling + library to encode/decode its information. + */ + unsigned exif_defined; /* Whether exif metadata is present, that is, the PNG + image has an eXIf chunk */ + unsigned char* exif; /* The bytes of the exif metadata, if present */ + unsigned exif_size; /* The size of the exif data in bytes */ + + /*time chunk (tIME)*/ + unsigned time_defined; /*set to 1 to make the encoder generate a tIME chunk*/ + LodePNGTime time; + + /*phys chunk (pHYs)*/ + unsigned phys_defined; /*if 0, there is no pHYs chunk and the values below are + undefined, if 1 else there is one*/ + unsigned phys_x; /*pixels per unit in x direction*/ + unsigned phys_y; /*pixels per unit in y direction*/ + unsigned phys_unit; /*may be 0 (unknown unit) or 1 (metre)*/ + + /* + Color profile related chunk types: cICP, iCPP, sRGB, gAMA, cHRM, sBIT + + LodePNG does not apply any color conversions on pixels in the encoder or + decoder and does not interpret these color profile values. It merely passes on + the information. If you wish to use color profiles and convert colors, a + separate color management library should be used. There is also a limited + library for this in lodepng_util.h. + + There are 4 types of (sets of) chunks providing color information. If multiple + are present, each will be decoded by LodePNG, but only one should be handled + by the user, with the following order of priority depending on what the user + supports: + 1: cICP: Coding-independent code points (CICP) + 2: iCCP: ICC profile + 3: sRGB: indicates the image is in the sRGB color profile + 4: gAMA and cHRM: indicates a gamma and chromaticity value to define the color + profile + */ + + /* + gAMA chunk: Image gamma + Optional, overridden by cICP, iCCP or sRGB if those are present. + Together with cHRM, this is a primitive way of specifying the image color + profile. + */ + unsigned gama_defined; /* Whether a gAMA chunk is present (0 = not present, 1 + = present). */ + unsigned gama_gamma; /* Gamma exponent times 100000 */ + + /* + cHRM chunk: Primary chromaticities and white point + Optional, overridden by cICP, iCCP or sRGB if those are present. + Together with gAMA, this is a primitive way of specifying the image color + profile. + */ + unsigned chrm_defined; /* Whether a cHRM chunk is present (0 = not present, 1 + = present). */ + unsigned chrm_white_x; /* White Point x times 100000 */ + unsigned chrm_white_y; /* White Point y times 100000 */ + unsigned chrm_red_x; /* Red x times 100000 */ + unsigned chrm_red_y; /* Red y times 100000 */ + unsigned chrm_green_x; /* Green x times 100000 */ + unsigned chrm_green_y; /* Green y times 100000 */ + unsigned chrm_blue_x; /* Blue x times 100000 */ + unsigned chrm_blue_y; /* Blue y times 100000 */ + + /* + sRGB chunk: Indicates the image is in the sRGB color space. + Optional. Should not appear at the same time as iCCP. + If gAMA is also present gAMA must contain value 45455. + If cHRM is also present cHRM must contain respectively + 31270,32900,64000,33000,30000,60000,15000,6000. + */ + unsigned srgb_defined; /* Whether an sRGB chunk is present (0 = not present, 1 + = present). */ + unsigned srgb_intent; /* Rendering intent: 0=perceptual, 1=rel. colorimetric, + 2=saturation, 3=abs. colorimetric */ + + /* + iCCP chunk: Embedded ICC profile. + Optional. Should not appear at the same time as sRGB. + + Contains ICC profile, which can use any version of the ICC.1 specification by + the International Color Consortium. See its specification for more details. + LodePNG does not parse or use the ICC profile (except its color space header + field for "RGB" or "GRAY", see below), a separate library to handle the ICC + data format is needed to use it for color management and conversions. + + For encoding, if iCCP is present, the PNG specification recommends to also add + gAMA and cHRM chunks that approximate the ICC profile, for compatibility with + applications that don't use the ICC chunk. This is not required, and it's up + to the user to compute approximate values and set then in the appropriate + gama_ and chrm_ fields, LodePNG does not do this automatically since it does + not interpret the ICC profile. + + For encoding, the ICC profile is required by the PNG specification to be an + "RGB" profile for non-gray PNG color types (types 2, 3 and 6) and a "GRAY" + profile for gray PNG color types (types 1 and 4). If you disable auto_convert, + you must ensure the ICC profile type matches your requested color type, else + the encoder gives an error. If auto_convert is enabled (the default), and the + ICC profile is not a correct match for the pixel data, this will result in an + encoder error if the pixel data has non-gray pixels for a GRAY profile, or a + silent less-optimal compression of the pixel data if the pixels could be + encoded as grayscale but the ICC profile is RGB. + + To avoid this do not set an ICC profile in the image unless there is a good + reason for it, and when doing so make sure you compute it carefully to avoid + the above problems. + */ + unsigned iccp_defined; /* Whether an iCCP chunk is present (0 = not present, 1 + = present). */ + char* iccp_name; /* Null terminated string with profile name, 1-79 bytes */ + /* + The ICC profile in iccp_profile_size bytes. + Don't allocate this buffer yourself. Use the init/cleanup functions + correctly and use lodepng_set_icc and lodepng_clear_icc. + */ + unsigned char* iccp_profile; + unsigned iccp_profile_size; /* The size of iccp_profile in bytes */ + + /* + cICP chunk: Coding-independent code points for video signal type + identification. Optional. If present, and supported, overrides iCCP, sRGB, + gAMA and cHRM. The meaning of the values are as defined in the specification + ITU-T-H.273. LodePNG does not use these values, only passes on the metadata. + The meaning of the values is they are enum values representing certain color + spaces, including HDR color spaces, such as Display P3, PQ and HLG. The video + full range flag value should typically be 1 for the use cases of PNG images, + but can be 0 for narrow-range images in certain video editing workflows. + */ + unsigned cicp_defined; /* Whether an cICP chunk is present (0 = not present, 1 + = present). */ + unsigned cicp_color_primaries; /* Colour primaries value */ + unsigned cicp_transfer_function; /* Transfer characteristics value */ + unsigned cicp_matrix_coefficients; /* Matrix coefficients value */ + unsigned cicp_video_full_range_flag; /* Video full range flag value */ + + /* + mDCV chunk: Mastering Display Color Volume. + Optional, typically used in conjunction with certain HDR color spaces that can + be represented by the cICP chunk. + See the PNG specification, third edition, for more information on this chunk. + All the red, green, blue and white x and y values are encoded as 16-bit + integers and therefore must be in range 0-65536. The min and max luminance + values are 32-bit integers. + */ + unsigned mdcv_defined; /* Whether an mDCV chunk is present (0 = not present, 1 + = present). */ + /* Mastering display color primary chromaticities (CIE 1931 x,y of R,G,B) */ + unsigned mdcv_red_x; /* Red x times 50000 */ + unsigned mdcv_red_y; /* Red y times 50000 */ + unsigned mdcv_green_x; /* Green x times 50000 */ + unsigned mdcv_green_y; /* Green y times 50000 */ + unsigned mdcv_blue_x; /* Blue x times 50000 */ + unsigned mdcv_blue_y; /* Blue y times 50000 */ + /* Mastering display white point chromaticity (CIE 1931 x,y) */ + unsigned mdcv_white_x; /* White Point x times 50000 */ + unsigned mdcv_white_y; /* White Point y times 50000 */ + /* Mastering display luminance */ + unsigned mdcv_max_luminance; /* Max luminance in cd/m^2 times 10000 */ + unsigned mdcv_min_luminance; /* Min luminance in cd/m^2 times 10000 */ + + /* + cLLI chunk: Content Light Level Information. + Optional, typically used in conjunction with certain HDR color spaces that can + be represented by the cICP chunk. + See the PNG specification, third edition, for more information on this chunk. + The clli_max_cll and clli_max_fall values are 32-bit integers. + */ + unsigned clli_defined; /* Whether a cLLI chunk is present (0 = not present, 1 + = present). */ + unsigned clli_max_cll; /* Maximum Content Light Level (MaxCLL) in cd/m^2 times + 10000 */ + unsigned clli_max_fall; /* Maximum Frame-Average Light Level (MaxFALL) in + cd/m^2 times 10000 */ + + /* + sBIT chunk: significant bits. + Optional metadata, only set this if needed. + + If defined, these values give the bit depth of the original data. Since PNG + only stores 1, 2, 4, 8 or 16-bit per channel data, the significant bits value + can be used to indicate the original encoded data has another sample depth, + such as 10 or 12. + + Encoders using this value, when storing the pixel data, should use the most + significant bits of the data to store the original bits, and use a good sample + depth scaling method such as "left bit replication" to fill in the least + significant bits, rather than fill zeroes. + + Decoders using this value, if able to work with data that's e.g. 10-bit or + 12-bit, should right shift the data to go back to the original bit depth, but + decoders are also allowed to ignore sbit and work e.g. with the 8-bit or + 16-bit data from the PNG directly, since thanks to the encoder contract, the + values encoded in PNG are in valid range for the PNG bit depth. + + For grayscale images, sbit_g and sbit_b are not used, and for images that + don't use color type RGBA or grayscale+alpha, sbit_a is not used (it's not + used even for palette images with translucent palette values, or images with + color key). The values that are used must be greater than zero and smaller + than or equal to the PNG bit depth. + + The color type from the header in the PNG image defines these used and unused + fields: if decoding with a color mode conversion, such as always decoding to + RGBA, this metadata still only uses the color type of the original PNG, and + may e.g. lack the alpha channel info if the PNG was RGB. When encoding with + auto_convert (as well as without), also always the color model defined in + info_png.color determines this. + + NOTE: enabling sbit can hurt compression, because the encoder can then not + always use auto_convert to choose a more optimal color mode for the data, + because the PNG format has strict requirements for the allowed sbit values in + combination with color modes. For example, setting these fields to 10-bit will + force the encoder to keep using a 16-bit per channel color mode, even if the + pixel data would in fact fit in a more efficient 8-bit mode. + */ + unsigned sbit_defined; /*is significant bits given? if not, the values below + are unused*/ + unsigned sbit_r; /*red or gray component of significant bits*/ + unsigned sbit_g; /*green component of significant bits*/ + unsigned sbit_b; /*blue component of significant bits*/ + unsigned sbit_a; /*alpha component of significant bits*/ + + /* End of color profile related chunks */ + + /* + unknown chunks: chunks not known by LodePNG, passed on byte for byte. + + There are 3 buffers, one for each position in the PNG where unknown chunks can + appear. Each buffer contains all unknown chunks for that position + consecutively. The 3 positions are: 0: between IHDR and PLTE, 1: between PLTE + and IDAT, 2: between IDAT and IEND. + + For encoding, do not store critical chunks or known chunks that are enabled + with a "_defined" flag above in here, since the encoder will blindly follow + this and could then encode an invalid PNG file (such as one with two IHDR + chunks or the disallowed combination of sRGB with iCCP). But do use this if + you wish to store an ancillary chunk that is not supported by LodePNG (such as + sPLT or hIST), or any non-standard PNG chunk. + + Do not allocate or traverse this data yourself. Use the chunk traversing + functions declared later, such as lodepng_chunk_next and lodepng_chunk_append, + to read/write this struct. + */ + unsigned char* unknown_chunks_data[3]; + size_t unknown_chunks_size[3]; /*size in bytes of the unknown chunks, given + for protection*/ +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} LodePNGInfo; + +/*init, cleanup and copy functions to use with this struct*/ +void lodepng_info_init(LodePNGInfo* info); +void lodepng_info_cleanup(LodePNGInfo* info); +/*return value is error code (0 means no error)*/ +unsigned lodepng_info_copy(LodePNGInfo* dest, const LodePNGInfo* source); + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +unsigned lodepng_add_text(LodePNGInfo* info, + const char* key, + const char* str); /*push back both texts at once*/ +void lodepng_clear_text(LodePNGInfo* info); /*use this to clear the texts again + after you filled them in*/ + +unsigned lodepng_add_itext( + LodePNGInfo* info, + const char* key, + const char* langtag, + const char* transkey, + const char* str); /*push back the 4 texts of 1 chunk at once*/ +void lodepng_clear_itext(LodePNGInfo* info); /*use this to clear the itexts + again after you filled them in*/ + +/*replaces if exists*/ +unsigned lodepng_set_icc(LodePNGInfo* info, + const char* name, + const unsigned char* profile, + unsigned profile_size); +void lodepng_clear_icc(LodePNGInfo* info); /*use this to clear the profile again + after you filled it in*/ + +/*replaces if exists*/ +unsigned lodepng_set_exif(LodePNGInfo* info, + const unsigned char* exif, + unsigned exif_size); +void lodepng_clear_exif( + LodePNGInfo* info); /*use this to clear the exif metadata again after you + filled it in*/ +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +/* +Converts raw buffer from one color type to another color type, based on +LodePNGColorMode structs to describe the input and output color type. +See the reference manual at the end of this header file to see which color +conversions are supported. return value = LodePNG error code (0 if all went ok, +an error if the conversion isn't supported) The out buffer must have size (w * h +* bpp + 7) / 8, where bpp is the bits per pixel of the output color type +(lodepng_get_bpp). For < 8 bpp images, there should not be padding bits at the +end of scanlines. For 16-bit per channel colors, uses big endian format like PNG +does. Return value is LodePNG error code +*/ +unsigned lodepng_convert(unsigned char* out, + const unsigned char* in, + const LodePNGColorMode* mode_out, + const LodePNGColorMode* mode_in, + unsigned w, + unsigned h); + +#ifdef LODEPNG_COMPILE_DECODER +/* +Settings for the decoder. This contains settings for the PNG and the Zlib +decoder, but not the Info settings from the Info structs. +*/ +typedef struct LodePNGDecoderSettings +{ + LodePNGDecompressSettings + zlibsettings; /*in here is the setting to ignore Adler32 checksums*/ + + /* Check LodePNGDecompressSettings for more ignorable errors such as + * ignore_adler32 */ + unsigned ignore_crc; /*ignore CRC checksums*/ + unsigned ignore_critical; /*ignore unknown critical chunks*/ + unsigned ignore_end; /*ignore issues at end of file if possible (missing IEND + chunk, too large chunk, ...)*/ + /* TODO: make a system involving warnings with levels and a strict mode + instead. Other potentially recoverable errors: srgb rendering intent value, + size of content of ancillary chunks, more than 79 characters for some + strings, placement/combination rules for ancillary chunks, crc of unknown + chunks, allowed characters in string keys, invalid characters in chunk + types names, etc... */ + + unsigned color_convert; /*whether to convert the PNG to the color type you + want. Default: yes*/ + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + unsigned read_text_chunks; /*if false but remember_unknown_chunks is true, + they're stored in the unknown chunks*/ + + /*store all bytes from unknown chunks in the LodePNGInfo (off by default, + * useful for a png editor)*/ + unsigned remember_unknown_chunks; + + /* maximum size for decompressed text chunks. If a text chunk's text is larger + than this, an error is returned, unless reading text chunks is disabled or + this limit is set higher or disabled. Set to 0 to allow any size. By default + it is a value that prevents unreasonably large strings from hogging memory. */ + size_t max_text_size; + + /* maximum size for compressed ICC chunks. If the ICC profile is larger than + this, an error will be returned. Set to 0 to allow any size. By default this + is a value that prevents ICC profiles that would be much larger than any + legitimate profile could be to hog memory. */ + size_t max_icc_size; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} LodePNGDecoderSettings; + +void lodepng_decoder_settings_init(LodePNGDecoderSettings* settings); +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/*strategy to use to choose the PNG filter per scanline. Strategies 0-4 +correspond to each of the 5 filter types PNG supports, the next values are +adaptive strategies*/ +typedef enum LodePNGFilterStrategy +{ + /*every filter at zero*/ + LFS_ZERO = 0, + /*every filter at 1, 2, 3 or 4 (paeth), unlike LFS_ZERO not a good choice, but + for testing*/ + LFS_ONE = 1, + LFS_TWO = 2, + LFS_THREE = 3, + LFS_FOUR = 4, + /*Use the filter out of the 5 above types that gives minimum sum, by trying + each one. This is the adaptive filtering suggested heuristic in the PNG + standard chapter 'Filter selection'.*/ + LFS_MINSUM, + /*Use the filter type that gives smallest Shannon entropy for this scanline. + Depending on the image, this is better or worse than minsum.*/ + LFS_ENTROPY, + /* + Brute-force-search PNG filters by compressing each filter for each scanline. + Experimental, very slow, and only rarely gives better compression than MINSUM. + */ + LFS_BRUTE_FORCE, + /*use predefined_filters buffer: you specify the filter type for each + scanline*/ + LFS_PREDEFINED +} LodePNGFilterStrategy; + +/*Gives characteristics about the integer RGBA colors of the image (count, alpha +channel usage, bit depth, ...), which helps decide which color model to use for +encoding. Used internally by default if "auto_convert" is enabled. Public +because it's useful for custom algorithms.*/ +typedef struct LodePNGColorStats +{ + unsigned colored; /*not grayscale*/ + unsigned key; /*image is not opaque and color key is possible instead of full + alpha*/ + unsigned short key_r; /*key values, always as 16-bit, in 8-bit case the byte + is duplicated, e.g. 65535 means 255*/ + unsigned short key_g; + unsigned short key_b; + unsigned + alpha; /*image is not opaque and alpha channel or alpha palette required*/ + unsigned numcolors; /*amount of colors, up to 257. Not valid if bits == 16 or + allow_palette is disabled.*/ + unsigned char + palette[1024]; /*Remembers up to the first 256 RGBA colors, in no + particular order, only valid when numcolors is valid*/ + unsigned bits; /*bits per channel (not for palette). 1,2 or 4 for grayscale + only. 16 if 16-bit per channel required.*/ + size_t numpixels; + + /*user settings for computing/using the stats*/ + unsigned allow_palette; /*default 1. if 0, disallow choosing palette colortype + in auto_choose_color, and don't count numcolors*/ + unsigned allow_greyscale; /*default 1. if 0, choose RGB or RGBA even if the + image only has gray colors*/ +} LodePNGColorStats; + +void lodepng_color_stats_init(LodePNGColorStats* stats); + +/*Get a LodePNGColorStats of the image. The stats must already have been inited. +Returns error code (e.g. alloc fail) or 0 if ok.*/ +unsigned lodepng_compute_color_stats(LodePNGColorStats* stats, + const unsigned char* image, + unsigned w, + unsigned h, + const LodePNGColorMode* mode_in); + +/*Settings for the encoder.*/ +typedef struct LodePNGEncoderSettings +{ + LodePNGCompressSettings + zlibsettings; /*settings for the zlib encoder, such as window size, ...*/ + + /*automatically choose output PNG color type. If false, must explicitely + choose the output color type in state.info_png.color.colortype, + info_png.color.bitdepth and optionally its palette. Default: true*/ + unsigned auto_convert; + + /*If true, follows the suggestion in the PNG standard in chapter 'Filter + selection': if the PNG uses a palette or lower than 8 bit depth, set all + filters to zero. In other cases this will use the heuristic from the chosen + filter_strategy. The PNG standard suggests LFS_MINSUM for those cases.*/ + unsigned filter_palette_zero; + /*Which filter strategy to use when not using zeroes due to + filter_palette_zero. Set filter_palette_zero to 0 to ensure always using your + chosen strategy. Default: LFS_MINSUM*/ + LodePNGFilterStrategy filter_strategy; + /*used if filter_strategy is LFS_PREDEFINED. In that case, this must point to + a buffer with the same length as the amount of scanlines in the image, and + each value must <= 5. You have to cleanup this buffer, LodePNG will never free + it. Don't forget that filter_palette_zero must be set to 0 to ensure this is + also used on palette or low bitdepth images.*/ + const unsigned char* predefined_filters; + + /*force creating a PLTE chunk if colortype is 2 or 6 (= a suggested palette). + If colortype is 3, PLTE is always created. If color type is explicitely set + to a grayscale type (1 or 4), this is not done and is ignored. If enabling + this, a palette must be present in the info_png. NOTE: enabling this may + worsen compression if auto_convert is used to choose optimal color mode, + because it cannot use grayscale color modes in this case*/ + unsigned force_palette; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*add LodePNG identifier and version as a text chunk, for debugging*/ + unsigned add_id; + /*encode text chunks as zTXt chunks instead of tEXt chunks, and use + * compression in iTXt chunks*/ + unsigned text_compression; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} LodePNGEncoderSettings; + +void lodepng_encoder_settings_init(LodePNGEncoderSettings* settings); +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) +/*The settings, state and information for extended encoding and decoding.*/ +typedef struct LodePNGState +{ +#ifdef LODEPNG_COMPILE_DECODER + LodePNGDecoderSettings decoder; /*the decoding settings*/ +#endif /*LODEPNG_COMPILE_DECODER*/ +#ifdef LODEPNG_COMPILE_ENCODER + LodePNGEncoderSettings encoder; /*the encoding settings*/ +#endif /*LODEPNG_COMPILE_ENCODER*/ + LodePNGColorMode info_raw; /*specifies the format in which you would like to + get the raw pixel buffer*/ + LodePNGInfo info_png; /*info of the PNG image obtained after decoding*/ + unsigned error; +} LodePNGState; + +/*init, cleanup and copy functions to use with this struct*/ +void lodepng_state_init(LodePNGState* state); +void lodepng_state_cleanup(LodePNGState* state); +void lodepng_state_copy(LodePNGState* dest, const LodePNGState* source); +#endif /* defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) \ + */ + +#ifdef LODEPNG_COMPILE_DECODER +/* +Same as lodepng_decode_memory, but uses a LodePNGState to allow custom settings +and getting much more information about the PNG image and color mode. +*/ +unsigned lodepng_decode(unsigned char** out, + unsigned* w, + unsigned* h, + LodePNGState* state, + const unsigned char* in, + size_t insize); + +/* +Read the PNG header, but not the actual data. This returns only the information +that is in the IHDR chunk of the PNG, such as width, height and color type. The +information is placed in the info_png field of the LodePNGState. +*/ +unsigned lodepng_inspect(unsigned* w, + unsigned* h, + LodePNGState* state, + const unsigned char* in, + size_t insize); +#endif /*LODEPNG_COMPILE_DECODER*/ + +/* +Reads one metadata chunk (other than IHDR, which is handled by lodepng_inspect) +of the PNG file and outputs what it read in the state. Returns error code on +failure. Use lodepng_inspect first with a new state, then e.g. +lodepng_chunk_find_const to find the desired chunk type, and if non null use +lodepng_inspect_chunk (with chunk_pointer - start_of_file as pos). Supports most +metadata chunks from the PNG standard (gAMA, bKGD, tEXt, ...). Ignores +unsupported, unknown, non-metadata or IHDR chunks (without error). Requirements: +&in[pos] must point to start of a chunk, must use regular lodepng_inspect first +since format of most other chunks depends on IHDR, and if there is a PLTE chunk, +that one must be inspected before tRNS or bKGD. +*/ +unsigned lodepng_inspect_chunk(LodePNGState* state, + size_t pos, + const unsigned char* in, + size_t insize); + +#ifdef LODEPNG_COMPILE_ENCODER +/*This function allocates the out buffer with standard malloc and stores the + * size in *outsize.*/ +unsigned lodepng_encode(unsigned char** out, + size_t* outsize, + const unsigned char* image, + unsigned w, + unsigned h, + LodePNGState* state); +#endif /*LODEPNG_COMPILE_ENCODER*/ + +/* +The lodepng_chunk functions are normally not needed, except to traverse the +unknown chunks stored in the LodePNGInfo struct, or add new ones to it. +It also allows traversing the chunks of an encoded PNG file yourself. + +The chunk pointer always points to the beginning of the chunk itself, that is +the first byte of the 4 length bytes. + +In the PNG file format, chunks have the following format: +-4 bytes length: length of the data of the chunk in bytes (chunk itself is 12 +bytes longer) -4 bytes chunk type (ASCII a-z,A-Z only, see below) -length bytes +of data (may be 0 bytes if length was 0) -4 bytes of CRC, computed on chunk name ++ data + +The first chunk starts at the 8th byte of the PNG file, the entire rest of the +file exists out of concatenated chunks with the above format. + +PNG standard chunk ASCII naming conventions: +-First byte: uppercase = critical, lowercase = ancillary +-Second byte: uppercase = public, lowercase = private +-Third byte: must be uppercase +-Fourth byte: uppercase = unsafe to copy, lowercase = safe to copy +*/ + +/* +Gets the length of the data of the chunk. Total chunk length has 12 bytes more. +There must be at least 4 bytes to read from. If the result value is too large, +it may be corrupt data. +*/ +unsigned lodepng_chunk_length(const unsigned char* chunk); + +/*puts the 4-byte type in null terminated string*/ +void lodepng_chunk_type(char type[5], const unsigned char* chunk); + +/*check if the type is the given type*/ +unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, + const char* type); + +/*0: it's one of the critical chunk types, 1: it's an ancillary chunk (see PNG + * standard)*/ +unsigned char lodepng_chunk_ancillary(const unsigned char* chunk); + +/*0: public, 1: private (see PNG standard)*/ +unsigned char lodepng_chunk_private(const unsigned char* chunk); + +/*0: the chunk is unsafe to copy, 1: the chunk is safe to copy (see PNG + * standard)*/ +unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk); + +/*get pointer to the data of the chunk, where the input points to the header of + * the chunk*/ +unsigned char* lodepng_chunk_data(unsigned char* chunk); +const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk); + +/*returns 0 if the crc is correct, 1 if it's incorrect (0 for OK as usual!)*/ +unsigned lodepng_chunk_check_crc(const unsigned char* chunk); + +/*generates the correct CRC from the data and puts it in the last 4 bytes of the + * chunk*/ +void lodepng_chunk_generate_crc(unsigned char* chunk); + +/* +Iterate to next chunks, allows iterating through all chunks of the PNG file. +Input must be at the beginning of a chunk (result of a previous +lodepng_chunk_next call, or the 8th byte of a PNG file which always has the +first chunk), or alternatively may point to the first byte of the PNG file +(which is not a chunk but the magic header, the function will then skip over it +and return the first real chunk). Will output pointer to the start of the next +chunk, or at or beyond end of the file if there is no more chunk after this or +possibly if the chunk is corrupt. Start this process at the 8th byte of the PNG +file. In a non-corrupt PNG file, the last chunk should have name "IEND". +*/ +unsigned char* lodepng_chunk_next(unsigned char* chunk, unsigned char* end); +const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk, + const unsigned char* end); + +/*Finds the first chunk with the given type in the range [chunk, end), or + * returns NULL if not found.*/ +unsigned char* lodepng_chunk_find(unsigned char* chunk, + unsigned char* end, + const char type[5]); +const unsigned char* lodepng_chunk_find_const(const unsigned char* chunk, + const unsigned char* end, + const char type[5]); + +/* +Appends chunk to the data in out. The given chunk should already have its chunk +header. The out variable and outsize are updated to reflect the new reallocated +buffer. Returns error code (0 if it went ok) +*/ +unsigned lodepng_chunk_append(unsigned char** out, + size_t* outsize, + const unsigned char* chunk); + +/* +Appends new chunk to out. The chunk to append is given by giving its length, +type and data separately. The type is a 4-letter string. The out variable and +outsize are updated to reflect the new reallocated buffer. Returne error code (0 +if it went ok) +*/ +unsigned lodepng_chunk_create(unsigned char** out, + size_t* outsize, + size_t length, + const char* type, + const unsigned char* data); + +/*Calculate CRC32 of buffer*/ +unsigned lodepng_crc32(const unsigned char* buf, size_t len); +#endif /*LODEPNG_COMPILE_PNG*/ + +#ifdef LODEPNG_COMPILE_ZLIB +/* +This zlib part can be used independently to zlib compress and decompress a +buffer. It cannot be used to create gzip files however, and it only supports the +part of zlib that is required for PNG, it does not support dictionaries. +*/ + +#ifdef LODEPNG_COMPILE_DECODER +/*Inflate a buffer. Inflate is the decompression step of deflate. Out buffer + * must be freed after use.*/ +unsigned lodepng_inflate(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings); + +/* +Decompresses Zlib data. Reallocates the out buffer and appends the data. The +data must be according to the zlib specification. +Either, *out must be NULL and *outsize must be 0, or, *out must be a valid +buffer and *outsize its size in bytes. out must be freed by user after usage. +*/ +unsigned lodepng_zlib_decompress(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings* settings); +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/* +Compresses data with Zlib. Reallocates the out buffer and appends the data. +Zlib adds a small header and trailer around the deflate data. +The data is output in the format of the zlib specification. +Either, *out must be NULL and *outsize must be 0, or, *out must be a valid +buffer and *outsize its size in bytes. out must be freed by user after usage. +*/ +unsigned lodepng_zlib_compress(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings* settings); + +/* +Find length-limited Huffman code for given frequencies. This function is in the +public interface only for tests, it's used internally by lodepng_deflate. +*/ +unsigned lodepng_huffman_code_lengths(unsigned* lengths, + const unsigned* frequencies, + size_t numcodes, + unsigned maxbitlen); + +/*Compress a buffer with deflate. See RFC 1951. Out buffer must be freed after + * use.*/ +unsigned lodepng_deflate(unsigned char** out, + size_t* outsize, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings* settings); + +#endif /*LODEPNG_COMPILE_ENCODER*/ +#endif /*LODEPNG_COMPILE_ZLIB*/ + +#ifdef LODEPNG_COMPILE_DISK +/* +Load a file from disk into buffer. The function allocates the out buffer, and +after usage you should free it. +out: output parameter, contains pointer to loaded buffer. +outsize: output parameter, size of the allocated out buffer +filename: the path to the file to load +return value: error code (0 means ok) + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory. +*/ +unsigned lodepng_load_file(unsigned char** out, + size_t* outsize, + const char* filename); + +/* +Save a file from buffer to disk. Warning, if it exists, this function overwrites +the file without warning! +buffer: the buffer to write +buffersize: size of the buffer to write +filename: the path to the file to save to +return value: error code (0 means ok) + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory +*/ +unsigned lodepng_save_file(const unsigned char* buffer, + size_t buffersize, + const char* filename); +#endif /*LODEPNG_COMPILE_DISK*/ + +#ifdef LODEPNG_COMPILE_CPP +/* The LodePNG C++ wrapper uses std::vectors instead of manually allocated + * memory buffers. */ +namespace lodepng { +#ifdef LODEPNG_COMPILE_PNG +class State : public LodePNGState +{ + public: + State(); + State(const State& other); + ~State(); + State& operator=(const State& other); +}; + +#ifdef LODEPNG_COMPILE_DECODER +/* Same as other lodepng::decode, but using a State for more settings and + * information. */ +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + State& state, + const unsigned char* in, + size_t insize); +unsigned decode(std::vector& out, + unsigned& w, + unsigned& h, + State& state, + const std::vector& in); +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/* Same as other lodepng::encode, but using a State for more settings and + * information. */ +unsigned encode(std::vector& out, + const unsigned char* in, + unsigned w, + unsigned h, + State& state); +unsigned encode(std::vector& out, + const std::vector& in, + unsigned w, + unsigned h, + State& state); +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_DISK +/* +Load a file from disk into an std::vector. +return value: error code (0 means ok) + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory +*/ +unsigned load_file(std::vector& buffer, + const std::string& filename); + +/* +Save the binary data in an std::vector to a file on disk. The file is +overwritten without warning. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory +*/ +unsigned save_file(const std::vector& buffer, + const std::string& filename); +#endif /* LODEPNG_COMPILE_DISK */ +#endif /* LODEPNG_COMPILE_PNG */ + +#ifdef LODEPNG_COMPILE_ZLIB +#ifdef LODEPNG_COMPILE_DECODER +/* Zlib-decompress an unsigned char buffer */ +unsigned decompress(std::vector& out, + const unsigned char* in, + size_t insize, + const LodePNGDecompressSettings& settings + = lodepng_default_decompress_settings); + +/* Zlib-decompress an std::vector */ +unsigned decompress(std::vector& out, + const std::vector& in, + const LodePNGDecompressSettings& settings + = lodepng_default_decompress_settings); +#endif /* LODEPNG_COMPILE_DECODER */ + +#ifdef LODEPNG_COMPILE_ENCODER +/* Zlib-compress an unsigned char buffer */ +unsigned compress(std::vector& out, + const unsigned char* in, + size_t insize, + const LodePNGCompressSettings& settings + = lodepng_default_compress_settings); + +/* Zlib-compress an std::vector */ +unsigned compress(std::vector& out, + const std::vector& in, + const LodePNGCompressSettings& settings + = lodepng_default_compress_settings); +#endif /* LODEPNG_COMPILE_ENCODER */ +#endif /* LODEPNG_COMPILE_ZLIB */ +} /* namespace lodepng */ +#endif /*LODEPNG_COMPILE_CPP*/ + +/* +TODO: +[.] test if there are no memory leaks or security exploits - done a lot but +needs to be checked often +[.] check compatibility with various compilers - done but needs to be redone +for every newer version [X] converting color to 16-bit per channel types [X] +support color profile chunk types (but never let them touch RGB values by +default) [ ] support all second edition public PNG chunk types (almost done +except sPLT and hIST) [X] support non-animation third edition public PNG chunk +types: eXIf, cICP, mDCV, cLLI [ ] make sure encoder generates no chunks with +size > (2^31)-1 [ ] partial decoding (stream processing) [X] let the +"isFullyOpaque" function check color keys and transparent palettes too [X] +better name for the variables "codes", "codesD", "codelengthcodes", "clcl" and +"lldl" [ ] allow treating some errors like warnings, when image is recoverable +(e.g. 69, 57, 58) [ ] make warnings like: oob palette, checksum fail, data after +iend, wrong/unknown crit chunk, no null terminator in text, ... [ ] error +messages with line numbers (and version) [ ] errors in state instead of as +return code? [ ] new errors/warnings like suspiciously big decompressed ztxt or +iccp chunk [ ] let the C++ wrapper catch exceptions coming from the standard +library and return LodePNG error codes [ ] allow user to provide custom color +conversion functions, e.g. for premultiplied alpha, padding bits or not, ... [ ] +allow user to give data (void*) to custom allocator [X] provide alternatives for +C library functions not present on some platforms (memcpy, ...) +*/ + +#endif /*LODEPNG_H inclusion guard*/ + +/* +LodePNG Documentation +--------------------- + +0. table of contents +-------------------- + + 1. about + 1.1. supported features + 1.2. features not supported + 2. C and C++ version + 3. security + 4. decoding + 5. encoding + 6. color conversions + 6.1. PNG color types + 6.2. color conversions + 6.3. padding bits + 6.4. A note about 16-bits per channel and endianness + 7. error values + 8. chunks and PNG editing + 9. compiler support + 10. examples + 10.1. decoder C++ example + 10.2. decoder C example + 11. state settings reference + 12. changes + 13. contact information + + +1. about +-------- + +PNG is a file format to store raster images losslessly with good compression, +supporting different color types and alpha channel. + +LodePNG is a PNG codec according to the Portable Network Graphics (PNG) +Specification (Second Edition) - W3C Recommendation 10 November 2003. + +The specifications used are: + +*) Portable Network Graphics (PNG) Specification (Second Edition): + http://www.w3.org/TR/2003/REC-PNG-20031110 +*) RFC 1950 ZLIB Compressed Data Format version 3.3: + http://www.gzip.org/zlib/rfc-zlib.html +*) RFC 1951 DEFLATE Compressed Data Format Specification ver 1.3: + http://www.gzip.org/zlib/rfc-deflate.html + +The most recent version of LodePNG can currently be found at +http://lodev.org/lodepng/ + +LodePNG works both in C (ISO C90) and C++, with a C++ wrapper that adds +extra functionality. + +LodePNG exists out of two files: +-lodepng.h: the header file for both C and C++ +-lodepng.c(pp): give it the name lodepng.c or lodepng.cpp (or .cc) depending on +your usage + +If you want to start using LodePNG right away without reading this doc, get the +examples from the LodePNG website to see how to use it in code, or check the +smaller examples in chapter 13 here. + +LodePNG is simple but only supports the basic requirements. To achieve +simplicity, the following design choices were made: There are no dependencies +on any external library. There are functions to decode and encode a PNG with +a single function call, and extended versions of these functions taking a +LodePNGState struct allowing to specify or get more information. By default +the colors of the raw image are always RGB or RGBA, no matter what color type +the PNG file uses. To read and write files, there are simple functions to +convert the files to/from buffers in memory. + +This all makes LodePNG suitable for loading textures in games, demos and small +programs, ... It's less suitable for full fledged image editors, loading PNGs +over network (it requires all the image data to be available before decoding can +begin), life-critical systems, ... + +1.1. supported features +----------------------- + +The following features are supported by the decoder: + +*) decoding of PNGs with any color type, bit depth and interlace mode, to a 24- +or 32-bit color raw image, or the same color type as the PNG +*) encoding of PNGs, from any raw image to 24- or 32-bit color, or the same +color type as the raw image +*) Adam7 interlace and deinterlace for any color type +*) loading the image from harddisk or decoding it from a buffer from other +sources than harddisk +*) support for alpha channels, including RGBA color model, translucent palettes +and color keying +*) zlib decompression (inflate) +*) zlib compression (deflate) +*) CRC32 and ADLER32 checksums +*) colorimetric color profile conversions: currently experimentally available in +lodepng_util.cpp only, plus alternatively ability to pass on chroma/gamma/ICC +profile information to other color management system. +*) handling of unknown chunks, allowing making a PNG editor that stores custom +and unknown chunks. +*) the following chunks are supported by both encoder and decoder: + IHDR: header information + PLTE: color palette + IDAT: pixel data + IEND: the final chunk + tRNS: transparency for palettized images + tEXt: textual information + zTXt: compressed textual information + iTXt: international textual information + bKGD: suggested background color + pHYs: physical dimensions + tIME: modification time + cHRM: RGB chromaticities + gAMA: RGB gamma correction + iCCP: ICC color profile + sRGB: rendering intent + sBIT: significant bits + +1.2. features not supported +--------------------------- + +The following features are not (yet) supported: + +*) some features needed to make a conformant PNG-Editor might be still missing. +*) partial loading/stream processing. All data must be available and is +processed in one call. +*) The hIST and sPLT public chunks are not (yet) supported but treated as +unknown chunks + + +2. C and C++ version +-------------------- + +The C version uses buffers allocated with alloc that you need to free() +yourself. You need to use init and cleanup functions for each struct whenever +using a struct from the C version to avoid exploits and memory leaks. + +The C++ version has extra functions with std::vectors in the interface and the +lodepng::State class which is a LodePNGState with constructor and destructor. + +These files work without modification for both C and C++ compilers because all +the additional C++ code is in "#ifdef __cplusplus" blocks that make C-compilers +ignore it, and the C code is made to compile both with strict ISO C90 and C++. + +To use the C++ version, you need to rename the source file to lodepng.cpp +(instead of lodepng.c), and compile it with a C++ compiler. + +To use the C version, you need to rename the source file to lodepng.c (instead +of lodepng.cpp), and compile it with a C compiler. + + +3. Security +----------- + +Even if carefully designed, it's always possible that LodePNG contains possible +exploits. If you discover one, please let me know, and it will be fixed. + +When using LodePNG, care has to be taken with the C version of LodePNG, as well +as the C-style structs when working with C++. The following conventions are used +for all C-style structs: + +-if a struct has a corresponding init function, always call the init function +when making a new one -if a struct has a corresponding cleanup function, call it +before the struct disappears to avoid memory leaks -if a struct has a +corresponding copy function, use the copy function instead of "=". The +destination must also be inited already. + + +4. Decoding +----------- + +Decoding converts a PNG compressed image to a raw pixel buffer. + +Most documentation on using the decoder is at its declarations in the header +above. For C, simple decoding can be done with functions such as +lodepng_decode32, and more advanced decoding can be done with the struct +LodePNGState and lodepng_decode. For C++, all decoding can be done with the +various lodepng::decode functions, and lodepng::State can be used for advanced +features. + +When using the LodePNGState, it uses the following fields for decoding: +*) LodePNGInfo info_png: it stores extra information about the PNG (the input) +in here +*) LodePNGColorMode info_raw: here you can say what color mode of the raw image +(the output) you want to get +*) LodePNGDecoderSettings decoder: you can specify a few extra settings for the +decoder to use + +LodePNGInfo info_png +-------------------- + +After decoding, this contains extra information of the PNG image, except the +actual pixels, width and height because these are already gotten directly from +the decoder functions. + +It contains for example the original color type of the PNG image, text comments, +suggested background color, etc... More details about the LodePNGInfo struct are +at its declaration documentation. + +LodePNGColorMode info_raw +------------------------- + +When decoding, here you can specify which color type you want +the resulting raw image to be. If this is different from the colortype of the +PNG, then the decoder will automatically convert the result. This conversion +always works, except if you want it to convert a color PNG to grayscale or to +a palette with missing colors. + +By default, 32-bit color is used for the result. + +LodePNGDecoderSettings decoder +------------------------------ + +The settings can be used to ignore the errors created by invalid CRC and Adler32 +chunks, and to disable the decoding of tEXt chunks. + +There's also a setting color_convert, true by default. If false, no conversion +is done, the resulting data will be as it was in the PNG (after decompression) +and you'll have to puzzle the colors of the pixels together yourself using the +color type information in the LodePNGInfo. + + +5. Encoding +----------- + +Encoding converts a raw pixel buffer to a PNG compressed image. + +Most documentation on using the encoder is at its declarations in the header +above. For C, simple encoding can be done with functions such as +lodepng_encode32, and more advanced decoding can be done with the struct +LodePNGState and lodepng_encode. For C++, all encoding can be done with the +various lodepng::encode functions, and lodepng::State can be used for advanced +features. + +Like the decoder, the encoder can also give errors. However it gives less errors +since the encoder input is trusted, the decoder input (a PNG image that could +be forged by anyone) is not trusted. + +When using the LodePNGState, it uses the following fields for encoding: +*) LodePNGInfo info_png: here you specify how you want the PNG (the output) to +be. +*) LodePNGColorMode info_raw: here you say what color type of the raw image (the +input) has +*) LodePNGEncoderSettings encoder: you can specify a few settings for the +encoder to use + +LodePNGInfo info_png +-------------------- + +When encoding, you use this the opposite way as when decoding: for encoding, +you fill in the values you want the PNG to have before encoding. By default it's +not needed to specify a color type for the PNG since it's automatically chosen, +but it's possible to choose it yourself given the right settings. + +The encoder will not always exactly match the LodePNGInfo struct you give, +it tries as close as possible. Some things are ignored by the encoder. The +encoder uses, for example, the following settings from it when applicable: +colortype and bitdepth, text chunks, time chunk, the color key, the palette, the +background color, the interlace method, unknown chunks, ... + +When encoding to a PNG with colortype 3, the encoder will generate a PLTE chunk. +If the palette contains any colors for which the alpha channel is not 255 (so +there are translucent colors in the palette), it'll add a tRNS chunk. + +LodePNGColorMode info_raw +------------------------- + +You specify the color type of the raw image that you give to the input here, +including a possible transparent color key and palette you happen to be using in +your raw image data. + +By default, 32-bit color is assumed, meaning your input has to be in RGBA +format with 4 bytes (unsigned chars) per pixel. + +LodePNGEncoderSettings encoder +------------------------------ + +The following settings are supported (some are in sub-structs): +*) auto_convert: when this option is enabled, the encoder will +automatically choose the smallest possible color mode (including color key) that +can encode the colors of all pixels without information loss. +*) btype: the block type for LZ77. 0 = uncompressed, 1 = fixed huffman tree, + 2 = dynamic huffman tree (best compression). Should be 2 for proper + compression. +*) use_lz77: whether or not to use LZ77 for compressed block types. Should be + true for proper compression. +*) windowsize: the window size used by the LZ77 encoder (1 - 32768). Has value + 2048 by default, but can be set to 32768 for better, but slow, compression. +*) force_palette: if colortype is 2 or 6, you can make the encoder write a PLTE + chunk if force_palette is true. This can used as suggested palette to convert + to by viewers that don't support more than 256 colors (if those still exist) +*) add_id: add text chunk "Encoder: LodePNG " to the image. +*) text_compression: default 1. If 1, it'll store texts as zTXt instead of tEXt +chunks. zTXt chunks use zlib compression on the text. This gives a smaller +result on large texts but a larger result on small texts (such as a single +program name). It's all tEXt or all zTXt though, there's no separate setting per +text yet. + + +6. color conversions +-------------------- + +An important thing to note about LodePNG, is that the color type of the PNG, and +the color type of the raw image, are completely independent. By default, when +you decode a PNG, you get the result as a raw image in the color type you want, +no matter whether the PNG was encoded with a palette, grayscale or RGBA color. +And if you encode an image, by default LodePNG will automatically choose the PNG +color type that gives good compression based on the values of colors and amount +of colors in the image. It can be configured to let you control it instead as +well, though. + +To be able to do this, LodePNG does conversions from one color mode to another. +It can convert from almost any color type to any other color type, except the +following conversions: RGB to grayscale is not supported, and converting to a +palette when the palette doesn't have a required color is not supported. This is +not supported on purpose: this is information loss which requires a color +reduction algorithm that is beyond the scope of a PNG encoder (yes, RGB to gray +is easy, but there are multiple ways if you want to give some channels more +weight). + +By default, when decoding, you get the raw image in 32-bit RGBA or 24-bit RGB +color, no matter what color type the PNG has. And by default when encoding, +LodePNG automatically picks the best color model for the output PNG, and expects +the input image to be 32-bit RGBA or 24-bit RGB. So, unless you want to control +the color format of the images yourself, you can skip this chapter. + +6.1. PNG color types +-------------------- + +A PNG image can have many color types, ranging from 1-bit color to 64-bit color, +as well as palettized color modes. After the zlib decompression and unfiltering +in the PNG image is done, the raw pixel data will have that color type and thus +a certain amount of bits per pixel. If you want the output raw image after +decoding to have another color type, a conversion is done by LodePNG. + +The PNG specification gives the following color types: + +0: grayscale, bit depths 1, 2, 4, 8, 16 +2: RGB, bit depths 8 and 16 +3: palette, bit depths 1, 2, 4 and 8 +4: grayscale with alpha, bit depths 8 and 16 +6: RGBA, bit depths 8 and 16 + +Bit depth is the amount of bits per pixel per color channel. So the total amount +of bits per pixel is: amount of channels * bitdepth. + +6.2. color conversions +---------------------- + +As explained in the sections about the encoder and decoder, you can specify +color types and bit depths in info_png and info_raw to change the default +behaviour. + +If, when decoding, you want the raw image to be something else than the default, +you need to set the color type and bit depth you want in the LodePNGColorMode, +or the parameters colortype and bitdepth of the simple decoding function. + +If, when encoding, you use another color type than the default in the raw input +image, you need to specify its color type and bit depth in the LodePNGColorMode +of the raw image, or use the parameters colortype and bitdepth of the simple +encoding function. + +If, when encoding, you don't want LodePNG to choose the output PNG color type +but control it yourself, you need to set auto_convert in the encoder settings +to false, and specify the color type you want in the LodePNGInfo of the +encoder (including palette: it can generate a palette if auto_convert is true, +otherwise not). + +If the input and output color type differ (whether user chosen or auto chosen), +LodePNG will do a color conversion, which follows the rules below, and may +sometimes result in an error. + +To avoid some confusion: +-the decoder converts from PNG to raw image +-the encoder converts from raw image to PNG +-the colortype and bitdepth in LodePNGColorMode info_raw, are those of the raw +image -the colortype and bitdepth in the color field of LodePNGInfo info_png, +are those of the PNG -when encoding, the color type in LodePNGInfo is ignored if +auto_convert is enabled, it is automatically generated instead -when decoding, +the color type in LodePNGInfo is set by the decoder to that of the original PNG +image, but it can be ignored since the raw image has the color type you +requested instead -if the color type of the LodePNGColorMode and PNG image +aren't the same, a conversion between the color types is done if the color types +are supported. If it is not supported, an error is returned. If the types are +the same, no conversion is done. -even though some conversions aren't supported, +LodePNG supports loading PNGs from any colortype and saving PNGs to any +colortype, sometimes it just requires preparing the raw image correctly before +encoding. -both encoder and decoder use the same color converter. + +The function lodepng_convert does the color conversion. It is available in the +interface but normally isn't needed since the encoder and decoder already call +it. + +Non supported color conversions: +-color to grayscale when non-gray pixels are present: no error is thrown, but +the result will look ugly because only the red channel is taken (it assumes all +three channels are the same in this case so ignores green and blue). The reason +no error is given is to allow converting from three-channel grayscale images to +one-channel even if there are numerical imprecisions. +-anything to palette when the palette does not have an exact match for a +from-color in it: in this case an error is thrown + +Supported color conversions: +-anything to 8-bit RGB, 8-bit RGBA, 16-bit RGB, 16-bit RGBA +-any gray or gray+alpha, to gray or gray+alpha +-anything to a palette, as long as the palette has the requested colors in it +-removing alpha channel +-higher to smaller bitdepth, and vice versa + +If you want no color conversion to be done (e.g. for speed or control): +-In the encoder, you can make it save a PNG with any color type by giving the +raw color mode and LodePNGInfo the same color mode, and setting auto_convert to +false. +-In the decoder, you can make it store the pixel data in the same color type +as the PNG has, by setting the color_convert setting to false. Settings in +info_raw are then ignored. + +6.3. padding bits +----------------- + +In the PNG file format, if a less than 8-bit per pixel color type is used and +the scanlines have a bit amount that isn't a multiple of 8, then padding bits +are used so that each scanline starts at a fresh byte. But that is NOT true for +the LodePNG raw input and output. The raw input image you give to the encoder, +and the raw output image you get from the decoder will NOT have these padding +bits, e.g. in the case of a 1-bit image with a width of 7 pixels, the first +pixel of the second scanline will the 8th bit of the first byte, not the first +bit of a new byte. + +6.4. A note about 16-bits per channel and endianness +---------------------------------------------------- + +LodePNG uses unsigned char arrays for 16-bit per channel colors too, just like +for any other color format. The 16-bit values are stored in big endian (most +significant byte first) in these arrays. This is the opposite order of the +little endian used by x86 CPU's. + +LodePNG always uses big endian because the PNG file format does so internally. +Conversions to other formats than PNG uses internally are not supported by +LodePNG on purpose, there are myriads of formats, including endianness of 16-bit +colors, the order in which you store R, G, B and A, and so on. Supporting and +converting to/from all that is outside the scope of LodePNG. + +This may mean that, depending on your use case, you may want to convert the big +endian output of LodePNG to little endian with a for loop. This is certainly not +always needed, many applications and libraries support big endian 16-bit colors +anyway, but it means you cannot simply cast the unsigned char* buffer to an +unsigned short* buffer on x86 CPUs. + + +7. error values +--------------- + +All functions in LodePNG that return an error code, return 0 if everything went +OK, or a non-zero code if there was an error. + +The meaning of the LodePNG error values can be retrieved with the function +lodepng_error_text: given the numerical error code, it returns a description +of the error in English as a string. + +Check the implementation of lodepng_error_text to see the meaning of each code. + +It is not recommended to use the numerical values to programmatically make +different decisions based on error types as the numbers are not guaranteed to +stay backwards compatible. They are for human consumption only. Programmatically +only 0 or non-0 matter. + + +8. chunks and PNG editing +------------------------- + +If you want to add extra chunks to a PNG you encode, or use LodePNG for a PNG +editor that should follow the rules about handling of unknown chunks, or if your +program is able to read other types of chunks than the ones handled by LodePNG, +then that's possible with the chunk functions of LodePNG. + +A PNG chunk has the following layout: + +4 bytes length +4 bytes type name +length bytes data +4 bytes CRC + +8.1. iterating through chunks +----------------------------- + +If you have a buffer containing the PNG image data, then the first chunk (the +IHDR chunk) starts at byte number 8 of that buffer. The first 8 bytes are the +signature of the PNG and are not part of a chunk. But if you start at byte 8 +then you have a chunk, and can check the following things of it. + +NOTE: none of these functions check for memory buffer boundaries. To avoid +exploits, always make sure the buffer contains all the data of the chunks. +When using lodepng_chunk_next, make sure the returned value is within the +allocated memory. + +unsigned lodepng_chunk_length(const unsigned char* chunk): + +Get the length of the chunk's data. The total chunk length is this length + 12. + +void lodepng_chunk_type(char type[5], const unsigned char* chunk): +unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, const char* +type): + +Get the type of the chunk or compare if it's a certain type + +unsigned char lodepng_chunk_critical(const unsigned char* chunk): +unsigned char lodepng_chunk_private(const unsigned char* chunk): +unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk): + +Check if the chunk is critical in the PNG standard (only IHDR, PLTE, IDAT and +IEND are). Check if the chunk is private (public chunks are part of the +standard, private ones not). Check if the chunk is safe to copy. If it's not, +then, when modifying data in a critical chunk, unsafe to copy chunks of the old +image may NOT be saved in the new one if your program doesn't handle that type +of unknown chunk. + +unsigned char* lodepng_chunk_data(unsigned char* chunk): +const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk): + +Get a pointer to the start of the data of the chunk. + +unsigned lodepng_chunk_check_crc(const unsigned char* chunk): +void lodepng_chunk_generate_crc(unsigned char* chunk): + +Check if the crc is correct or generate a correct one. + +unsigned char* lodepng_chunk_next(unsigned char* chunk): +const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk): + +Iterate to the next chunk. This works if you have a buffer with consecutive +chunks. Note that these functions do no boundary checking of the allocated data +whatsoever, so make sure there is enough data available in the buffer to be able +to go to the next chunk. + +unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const +unsigned char* chunk): unsigned lodepng_chunk_create(unsigned char** out, +size_t* outsize, unsigned length, const char* type, const unsigned char* data): + +These functions are used to create new chunks that are appended to the data in +*out that has length *outsize. The append function appends an existing chunk to +the new data. The create function creates a new chunk with the given parameters +and appends it. Type is the 4-letter name of the chunk. + +8.2. chunks in info_png +----------------------- + +The LodePNGInfo struct contains fields with the unknown chunk in it. It has 3 +buffers (each with size) to contain 3 types of unknown chunks: +the ones that come before the PLTE chunk, the ones that come between the PLTE +and the IDAT chunks, and the ones that come after the IDAT chunks. +It's necessary to make the distinction between these 3 cases because the PNG +standard forces to keep the ordering of unknown chunks compared to the critical +chunks, but does not force any other ordering rules. + +info_png.unknown_chunks_data[0] is the chunks before PLTE +info_png.unknown_chunks_data[1] is the chunks after PLTE, before IDAT +info_png.unknown_chunks_data[2] is the chunks after IDAT + +The chunks in these 3 buffers can be iterated through and read by using the same +way described in the previous subchapter. + +When using the decoder to decode a PNG, you can make it store all unknown chunks +if you set the option settings.remember_unknown_chunks to 1. By default, this +option is off (0). + +The encoder will always encode unknown chunks that are stored in the info_png. +If you need it to add a particular chunk that isn't known by LodePNG, you can +use lodepng_chunk_append or lodepng_chunk_create to the chunk data in +info_png.unknown_chunks_data[x]. + +Chunks that are known by LodePNG should not be added in that way. E.g. to make +LodePNG add a bKGD chunk, set background_defined to true and add the correct +parameters there instead. + + +9. compiler support +------------------- + +No libraries other than the current standard C library are needed to compile +LodePNG. For the C++ version, only the standard C++ library is needed on top. +Add the files lodepng.c(pp) and lodepng.h to your project, include +lodepng.h where needed, and your program can read/write PNG files. + +It is compatible with C90 and up, and C++03 and up. + +If performance is important, use optimization when compiling! For both the +encoder and decoder, this makes a large difference. + +Make sure that LodePNG is compiled with the same compiler of the same version +and with the same settings as the rest of the program, or the interfaces with +std::vectors and std::strings in C++ can be incompatible. + +CHAR_BITS must be 8 or higher, because LodePNG uses unsigned chars for octets. + +*) gcc and g++ + +LodePNG is developed in gcc so this compiler is natively supported. It gives no +warnings with compiler options "-Wall -Wextra -pedantic -ansi", with gcc and g++ +version 4.7.1 on Linux, 32-bit and 64-bit. + +*) Clang + +Fully supported and warning-free. + +*) Mingw + +The Mingw compiler (a port of gcc for Windows) should be fully supported by +LodePNG. + +*) Visual Studio and Visual C++ Express Edition + +LodePNG should be warning-free with warning level W4. Two warnings were disabled +with pragmas though: warning 4244 about implicit conversions, and warning 4996 +where it wants to use a non-standard function fopen_s instead of the standard C +fopen. + +Visual Studio may want "stdafx.h" files to be included in each source file and +give an error "unexpected end of file while looking for precompiled header". +This is not standard C++ and will not be added to the stock LodePNG. You can +disable it for lodepng.cpp only by right clicking it, Properties, C/C++, +Precompiled Headers, and set it to Not Using Precompiled Headers there. + +NOTE: Modern versions of VS should be fully supported, but old versions, e.g. +VS6, are not guaranteed to work. + +*) Compilers on Macintosh + +LodePNG has been reported to work both with gcc and LLVM for Macintosh, both for +C and C++. + +*) Other Compilers + +If you encounter problems on any compilers, feel free to let me know and I may +try to fix it if the compiler is modern and standards compliant. + + +10. examples +------------ + +This decoder example shows the most basic usage of LodePNG. More complex +examples can be found on the LodePNG website. + +NOTE: these examples do not support wide-character filenames, you can use an +external method to handle such files and encode or decode in-memory + +10.1. decoder C++ example +------------------------- + +#include + +#include "lodepng.h" + +int main(int argc, char *argv[]) { + const char* filename = argc > 1 ? argv[1] : "test.png"; + + //load and decode + std::vector image; + unsigned width, height; + unsigned error = lodepng::decode(image, width, height, filename); + + //if there's an error, display it + if(error) std::cout << "decoder error " << error << ": " << +lodepng_error_text(error) << std::endl; + + //the pixels are now in the vector "image", 4 bytes per pixel, ordered +RGBARGBA..., use it as texture, draw it, ... +} + +10.2. decoder C example +----------------------- + +#include "lodepng.h" + +int main(int argc, char *argv[]) { + unsigned error; + unsigned char* image; + size_t width, height; + const char* filename = argc > 1 ? argv[1] : "test.png"; + + error = lodepng_decode32_file(&image, &width, &height, filename); + + if(error) printf("decoder error %u: %s\n", error, lodepng_error_text(error)); + + / * use image here * / + + free(image); + return 0; +} + +11. state settings reference +---------------------------- + +A quick reference of some settings to set on the LodePNGState + +For decoding: + +state.decoder.zlibsettings.ignore_adler32: ignore ADLER32 checksums +state.decoder.zlibsettings.custom_...: use custom inflate function +state.decoder.ignore_crc: ignore CRC checksums +state.decoder.ignore_critical: ignore unknown critical chunks +state.decoder.ignore_end: ignore missing IEND chunk. May fail if this corruption +causes other errors state.decoder.color_convert: convert internal PNG color to +chosen one state.decoder.read_text_chunks: whether to read in text metadata +chunks state.decoder.remember_unknown_chunks: whether to read in unknown chunks +state.info_raw.colortype: desired color type for decoded image +state.info_raw.bitdepth: desired bit depth for decoded image +state.info_raw....: more color settings, see struct LodePNGColorMode +state.info_png....: no settings for decoder but ouput, see struct LodePNGInfo + +For encoding: + +state.encoder.zlibsettings.btype: disable compression by setting it to 0 +state.encoder.zlibsettings.use_lz77: use LZ77 in compression +state.encoder.zlibsettings.windowsize: tweak LZ77 windowsize +state.encoder.zlibsettings.minmatch: tweak min LZ77 length to match +state.encoder.zlibsettings.nicematch: tweak LZ77 match where to stop searching +state.encoder.zlibsettings.lazymatching: try one more LZ77 matching +state.encoder.zlibsettings.custom_...: use custom deflate function +state.encoder.auto_convert: choose optimal PNG color type, if 0 uses info_png +state.encoder.filter_palette_zero: PNG filter strategy for palette +state.encoder.filter_strategy: PNG filter strategy to encode with +state.encoder.force_palette: add palette even if not encoding to one +state.encoder.add_id: add LodePNG identifier and version as a text chunk +state.encoder.text_compression: use compressed text chunks for metadata +state.info_raw.colortype: color type of raw input image you provide +state.info_raw.bitdepth: bit depth of raw input image you provide +state.info_raw: more color settings, see struct LodePNGColorMode +state.info_png.color.colortype: desired color type if auto_convert is false +state.info_png.color.bitdepth: desired bit depth if auto_convert is false +state.info_png.color....: more color settings, see struct LodePNGColorMode +state.info_png....: more PNG related settings, see struct LodePNGInfo + + +12. changes +----------- + +The version number of LodePNG is the date of the change given in the format +yyyymmdd. + +Some changes aren't backwards compatible. Those are indicated with a (!) +symbol. + +Not all changes are listed here, the commit history in github lists more: +https://github.com/lvandeve/lodepng + +*) 6 may 2025: renamed mDCv to mDCV and cLLi to cLLI as per the recent rename + in the draft png third edition spec. Please note that while the third + edition is not finalized, backwards-incompatible changes to its features are + possible. +*) 23 dec 2024: added support for the mDCv and cLLi chunks (for png third + edition spec) +*) 22 dec 2024: added support for the cICP chunk (for png third edition spec) +*) 15 dec 2024: added support for the eXIf chunk (for png third edition spec) +*) 10 apr 2023: faster CRC32 implementation, but with larger lookup table. +*) 13 jun 2022: added support for the sBIT chunk. +*) 09 jan 2022: minor decoder speed improvements. +*) 27 jun 2021: added warnings that file reading/writing functions don't support + wide-character filenames (support for this is not planned, opening files is + not the core part of PNG decoding/decoding and is platform dependent). +*) 17 oct 2020: prevent decoding too large text/icc chunks by default. +*) 06 mar 2020: simplified some of the dynamic memory allocations. +*) 12 jan 2020: (!) added 'end' argument to lodepng_chunk_next to allow correct + overflow checks. +*) 14 aug 2019: around 25% faster decoding thanks to huffman lookup tables. +*) 15 jun 2019: (!) auto_choose_color API changed (for bugfix: don't use palette + if gray ICC profile) and non-ICC LodePNGColorProfile renamed to + LodePNGColorStats. +*) 30 dec 2018: code style changes only: removed newlines before opening braces. +*) 10 sep 2018: added way to inspect metadata chunks without full decoding. +*) 19 aug 2018: (!) fixed color mode bKGD is encoded with and made it use + palette index in case of palette. +*) 10 aug 2018: (!) added support for gAMA, cHRM, sRGB and iCCP chunks. This + change is backwards compatible unless you relied on unknown_chunks for those. +*) 11 jun 2018: less restrictive check for pixel size integer overflow +*) 14 jan 2018: allow optionally ignoring a few more recoverable errors +*) 17 sep 2017: fix memory leak for some encoder input error cases +*) 27 nov 2016: grey+alpha auto color model detection bugfix +*) 18 apr 2016: Changed qsort to custom stable sort (for platforms w/o qsort). +*) 09 apr 2016: Fixed colorkey usage detection, and better file loading (within + the limits of pure C90). +*) 08 dec 2015: Made load_file function return error if file can't be opened. +*) 24 oct 2015: Bugfix with decoding to palette output. +*) 18 apr 2015: Boundary PM instead of just package-merge for faster encoding. +*) 24 aug 2014: Moved to github +*) 23 aug 2014: Reduced needless memory usage of decoder. +*) 28 jun 2014: Removed fix_png setting, always support palette OOB for + simplicity. Made ColorProfile public. +*) 09 jun 2014: Faster encoder by fixing hash bug and more zeros optimization. +*) 22 dec 2013: Power of two windowsize required for optimization. +*) 15 apr 2013: Fixed bug with LAC_ALPHA and color key. +*) 25 mar 2013: Added an optional feature to ignore some PNG errors (fix_png). +*) 11 mar 2013: (!) Bugfix with custom free. Changed from "my" to "lodepng_" + prefix for the custom allocators and made it possible with a new #define to + use custom ones in your project without needing to change lodepng's code. +*) 28 jan 2013: Bugfix with color key. +*) 27 oct 2012: Tweaks in text chunk keyword length error handling. +*) 8 oct 2012: (!) Added new filter strategy (entropy) and new auto color mode. + (no palette). Better deflate tree encoding. New compression tweak settings. + Faster color conversions while decoding. Some internal cleanups. +*) 23 sep 2012: Reduced warnings in Visual Studio a little bit. +*) 1 sep 2012: (!) Removed #define's for giving custom (de)compression functions + and made it work with function pointers instead. +*) 23 jun 2012: Added more filter strategies. Made it easier to use custom alloc + and free functions and toggle #defines from compiler flags. Small fixes. +*) 6 may 2012: (!) Made plugging in custom zlib/deflate functions more flexible. +*) 22 apr 2012: (!) Made interface more consistent, renaming a lot. Removed + redundant C++ codec classes. Reduced amount of structs. Everything changed, + but it is cleaner now imho and functionality remains the same. Also fixed + several bugs and shrunk the implementation code. Made new samples. +*) 6 nov 2011: (!) By default, the encoder now automatically chooses the best + PNG color model and bit depth, based on the amount and type of colors of the + raw image. For this, autoLeaveOutAlphaChannel replaced by auto_choose_color. +*) 9 oct 2011: simpler hash chain implementation for the encoder. +*) 8 sep 2011: lz77 encoder lazy matching instead of greedy matching. +*) 23 aug 2011: tweaked the zlib compression parameters after benchmarking. + A bug with the PNG filtertype heuristic was fixed, so that it chooses much + better ones (it's quite significant). A setting to do an experimental, slow, + brute force search for PNG filter types is added. +*) 17 aug 2011: (!) changed some C zlib related function names. +*) 16 aug 2011: made the code less wide (max 120 characters per line). +*) 17 apr 2011: code cleanup. Bugfixes. Convert low to 16-bit per sample colors. +*) 21 feb 2011: fixed compiling for C90. Fixed compiling with sections disabled. +*) 11 dec 2010: encoding is made faster, based on suggestion by Peter Eastman + to optimize long sequences of zeros. +*) 13 nov 2010: added LodePNG_InfoColor_hasPaletteAlpha and + LodePNG_InfoColor_canHaveAlpha functions for convenience. +*) 7 nov 2010: added LodePNG_error_text function to get error code description. +*) 30 oct 2010: made decoding slightly faster +*) 26 oct 2010: (!) changed some C function and struct names (more consistent). + Reorganized the documentation and the declaration order in the header. +*) 08 aug 2010: only changed some comments and external samples. +*) 05 jul 2010: fixed bug thanks to warnings in the new gcc version. +*) 14 mar 2010: fixed bug where too much memory was allocated for char buffers. +*) 02 sep 2008: fixed bug where it could create empty tree that linux apps could + read by ignoring the problem but windows apps couldn't. +*) 06 jun 2008: added more error checks for out of memory cases. +*) 26 apr 2008: added a few more checks here and there to ensure more safety. +*) 06 mar 2008: crash with encoding of strings fixed +*) 02 feb 2008: support for international text chunks added (iTXt) +*) 23 jan 2008: small cleanups, and #defines to divide code in sections +*) 20 jan 2008: support for unknown chunks allowing using LodePNG for an editor. +*) 18 jan 2008: support for tIME and pHYs chunks added to encoder and decoder. +*) 17 jan 2008: ability to encode and decode compressed zTXt chunks added + Also various fixes, such as in the deflate and the padding bits code. +*) 13 jan 2008: Added ability to encode Adam7-interlaced images. Improved + filtering code of encoder. +*) 07 jan 2008: (!) changed LodePNG to use ISO C90 instead of C++. A + C++ wrapper around this provides an interface almost identical to before. + Having LodePNG be pure ISO C90 makes it more portable. The C and C++ code + are together in these files but it works both for C and C++ compilers. +*) 29 dec 2007: (!) changed most integer types to unsigned int + other tweaks +*) 30 aug 2007: bug fixed which makes this Borland C++ compatible +*) 09 aug 2007: some VS2005 warnings removed again +*) 21 jul 2007: deflate code placed in new namespace separate from zlib code +*) 08 jun 2007: fixed bug with 2- and 4-bit color, and small interlaced images +*) 04 jun 2007: improved support for Visual Studio 2005: crash with accessing + invalid std::vector element [0] fixed, and level 3 and 4 warnings removed +*) 02 jun 2007: made the encoder add a tag with version by default +*) 27 may 2007: zlib and png code separated (but still in the same file), + simple encoder/decoder functions added for more simple usage cases +*) 19 may 2007: minor fixes, some code cleaning, new error added (error 69), + moved some examples from here to lodepng_examples.cpp +*) 12 may 2007: palette decoding bug fixed +*) 24 apr 2007: changed the license from BSD to the zlib license +*) 11 mar 2007: very simple addition: ability to encode bKGD chunks. +*) 04 mar 2007: (!) tEXt chunk related fixes, and support for encoding + palettized PNG images. Plus little interface change with palette and texts. +*) 03 mar 2007: Made it encode dynamic Huffman shorter with repeat codes. + Fixed a bug where the end code of a block had length 0 in the Huffman tree. +*) 26 feb 2007: Huffman compression with dynamic trees (BTYPE 2) now implemented + and supported by the encoder, resulting in smaller PNGs at the output. +*) 27 jan 2007: Made the Adler-32 test faster so that a timewaste is gone. +*) 24 jan 2007: gave encoder an error interface. Added color conversion from any + greyscale type to 8-bit greyscale with or without alpha. +*) 21 jan 2007: (!) Totally changed the interface. It allows more color types + to convert to and is more uniform. See the manual for how it works now. +*) 07 jan 2007: Some cleanup & fixes, and a few changes over the last days: + encode/decode custom tEXt chunks, separate classes for zlib & deflate, and + at last made the decoder give errors for incorrect Adler32 or Crc. +*) 01 jan 2007: Fixed bug with encoding PNGs with less than 8 bits per channel. +*) 29 dec 2006: Added support for encoding images without alpha channel, and + cleaned out code as well as making certain parts faster. +*) 28 dec 2006: Added "Settings" to the encoder. +*) 26 dec 2006: The encoder now does LZ77 encoding and produces much smaller +files now. Removed some code duplication in the decoder. Fixed little bug in an +example. +*) 09 dec 2006: (!) Placed output parameters of public functions as first +parameter. Fixed a bug of the decoder with 16-bit per color. +*) 15 oct 2006: Changed documentation structure +*) 09 oct 2006: Encoder class added. It encodes a valid PNG image from the + given image buffer, however for now it's not compressed. +*) 08 sep 2006: (!) Changed to interface with a Decoder class +*) 30 jul 2006: (!) LodePNG_InfoPng , width and height are now retrieved in +different way. Renamed decodePNG to decodePNGGeneric. +*) 29 jul 2006: (!) Changed the interface: image info is now returned as a + struct of type LodePNG::LodePNG_Info, instead of a vector, which was a bit +clumsy. +*) 28 jul 2006: Cleaned the code and added new error checks. + Corrected terminology "deflate" into "inflate". +*) 23 jun 2006: Added SDL example in the documentation in the header, this + example allows easy debugging by displaying the PNG and its transparency. +*) 22 jun 2006: (!) Changed way to obtain error value. Added + loadFile function for convenience. Made decodePNG32 faster. +*) 21 jun 2006: (!) Changed type of info vector to unsigned. + Changed position of palette in info vector. Fixed an important bug that + happened on PNGs with an uncompressed block. +*) 16 jun 2006: Internally changed unsigned into unsigned where + needed, and performed some optimizations. +*) 07 jun 2006: (!) Renamed functions to decodePNG and placed them + in LodePNG namespace. Changed the order of the parameters. Rewrote the + documentation in the header. Renamed files to lodepng.cpp and lodepng.h +*) 22 apr 2006: Optimized and improved some code +*) 07 sep 2005: (!) Changed to std::vector interface +*) 12 aug 2005: Initial release (C++, decoder only) +*/